[Vue.js進(jìn)階]從源碼角度剖析計(jì)算屬性的原理

image

前言

最近在學(xué)習(xí)Vue計(jì)算屬性的源碼竟宋,發(fā)現(xiàn)和普通的響應(yīng)式變量?jī)?nèi)部的實(shí)現(xiàn)還有一些不同聊品,特地寫了這篇博客鹿霸,記錄下自己學(xué)習(xí)的成果

文中的源碼截圖只保留核心邏輯 完整源碼地址

可能需要了解一些Vue響應(yīng)式的原理

Vue 版本:2.5.21

計(jì)算屬性的概念

一般的計(jì)算屬性值是一個(gè)函數(shù)宽闲,這個(gè)函數(shù)會(huì)返回一個(gè)值拉队,并且其函數(shù)內(nèi)部還可能會(huì)依賴別的變量

一般的計(jì)算屬性看起來和 method 很像弊知,值都是一個(gè)函數(shù),那他們有什么區(qū)別呢

計(jì)算屬性和method的區(qū)別

將一個(gè)計(jì)算屬性的函數(shù)放在 methods 中同樣也能達(dá)到相同的效果

但是如果視圖中依賴了這個(gè) method 的返回值,并且當(dāng)另外一個(gè)其他的響應(yīng)式變量的修改導(dǎo)致視圖被更新時(shí)粱快, method 會(huì)重新執(zhí)行一遍秩彤,即使這次的更新和 method 中依賴的變量沒有任何關(guān)系!

而對(duì)于計(jì)算屬性,只有當(dāng)計(jì)算屬性依賴的變量改變后事哭,才會(huì)重新執(zhí)行一遍函數(shù),并重新返回一個(gè)新的值

點(diǎn)我看示例

當(dāng) otherProp 變量被修改導(dǎo)致更新視圖的時(shí)候漫雷,methodFullName 每次都會(huì)執(zhí)行,而 computedFullName 只會(huì)在頁面初始化的時(shí)候執(zhí)行一次,Vue 推薦開發(fā)者將 method 和 compute 屬性區(qū)分開來鳍咱,能夠有效的提升性能降盹,避免執(zhí)行一些不必要的代碼

回顧過計(jì)算屬性的概念,接下來我們深入源碼谤辜,來了解一下計(jì)算屬性到底是怎么實(shí)現(xiàn)的澎现,為什么只有計(jì)算屬性的依賴項(xiàng)被改變了才會(huì)重新求值

從例子入手

這里我寫了一個(gè)簡(jiǎn)單的例子仅胞,幫助各位理解計(jì)算屬性的運(yùn)行原理,下面的解析會(huì)圍繞這個(gè)例子進(jìn)行解析

const App = {
    template: `
     <div id="app">
        <div>{{fullName}}</div>
        <button @click="handleChangeName">修改lastName</button>
  </div>
    `,
    data() {
        return {
            firstName: '尤',
            lastName: '雨溪',
        }
    },
    methods: {
        handleChangeName() {
            this.lastName = '大大'
        }
    },
    computed: {
        fullName() {
            return this.firstName + this.lastName
        }
    }
}

new Vue({
    el: '#app',
    components: {
        App
    },
    template: `
    <App></App>
    `
}).$mount()

fullName 依賴了 firstName 和 lastName剑辫,點(diǎn)擊 button 會(huì)修改 lastName, 同時(shí) fullName 會(huì)重新計(jì)算渠欺,視圖變成"尤大大"

深入計(jì)算屬性的源碼

在日常開發(fā)中書寫的計(jì)算屬性,實(shí)際上內(nèi)部都會(huì)保存一個(gè) watcher , watcher 的作用是觀察某個(gè)響應(yīng)式對(duì)象的改變?nèi)缓髨?zhí)行相應(yīng)的回調(diào),由 Watcher 類實(shí)例化而成, Vue 中定義了3個(gè) watcher

  • render watcher: 模板依賴并且需要顯示在視圖上變量妹蔽,其內(nèi)部保存了一個(gè) render watcher
  • computed watcher: 計(jì)算屬性內(nèi)部保存了一個(gè) computed watcher
  • user watcher: 使用 watch 屬性觀察的變量?jī)?nèi)部保存了一個(gè) user watcher

