這方面的文章很多,但是我感覺(jué)很多寫(xiě)的比較抽象艾栋,本文會(huì)通過(guò)舉例更詳細(xì)的解釋爆存。(此文面向的Vue新手們,如果你是個(gè)大牛蝗砾,看到這篇文章就可以點(diǎn)個(gè)贊先较,關(guān)掉頁(yè)面了。)通過(guò)閱讀這篇文章遥诉,你將了解到:
1.Vue數(shù)據(jù)響應(yīng)式的設(shè)計(jì)思想
2.了解Observer,Dep,Watcher的源碼實(shí)現(xiàn)原理
3.getter/setter 攔截?cái)?shù)據(jù)方式的不足及解決方案
一拇泣、設(shè)計(jì)模式
Vue 采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)的響應(yīng)式,通過(guò)Object.defineProperty(點(diǎn)我查看該屬性)來(lái)劫持?jǐn)?shù)據(jù)的setter矮锈,getter霉翔,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,訂閱者收到消息后進(jìn)行相應(yīng)的處理苞笨。
現(xiàn)在我們來(lái)看一下下面的圖债朵,共涉及5個(gè)概念,data和view的意義很明顯瀑凝,主要講解Observer,Dep和Watcher序芦。
Observer:數(shù)據(jù)的觀察者,讓數(shù)據(jù)對(duì)象的讀寫(xiě)操作都處于自己的監(jiān)管之下。當(dāng)初始化實(shí)例的時(shí)候粤咪,會(huì)遞歸遍歷data谚中,用Object.defineProperty來(lái)攔截?cái)?shù)據(jù)(包含數(shù)組里的每個(gè)數(shù)據(jù))。
Dep:數(shù)據(jù)更新的發(fā)布者寥枝,get數(shù)據(jù)的時(shí)候宪塔,收集訂閱者,觸發(fā)Watcher的依賴(lài)收集囊拜;set數(shù)據(jù)時(shí)發(fā)布更新某筐,通知Watcher 。
Watcher:數(shù)據(jù)更新的訂閱者冠跷,訂閱的數(shù)據(jù)改變時(shí)執(zhí)行相應(yīng)的回調(diào)函數(shù)(更新視圖或表達(dá)式的值)南誊。
一個(gè)Watcher可以更新視圖身诺,如html模板中用到的{{test}},也可以執(zhí)行一個(gè)$watch監(jiān)督的表達(dá)式的回調(diào)函數(shù)(Vue實(shí)例中的watch項(xiàng)底層是調(diào)用的$watch實(shí)現(xiàn)的),還可以更新一個(gè)計(jì)算屬性(即Vue實(shí)例中的computed項(xiàng))抄囚。
圖中紅色的箭頭表示的是收集依賴(lài)時(shí)獲取數(shù)據(jù)的流程霉赡。Watcher會(huì)收集依賴(lài)的時(shí)候(這個(gè)時(shí)機(jī)可能是實(shí)例創(chuàng)建時(shí),解析模板怠苔、初始化watch同廉、初始化computed,也可能是數(shù)據(jù)改變后柑司,Watcher執(zhí)行回調(diào)函數(shù)前)迫肖,會(huì)獲取數(shù)據(jù)的值,此時(shí)Observer會(huì)攔截?cái)?shù)據(jù)(即調(diào)用get函數(shù))攒驰,然后通知Dep可以收集訂閱者啦蟆湖。Dep將訂閱數(shù)據(jù)的Watcher保存下來(lái),便于后面通知更新。
圖中綠色的箭頭表示的是數(shù)據(jù)改變時(shí)玻粪,發(fā)布更新的流程隅津。當(dāng)數(shù)據(jù)改變時(shí),即設(shè)置數(shù)據(jù)時(shí)劲室,此時(shí)Observer會(huì)攔截?cái)?shù)據(jù)(即調(diào)用set函數(shù))伦仍,然后通知Dep,數(shù)據(jù)改變了很洋,此時(shí)Dep通知Watcher充蓝,可以更新視圖啦。
二喉磁、 代碼實(shí)現(xiàn)
Observer
下圖是Observer類(lèi)的結(jié)構(gòu)
我們先來(lái)看看Observer的實(shí)現(xiàn)(大家看代碼的時(shí)候可以注意下引文的注釋?zhuān)⑽牡淖⑨屖枪俜降奈焦叮f(shuō)的很棒。中文注釋是我加的)
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
(observer實(shí)例的生成函數(shù)协怒,如果數(shù)據(jù)沒(méi)有被observe過(guò)涝焙,那么新建一個(gè)observer類(lèi)并返回,否則直接返回observer類(lèi))
*/
function observe (value, asRootData) {
if (!isObject(value)) {
return
}
var ob;
//如果存在__ob__屬性孕暇,說(shuō)明該對(duì)象沒(méi)被observe過(guò)仑撞,不是observer類(lèi)
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
//如果數(shù)據(jù)沒(méi)有被observe過(guò),且數(shù)據(jù)是array或object類(lèi)型妖滔,那么將數(shù)據(jù)轉(zhuǎn)化為observer類(lèi)型派草,所以observer類(lèi)接收的是對(duì)象和數(shù)組。
ob = new Observer(value);
}
//如果是RootData铛楣,即咱們?cè)谛陆╒ue實(shí)例時(shí),傳到data里的值艺普,只有RootData在每次observe的時(shí)候簸州,會(huì)進(jìn)行計(jì)數(shù)鉴竭。
vmCount是用來(lái)記錄此Vue實(shí)例被使用的次數(shù)的,
比如岸浑,我們有一個(gè)組件logo搏存,頁(yè)面頭部和尾部都需要展示logo,都用了這個(gè)組件矢洲,那么這個(gè)時(shí)候vmCount就會(huì)計(jì)數(shù)璧眠,值為2
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
//下面是oberver類(lèi)
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*/
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
//def是定義的函數(shù),使用Object.defineProperty()給value添加不可枚舉的屬性,__ob__是一個(gè)對(duì)象被observe的標(biāo)志读虏。
我們?cè)陂_(kāi)發(fā)的過(guò)程中踱蛀,有時(shí)會(huì)遇到题暖,數(shù)據(jù)改變但視圖沒(méi)有更新的問(wèn)題。
這個(gè)時(shí)候,你可以log一下谒所,看看該對(duì)象是否有__ob__屬性來(lái)判斷該對(duì)象是不是被observe了,如果沒(méi)有似芝,那么數(shù)據(jù)改變后視圖是不可能更新的蝶棋。
def(value, '__ob__', this);
//數(shù)組特殊處理,下面詳細(xì)講解
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
//對(duì)于對(duì)象塑荒,遍歷對(duì)象熄赡,并用Object.defineProperty轉(zhuǎn)化為getter/setter,便于監(jiān)控?cái)?shù)據(jù)的get和set
this.walk(value);
}
};
//遍歷對(duì)象齿税,調(diào)用defineReactive將每個(gè)屬性轉(zhuǎn)化為getter/setter
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
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]]);
}
};
/**
* Observe a list of Array items.
*/
//observe每個(gè)數(shù)組元素(observe會(huì)生成Observer類(lèi))
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter
) {
//實(shí)例化一個(gè)Dep彼硫,這個(gè)Dep存在在下面的get和set函數(shù)的作用域中,用于收集訂閱數(shù)據(jù)更新的Watcher偎窘。這里一個(gè)Dep與一個(gè)屬性(即參數(shù)里的key)相對(duì)應(yīng)乌助,一個(gè)Dep可以有多個(gè)訂閱者。
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
//注意下面這行代碼陌知,設(shè)置getter/setter之前他托,會(huì)observe該屬性對(duì)應(yīng)的值(val)。
比如此時(shí)參數(shù)傳入的obj是{ objKey: { objValueKey1:{ objValueKey2: objValueValue2 } } },
key是objKey仆葡,
val是{ objValueKey1:{ objValueKey2: objValueValue2 } }赏参,
那么這個(gè)時(shí)候{ objValueKey1:{ objValueKey2: objValueValue2 } }對(duì)象也會(huì)被observe到,在observe該對(duì)象的時(shí)候沿盅,{ objValueKey2: objValueValue2 }也會(huì)被observe到把篓。
以此類(lèi)推,不管對(duì)象的結(jié)構(gòu)有多深都會(huì)被observe到腰涧。
var childOb = observe(val);
//設(shè)置getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//獲取屬性的值韧掩,如果這個(gè)屬性在轉(zhuǎn)化之前定義過(guò)getter,那么調(diào)用該getter得到value的值窖铡,否則直接返回val疗锐。
var value = getter ? getter.call(obj) : val;
注意這里坊谁,這里是Dep收集訂閱者的過(guò)程,只有在Dep.target存在的情況下才進(jìn)行這個(gè)操作滑臊,在Watcher收集依賴(lài)的時(shí)候才會(huì)設(shè)置Dep.target口芍,所以Watcher收集依賴(lài)的時(shí)機(jī)就是Dep收集訂閱者的時(shí)機(jī)。
調(diào)用get的情況有兩種雇卷,一是Watcher收集依賴(lài)的時(shí)候(此時(shí)Dep收集訂閱者)鬓椭,二是模板或js代碼里用到這個(gè)值,這個(gè)時(shí)候是不需要收集依賴(lài)的关划,只要返回值就可以了小染。
if (Dep.target) {
dep.depend();
//注意這里,不僅這個(gè)屬性需要添加到依賴(lài)列表中,如果這個(gè)屬性對(duì)應(yīng)的值是對(duì)象或數(shù)組祭玉,那么這個(gè)屬性對(duì)應(yīng)的值也需要添加到依賴(lài)列表中氧映,原因后面詳細(xì)解釋
if (childOb) {
childOb.dep.depend();
}
//如果是數(shù)組,那么數(shù)組中的每個(gè)值都添加到依賴(lài)列表里
if (Array.isArray(value)) {
dependArray(value);
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
//當(dāng)為屬性設(shè)置了新的值脱货,是需要observe的
childOb = observe(newVal);
//set的時(shí)候數(shù)據(jù)變化了岛都,通知更新數(shù)據(jù)
dep.notify();
}
});
}
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
//在調(diào)用這個(gè)函數(shù)的時(shí)候,數(shù)組已經(jīng)被observe過(guò)了振峻,且會(huì)遞歸observe臼疫。(看上面defineReactive函數(shù)里的這行代碼:var childOb = observe(val);)
所以正常情況下都會(huì)存在__ob__屬性,這個(gè)時(shí)候就可以調(diào)用dep添加依賴(lài)了扣孟。
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
我們關(guān)注以下幾點(diǎn):
1.Observer類(lèi)中的屬性和方法都比較好理解烫堤,我在這里只說(shuō)一下vmCount屬性:
vmCount屬性是用來(lái)記錄該實(shí)例被創(chuàng)建的次數(shù),我們看下面的代碼(戳我查看demo源碼及效果)凤价,調(diào)用了兩次my-component組件鸽斟,這個(gè)時(shí)候vmCount為2.
<div >
<div id="example">
<my-component></my-component>
<my-component></my-component>
</div>
</div>
<script src="./vue.js"></script>
<script>
var data = { counter: 1 }
Vue.component('my-component', {
template: '<div>{{ counter }}</div>',
data: function () {
return data
}
})
// 創(chuàng)建根實(shí)例
var app2 = new Vue({
el: '#example'
})
</script>
效果:
getter/setter方法攔截?cái)?shù)據(jù)的不足:
- 當(dāng)對(duì)象增刪的時(shí)候,是監(jiān)控不到的利诺。比如:data={a:"a"},這個(gè)時(shí)候如果我們?cè)O(shè)置data.test="test",這個(gè)時(shí)候是監(jiān)控不到的富蓄。因?yàn)樵趏bserve data的時(shí)候,會(huì)遍歷已有的每個(gè)屬性(比如a)慢逾,添加getter/setter立倍,而后面設(shè)置的test屬性并沒(méi)有機(jī)會(huì)設(shè)置getter/setter,所以檢測(cè)不到變化侣滩。同樣的口注,刪除對(duì)象屬性的時(shí)候,getter/setter會(huì)跟著屬性一起被刪除掉君珠,攔截不到變化寝志。
- getter/setter是針對(duì)對(duì)象的,那么對(duì)于數(shù)組的修改,怎樣監(jiān)控變化呢
- 每次給數(shù)據(jù)設(shè)置值得時(shí)候澈段,都會(huì)調(diào)用setter函數(shù)悠菜,這個(gè)時(shí)候就會(huì)發(fā)布屬性更新消息,即使數(shù)據(jù)的值沒(méi)有變败富。從性能方便考慮我們肯定希望值沒(méi)有變化的時(shí)候,不更新模板摩窃。
對(duì)于第一個(gè)問(wèn)題兽叮,Vue官方給出了vm.delete/Vue.delete這樣的api來(lái)解決這個(gè)問(wèn)題。我們來(lái)看下$set的代碼:
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
function set (target, key, val) {
//對(duì)于數(shù)組的處理猾愿,調(diào)用變異方法splice鹦聪,這個(gè)時(shí)候數(shù)組的Dep會(huì)發(fā)布更新消息
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
//如果set的是對(duì)象已經(jīng)有的屬性,那么該屬性已經(jīng)有g(shù)etter/setter函數(shù)了蒂秘,此時(shí)直接修改即可
if (hasOwn(target, key)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
"development" !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
//如果是對(duì)象沒(méi)有的屬性泽本,則添加getter/setter
defineReactive$$1(ob.value, key, val);
//注意此處,對(duì)象的Dep會(huì)發(fā)布更新
ob.dep.notify();
return val
}
上面的代碼比較簡(jiǎn)單姻僧,看過(guò)注釋?xiě)?yīng)該就能明白规丽,我不做過(guò)多解釋。我們著重注意下這句代碼:ob.dep.notify()撇贺,對(duì)象的Dep發(fā)布更新赌莺。可是這個(gè)dep是在什么地方收集的訂閱者呢松嘶?
還記得defineReactive函數(shù)里讓大家注意的這句代碼嗎:childOb.dep.depend(),這句代碼就是在收集訂閱者艘狭。
仔細(xì)閱讀Observer相關(guān)的代碼,我們會(huì)發(fā)現(xiàn)翠订,dep實(shí)例化的地方有兩處:
一處是在defineReactive函數(shù)里巢音,每次調(diào)用這個(gè)函數(shù)的時(shí)候都會(huì)創(chuàng)建一個(gè)新的Dep實(shí)例,存在在getter/setter閉包函數(shù)的作用域鏈上尽超,是為對(duì)象屬性服務(wù)的官撼。在Watcher獲取屬性的值的時(shí)候收集訂閱者,在設(shè)置屬性值的時(shí)候發(fā)布更新橙弱。
另一處是在observe函數(shù)中歧寺,此時(shí)的dep掛在被observe的數(shù)據(jù)的__ obj__屬性上,他是為對(duì)象或數(shù)組服務(wù)的棘脐,在Watcher獲取屬性的值的時(shí)候斜筐,如果值被observe后返回observer對(duì)象(對(duì)象和數(shù)組才會(huì)返回observer),那么就會(huì)在此時(shí)收集訂閱者蛀缝,在對(duì)象或數(shù)組增刪元素時(shí)調(diào)用$set等api時(shí)發(fā)布更新的顷链;
defineReactive函數(shù)的getter函數(shù)里這段代碼就是在收集訂閱者:
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
//注意這里,此處的dep就是在執(zhí)行var childOb = observe(val)時(shí)產(chǎn)生的屈梁,是用來(lái)收集childOb的訂閱者的
childOb.dep.depend();
}
if (Array.isArray(value)) {
dependArray(value);
}
}
我們來(lái)看個(gè)例子嗤练,data={testkey:{testValueKey:testValueValue}}榛了,這個(gè)時(shí)候模板里有{{testkey}},模板的Watcher在執(zhí)行g(shù)etter函數(shù)的時(shí)候煞抬,testkey屬性的getter對(duì)應(yīng)的dep會(huì)將此Watcher收集為訂閱者霜大;同時(shí){testValueKey:testValueValue}對(duì)象也會(huì)將此Watcher收集為訂閱者(我們?cè)诮otestkey屬性設(shè)置getter/setter函數(shù)時(shí),會(huì)執(zhí)行var childOb = observe(val)和childOb.dep.depend()革答,而此時(shí)的val就是{testValueKey:testValueValue}對(duì)象)战坤。
在這個(gè)時(shí)候我們?cè)O(shè)置this.$set(this.testkey, "addKey", "addValue"),就會(huì)觸發(fā)this.testkey對(duì)應(yīng)的值:{testValueKey:testValueValue}對(duì)象的dep發(fā)布更新残拐,而此時(shí)dep的訂閱者中包含模板{{testkey}}的watcher,此時(shí)模板更新視圖途茫。同理每個(gè)數(shù)組也是有相應(yīng)的dep來(lái)發(fā)布更新的,比如data={arr:[1,2,2]}}溪食,此時(shí)[1,2,2]這個(gè)數(shù)組的__ obj__屬性下也會(huì)有dep的囊卜。
其實(shí)只有對(duì)象和數(shù)組才會(huì)有這種刪除和增加的操作,而其他的字符串等都是直接賦值修改的错沃,getter/setter都是能檢測(cè)到的栅组,所以observe對(duì)象和數(shù)組的時(shí)候會(huì)創(chuàng)建一個(gè)dep,用來(lái)收集訂閱和發(fā)布更新
對(duì)于第二個(gè)問(wèn)題捎废,我們先來(lái)考慮下數(shù)組修改有哪幾種情況
1.當(dāng)你利用索引直接設(shè)置一個(gè)項(xiàng)時(shí)
比如:data={arr:[1,2,3]}笑窜,這個(gè)時(shí)候我設(shè)置this.arr[0] = 4,會(huì)發(fā)現(xiàn)數(shù)據(jù)改變了,但是視圖沒(méi)有更新登疗,Vue根本沒(méi)有檢測(cè)到變化排截。
這個(gè)時(shí)候可能你會(huì)說(shuō),observeArray的時(shí)候不是會(huì)遍歷數(shù)組辐益,observe每個(gè)元素嗎断傲?可是Observe數(shù)據(jù)的時(shí)候是會(huì)判斷數(shù)據(jù)類(lèi)型的,只會(huì)處理數(shù)組和對(duì)象智政,而this.arr里面的元素是字符串认罩,所以無(wú)法轉(zhuǎn)化成observer類(lèi),也就不會(huì)有g(shù)etter/setter续捂。另一方面垦垂,即便arr里面是對(duì)象,比如{arr:[{testobj: true}]}牙瓢,數(shù)組元素{testobj: true}會(huì)被observe到劫拗,那也只是在{testobj: true}對(duì)象里面的屬性改變的時(shí)候響應(yīng),而{testobj: true}對(duì)象被替換是無(wú)法感知的矾克。
2.調(diào)用數(shù)組的變異方法(push(),pop(),shift(),unshift(),splice(),sort(),reverse())页慷,這些方法是會(huì)讓數(shù)組的值發(fā)生改變的,比如:arr=[0,1];arr.puah(3);此時(shí)arr=[1,2,3],arr發(fā)生了改變酒繁,此時(shí)是需要更新視圖的滓彰,但是arr的getter/setter攔截不到變化(只有在賦值的時(shí)候才會(huì)調(diào)用setter,比如:arr=[6,7,8])州袒。
3.當(dāng)你修改數(shù)組的長(zhǎng)度時(shí)揭绑,例如:vm.items.length = newLength
對(duì)于第一種情況,和對(duì)象的增減一樣郎哭,可以使用vm.delete/Vue.delete這幾個(gè)api.
對(duì)于第二種情況洗做,可以通過(guò)改寫(xiě)這些變異方法完成,在調(diào)用這些方法的時(shí)候發(fā)布更新消息彰居。下面我們來(lái)看代碼
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
//遍歷變異方法
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method];
//重寫(xiě)arrayMethods里的變異方法
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
//inserted存儲(chǔ)的是新加到數(shù)組里的元素,需要被observe
if (inserted) { ob.observeArray(inserted); }
// notify change
//發(fā)布更新
ob.dep.notify();
return result
});
});
回過(guò)頭再看Observer類(lèi)中對(duì)于數(shù)組的處理撰筷,先覆蓋變異數(shù)組陈惰,再observe每個(gè)數(shù)組元素。所以每當(dāng)調(diào)用數(shù)組的變異方法的時(shí)候毕籽,都會(huì)更新視圖抬闯。
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
//用改變變異數(shù)組后的arrayMethods的方法覆蓋被observe的數(shù)組的方法
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
}
對(duì)于第三種情況,可以使用splice來(lái)完成关筒,splice是變異方法溶握,會(huì)發(fā)布更新。
戳這蒸播,查看官網(wǎng)對(duì)此的解決方案睡榆。
對(duì)于上面提到的第三個(gè)問(wèn)題,Watcher在run方法里解決了這個(gè)問(wèn)題袍榆,他會(huì)檢測(cè)value !== this.value胀屿,只更新值變化的情況。
Observer相關(guān)代碼就看到這里包雀,下面來(lái)看Dep
Dep
下圖是Dep的結(jié)構(gòu)圖
下面是源碼:
//全局變量宿崭,每個(gè)實(shí)例中的dep實(shí)例的id都是從0開(kāi)始累加的
var uid$1 = 0;
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
var Dep = function Dep () {
this.id = uid$1++;
//subscribe的簡(jiǎn)寫(xiě),存放訂閱者
this.subs = [];
};
//添加一個(gè)訂閱者
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
//刪除一個(gè)訂閱者
Dep.prototype.removeSub = function removeSub (sub) {
remove$1(this.subs, sub);
};
//讓W(xué)atcher收集依賴(lài)并添加訂閱者才写。
Dep.target是一個(gè)Watcher, 可以查看Watcher的addDep方法葡兑。
這個(gè)方法做的事情是:收集依賴(lài)后,調(diào)用了Dep的addSub方法赞草,給Dep添加了一個(gè)訂閱者
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
//發(fā)布數(shù)據(jù)更新:通過(guò)調(diào)用subs里面的每個(gè)Watcher的update發(fā)布更新
Dep.prototype.notify = function notify () {
// stablize the subscriber list first
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
//注意這里讹堤,target是全局唯一的。下面詳細(xì)講解房资。
Dep.target = null;
關(guān)于Dep主要注意以下幾點(diǎn):
1.Dep是發(fā)布訂閱者模型中的發(fā)布者蜕劝,Watcher是觀察者,一個(gè)Dep實(shí)例對(duì)應(yīng)一個(gè)對(duì)象屬性或一個(gè)被觀察的對(duì)象,用來(lái)收集訂閱者和在數(shù)據(jù)改變時(shí)岖沛,發(fā)布更新暑始。
2.Dep實(shí)例有兩種實(shí)例:
*第一種:在observe方法里生成的,用來(lái)給被觀察的對(duì)象收集訂閱者和發(fā)布更新婴削,掛在對(duì)象的__ ob__對(duì)象上廊镜,通常在defineReactive函數(shù)里的getter函數(shù)里調(diào)用childOb.dep.depend()來(lái)收集依賴(lài),在vm.delete/Vue.delete這些api中調(diào)用來(lái)發(fā)布更新唉俗。
*第二種:在defineReactive函數(shù)里嗤朴,是用來(lái)set/get數(shù)據(jù)時(shí)收集訂閱者和發(fā)布更新的,保存在getter/setter閉包函數(shù)的作用域上虫溜。set數(shù)據(jù)時(shí)收集依賴(lài)雹姊,get數(shù)據(jù)時(shí)發(fā)布更新。
比如我們有一個(gè)data:data={testVal: "testVal", testObj: {testObjFirstEle: "testObjFirstEle"}};
這個(gè)時(shí)候衡楞,這個(gè)Vue實(shí)例上會(huì)有四個(gè)Dep實(shí)例:
第一個(gè)是調(diào)用data的observe方法時(shí)生成的掛在{testVal: "testVal", testObj: {testObjFirstEle: "testObjFirstEle"}}對(duì)象的__ ob__方法上的吱雏,屬于上面說(shuō)的第一種實(shí)例;
第二個(gè)是調(diào)用defineReactive函數(shù)給屬性testVal添加getter,setter函數(shù)時(shí)生成的瘾境。保存在getter/setter閉包函數(shù)的作用域上歧杏,屬于第二種實(shí)例。
第三個(gè)是調(diào)用defineReactive函數(shù)給屬性testObj添加getter,setter函數(shù)時(shí)生成的迷守。保存在getter/setter閉包函數(shù)的作用域上犬绒,屬于第二種實(shí)例。
第四種是Watcher收集依賴(lài)時(shí)兑凿,調(diào)用testObj屬性的set函數(shù)添加依賴(lài)時(shí)observe屬性的值(即{testObjFirstEle: "testObjFirstEle"}對(duì)象)生成的凯力。
現(xiàn)在我們來(lái)驗(yàn)證一下:
*源碼:http://runjs.cn/code/9p5ydg84
*效果:http://sandbox.runjs.cn/show/9p5ydg84
dep的id從0開(kāi)始,到3結(jié)束一共四個(gè)急膀。這個(gè)時(shí)候你可能會(huì)問(wèn)沮协,dep的id為2和3的實(shí)例去哪里了?注意上面我們說(shuō)的第二種Dep實(shí)例對(duì)存在在getter/setter閉包函數(shù)的作用域中的卓嫂,我們獲取不到慷暂,你可以在源碼里debugger來(lái)看。
3.當(dāng)我們想要給{testObjFirstEle: "testObjFirstEle"}對(duì)象添加屬性并更新視圖時(shí)有兩種方式:
一晨雳、利用getter/setter行瑞,重新設(shè)置testObj屬性的值,testObj屬性的setter執(zhí)行的過(guò)程中會(huì)調(diào)用dep.notify()發(fā)布更新餐禁。比如:this.testObj = {testObjFirstEle: "testObjFirstEle", "newEle": "newEle};
二血久、利用set(this.testObj, "newEle", "newEle")。此時(shí)是{testObjFirstEle: "testObjFirstEle"}對(duì)象的__obj __對(duì)象上的dep發(fā)布的更新帮非。
4.Dep.target:Dep.target為什么是全局唯一的呢氧吐?這是我以前一直不理解的地方讹蘑。這一點(diǎn)我想,在講完Watcher時(shí)會(huì)更清晰筑舅,所以請(qǐng)大家耐心讀完下面的內(nèi)容座慰,就明白了。
Watcher
Watcher里面的屬性很多翠拣,我們下面只注釋本文關(guān)心的內(nèi)容
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options
) {
this.vm = vm;
vm._watchers.push(this);
// options
if (options) {
//對(duì)應(yīng)$watch參數(shù)的deep版仔,具體的可以參考官網(wǎng)文檔:https://cn.vuejs.org/v2/api/#vm-watch
this.deep = !!options.deep;
this.user = !!options.user;
//跟computed相關(guān),這里不具體講解
this.lazy = !!options.lazy;
this.sync = !!options.sync;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
//注意這里误墓,關(guān)于deps和newDeps下面詳細(xì)講解
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
//這里的getter會(huì)有兩種情況:
一蛮粮、一個(gè)函數(shù),比如在生命周期mount的時(shí)候谜慌,需要watch模板中的值然想,這個(gè)時(shí)候傳過(guò)來(lái)的是一個(gè)函數(shù),后面在get函數(shù)里調(diào)用時(shí)這個(gè)函數(shù)時(shí)欣范,這個(gè)函數(shù)會(huì)調(diào)用數(shù)據(jù)的getter函數(shù)又沾。
二、一個(gè)表達(dá)式熙卡,比如我們?cè)赩ue實(shí)例的watch中寫(xiě)的表達(dá)式,后面在get函數(shù)里獲取表達(dá)式的值的時(shí)候會(huì)調(diào)用數(shù)據(jù)的getter函數(shù)励饵。
expOrFn參數(shù)是一個(gè)字符串驳癌,比如testObj.testObjFirstVal,此時(shí)testObj僅僅是一個(gè)字符串役听,而不是對(duì)象颓鲜,我們無(wú)法直接獲取testObjFirstVal屬性的值。
所以我們?cè)讷@取值得時(shí)候不能直接拿到值典予,parsePath函數(shù)就是用來(lái)解決這個(gè)問(wèn)題的甜滨,這個(gè)函數(shù)具體的操作,在后面的代碼里瘤袖。
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
//這里是針對(duì)表達(dá)式
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function () {};
"development" !== 'production' && warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
//注意這個(gè)地方衣摩,在非computed調(diào)用Watch函數(shù)外,都會(huì)調(diào)用get函數(shù)(computed有自己的邏輯)
this.value = this.lazy
? undefined
: this.get();
};
/**
* Evaluate the getter, and re-collect dependencies.
*/
//get函數(shù)捂敌,用來(lái)收集依賴(lài)和獲取數(shù)據(jù)的值
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
//注意這里下面詳細(xì)講解
this.cleanupDeps();
}
return value
};
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;
var targetStack = [];
function pushTarget (_target) {
if (Dep.target) { targetStack.push(Dep.target); }
Dep.target = _target;
}
function popTarget () {
Dep.target = targetStack.pop();
}
/**
* Add a dependency to this directive.
*/
//添加一個(gè)依賴(lài)
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
//這里做了一個(gè)去重艾扮,如果depIds里包含這個(gè)id,說(shuō)明在之前給depIds添加這個(gè)id的時(shí)候占婉,已經(jīng)調(diào)用過(guò) dep.addSub(this)泡嘴,即添加過(guò)訂閱,不需要重復(fù)添加逆济。
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
/**
* Clean up for dependency collection.
*/
//下面詳細(xì)講
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var this$1 = this;
var i = this.deps.length;
//去除多余的訂閱者
while (i--) {
var dep = this$1.deps[i];
//如果Watcher不依賴(lài)于某個(gè)數(shù)據(jù)酌予,即某個(gè)Dep,那么不需要再訂閱這個(gè)數(shù)據(jù)的消息磺箕。
if (!this$1.newDepIds.has(dep.id)) {
dep.removeSub(this$1);
}
}
var tmp = this.depIds;
//更新depIds
this.depIds = this.newDepIds;
this.newDepIds = tmp;
//清空newDepIds
this.newDepIds.clear();
tmp = this.deps;
//更新deps
this.deps = this.newDeps;
this.newDeps = tmp;
//清空newDeps
this.newDeps.length = 0;
};
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
//更新模板或表達(dá)式:調(diào)用run方法
Watcher.prototype.update = function update () {
/* istanbul ignore else */
//下面三種情況均會(huì)調(diào)用run方法
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
//queueWatcher這個(gè)函數(shù)最終會(huì)調(diào)用run方法。
queueWatcher(this);
}
};
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
//注意這里調(diào)用了get方法抛虫,會(huì)更新模板松靡,且重新收集依賴(lài)
Watcher.prototype.run = function run () {
if (this.active) {
//獲取值,且重新收集依賴(lài)
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
//注意下面 this.cb.call莱褒,調(diào)用回調(diào)函數(shù)來(lái)更新模板或表達(dá)式的值($watch表達(dá)式的時(shí)候击困,會(huì)更新表達(dá)式的值)
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
/**
* Parse simple path.
*/
var bailRE = /[^\w.$]/;
function parsePath (path) {
//如果字符串中沒(méi)有符號(hào)".",直接返回即可广凸,比如:testVal
if (bailRE.test(path)) {
return
}
//用符號(hào)"."分割字符串阅茶,遍歷數(shù)組,依次獲取obj對(duì)象上相應(yīng)的值谅海。
比如:testObj.testObjFirstVal脸哀,先分割成數(shù)組[testObj,testObjFirstVal],其次遍歷數(shù)組,獲取obj[testObj]的值扭吁,最后獲取obj[testObj][testObjFirstVal]的值撞蜂。
var segments = path.split('.');
//這個(gè)地方將函數(shù)作為返回值,感興趣的話侥袜,可以看一下函數(shù)式編程
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
我們先來(lái)理一理watch函數(shù)做的事情:初始化變量——>獲取getter函數(shù)蝌诡,這里的getter函數(shù)是用來(lái)獲取數(shù)據(jù)的值,函數(shù)執(zhí)行過(guò)程中會(huì)調(diào)用數(shù)據(jù)的getter函數(shù)枫吧,會(huì)收集依賴(lài)——>調(diào)用watcher的get方法浦旱,收集依賴(lài),獲取值九杂,并將這些東西記錄下來(lái)颁湖。
這個(gè)過(guò)程就完成了收集依賴(lài)的過(guò)程,而update函數(shù)是用來(lái)接收數(shù)據(jù)發(fā)布更新的消息并更新模板或表達(dá)式的例隆。
下面我們重點(diǎn)來(lái)關(guān)注這幾點(diǎn)甥捺,這些是我剛接觸Vue時(shí)想不清楚的地方:
1.收集依賴(lài)指的是誰(shuí)收集依賴(lài),依賴(lài)又是指的什么镀层?這是我一直很迷惑的問(wèn)題镰禾。看英文注釋?zhuān)篧atcher的作用是分割表達(dá)式唱逢,收集依賴(lài)并且在值變化的時(shí)候調(diào)用回調(diào)函數(shù)羡微。那么我們很明確知道是Watcher在收集依賴(lài),依賴(lài)到底指什么呢惶我?
我們上面說(shuō)過(guò)一個(gè)Dep對(duì)應(yīng)著一個(gè)數(shù)據(jù)(這個(gè)數(shù)據(jù)可能是:對(duì)象的屬性妈倔、一個(gè)對(duì)象、一個(gè)數(shù)組);一個(gè)Watcher對(duì)應(yīng)可以是一個(gè)模板也可以是一個(gè)watch對(duì)應(yīng)的函數(shù)依賴(lài)的數(shù)據(jù)就是testWatcher和testVal。所以這個(gè)$watch對(duì)應(yīng)的Watcher收集的依賴(lài)就是testWatcher和testVal對(duì)應(yīng)的Dep闽烙。
app.$watch(function(){
return this.testWatcher + this.testVal;
},function(newVal){
console.log(newVal
})
2.Watcher有四個(gè)使用的場(chǎng)景翅睛,只有在這四種場(chǎng)景中,Watcher才會(huì)收集依賴(lài)黑竞,更新模板或表達(dá)式捕发,否則,數(shù)據(jù)改變后很魂,無(wú)法通知依賴(lài)這個(gè)數(shù)據(jù)的模板或表達(dá)式:
*第一種:觀察模板中的數(shù)據(jù)
*第二種:觀察創(chuàng)建Vue實(shí)例時(shí)watch選項(xiàng)里的數(shù)據(jù)
*第三種:觀察創(chuàng)建Vue實(shí)例時(shí)computed選項(xiàng)里的數(shù)據(jù)所依賴(lài)的數(shù)據(jù)
*第四種:調(diào)用$watch api觀察的數(shù)據(jù)或表達(dá)式
所以在解決數(shù)據(jù)改變扎酷,模板或表達(dá)式?jīng)]有改變的問(wèn)題時(shí),可以這么做:
首先仔細(xì)看一看數(shù)據(jù)是否在上述四種應(yīng)用場(chǎng)景中遏匆,以便確認(rèn)數(shù)據(jù)已經(jīng)收集依賴(lài)法挨;其次查看改變數(shù)據(jù)的方式,確定這種方式會(huì)使數(shù)據(jù)的改變被攔截(關(guān)于這一點(diǎn)幅聘,上面Obsever相關(guān)內(nèi)容中說(shuō)的比較多)凡纳。
3.Dep.target的作用:我們前面說(shuō)過(guò)收集依賴(lài)的時(shí)機(jī)是在調(diào)用數(shù)據(jù)的getter函數(shù)的時(shí)候,但是在這個(gè)時(shí)候數(shù)據(jù)的getter函數(shù)不知道當(dāng)前的Watcher是哪一個(gè)帝蒿,所以這里使用了一個(gè)全局變量來(lái)記錄當(dāng)前的Watcher惫企,方便添加依賴(lài)到正在執(zhí)行的Watcher。關(guān)于這點(diǎn)官方的英文注釋寫(xiě)的挺清楚的陵叽。
4.targetStack的作用(Watcher函數(shù)的get方法中pushTarget和popTarget方法中用到):Vue2 中(本文源碼為Vue2),視圖被抽象為一個(gè) render 函數(shù)丛版,一個(gè) render 函數(shù)只會(huì)生成一個(gè) watcher巩掺。比如我們有如下一個(gè)模板,模板中使用了Header組件页畦。Vue2 中組件數(shù)的結(jié)構(gòu)在視圖渲染時(shí)就映射為 render 函數(shù)的嵌套調(diào)用胖替,有嵌套調(diào)用就會(huì)有調(diào)用棧。當(dāng) render模板時(shí)豫缨,遇到Header組件會(huì)調(diào)用Header組件的render函數(shù)独令,兩個(gè)render函數(shù)依次入棧,執(zhí)行完函數(shù)好芭,依次出棧燃箭。
<div id="app">
<Header></Header>
</div>
5.Watcher函數(shù)的get方法中調(diào)用this.getter.call(vm, vm)收集完依賴(lài)后,又調(diào)用this.cleanupDeps()清除依賴(lài)舍败。excus me ??招狸?第一次看這個(gè)地方的時(shí)候敬拓,我很困擾,為什么添加完依賴(lài)后要清楚裙戏。后面仔細(xì)看了代碼發(fā)現(xiàn)是這個(gè)樣子的:
Watcher里面有兩個(gè)屬性:deps和newDeps乘凸。他們是用來(lái)記錄上一次Watcher收集的依賴(lài)和新一輪Watcher收集的依賴(lài),每一次有數(shù)據(jù)的更新都需要重新收集依賴(lài)(數(shù)據(jù)發(fā)布更新后累榜,會(huì)調(diào)用Watcher的notify方法营勤,notify方法會(huì)調(diào)用run方法,run方法會(huì)調(diào)用get方法壹罚,重新獲取值葛作,并重新收集依賴(lài))。舉個(gè)簡(jiǎn)單的例子:我們點(diǎn)擊一個(gè)按鈕渔嚷,用$set給data添加了一個(gè)新屬性newVal进鸠。上一輪收集的依賴(lài)中并沒(méi)有newVal的依賴(lài),所以需要重新收集依賴(lài)形病。
this.cleanupDeps()這個(gè)函數(shù)的作用就是將新收集的依賴(lài)newDeps賦值給deps客年,并將newDeps清空,準(zhǔn)備在下一次數(shù)據(jù)更新時(shí)收集依賴(lài)漠吻。所以這個(gè)函數(shù)不是真正的清空Watcher的依賴(lài)量瓜,而是清除臨時(shí)保存依賴(lài)的newDeps。
看完上面的這些后途乃,再看官方給出的圖绍傲,就更明白了,不過(guò)官方的圖中耍共,并沒(méi)有標(biāo)提到Dep和Observer烫饼。
三、相關(guān)概念
1.雙向數(shù)據(jù)綁定
M 试读,即 model杠纵,指的是模型,也就是數(shù)據(jù)钩骇;V 即view比藻,指的是視圖,也就是頁(yè)面展現(xiàn)的部分倘屹。
雙向數(shù)據(jù)綁定大概概括為:每當(dāng)數(shù)據(jù)有變更時(shí)银亲,會(huì)進(jìn)行渲染,從而更新視圖纽匙,使得視圖與數(shù)據(jù)保持一致(model到view層)务蝠;而另一方面,頁(yè)面也會(huì)通過(guò)用戶(hù)的交互烛缔,產(chǎn)生狀態(tài)请梢、數(shù)據(jù)的變化赠尾,這個(gè)時(shí)候,這時(shí)需要將視圖對(duì)數(shù)據(jù)的更新同步到數(shù)據(jù)(view到model層)毅弧。
不同的前端 MV* 框架對(duì)于這種 Model 和 View 間的數(shù)據(jù)同步有不同的處理气嫁,如:
臟值檢查(angular.js)
數(shù)據(jù)劫持 + 發(fā)布者-訂閱者模式(Vue)
我們上面說(shuō)的Vue的數(shù)據(jù)響應(yīng)式原理其實(shí)就是實(shí)現(xiàn)數(shù)據(jù)到視圖更新原理,而視圖到數(shù)據(jù)的更新够坐,其實(shí)就是此基礎(chǔ)上給可表單元素(input等)添加了change等事件監(jiān)聽(tīng)寸宵,來(lái)動(dòng)態(tài)修改model和 view。
2.發(fā)布-訂閱者模型
訂閱發(fā)布模式定義了一種一對(duì)多的依賴(lài)關(guān)系元咙,讓多個(gè)訂閱者對(duì)象同時(shí)監(jiān)聽(tīng)某一個(gè)主題對(duì)象梯影。這個(gè)主題對(duì)象在自身狀態(tài)變化時(shí),會(huì)通知所有訂閱者對(duì)象庶香,使它們能夠自動(dòng)更新自己的狀態(tài)甲棍。
Vue中的Dep和Watcher共同實(shí)現(xiàn)了這個(gè)模型