<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Vue 的計(jì)算屬性如何實(shí)現(xiàn)緩存?(原理深入揭秘)

          共 7094字,需瀏覽 15分鐘

           ·

          2021-02-09 18:43

          前言

          很多人提起 Vue 中的 computed,第一反應(yīng)就是計(jì)算屬性會(huì)緩存,那么它到底是怎么緩存的呢?緩存的到底是什么,什么時(shí)候緩存會(huì)失效,相信還是有很多人對(duì)此很模糊。

          本文以 Vue 2.6.11 版本為基礎(chǔ),就深入原理,帶你來(lái)看看所謂的緩存到底是什么樣的。

          注意

          本文假定你對(duì) Vue 響應(yīng)式原理已經(jīng)有了基礎(chǔ)的了解,如果對(duì)于 WatcherDep和什么是 渲染watcher 等概念還不是很熟悉的話,可以先找一些基礎(chǔ)的響應(yīng)式原理的文章或者教程看一下。視頻教程的話推薦黃軼老師的,如果想要看簡(jiǎn)化實(shí)現(xiàn),也可以先看我寫(xiě)的文章:

          手把手帶你實(shí)現(xiàn)一個(gè)最精簡(jiǎn)的響應(yīng)式系統(tǒng)來(lái)學(xué)習(xí)Vue的data、computed、watch源碼[1]

          注意,這篇文章里我也寫(xiě)了 computed 的原理,但是這篇文章里的 computed 是基于 Vue 2.5 版本的,和當(dāng)前 2.6 版本的變化還是非常大的,可以僅做參考。

          示例

          按照我的文章慣例,還是以一個(gè)最簡(jiǎn)的示例來(lái)演示。

          <div?id="app">
          ??<span?@click="change">{{sum}}span>
          div>
          <script?src="./vue2.6.js">script>
          <script>
          ??new?Vue({
          ????el:?"#app",
          ????data()?{
          ??????return?{
          ????????count:?1,
          ??????}
          ????},
          ????methods:?{
          ??????change()?{
          ????????this.count?=?2
          ??????},
          ????},
          ????computed:?{
          ??????sum()?{
          ????????return?this.count?+?1
          ??????},
          ????},
          ??})
          script>

          這個(gè)例子很簡(jiǎn)單,剛開(kāi)始頁(yè)面上顯示數(shù)字 2,點(diǎn)擊數(shù)字后變成 3

          解析

          回顧 watcher 的流程

          進(jìn)入正題,Vue 初次運(yùn)行時(shí)會(huì)對(duì) computed 屬性做一些初始化處理,首先我們回顧一下 watcher 的概念,它的核心概念是 get 求值,和 update 更新。

          1. 在求值的時(shí)候,它會(huì)先把自身也就是 watcher 本身賦值給 Dep.target 這個(gè)全局變量。

          2. 然后求值的過(guò)程中,會(huì)讀取到響應(yīng)式屬性,那么響應(yīng)式屬性的 dep 就會(huì)收集到這個(gè) watcher 作為依賴。

          3. 下次響應(yīng)式屬性更新了,就會(huì)從 dep 中找出它收集到的 watcher,觸發(fā) watcher.update() 去更新。

          所以最關(guān)鍵的就在于,這個(gè) get 到底用來(lái)做什么,這個(gè) update 會(huì)觸發(fā)什么樣的更新。

          在基本的響應(yīng)式更新視圖的流程中,以上概念的 get 求值就是指 Vue 的組件重新渲染的函數(shù),而 update 的時(shí)候,其實(shí)就是重新調(diào)用組件的渲染函數(shù)去更新視圖。

          而 Vue 中很巧妙的一點(diǎn),就是這套流程也同樣適用于 computed 的更新。

          初始化 computed

          這里先提前劇透一下,Vue 會(huì)對(duì) options 中的每個(gè) computed 屬性也用 watcher 去包裝起來(lái),它的 get 函數(shù)顯然就是要執(zhí)行用戶定義的求值函數(shù),而 update 則是一個(gè)比較復(fù)雜的流程,接下來(lái)我會(huì)詳細(xì)講解。

          首先在組件初始化的時(shí)候,會(huì)進(jìn)入到初始化 computed 的函數(shù)

          if?(opts.computed)?{?initComputed(vm,?opts.computed);?}

          進(jìn)入 initComputed 看看

          var?watchers?=?vm._computedWatchers?=?Object.create(null);

          //?依次為每個(gè)?computed?屬性定義
          for?(const?key?in?computed)?{
          ??const?userDef?=?computed[key]
          ??watchers[key]?=?new?Watcher(
          ??????vm,?//?實(shí)例
          ??????getter,?//?用戶傳入的求值函數(shù)?sum
          ??????noop,?//?回調(diào)函數(shù)?可以先忽視
          ??????{?lazy:?true?}?//?聲明?lazy?屬性?標(biāo)記?computed?watcher
          ??)

          ??//?用戶在調(diào)用?this.sum?的時(shí)候,會(huì)發(fā)生的事情
          ??defineComputed(vm,?key,?userDef)
          }

          首先定義了一個(gè)空的對(duì)象,用來(lái)存放所有計(jì)算屬性相關(guān)的 watcher,后文我們會(huì)把它叫做 計(jì)算watcher

          然后循環(huán)為每個(gè) computed 屬性生成了一個(gè) 計(jì)算watcher

          它的形態(tài)保留關(guān)鍵屬性簡(jiǎn)化后是這樣的:

          {
          ????deps:?[],
          ????dirty:?true,
          ????getter:???sum(),
          ????lazy:?true,
          ????value:?undefined
          }

          可以看到它的 value 剛開(kāi)始是 undefined,lazy 是 true,說(shuō)明它的值是惰性計(jì)算的,只有到真正在模板里去讀取它的值后才會(huì)計(jì)算。

          這個(gè) dirty 屬性其實(shí)是緩存的關(guān)鍵,先記住它。

          接下來(lái)看看比較關(guān)鍵的 defineComputed,它決定了用戶在讀取 this.sum 這個(gè)計(jì)算屬性的值后會(huì)發(fā)生什么,繼續(xù)簡(jiǎn)化,排除掉一些不影響流程的邏輯。

          Object.defineProperty(target,?key,?{?
          ????get()?{
          ????????//?從剛剛說(shuō)過(guò)的組件實(shí)例上拿到?computed?watcher
          ????????const?watcher?=?this._computedWatchers?&&?this._computedWatchers[key]
          ????????if?(watcher)?{
          ??????????//???注意!這里只有dirty了才會(huì)重新求值
          ??????????if?(watcher.dirty)?{
          ????????????//?這里會(huì)求值?調(diào)用?get
          ????????????watcher.evaluate()
          ??????????}
          ??????????//???這里也是個(gè)關(guān)鍵?等會(huì)細(xì)講
          ??????????if?(Dep.target)?{
          ????????????watcher.depend()
          ??????????}
          ??????????//?最后返回計(jì)算出來(lái)的值
          ??????????return?watcher.value
          ????????}
          ????}
          })

          這個(gè)函數(shù)需要仔細(xì)看看,它做了好幾件事,我們以初始化的流程來(lái)講解它:

          首先 dirty 這個(gè)概念代表臟數(shù)據(jù),說(shuō)明這個(gè)數(shù)據(jù)需要重新調(diào)用用戶傳入的 sum 函數(shù)來(lái)求值了。我們暫且不管更新時(shí)候的邏輯,第一次在模板中讀取到 ?{{sum}} 的時(shí)候它一定是 true,所以初始化就會(huì)經(jīng)歷一次求值。

          evaluate?()?{
          ??//?調(diào)用?get?函數(shù)求值
          ??this.value?=?this.get()
          ??//?把?dirty?標(biāo)記為?false
          ??this.dirty?=?false
          }

          這個(gè)函數(shù)其實(shí)很清晰,它先求值,然后把 dirty 置為 false。

          再回頭看看我們剛剛那段 Object.defineProperty 的邏輯,

          下次沒(méi)有特殊情況再讀取到 sum 的時(shí)候,發(fā)現(xiàn) dirty是false了,是不是直接就返回 watcher.value 這個(gè)值就可以了,這其實(shí)就是計(jì)算屬性緩存的概念。

          更新

          初始化的流程講完了,相信大家也對(duì) dirty緩存 有了個(gè)大概的概念(如果沒(méi)有,再仔細(xì)回頭看一看)。

          接下來(lái)就講更新的流程,細(xì)化到本文的例子中,也就是 count 的更新到底是怎么觸發(fā) sum 在頁(yè)面上的變更。

          首先回到剛剛提到的 evalute 函數(shù)里,也就是讀取 sum 時(shí)發(fā)現(xiàn)是臟數(shù)據(jù)的時(shí)候做的求值操作。

          evaluate?()?{
          ??//?調(diào)用?get?函數(shù)求值
          ??this.value?=?this.get()
          ??//?把?dirty?標(biāo)記為?false
          ??this.dirty?=?false
          }

          Dep.target 變更為 渲染watcher

          這里進(jìn)入 this.get(),首先要明確一點(diǎn),在模板中讀取 {{ sum }} 變量的時(shí)候,全局的 Dep.target 應(yīng)該是 渲染watcher,這里不理解的話可以到我最開(kāi)始提到的文章里去理解下。

          全局的 Dep.target 狀態(tài)是用一個(gè)棧 targetStack 來(lái)保存,便于前進(jìn)和回退 Dep.target,至于什么時(shí)候會(huì)回退,接下來(lái)的函數(shù)里就可以看到。

          此時(shí)的 Dep.target 是 渲染watcher,targetStack 是 [ 渲染watcher ] 。
          get?()?{
          ??pushTarget(this)
          ??let?value
          ??const?vm?=?this.vm
          ??try?{
          ????value?=?this.getter.call(vm,?vm)
          ??}?finally?{
          ????popTarget()
          ??}
          ??return?value
          }

          首先剛進(jìn)去就 pushTarget,也就是把 計(jì)算watcher 自身置為 Dep.target,等待收集依賴。

          執(zhí)行完 pushTarget(this) 后,

          Dep.target 變更為 計(jì)算watcher

          此時(shí)的 Dep.target 是 計(jì)算watcher,targetStack 是 [ 渲染watcher,計(jì)算watcher ] 。

          getter 函數(shù),上一章的 watcher 形態(tài)里已經(jīng)說(shuō)明了,其實(shí)就是用戶傳入的 sum 函數(shù)。

          sum()?{
          ????return?this.count?+?1
          }

          這里在執(zhí)行的時(shí)候,讀取到了 this.count,注意它是一個(gè)響應(yīng)式的屬性,所以冥冥之中它們開(kāi)始建立了千絲萬(wàn)縷的聯(lián)系……

          這里會(huì)觸發(fā) countget 劫持,簡(jiǎn)化一下

          //?在閉包中,會(huì)保留對(duì)于?count?這個(gè)?key?所定義的?dep
          const?dep?=?new?Dep()

          //?閉包中也會(huì)保留上一次?set?函數(shù)所設(shè)置的?val
          let?val

          Object.defineProperty(obj,?key,?{
          ??get:?function?reactiveGetter?()?{
          ????const?value?=?val
          ????//?Dep.target?此時(shí)就是計(jì)算watcher
          ????if?(Dep.target)?{
          ??????//?收集依賴
          ??????dep.depend()
          ????}
          ????return?value
          ??},
          })

          那么可以看出,count 會(huì)收集 計(jì)算watcher 作為依賴,具體怎么收集呢

          //?dep.depend()
          depend?()?{
          ??if?(Dep.target)?{
          ????Dep.target.addDep(this)
          ??}
          }

          其實(shí)這里是調(diào)用 Dep.target.addDep(this) 去收集,又繞回到 計(jì)算watcheraddDep 函數(shù)上去了,這其實(shí)主要是 Vue 內(nèi)部做了一些去重的優(yōu)化。

          //?watcher?的?addDep函數(shù)
          addDep?(dep:?Dep)?{
          ??//?這里做了一系列的去重操作?簡(jiǎn)化掉?
          ??
          ??//?這里會(huì)把?count?的?dep?也存在自身的?deps?上
          ??this.deps.push(dep)
          ??//?又帶著?watcher?自身作為參數(shù)
          ??//?回到?dep?的?addSub?函數(shù)了
          ??dep.addSub(this)
          }

          又回到 dep 上去了。

          class?Dep?{
          ??subs?=?[]

          ??addSub?(sub:?Watcher)?{
          ????this.subs.push(sub)
          ??}
          }

          這樣就保存了 計(jì)算watcher 作為 count 的 dep 里的依賴了。

          經(jīng)歷了這樣的一個(gè)收集的流程后,此時(shí)的一些狀態(tài):

          sum 的計(jì)算watcher

          {
          ????deps:?[?count的dep?],
          ????dirty:?false,?//?求值完了?所以是false
          ????value:?2,?//?1?+?1?=?2
          ????getter:???sum(),
          ????lazy:?true
          }

          count的dep:

          {
          ????subs:?[?sum的計(jì)算watcher?]
          }

          可以看出,計(jì)算屬性的 watcher 和它所依賴的響應(yīng)式值的 dep,它們之間互相保留了彼此,相依為命。

          此時(shí)求值結(jié)束,回到 計(jì)算watchergetter 函數(shù):

          get?()?{
          ??pushTarget(this)
          ??let?value
          ??const?vm?=?this.vm
          ??try?{
          ????value?=?this.getter.call(vm,?vm)
          ??}?finally?{
          ????//?此時(shí)執(zhí)行到這里了
          ????popTarget()
          ??}
          ??return?value
          }

          執(zhí)行到了 popTarget計(jì)算watcher 出棧。

          Dep.target 變更為 渲染watcher

          此時(shí)的 Dep.target 是 渲染watcher,targetStack 是 [ 渲染watcher ] 。

          然后函數(shù)執(zhí)行完畢,返回了 2 這個(gè) value,此時(shí)對(duì)于 sum 屬性的 get 訪問(wèn)還沒(méi)結(jié)束。

          Object.defineProperty(vm,?'sum',?{?
          ????get()?{
          ??????????//?此時(shí)函數(shù)執(zhí)行到了這里
          ??????????if?(Dep.target)?{
          ????????????watcher.depend()
          ??????????}
          ??????????return?watcher.value
          ????????}
          ????}
          })

          此時(shí)的 Dep.target 當(dāng)然是有值的,就是 渲染watcher,所以進(jìn)入了 watcher.depend() 的邏輯,這一步相當(dāng)關(guān)鍵

          //?watcher.depend
          depend?()?{
          ??let?i?=?this.deps.length
          ??while?(i--)?{
          ????this.deps[i].depend()
          ??}
          }

          還記得剛剛的 計(jì)算watcher 的形態(tài)嗎?它的 deps 里保存了 count 的 dep。

          也就是說(shuō),又會(huì)調(diào)用 count 上的 dep.depend()

          class?Dep?{
          ??subs?=?[]
          ??
          ??depend?()?{
          ????if?(Dep.target)?{
          ??????Dep.target.addDep(this)
          ????}
          ??}
          }

          這次的 Dep.target 已經(jīng)是 渲染watcher 了,所以這個(gè) count 的 dep 又會(huì)把 渲染watcher 存放進(jìn)自身的 subs 中。

          count的dep:

          {
          ????subs:?[?sum的計(jì)算watcher,渲染watcher?]
          }

          那么來(lái)到了此題的重點(diǎn),這時(shí)候 count 更新了,是如何去觸發(fā)視圖更新的呢?

          再回到 count 的響應(yīng)式劫持邏輯里去:

          //?在閉包中,會(huì)保留對(duì)于?count?這個(gè)?key?所定義的?dep
          const?dep?=?new?Dep()

          //?閉包中也會(huì)保留上一次?set?函數(shù)所設(shè)置的?val
          let?val

          Object.defineProperty(obj,?key,?{
          ??set:?function?reactiveSetter?(newVal)?{
          ??????val?=?newVal
          ??????//?觸發(fā)?count?的?dep?的?notify
          ??????dep.notify()
          ????}
          ??})
          })

          好,這里觸發(fā)了我們剛剛精心準(zhǔn)備的 count 的 dep 的 notify 函數(shù),感覺(jué)離成功越來(lái)越近了。

          class?Dep?{
          ??subs?=?[]
          ??
          ??notify?()?{
          ????for?(let?i?=?0,?l?=?subs.length;?i???????subs[i].update()
          ????}
          ??}
          }

          這里的邏輯就很簡(jiǎn)單了,把 subs 里保存的 watcher 依次去調(diào)用它們的 update 方法,也就是

          1. 調(diào)用 計(jì)算watcher 的 update
          2. 調(diào)用 渲染watcher 的 update

          拆解來(lái)看。

          計(jì)算watcher 的 update

          update?()?{
          ??if?(this.lazy)?{
          ????this.dirty?=?true
          ??}
          }

          wtf,就這么一句話…… 沒(méi)錯(cuò),就僅僅是把 計(jì)算watcherdirty 屬性置為 true,靜靜的等待下次讀取即可。

          渲染watcher 的 update

          這里其實(shí)就是調(diào)用 vm._update(vm._render()) 這個(gè)函數(shù),重新根據(jù) render 函數(shù)生成的 vnode 去渲染視圖了。

          而在 render 的過(guò)程中,一定會(huì)訪問(wèn)到 sum 這個(gè)值,那么又回回到 sum 定義的 get 上:

          Object.defineProperty(target,?key,?{?
          ????get()?{
          ????????const?watcher?=?this._computedWatchers?&&?this._computedWatchers[key]
          ????????if?(watcher)?{
          ??????????//??上一步中?dirty?已經(jīng)置為?true,?所以會(huì)重新求值
          ??????????if?(watcher.dirty)?{
          ????????????watcher.evaluate()
          ??????????}
          ??????????if?(Dep.target)?{
          ????????????watcher.depend()
          ??????????}
          ??????????//?最后返回計(jì)算出來(lái)的值
          ??????????return?watcher.value
          ????????}
          ????}
          })

          由于上一步中的響應(yīng)式屬性更新,觸發(fā)了 計(jì)算 watcherdirty 更新為 true。所以又會(huì)重新調(diào)用用戶傳入的 sum 函數(shù)計(jì)算出最新的值,頁(yè)面上自然也就顯示出了最新的值。

          至此為止,整個(gè)計(jì)算屬性更新的流程就結(jié)束了。

          緩存生效的情況

          根據(jù)上面的總結(jié),只有計(jì)算屬性依賴的響應(yīng)式值發(fā)生更新的時(shí)候,才會(huì)把 dirty 重置為 true,這樣下次讀取的時(shí)候才會(huì)發(fā)生真正的計(jì)算。

          這樣的話,假設(shè) sum 函數(shù)是一個(gè)用戶定義的一個(gè)比較耗費(fèi)時(shí)間的操作,優(yōu)化就比較明顯了。

          <div?id="app">
          ??<span?@click="change">{{sum}}span>
          ??<span?@click="changeOther">{{other}}span>
          div>
          <script?src="./vue2.6.js">script>
          <script>
          ??new?Vue({
          ????el:?"#app",
          ????data()?{
          ??????return?{
          ????????count:?1,
          ????????other:?'Hello'
          ??????}
          ????},
          ????methods:?{
          ??????change()?{
          ????????this.count?=?2
          ??????},
          ??????changeOther()?{
          ????????this.other?=?'ssh'
          ??????}
          ????},
          ????computed:?{
          ??????//?非常耗時(shí)的計(jì)算屬性
          ??????sum()?{
          ????????let?i?=?100000
          ????????while(i?>?0)?{
          ????????????i--
          ????????}
          ????????return?this.count?+?1
          ??????},
          ????},
          ??})
          script>

          在這個(gè)例子中,other 的值和計(jì)算屬性沒(méi)有任何關(guān)系,如果 other 的值觸發(fā)更新的話,就會(huì)重新渲染視圖,那么會(huì)讀取到 sum,如果計(jì)算屬性不做緩存的話,每次都要發(fā)生一次很耗費(fèi)性能的沒(méi)有必要的計(jì)算。

          所以,只有在 count 發(fā)生變化的時(shí)候,sum 才會(huì)重新計(jì)算,這是一個(gè)很巧妙的優(yōu)化。

          總結(jié)

          2.6 版本計(jì)算屬性更新的路徑是這樣的:

          1. 響應(yīng)式的值 count 更新
          2. 同時(shí)通知 computed watcher渲染 watcher 更新
          3. omputed watcher 把 dirty 設(shè)置為 true
          4. 視圖渲染讀取到 computed 的值,由于 dirty 所以 computed watcher 重新求值。

          通過(guò)本篇文章,相信你可以完全理解計(jì)算屬性的緩存到底是什么概念,在什么樣的情況下才會(huì)生效了吧?

          ??感謝大家

          如果你喜歡探討技術(shù),或者對(duì)本文有任何的意見(jiàn)或建議,非常歡迎加魚(yú)頭微信好友一起探討,當(dāng)然,魚(yú)頭也非常希望能跟你一起聊生活,聊愛(ài)好,談天說(shuō)地。魚(yú)頭的微信號(hào)是:krisChans95 也可以掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。


          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人在线免费观看 | 操B视频动漫免费网站 | 第四色色五月婷婷 | 亚洲精品在线中文字幕 | 麻豆中文字幕 |