2020-07-28

了解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泰讽。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市昔期,隨后出現(xiàn)的幾起案子已卸,更是在濱河造成了極大的恐慌,老刑警劉巖硼一,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件累澡,死亡現(xiàn)場離奇詭異,居然都是意外死亡般贼,警方通過查閱死者的電腦和手機(jī)愧哟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門奥吩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蕊梧,你說我怎么就攤上這事霞赫。” “怎么了肥矢?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵端衰,是天一觀的道長。 經(jīng)常有香客問我甘改,道長旅东,這世上最難降的妖魔是什么凰萨? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任瞬场,我火速辦了婚禮,結(jié)果婚禮上建瘫,老公的妹妹穿的比我還像新娘忘嫉。我一直安慰自己主守,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布榄融。 她就那樣靜靜地躺著参淫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪愧杯。 梳的紋絲不亂的頭發(fā)上涎才,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音力九,去河邊找鬼耍铜。 笑死,一個胖子當(dāng)著我的面吹牛跌前,可吹牛的內(nèi)容都是我干的棕兼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼抵乓,長吁一口氣:“原來是場噩夢啊……” “哼伴挚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起灾炭,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茎芋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜈出,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體田弥,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年铡原,在試婚紗的時候發(fā)現(xiàn)自己被綠了偷厦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片商叹。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖只泼,靈堂內(nèi)的尸體忽然破棺而出沈自,到底是詐尸還是另有隱情,我是刑警寧澤辜妓,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站忌怎,受9級特大地震影響籍滴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜榴啸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一孽惰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鸥印,春花似錦勋功、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至潜的,卻和暖如春骚揍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啰挪。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工信不, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亡呵。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓抽活,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锰什。 傳聞我的和親對象是個殘疾皇子下硕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355