天天看點

vue 顯示[hmr] waiting for update signal_前端經典面試題解密:Vue 的計算屬性如何實作緩存?(原理深入揭秘)...

前言

很多人提起 Vue 中的 computed,第一反應就是計算屬性會緩存,那麼它到底是怎麼緩存的呢?緩存的到底是什麼,什麼時候緩存會失效,相信還是有很多人對此很模糊。

本文以 Vue 2.6.11 版本為基礎,就深入原理,帶你來看看所謂的緩存到底是什麼樣的。

注意

本文假定你對 Vue 響應式原理已經有了基礎的了解,如果對于

Watcher

Dep

和什麼是

渲染watcher

等概念還不是很熟悉的話,可以先找一些基礎的響應式原理的文章或者教程看一下。視訊教程的話推薦黃轶老師的,如果想要看簡化實作,也可以先看我寫的文章:

手把手帶你實作一個最精簡的響應式系統來學習Vue的data、computed、watch源碼[1]

注意,這篇文章裡我也寫了 computed 的原理,但是這篇文章裡的 computed 是基于 Vue 2.5 版本的,和目前 2.6 版本的變化還是非常大的,可以僅做參考。

示例

按照我的文章慣例,還是以一個最簡的示例來示範。

這個例子很簡單,剛開始頁面上顯示數字

2

,點選數字後變成

3

解析

回顧 watcher 的流程

進入正題,Vue 初次運作時會對 computed 屬性做一些初始化處理,首先我們回顧一下 watcher 的概念,它的核心概念是

get

求值,和

update

更新。

  1. 在求值的時候,它會先把自身也就是 watcher 本身指派給

    Dep.target

    這個全局變量。
  2. 然後求值的過程中,會讀取到響應式屬性,那麼響應式屬性的 dep 就會收集到這個 watcher 作為依賴。
  3. 下次響應式屬性更新了,就會從 dep 中找出它收集到的 watcher,觸發

    watcher.update()

    去更新。

是以最關鍵的就在于,這個

get

到底用來做什麼,這個

update

會觸發什麼樣的更新。

在基本的響應式更新視圖的流程中,以上概念的

get

求值就是指 Vue 的元件重新渲染的函數,而

update

的時候,其實就是重新調用元件的渲染函數去更新視圖。

而 Vue 中很巧妙的一點,就是這套流程也同樣适用于 computed 的更新。

初始化 computed

這裡先提前劇透一下,Vue 會對 options 中的每個 computed 屬性也用 watcher 去包裝起來,它的

get

函數顯然就是要執行使用者定義的求值函數,而

update

則是一個比較複雜的流程,接下來我會詳細講解。

首先在元件初始化的時候,會進入到初始化 computed 的函數

進入

initComputed

看看

首先定義了一個空的對象,用來存放所有計算屬性相關的 watcher,後文我們會把它叫做

計算watcher

然後循環為每個 computed 屬性生成了一個

計算watcher

它的形态保留關鍵屬性簡化後是這樣的:

可以看到它的

value

剛開始是 undefined,

lazy

是 true,說明它的值是惰性計算的,隻有到真正在模闆裡去讀取它的值後才會計算。

這個

dirty

屬性其實是緩存的關鍵,先記住它。

接下來看看比較關鍵的

defineComputed

,它決定了使用者在讀取

this.sum

這個計算屬性的值後會發生什麼,繼續簡化,排除掉一些不影響流程的邏輯。

這個函數需要仔細看看,它做了好幾件事,我們以初始化的流程來講解它:

首先

dirty

這個概念代表髒資料,說明這個資料需要重新調用使用者傳入的

sum

函數來求值了。我們暫且不管更新時候的邏輯,第一次在模闆中讀取到  

{{sum}}

的時候它一定是 true,是以初始化就會經曆一次求值。

這個函數其實很清晰,它先求值,然後把

dirty

置為 false。

再回頭看看我們剛剛那段

Object.defineProperty

的邏輯,

下次沒有特殊情況再讀取到

sum

的時候,發現

dirty

是false了,是不是直接就傳回

watcher.value

這個值就可以了,這其實就是計算屬性緩存的概念。

更新

初始化的流程講完了,相信大家也對

dirty

緩存

有了個大概的概念(如果沒有,再仔細回頭看一看)。

接下來就講更新的流程,細化到本文的例子中,也就是

count

的更新到底是怎麼觸發

sum

在頁面上的變更。

首先回到剛剛提到的

evalute

函數裡,也就是讀取

sum

時發現是髒資料的時候做的求值操作。

Dep.target 變更為 渲染watcher

這裡進入

this.get()

,首先要明确一點,在模闆中讀取

{{ sum }}

變量的時候,全局的

Dep.target

應該是

渲染watcher

,這裡不了解的話可以到我最開始提到的文章裡去了解下。

全局的

Dep.target

狀态是用一個棧

