Vue響應式系統的核心-----變化偵測(Object)
什麼是變化偵測
變化偵測的作用是偵測資料的變化,當資料變化時,會通知檢視進行相應的更新
在執行時應用內部的狀態會不斷髮生變化,此時需要不停地重新渲染。這時如何確定狀態中發生了什麼變化?變化偵測主要用來解決這個問題。
Vue的變化偵測屬推。當狀態發生變化時,Vue立刻知道了,在一定程度上知道哪些狀態變了。
有一個狀態繫結多個依賴,每個依賴表示一個具體的DOM節點,當狀態變化了,向這個狀態的所有依賴傳送通知,讓他們進行DOM更新操作。每個狀態繫結的依賴越多,依賴追蹤在記憶體上的開銷就會越大。
從vue.js 2.0開始引入了虛擬DOM,一個狀態所繫結的依賴不再是具體的DOM節點,而是一個元件。當狀態變化後,會通知到元件,元件內部在使用虛擬DOM進行比對。這樣可以大大降低依賴數量,從而降低依賴追蹤所消耗的記憶體。
Object的變化偵測
-
Data通過Observer轉換成了getter/setter的形式來追蹤變化
-
當外界通過watcher讀取資料時,會觸發getter從而將watcher新增到依賴中
-
當資料發生變化時,會觸發setter,從而向Dep中的依賴傳送通知。
-
watcher接收到通知後,會向外界傳送通知,變化通知到外界後可能會觸發檢視更新,也有可能觸發使用者的某個回撥函式等。
如何追蹤變化
在vue中如何偵測一個物件的變化?使用Object.defineProperty和ES6的Proxy
function defineReactive(data,key,val){ Object.defineProperty(data,key,{ enumerable: true, configuration: true, get: function(){ return val }, set: function(newVal){ if(val === newVal){ return } val = newVal } }) }
每當從data的key中讀取資料時,get函式被觸發;每當往data的key中設定資料時,set函式被觸發。
如何收集依賴
觀察資料目的是當資料的屬性發生變化時,可以通知那些曾經使用該資料的地方。
<template>
<h1>{{name}}</h1>
</template>
該模板使用了資料name,所以當它發生變化時,要向使用了它的地方傳送通知。
收集依賴:把用到資料name的地方收集起來,然後等屬性發生變化時,把之前收集好的依賴迴圈觸發一遍。
依賴收集在哪裡
在getter中收集依賴,在setter中觸發依賴
dep類專門管理依賴,使用這個類可以收集依賴、刪除依賴或向依賴傳送通知等。
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
// 刪除一個依賴
removeSub (sub) {
remove(this.subs, sub)
}
// 新增一個依賴
depend () {
if (window.target) {
this.addSub(window.target)
}
}
// 通知所有依賴更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
function defineReactive (obj,key,val) {
const dep = new Dep() //例項化一個依賴管理器,生成一個依賴管理陣列dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依賴
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依賴更新
}
})
}
依賴是誰?
當屬性發生變化後,需要通知誰
其實在Vue中還實現了一個叫做Watcher的類,而Watcher類的例項就是我們上面所說的那個"誰"。換句話說就是:誰用到了資料,誰就是依賴,我們就為誰建立一個Watcher例項。在之後資料變化時,我們不直接去通知依賴更新,而是通知依賴對應的Watch例項,由Watcher例項去通知真正的檢視。
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一個形如'data.a.b.c'的字串路徑所表示的值,從真實的data物件中取出來
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
遞迴偵測所有key
前面的程式碼只能偵測資料中的某一個屬性,我們希望把資料中的所有屬性都偵測到,所以需要封裝一個Observer類。這個類的作用將一個數據內的所有屬性都轉換成getter/setter的形式,然後再追蹤它們的變化。
function Observer (value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
};
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i],obj[keys[i]]);
}
};
function defineReactive (obj,key,val) {
//新增,遞迴子屬性
if(typeof val === 'object'){
new Observer(val);
}
const dep = new Dep() //例項化一個依賴管理器,生成一個依賴管理陣列dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依賴
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依賴更新
}
})
}
不足之處
雖然我們通過Object.defineProperty方法實現了對object資料的可觀測,但是這個方法僅僅只能觀測到object資料的取值及設定值,當我們向object資料裡新增一對新的key/value或刪除一對已有的key/value時,它是無法觀測到的,導致當我們對object資料新增或刪除值時,無法通知依賴,無法驅動檢視進行響應式更新。
當然,Vue也注意到了這一點,為了解決這一問題,Vue增加了兩個全域性API:Vue.set和Vue.delete。