如何理解Vue計算屬性?80%的人都沒搞懂


Vue計算屬性

1、提到計算屬性,我們馬上就會想到它的一個特性:緩存,Vue 文檔也如是說:

計算屬性是基於它們的響應式依賴進行緩存的/

2。很榮幸給大家分享,我是一名08年出道的前端高級老鳥,大家如果想跟我交流學習經驗,可以進我的扣扣裙 519293536 有問題我都會盡力幫大家,喜歡中高級問題,小白勿進哦
接下來正題:
   那么計算屬性如何緩存的呢?計算屬性的觀察者是如何進行依賴收集的呢,接下來深入原理看一下。
    本文需要對基礎的響應式原理和幾個關鍵角色 Observer、Dep、Watcher 等有一定的了解,可參考 Observer、Dep、Watcher 傻傻搞不清楚

以一個簡單的例子開始:

<div id="app">
    <h2>{{ this.text }}</h2>
    <button @click="changeName">Change name</button>
</div>

const vm = new Vue({
    el: '#app',
    data() {
        return {
            name: 'xiaoming',
        }
    },
    computed: {
        text() {
            return `Hello, ${this.name}!`
        }
    },
    methods: {
        changeName() {
            this.name = 'onlyil'
        },
    },
})
復制代碼

初始展示 Hello, xiaoming!,點擊按鈕后展示 Hello, onlyil!

Vue 初始化

還是從 vue 初始化看起,從 new Vue() 開始,構造函數會執行 this._init,在 _init 中會進行合並配置、初始化生命周期、事件、渲染等,最后執行 vm.$mount 進行掛載。

// src/core/instance/index.js function Vue (options) { // ... this._init(options) } // src/core/instance/init.js Vue.prototype._init = function (options?: Object) { // 合並選項 // ... // 一系列初始化 // ... initState(vm) // ... // 掛載 if (vm.$options.el) { vm.$mount(vm.$options.el) } } 復制代碼

計算屬性的初始化就在 initState 中:

// src/core/instance/state.js export function initState (vm: Component) { const opts = vm.$options // ... // 初始化 computed if (opts.computed) initComputed(vm, opts.computed) // ... } 復制代碼

computed 初始化

看一下 initComputed :