targetStack

來儲存,便于前進和回退

Dep.target

,至于什麼時候會回退,接下來的函數裡就可以看到。

首先剛進去就

pushTarget

,也就是把

計算watcher

自身置為

Dep.target

,等待收集依賴。

執行完

pushTarget(this)

後,

Dep.target 變更為 計算watcher

getter

函數,上一章的 watcher 形态裡已經說明了,其實就是使用者傳入的

sum

函數。

這裡在執行的時候,讀取到了

this.count

,注意它是一個響應式的屬性,是以冥冥之中它們開始建立了千絲萬縷的聯系……

這裡會觸發

count

get

劫持,簡化一下

那麼可以看出,

count

會收集

計算watcher

作為依賴,具體怎麼收集呢

其實這裡是調用

Dep.target.addDep(this)

去收集,又繞回到

計算watcher

addDep

函數上去了,這其實主要是 Vue 内部做了一些去重的優化。

又回到

dep

上去了。

這樣就儲存了

計算watcher

作為

count

的 dep 裡的依賴了。

經曆了這樣的一個收集的流程後,此時的一些狀态:

sum 的計算watcher

count的dep

:

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

此時求值結束,回到

計算watcher

getter

函數:

執行到了

popTarget

計算watcher

出棧。

Dep.target 變更為 渲染watcher

然後函數執行完畢,傳回了

2

這個 value,此時對于

sum

屬性的

get

通路還沒結束。

此時的

Dep.target

當然是有值的,就是

渲染watcher

,是以進入了

watcher.depend()

的邏輯,這一步相當關鍵。

還記得剛剛的

計算watcher

的形态嗎?它的

deps

裡儲存了

count

的 dep。

也就是說,又會調用

count

上的

dep.depend()

這次的

Dep.target

已經是

渲染watcher

了,是以這個

count

的 dep 又會把

渲染watcher

存放進自身的

subs

中。

count的dep

:

那麼來到了此題的重點,這時候

count

更新了,是如何去觸發視圖更新的呢?

再回到

count

的響應式劫持邏輯裡去:

好,這裡觸發了我們剛剛精心準備的

count

的 dep 的

notify

函數,感覺離成功越來越近了。

這裡的邏輯就很簡單了,把

subs

裡儲存的 watcher 依次去調用它們的

update

方法,也就是

  1. 調用

    計算watcher

    的 update
  2. 調用

    渲染watcher

    的 update

拆解來看。

計算watcher 的 update

wtf,就這麼一句話…… 沒錯,就僅僅是把

計算watcher

dirty

屬性置為 true,靜靜的等待下次讀取即可。

渲染watcher 的 update

這裡其實就是調用

vm._update(vm._render())

這個函數,重新根據

render

函數生成的

vnode

去渲染視圖了。

而在

render

的過程中,一定會通路到

sum

這個值,那麼又回回到

sum

定義的

get

上:

由于上一步中的響應式屬性更新,觸發了

計算 watcher

dirty

更新為 true。是以又會重新調用使用者傳入的

sum

函數計算出最新的值,頁面上自然也就顯示出了最新的值。

至此為止,整個計算屬性更新的流程就結束了。

緩存生效的情況

根據上面的總結,隻有計算屬性依賴的響應式值發生更新的時候,才會把

dirty

重置為 true,這樣下次讀取的時候才會發生真正的計算。

這樣的話,假設

sum

函數是一個使用者定義的一個比較耗費時間的操作,優化就比較明顯了。

在這個例子中,

other

的值和計算屬性沒有任何關系,如果

other

的值觸發更新的話,就會重新渲染視圖,那麼會讀取到

sum

,如果計算屬性不做緩存的話,每次都要發生一次很耗費性能的沒有必要的計算。

是以,隻有在

count

發生變化的時候,

sum

才會重新計算,這是一個很巧妙的優化。

總結

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

  1. 響應式的值

    count

    更新
  2. 同時通知

    computed watcher

    渲染 watcher

    更新
  3. omputed watcher

    把 dirty 設定為 true
  4. 視圖渲染讀取到 computed 的值,由于 dirty 是以

    computed watcher

    重新求值。

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

後記

以上就是胡哥今天給大家分享的内容,喜歡的小夥伴記得

收藏

轉發

、點選右下角按鈕

在看

,推薦給更多小夥伴呦,歡迎多多留言交流...

胡哥有話說,一個有技術,有情懷的胡哥!現任京東前端攻城獅一枚。

胡哥有話說,專注于大前端技術領域,分享前端系統架構,架構實作原理,最新最高效的技術實踐!

長按掃碼關注,更帥更漂亮呦!關注胡哥有話說公衆号,可與胡哥繼續深入交流呦!

vue 顯示[hmr] waiting for update signal_前端經典面試題解密:Vue 的計算屬性如何實作緩存?(原理深入揭秘)...