理解這3個(gè) watcher 各自的作用非常重要,文本會(huì)著重圍繞 computed watcher 展開

一個(gè)計(jì)算屬性的初始化分為2部分

  1. 生成 computed watcher
  2. 定義計(jì)算屬性的 getter 函數(shù)

生成computed watcher

在初始化當(dāng)前組件時(shí),會(huì)執(zhí)行 initComputed 方法初始化計(jì)算屬性,會(huì)給每個(gè)計(jì)算屬性實(shí)例化一個(gè) computed watcher

在實(shí)例化 watcher 時(shí)傳入不同的配置項(xiàng)就可以生成不同的 watcher 實(shí)例 挠将,當(dāng)傳入{ lazy: true } 時(shí),實(shí)例化的 watcher 即為 computed watcher

定義計(jì)算屬性的 getter 函數(shù)

在創(chuàng)建完 computed watcher 后胳岂,接著會(huì)定義計(jì)算屬性的 getter 函數(shù),我們?cè)趫?zhí)行計(jì)算屬性的函數(shù)時(shí)舔稀,實(shí)際上執(zhí)行的是 computedGetter 這個(gè)函數(shù)

computedGetter代碼很少乳丰,但是卻是計(jì)算屬性的核心,我們一步步來分析

dirty屬性

通過 key 獲取到第一步中定義的 computed watcher内贮,隨后會(huì)判斷這個(gè) computed watcher 的 dirty 屬性是否為 true产园,當(dāng) dirty 為 true 時(shí), 會(huì)執(zhí)行 evaluate 方法, evaluate 內(nèi)部會(huì)執(zhí)行計(jì)算屬性的函數(shù),并且將 watcher 的 value 屬性等于函數(shù)執(zhí)行后的結(jié)果也就是最終計(jì)算出來的值,具體我們放到后面講

dirty 屬性是一個(gè)用來檢測(cè)當(dāng)前的 computed watcher是否需要重新執(zhí)行的一個(gè)標(biāo)志,這也是計(jì)算屬性和普通method的區(qū)別夜郁,結(jié)合上圖可以發(fā)現(xiàn)什燕,當(dāng) dirty 為 false 時(shí)昏鹃,就不會(huì)去執(zhí)行 evaluate 也就不會(huì)執(zhí)行計(jì)算屬性的函數(shù)献丑,可以看到最后直接就返回了 watcher.value 表示這次不會(huì)進(jìn)行計(jì)算赏迟,會(huì)直接使用以前的 value 的值

當(dāng)?shù)谝淮斡|發(fā)computedGetter 時(shí)遣疯,dirty 屬性的默認(rèn)值是 true 魂迄,那是因?yàn)樵诔跏蓟?computed watcher時(shí)候 Vue 將 dirty 屬性等于了 lazy 屬性梨与,即為 true

知道 dirty 的默認(rèn)值為 true鸣驱,什么時(shí)候?yàn)?false 呢吏口?我們接著來看 evalutate 具體的實(shí)現(xiàn)

evaluate方法

evaluate 方法是 computed watcher 獨(dú)有的方法统台,代碼也只有短短2行

get方法

第一行執(zhí)行了 get 方法, get 方法是所有 watcher 用來求值的通用方法

get 主要就做了這三步

  1. 將當(dāng)前這個(gè) watcher 作為棧頂?shù)?watcher 推入棧
  2. 執(zhí)行g(shù)etter方法
  3. 將這個(gè) watcher 彈出棧

我們知道 Vue.js 會(huì)維護(hù)一個(gè)全局的棧用來存放 watcher 雕擂,每當(dāng)觸發(fā)響應(yīng)式變量?jī)?nèi)部的 getter 時(shí),就會(huì)收集這個(gè)全局的棧的頂部的 watcher(即Dep.target)饺谬,將這個(gè) watcher 存入響應(yīng)式變量?jī)?nèi)部保存的dep中

第一步通過 pushTarget 將當(dāng)前的 computed watcher 推入全局的棧中捂刺,此時(shí)Dep.target就指向這個(gè)棧頂?shù)?computed watcher