function initComputed(vm, computed) { const watchers = vm._computedWatchers = Object.create(null) // 遍歷 computed 選項,依次進行定義 for (const key in computed) { const getter = computed[key] // 為計算屬性創建內部 watcher watchers[key] = new Watcher( vm, getter || noop, // 計算屬性 text 函數 noop, computedWatcherOptions // { lazy: true } ,指定 lazy 屬性,表示要實例化 computedWatcher ) // 為計算屬性定義 getter defineComputed(vm, key, userDef) } } 復制代碼

1. 定義 _computedWatchers

首先定義一個 watchers 空對象,同時掛在 vm._computedWatchers 上,用來存放該 vm 實例的所有 computedWatcher。

2. 實例化 computedWatcher

遍歷 computed 選項並實例化 watcher,參數中的 getter 就是上邊示例中計算屬性 text 對應的函數:

function () { return `Hello, ${this.name}!` } 復制代碼

參數中的 computedWatcherOptions 為 { lazy: true } ,指定 lazy 屬性,表示要實例化的是 computedWatcher。

實例化 computedWatcher :

class Watcher { constructor(vm, expOrFn, cb, options) { // options 為 { lazy: true } if (options) { // ... this.lazy = !!options.lazy // ... } this.dirty = this.lazy // for lazy watchers, 初始 dirty 為 true this.getter = expOrFn // lazy 為 true,不進行求值,直接返回 undefined this.value = this.lazy ? undefined : this.get() } } 復制代碼

執行構造函數時指定 lazy dirty 為 true,最后執行 watcher.value = undefined 並未執行 get 方法進行求值。什么時候求值呢?后面就知道了。

回到上邊,為計算屬性創建內部 watcher 之后的 watchers 對象是這樣的:

{
    text: Watcher { lazy: true, dirty: true, deps: [], getter: function () { return `Hello, ${this.name}!` }, value: undefined, // 直接賦值為 undefined , } } 復制代碼

3. 定義計算屬性的 getter

看一下 defineComputed 做了什么:

function defineComputed(target, key, userDef) { Object.defineProperty(target, key, { get: function () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }) } 復制代碼

有沒有很熟悉,在定義響應式數據的 defineReactive 方法中也使用了 Object.defineProperty 方法來定義訪問器屬性。這里在該 vm 實例上定義了 text 屬性,每當訪問到 this.text 時就會執行對應的 getter ,函數做了什么暫時可以不看。那什么時候會讀取到 this.text 呢?答案在下邊。

總結一下 computed 初始化過程:

  • 定義 vm._computedWatchers 用來存放該 vm 實例的所有 computedWatcher
  • 遍歷 computed 選項並實例化 watcher,不求值,直接將 watcher.value = undefined
  • 通過 defineComputed 定義計算屬性的 getter ,等待后邊讀取時觸發

首次渲染

初始化完成后,會進入 mount 階段,在執行 render 生成 vnode 時會讀取到計算屬性 text ,本文示例的 render 函數是這樣:

function render() { var h = arguments[0]; return h("div", [ h("h2", [this.text]), // 這里讀取了計算屬性 text h("button", { "on": { "click": this.changeName } }, ["changeName"]), ]); } 復制代碼

1. 觸發計算屬性的 getter

這時會觸發計算屬性的 getter ,也就是上邊定義的訪問器屬性:

get: function () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 此時 dirty 為 true ,進行求值 if (watcher.dirty) { // 求值,對 data 進行依賴收集,使 computedWatcher 訂閱 data // 這里的 data 就是 "this.name" watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } 復制代碼

2. 求值 watcher.evaluate()

取出 vm._computedWatchers 中對應的 watcher ,此時 watcher.dirty 為 true,執行 watcher.evaluate() 。

// watcher.evaluate evaluate() { this.value = this.get() this.dirty = false } 復制代碼

這個函數做了兩件事:

  • 執行 get 進行求值,這里就解答了上面何時求值的問題;
  • 將 dirty 置為 false 。

先看求值:

// watcher.get get() { pushTarget(this) // Dep.target 置為當前 computedWatcher let value const vm = this.vm try { // 觸發響應數據的依賴收集 value = this.getter.call(vm, vm) } catch (e) { // ... } finally { popTarget() // Dep.target 重新置為渲染 watcher } return value } 復制代碼

這里需要知道一個前置內容,全局的 Dep.target 存的是當前正在求值的 watcher,它是用一個棧 targetStack 來維護的。當前是在渲染過程中,所以此時棧是這樣:[ 渲染watcher ]。

function pushTarget(target: ?Watcher) { targetStack.push(target) Dep.target = target } 復制代碼

首先 pushTarget(this) , Dep.target 成為當前 computedWatcher,此時棧是這樣的:[ 渲染watcher, computedWatcher ]。

然后執行 getter 觸發響應數據的依賴收集。再回顧一下 getter :

function () { return `Hello, ${this.name}!` } 復制代碼

很顯然執行這個函數會讀取 this.name ,name 的 dep 就會收集當前 computedWatcher ,當前 computedWatcher 就會訂閱 name 的變化(這里不做詳細介紹,參考響應式原理的依賴收集)。

收集完成后執行 popTarget() ,Dep.target 重新成為渲染 watcher ,此時的棧是這樣:[ 渲染watcher ]。

然后返回計算得出的值,再將 dirty 置為 false 。此時 name 的 dep 是這樣的:

{
    id: 3, subs: [ computedWatcher ], // 收集了 computedWatcher } 復制代碼

computedWatcher 是這樣的:

{
    dirty: false, // 求值完成,dirty 置為 false deps: [ name 的 dep ], // 訂閱了 name value: "Hello, xiaoming!", } 復制代碼

3. watcher.depend()

此時計算屬性的 get 訪問執行到了這里:

get: function () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 求值... // 執行到了這里 if (Dep.target) { watcher.depend() } return watcher.value } } 復制代碼

此時 Dep.target 是渲染 watcher ,所以會執行 watcher.depend() 。

// watcher.depend depend() { let i = this.deps.length while (i--) { this.deps[i].depend() } } 復制代碼

可以看到將 computedWatcher 訂閱的 deps 依次執行 dep.depend ,熟悉響應式原理的應該馬上就知道了,這是在對這些 dep 的響應式數據進行依賴收集,也就是對示例中的 name 進行依賴收集,收集的是誰呢?上面提到此時的 Dep.target 是渲染 watcher ,那么總結下來,這一步做的是:

讓 computedWatcher 訂閱的響應式數據收集渲染 watcher

這一步操作之后 name 的 dep 是這樣的:

{
    id: 3, subs: [ computedWatcher, 渲染 watcher ], // 收集了渲染 watcher } 復制代碼

最后,返回 watcher.value ,get 訪問結束,render 函數繼續往下走,之后渲染出最終頁面。

觸發更新

當點擊按鈕時,執行 this.name = 'onlyil' ,會觸發 name 的訪問器屬性 set ,執行 dep.notify() ,依次觸發它所收集的 watcher 的更新邏輯,也就是 [ computedWatcher, 渲染 watcher ] 的 update 。

1. 觸發 computedWatcher 更新

// watcher.update update() { // computedWatcher 的 lazy 為 true if (this.lazy) { this.dirty = true } // ... } 復制代碼

只做了一件事,就是將 dirty 置為 true,表示該計算屬性“臟”了,需要重新計算,什么時候重新求值呢,往下看。

2. 觸發渲染 watcher 更新

// watcher.update update() { // ... // queueWatcher(this) } 復制代碼

這里就是加入異步更新隊列,最終又會執行到 render 函數來生成 vnode ,同首次渲染一樣,在 render 過程中又會讀取到計算屬性 text ,再次觸發它的 getter :

get: function () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 此時 dirty 為 true ,"臟"了 if (watcher.dirty) { // 重新求值 watcher.evaluate() } // ... return watcher.value } } 復制代碼

重新求值也就會重新執行我們在 computed 選項定義的函數,頁面就展示了新值 Hello, onlyil!

function () { return `Hello, ${this.name}!` } 復制代碼

至此,更新結束。

如何緩存

通過上邊的過程分析,可以做出如下總結:

  • 首次渲染時實例化 computedWatcher 並定義屬性 dirty: false ,在 render 過程中求值並進行依賴收集;
  • 當 computedWatcher 訂閱的響應式數據也就是 name 改變時,觸發 computedWatcher 的更新,修改 dirty: true ;
  • render 函數執行時讀取計算屬性 text ,發現 dirty 為 true ,重新求值,頁面視圖更新。

可以發現一個關鍵點,computedWatcher 的更新只做了一件事:修改 dirty: true ,求值操作始終都在 render 過程中。

現在我們修改示例,新增一個數據 count 和方法 add :

<div id="app">
    <h2>{{ this.text }}</h2>
    <h2>{{ this.count }}</h2>
    <button @click="changeName">Change name</button>
    <button @click="add">Add</button>
</div>

const vm = new Vue({
    el: '#app',
    data() {
        return {
            name: 'xiaoming',
			count: 0,
        }
    },
    computed: {
        text() {
            return `Hello, ${this.name}!`
        }
    },
    methods: {
        changeName() {
            this.name = 'onlyil'
        },
		add() {
            this.count += 1
        },
    },
})
復制代碼

點擊 Add 按鈕 count 會發生改變,那么在重渲染時 computedWatcher 會重新求值嗎?

答案是不會,緩存的關鍵就在於此。回頭再看下文章開頭引用 Vue 文檔里一句話:

計算屬性是基於它們的響應式依賴進行緩存的

結合上邊的分析是不是恍然大悟,計算屬性 text 的 getter 函數並沒有讀取 count ,所以它的 computedWatcher 不會訂閱 count 的變化,即 count 的 dep 也不會收集該 computedWatcher 。

所以當 count 改變時,不會觸發 computedWatcher 的更新,dirty 仍為 false ,說明這個計算屬性不“臟”。那在之后的 render 過程中讀取到計算屬性 text 時就不會重新求值,這樣就起到了緩存的效果。

后記

1、我是一名08年出道的前端高級老鳥,大家如果想跟我交流學習經驗,可以進我的扣扣裙 519293536 有問題我都會盡力幫大家,喜歡中高級問題,小白勿進哦

2、整個過程下來腦子還是有點吃力的,但多來幾遍就會逐漸理解 computed 實現的巧妙之處。其實緩存的原理很簡單,就是一個標志位而已~🤣

注:文中代碼做了刪減,結合源碼斷點捋捋更佳。
本文的文字及圖片來源於網絡加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM