了解Vue計算屬性的實(shí)現(xiàn)原理
computed的作用
在vue的開發(fā)中宵凌,我們不免會使用到計算屬性票编,使用計算屬性陷遮,vue會幫我們收集所有的該計算屬性所依賴的所有data屬性的依賴挠乳,當(dāng)data屬性改變時,便會重新獲取computed屬性讼呢,這樣我們就不用關(guān)注計算屬性所依賴的data屬性的改變撩鹿,而手動修改computed屬性,這是vue強(qiáng)大之處之一悦屏。那么我們不免會產(chǎn)生疑問节沦,computed屬性為啥能隨著data屬性的改變而跟著改變的?
帶著這個疑問础爬,我們來解析下vue的源碼甫贯,看看它是如何實(shí)現(xiàn)computed的依賴收集。
整體流程
computed的依賴收集是借助vue的watcher來實(shí)現(xiàn)的看蚜,我們稱之為computed watcher叫搁,每一個計算屬性會對應(yīng)一個computed watcher對象,
該watcher對象包含了getter屬性和get方法失乾,getter屬性就是計算屬性對應(yīng)的函數(shù),get方法是用來更新計算屬性(通過調(diào)用getter屬性)纬乍,并會把該computed watcher添加到計算屬性依賴的所有data屬性的訂閱器列表中碱茁,這樣當(dāng)任何計算屬性依賴的data屬性改變的時候,就會調(diào)用該computed watcher的update方法仿贬,把該watcher標(biāo)記為dirty纽竣,然后更新dom的dom watcher更新dom時,會觸發(fā)dirty的computed
watcher調(diào)用evaluate去計算最新的值,以便更新dom蜓氨。
所以computed的實(shí)現(xiàn)是需要兩個watcher來實(shí)現(xiàn)的聋袋,一個用來收集依賴,一個用來更新dom穴吹,并且兩種watcher是有關(guān)聯(lián)的幽勒。后續(xù)我們把更新DOM的watcher稱為domWatcher,另一種叫computedWatcher
initComputed
該方法是用來初始化computed屬性的港令,它會遍歷computed屬性啥容,然后做兩件事:
1、為每個計算屬性生成一個computedWathcer顷霹,后續(xù)計算屬性依賴的data屬性會把這個computedWatcher添加到自己訂閱器列表中咪惠,以此來實(shí)現(xiàn)依賴收集。
2淋淀、挾持每個計算屬性的get和set方法遥昧,set方法沒有意義,主要是get方法朵纷,后面會提到炭臭。
function initComputed (vm, computed) {
? varwatchers = vm._computedWatchers = Object.create(null);
? //遍歷所有的computed屬性
? for (varkey in computed) {
??? varuserDef = computed[key];
??? //每個計算屬性對應(yīng)的函數(shù)或者其get方法(computed屬性可以設(shè)置get方法)
??? vargetter = typeof userDef === 'function' ? userDef : userDef.get;
??? // ....
??? if(!isSSR) {
????? //為每個計算屬性生成一個Wathcer
?????watchers[key] = new Watcher(
??????? vm,
???????getter || noop,
??????? noop,
???????computedWatcherOptions
????? );
??? }
? if (!(keyin vm)) {
????? //defineComputed的作用就是挾持每個計算屬性的get和set方法
?????defineComputed(vm, key, userDef);
??? } else {
????? // ....
??? }
? }
}
defineComputed
如上面所述,definedComputed是挾持計算屬性get和set方法柴罐,當(dāng)然set方法對于計算屬性是沒什么作用徽缚,所以這里我們重點(diǎn)關(guān)注get方法,我們這里只需要知道get方法是觸發(fā)依賴收集的關(guān)鍵革屠,并且它把兩種watcher進(jìn)行了關(guān)聯(lián)凿试。
function defineComputed (
? target,
? key,
? userDef
) {
? varshouldCache = !isServerRendering();
? //下面這段代碼就是定義get和set方法了
? if (typeofuserDef === 'function') {
???sharedPropertyDefinition.get = shouldCache
????? ?createComputedGetter(key)
????? :userDef;
???sharedPropertyDefinition.set = noop;
? } else {
???sharedPropertyDefinition.get = userDef.get
????? ?shouldCache && userDef.cache !== false
??????? ?createComputedGetter(key)
??????? :userDef.get
????? : noop;
???sharedPropertyDefinition.set = userDef.set
????? ?userDef.set
????? : noop;
? }
? //...
? //這里進(jìn)行挾持
?Object.defineProperty(target, key, sharedPropertyDefinition);
}
createComputedGetter
createComputedGetter有兩個作用:
1、收集依賴
當(dāng)domWatcher獲取計算屬性的時候似芝,會觸發(fā)該方法那婉,然后computedWatcher會調(diào)用evaluate方法,最終會調(diào)用computedWatcher的get方法(下面會分析)党瓮,來完成依賴的收集
2详炬、關(guān)聯(lián)兩種watcher
通過第一步完成依賴收集后,computedWatcher能知道依賴的data屬性的改變寞奸,從而計算出最新的計算屬性值呛谜,那么它是怎么讓另外一個watcher,即domWatcher知道的呢枪萄,其實(shí)就是通過調(diào)用computedWatcher.depend方法把兩種watcher關(guān)聯(lián)起來的隐岛,這個方法會把Dep.target(就是domWatcher)放入到計算屬性依賴的所有data屬性的訂閱器列表中。
通過這兩個作用瓷翻,當(dāng)計算屬性依賴的data屬性有改變的時候聚凹,就會調(diào)用domWatcher的update方法割坠,它會獲取計算屬性的值,因此會觸發(fā)computedGetter方法妒牙,使得computedWatcher調(diào)用evaluate來計算最新的值彼哼,以便domWatcher更新dom。
function createComputedGetter (key) {
? returnfunction computedGetter () {
??? //取出initComputed創(chuàng)建的watcher
??? varwatcher = this._computedWatchers && this._computedWatchers[key];
??? if(watcher) {
????? //這個dirty的作用一個是避免重復(fù)計算湘今,比如我們的模板中兩次引用了這個計算屬性敢朱,那么我們只需要計算一次就夠了,一個是當(dāng)計算屬性依賴的data屬性改變象浑,會把這個計算屬性對應(yīng)的watcher給設(shè)置為dirty=true蔫饰,然后
????? if(watcher.dirty) {
??????? //這個會計算計算屬性的值,并且會調(diào)用watcher的get方法愉豺,完成依賴收集
???????watcher.evaluate();
????? }
????? //Dep.target指向的是模板中計算屬性對應(yīng)節(jié)點(diǎn)的domWatcher
????? //這個語句的意思就是把domWatcher放入到當(dāng)前computedWatcher的所有依賴中篓吁,這樣計算屬性依賴的data值一改,
????? //就會觸發(fā)domWatcher的update方法蚪拦,它會獲取計算屬性的值從而觸發(fā)這個computedGetter杖剪,然后computedWatcher會通過調(diào)用evaluate方法獲取最新值,
????? //然后交給domWatcher更新到dom
????? if(Dep.target) {
???????watcher.depend(); //關(guān)聯(lián)了兩種watcher
????? }
????? returnwatcher.value
??? }
? }
}
Computed Watcher
watcher是實(shí)現(xiàn)computed依賴的關(guān)鍵驰贷,它的第二個參數(shù)getter屬性即是計算屬性對應(yīng)的方法或get方法盛嘿。
var Watcher = function Watcher (
? vm,
? expOrFn,
? cb,
? options,
?isRenderWatcher
) {
? this.vm =vm;
? // ...
?// watcher的第二個參數(shù),即是我們計算屬性對應(yīng)的方法或get方法括袒,用于算出計算屬性的值
? if (typeofexpOrFn === 'function') {
???this.getter = expOrFn;
? } else {
???this.getter = parsePath(expOrFn);
??? if(!this.getter) {
?????this.getter = function () {};
??? }
? }
? //不會立即計算
? this.value= this.lazy
??? ?undefined
??? :this.get();
};
那么只要調(diào)用getter方法次兆,那么它就會觸發(fā)計算屬性所有依賴的data的get方法,我們看下get方法
?Object.defineProperty(obj, key, {
???enumerable: true,
???configurable: true,
??? get:function reactiveGetter () {
????? varvalue = getter ? getter.call(obj) : val;
????? //Dep.target保存的是當(dāng)前正在處理的Watcher锹锰,這里其實(shí)就是computedWatcher
????? if(Dep.target) {
? ??????//這句代碼其實(shí)就是將Dep.target放入到該data屬性的訂閱器列表當(dāng)中
???????dep.depend();
??????? //...
????? }
????? returnvalue
??? },
??? ...
})
如上所述芥炭,其實(shí)就是把Dep.taget(當(dāng)前的watcher)放入到該data屬性的訂閱器列表當(dāng)中,那么這個時候恃慧,Dep.target指向哪個Watcher呢园蝠?我們看下watcher的get方法
Watcher.prototype.get = function get () {
? //這句語句會把Dep.target執(zhí)行本watcher
?pushTarget(this);
? var value;
? var vm =this.vm;
? try {
??? //調(diào)用getter,會觸發(fā)上述講的get痢士,而get方法就會把Dep.target即本watcher放入到計算屬性所依賴的data屬性的訂閱器列表中
??? //這樣依賴的data屬性有改變就會調(diào)用該watcher的update方法
??? value =this.getter.call(vm, vm);
? } catch (e){
??? //...
? } finally {
??? //...
???popTarget(); //將Dep.target指回上次的watcher彪薛,這里就是計算屬性對應(yīng)的domWatcher
???this.cleanupDeps();
? }
? returnvalue
};
可以看到get方法開始運(yùn)行時,把Dep.target指向計算屬性對應(yīng)的computedWatcher怠蹂,然后調(diào)用watcher的getter方法善延,觸發(fā)這個計算屬性對應(yīng)的data屬性的get方法,就會把Dep.target指向的watcher加入到這些依賴的data的訂閱器列表當(dāng)中城侧,以此完成依賴收集易遣。
這樣當(dāng)我們的計算屬性依賴的data屬性改變的時候,就會調(diào)用訂閱器的notify方法赞庶,它會遍歷訂閱器列表训挡,其中就包含了該計算屬性對應(yīng)的computedWatcher和domWatcher,調(diào)用computedWatcher的update方法會把computedWatcher置為dirty歧强,調(diào)用domWathcer的update方法會觸發(fā)computedGetter澜薄,它會再次調(diào)用computedWatcher的evaluate計算出最新的值交給domWatcher去更新dom。
Watcher.prototype.update = function update () {
? if(this.lazy) {
??? //computed專屬的watcher走這里
???this.dirty = true;
? } else if(this.sync) {
??? // run方法會調(diào)用get方法摊册,get方法會重新計算計算屬性的值
??? //但這個時候get方法不會再收集依賴了肤京,vue會去重
???this.run();
? } else {
???queueWatcher(this);
? }
};
Watcher.prototype.run = function run () {
? if(this.active) {
??? //調(diào)用get方法,重新計算計算屬性的值
??? var value= this.get();
??? //值改變了茅特、Array或Object類型watch配置了deep屬性為true的
??? if (
????? value!== this.value ||
?????isObject(value) ||
?????this.deep
??? ) {
????? varoldValue = this.value;
?????this.value = value;
????? if(this.user) {
??????? //watch監(jiān)聽走此處
??????? try {
?????????this.cb.call(this.vm, value, oldValue);
??????? }catch (e) {
?????????handleError(e, this.vm, ("callback for watcher \"" +(this.expression) + "\""));
??????? }
????? } else{
??????? //data數(shù)據(jù)改變忘分,會觸發(fā)更新函數(shù)cb,從而更新dom
???????this.cb.call(this.vm, value, oldValue);
????? }
??? }
? }
};
總結(jié)
遍歷computed白修,為每個計算屬性新建一個computedWatcher對象妒峦,并將該computedWatcher的getter屬性賦值為計算屬性對應(yīng)的方法或者get方法。(大家應(yīng)該知道計算屬性不但可以是一個函數(shù)兵睛,還可以是一個包含get方法和set方法的對象吧)
使用Object.defineProperty挾持計算屬性的get方法肯骇,當(dāng)模版獲取計算屬性的值的時候,觸發(fā)get方法祖很,它會調(diào)用第一步創(chuàng)建的computedWatcher的evaluate方法笛丙,而evaluate方法就會調(diào)用watcher的get方法
computedWatcher的get方法會將Dep.target指向該computedWatcher,并調(diào)用getter方法假颇,getter方法會觸發(fā)該計算屬性依賴的所有data屬性的get方法胚鸯,從而把Dep.target指向的computedWatcher添加到data屬性的訂閱器列表中。同時笨鸡,computedWatcher保存了依賴的data屬性的訂閱器(deps屬性保存)姜钳。
同時調(diào)用computedWatcher的depend方法,它會把Dep.taget指向的domWatcher放入到計算屬性依賴的data屬性的訂閱器列表中镜豹,如此計算屬性依賴的data屬性改變了傲须,就會觸發(fā)domWatcher和computedWatcher的update方法,computedWatcher賦值獲取計算屬性的最新值趟脂,domWatcher負(fù)責(zé)更新dom泰讽。