第二步執(zhí)行 getter 方法, 對(duì)于 computed watcher募寨,getter 方法就是計(jì)算屬性的函數(shù)族展,執(zhí)行函數(shù)將返回的值賦值給 value 屬性,而當(dāng)計(jì)算屬性的函數(shù)執(zhí)行時(shí)拔鹰,如果內(nèi)部含有其他的響應(yīng)式變量仪缸,會(huì)觸發(fā)它們內(nèi)部的 getter ,將第一步放入作為當(dāng)前棧頂?shù)?computed watcher 存入響應(yīng)式變量?jī)?nèi)部的dep對(duì)象中

注意響應(yīng)式變量?jī)?nèi)部的 getter 和 getter 方法不是一個(gè)函數(shù)

第三步將這個(gè) computed watcher 彈出全局的棧

之所以將這個(gè) computed watcher 推入又彈出列肢,是為了讓第二步執(zhí)行內(nèi)部的 getter 時(shí)恰画,能讓計(jì)算屬性函數(shù)內(nèi)部依賴的響應(yīng)式變量收集到這個(gè) computed watcher

對(duì)于計(jì)算屬性來說宾茂,get 方法的作用就是進(jìn)行求值

??

在例子中,因?yàn)橐晥D需要依賴 fullName 這個(gè)響應(yīng)式變量拴还,所以會(huì)觸發(fā)它的內(nèi)部的 getter跨晴,同時(shí)它又是一個(gè)計(jì)算屬性,即會(huì)執(zhí)行 computedGetter 片林,此時(shí) dirty 屬性為默認(rèn)值 true端盆,執(zhí)行 evaluate => get => pushTarget

pushTarget 中由于是 computed watcher 執(zhí)行的 get 方法,所以 this 指向 這個(gè) computed watcher 將它推入全局棧中费封,隨后執(zhí)行計(jì)算屬性的函數(shù)

可以看到計(jì)算屬性 fullName 的函數(shù)依賴了 firstName 和 lastName這2個(gè)響應(yīng)式變量焕妙,Vue在內(nèi)部通過閉包的形式各自保存了一個(gè) dep 屬性,這個(gè) dep 屬性會(huì)收集當(dāng)前棧頂?shù)?watcher弓摘,即收集了 fullName 這個(gè)計(jì)算屬性的 computed watcher焚鹊,所以當(dāng)計(jì)算屬性的函數(shù)執(zhí)行完畢后, firstName 和 lastName 內(nèi)部的dep屬性都會(huì)保存一個(gè) computed watcher

收集完畢后韧献,將 computed watcher 彈出末患,讓棧恢復(fù)到之前的狀態(tài)

將dirty設(shè)為false

執(zhí)行完 get 方法势决,即一旦計(jì)算屬性執(zhí)行過一次求值阻塑,就會(huì)將 dirty 屬性設(shè)為 false,如果下次又觸發(fā)了這個(gè)計(jì)算屬性的 getter 會(huì)直接跳過求值階段

depend方法

計(jì)算屬性第二個(gè)特點(diǎn)就是它的 depend 方法果复,這個(gè)方法是 computed watcher 獨(dú)有的

當(dāng) Dep.target 存在陈莽,即全局的棧中仍有其他的 watcher。如果視圖中依賴了當(dāng)前的計(jì)算屬性虽抄,那當(dāng)前棧頂?shù)?watcher 就是 render watcher走搁,亦或者說是另外一個(gè)計(jì)算屬性內(nèi)部依賴了當(dāng)前的計(jì)算屬性,那棧頂?shù)?watcher 可能是另一個(gè) computed watcher迈窟,不管怎么說只要有地方使用到這個(gè)計(jì)算屬性私植,就會(huì)進(jìn)入 depend 方法

watcher 的 depend 方法:

depend 方法也非常簡(jiǎn)短,它會(huì)遍歷當(dāng)前 computed watcher 的deps屬性车酣,依次執(zhí)行 dep 的 depend 方法

deps又是什么呢曲稼,前面說到 dep 是每個(gè)響應(yīng)式變量?jī)?nèi)部保存的一個(gè)對(duì)象,deps 可想而知就是所有響應(yīng)式變量?jī)?nèi)部 dep 的集合湖员,那具體是哪些響應(yīng)式變量呢贫悄?其實(shí)了解過響應(yīng)式原理的朋友應(yīng)該知道,這個(gè) deps 實(shí)際上保存了所有收集了當(dāng)前 watcher 的響應(yīng)式變量?jī)?nèi)部的 dep 對(duì)象

這是一個(gè)互相依賴的關(guān)系娘摔,每個(gè)響應(yīng)式變量?jī)?nèi)部的 dep 會(huì)保存所有的 watchers窄坦,而每個(gè) watcher 的 deps 屬性會(huì)保存所有收集到這個(gè) watcher 的響應(yīng)式變量?jī)?nèi)部的 dep 對(duì)象

(Vue之所以在 watcher 中保存 deps,一方面需要讓計(jì)算屬性能夠收集依賴,另一方面也可以在注銷這個(gè) watcher 時(shí)能知道哪些 dep 依賴了這個(gè) watcher鸭津,直接調(diào)用 dep 里對(duì)應(yīng)的注銷方法即可)

接著就會(huì)遍歷每個(gè) dep 執(zhí)行 dep 里的 depend 方法:

這個(gè)方法的作用是給當(dāng)前的響應(yīng)式變量?jī)?nèi)部的 dep 收集當(dāng)前棧頂?shù)?watcher 彤侍,在例子中,因?yàn)橐晥D中依賴了 fullName逆趋,所以當(dāng) get 方法執(zhí)行結(jié)束 computed watcher 被彈出后盏阶,棧頂?shù)?watcher 就變?yōu)樵瓉淼?render watcher

computed watcher 中的 deps 屬性保存了2個(gè) dep,一個(gè)是 firstName 的 dep闻书,另一個(gè)是 lastName 的 dep般哼,因?yàn)檫@2個(gè)變量在執(zhí)行 get 方法第二步的時(shí)候收集了到這個(gè) computed watcher

這時(shí)候執(zhí)行 dep.depend 時(shí)會(huì)再次給這2個(gè)響應(yīng)式變量收集棧頂?shù)?watcher,即 render watcher惠窄,最終這2個(gè)變量?jī)?nèi)部的 dep 都保存了2個(gè)變量,一個(gè) computed watcher漾橙,一個(gè) render watcher

最終返回 watcher.value 作為顯示在視圖中的值

修改計(jì)算屬性的依賴項(xiàng)

前面說過杆融,只有當(dāng)計(jì)算屬性的依賴項(xiàng)被修改時(shí),計(jì)算屬性才會(huì)重新進(jìn)行計(jì)算霜运,生成一個(gè)新的值脾歇,而視圖中其他變量被修改導(dǎo)致視圖更新時(shí),計(jì)算屬性不會(huì)重新計(jì)算淘捡,這是怎么做到的呢藕各?

內(nèi)部依賴項(xiàng)被修改,重新執(zhí)行計(jì)算

當(dāng)計(jì)算屬性的依賴項(xiàng)焦除,即 firstName 和 lastName 被修改時(shí)激况,會(huì)觸發(fā)內(nèi)部的 setter,Vue 會(huì)遍歷響應(yīng)式變量?jī)?nèi)部的 dep 保存的 watcher膘魄,最終會(huì)執(zhí)行每個(gè) watcher 的 update 方法

可以看到 update 方法有3種情況:

  • lazy:只存在于 computed watcher
  • sync:只存在于 user watcher乌逐,當(dāng) user watcher 設(shè)置了 sync 會(huì)同步調(diào)用 watcher 不會(huì)延遲到 nextTick 后,基本不會(huì)用
  • 默認(rèn)情況:一般的 user watcher 和 render watcher 都會(huì)執(zhí)行 queueWatcher 將所有的 watcher 放到 nextTick 后執(zhí)行

通過前面的 evaluatedepend 方法创葡,firstName 和 lastName 內(nèi)部的 dep 中都會(huì)保存2個(gè) watcher浙踢,一個(gè) computed watcher,一個(gè) render watcher灿渴,所以會(huì)優(yōu)先執(zhí)行 computed watcher 的 update 方法

