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

来源:https://www.cnblogs.com/chengxuyuanaa/archive/2020/06/11/13092144.html
-Advertisement-
Play Games

Vue計算屬性 1、提到計算屬性,我們馬上就會想到它的一個特性:緩存,Vue 文檔也如是說: 計算屬性是基於它們的響應式依賴進行緩存的/ 2。很榮幸給大家分享,我是一名08年出道的前端高級老鳥,大家如果想跟我交流學習經驗,可以進我的扣扣裙 519293536 有問題我都會儘力幫大家,喜歡中高級問題, ...


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 實現的巧妙之處。其實緩存的原理很簡單,就是一個標誌位而已~

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 之前寫了兩篇vue2.0的響應式原理,鏈接在此,對響應式原理不清楚的請先看下麵兩篇 和尤雨溪一起進階vue 和尤雨溪一起進階vue(二) 現在來寫一個簡單的3.0的版本吧 大家都知道,2.0的響應式用的是Object.defineProperty,結合發佈訂閱模式實現的,3.0已經用Proxy改寫了 ...
  • 在最近的學習中,在GitHub上找了一些佈局練習,我發現了我自己對佈局超級不熟悉(很難受)。 在以前的學習中,感覺使用CSS就記住各個屬性的功能就OK了,但是呢?真的很打臉。不說了,太傷心了,進入正題吧! 最近我使用float和margin佈局,加深了我對這兩個一起使用的理解。(新生可以看一下,大神 ...
  • 列表項,有時需要判斷列表首尾,來篩選設置樣式 如上圖,三個項有間隔,怎麼保證設置了列表項之間的距離後,整體還水平居中顯示呢? .item:not(:first-child) { margin-left: 20px;} 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <me ...
  • 大家都說yarn比npm好,今天全局安裝yarn嘗試下 cnpm install -g yarn 安裝完成後,查看yarn版本 yarn --version 項目搭建 安裝vuepress yarn global add vuepress 初始化項目 創建項目目錄blog mkdir blog cd ...
  • 關於前端的話題知乎不能再多了,但作為一個自學者,希望以我的經歷給你們一點啟示或者幫助。 關於前端的學習資料和視頻可謂多之又多,學習方法也是大相徑庭,有推薦跟著慕課網的,也有W3C的,傳智播客,這裡就簡單提一下吧。我想大部分都是零基礎的居多吧,所以一些必備的基礎知識都沒有的話,後面的學習是很困難的。這 ...
  • 本次做後臺管理系統,採用的是 AntD 框架。涉及到圖片的上傳,用的是AntD的 upload 組件。 我在上一篇文章《前端AntD框架的upload組件上傳圖片時遇到的一些坑》中講到:AntD 的 upload 組件有很多坑,引起了很多人的關註。折騰過的人,自然明白其中的苦楚。 今天這篇文章,我們 ...
  • JavaScript是一種屬於網路的腳本語言,已經被廣泛用於Web應用開發,常用來為網頁添加各式各樣的動態功能,為用戶提供更流暢美觀的瀏覽效果。通常JavaScript腳本是通過嵌入在HTML中來實現自身的功能的。 CSS(Cascading Style Sheet)層疊樣式表單 表現(presen ...
  • ie8下透明度處理 css3新增屬性rgba和opacity:(0~1); 在ie8下無法使用 Alpha(opacity=[0~100]);//[0~100] 0是透明 100是不透明。 IE下的透明度屬性,子元素會繼承這個透明度。下麵有阻斷子元素繼承方法。 解決方法: 背景透明,文字不透明。 < ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...