同時(shí)前面說到在 computed watcher 求值結(jié)束后洛波,會(huì)將 dirty 置為 false,之后再獲取計(jì)算屬性的值時(shí)都會(huì)跳過 evaluate 方法直接返回以前的 value骚露,而執(zhí)行 computed watcher 的 update 方法會(huì)將 dirty 再次變成 true蹬挤,整個(gè)computed watcher 只做這一件事,即取消 computed watcher 使用以前的緩存的標(biāo)志

真正的求值操作是在 render watcher 中進(jìn)行的荸百,當(dāng)遍歷到第二個(gè) render watcher 時(shí)闻伶,由于視圖依賴了 fullName,會(huì)觸發(fā)計(jì)算屬性的 getter够话,再次執(zhí)行之前的 computedGetter蓝翰,此時(shí)由于上一步將 dirty 變成 true了光绕,所以就會(huì)進(jìn)入 evalutate 重新計(jì)算,此時(shí) fullName 就拿到了最新的值"尤大大"了

其他變量的修改不會(huì)影響到計(jì)算屬性

回到一開始計(jì)算屬性和 method 區(qū)別的那個(gè)例子畜份,因?yàn)橐晥D依賴了 otherProp 所以當(dāng)這個(gè)響應(yīng)式變量被修改時(shí)诞帐,會(huì)觸發(fā)它內(nèi)部 dep 保存的 render watcher 的 update 方法,它會(huì)重新收集依賴更新視圖

當(dāng)收集 methodFullName 時(shí)會(huì)執(zhí)行相應(yīng)的方法爆雹,所以會(huì)打印 "method"停蕉,當(dāng)收集 computedFullName 時(shí),會(huì)執(zhí)行 computedGetter钙态,但是此時(shí)沒有觸發(fā)過 computed watcher 的 update慧起,所以 dirty 屬性為 false,就會(huì)跳過evaluate 方法直接返回緩存的結(jié)果册倒,因此不會(huì)打印 "computed"

總結(jié)

只有當(dāng)計(jì)算屬性依賴的響應(yīng)式變量被修改時(shí)蚓挤,才會(huì)使得計(jì)算屬性被重新計(jì)算,否則使用的都是第一次的緩存值驻子,原因是因?yàn)橛?jì)算屬性內(nèi)部的 computed watcher 的 dirty 屬性如果為 false 就會(huì)始終使用以前緩存的值

而計(jì)算屬性依賴的響應(yīng)式變量?jī)?nèi)部的 dep 都會(huì)保存這個(gè) computed watcher灿意,當(dāng)它們被修改時(shí),會(huì)觸發(fā) computed watcher 的 update 方法崇呵,將 dirty 標(biāo)志位置為 true缤剧,這樣下次有別的 watcher 依賴這個(gè)計(jì)算屬性時(shí)就會(huì)觸發(fā)重新計(jì)算

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市域慷,隨后出現(xiàn)的幾起案子荒辕,更是在濱河造成了極大的恐慌,老刑警劉巖芒粹,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兄纺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡化漆,警方通過查閱死者的電腦和手機(jī)估脆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來座云,“玉大人疙赠,你說我怎么就攤上這事‰希” “怎么了圃阳?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)璧帝。 經(jīng)常有香客問我捍岳,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任锣夹,我火速辦了婚禮页徐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘银萍。我一直安慰自己变勇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布贴唇。 她就那樣靜靜地躺著搀绣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戳气。 梳的紋絲不亂的頭發(fā)上链患,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音瓶您,去河邊找鬼锣险。 笑死,一個(gè)胖子當(dāng)著我的面吹牛览闰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巷折,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼压鉴,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了锻拘?” 一聲冷哼從身側(cè)響起油吭,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎署拟,沒想到半個(gè)月后婉宰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡推穷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年心包,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片馒铃。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蟹腾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出区宇,到底是詐尸還是另有隱情娃殖,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布议谷,位于F島的核電站炉爆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜芬首,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一赴捞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧衩辟,春花似錦螟炫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至封寞,卻和暖如春然评,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狈究。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工碗淌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抖锥。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓亿眠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親磅废。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纳像,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354