手寫 Vue2 系列 之 computed
阿新 • • 發佈:2022-03-21
前言
上一篇文章 手寫 Vue2 系列 之 patch —— diff 實現了 DOM diff 過程,完成頁面響應式資料的更新。
目標
本篇的目標是實現 computed 計算屬性,完成模版中計算屬性的展示。涉及的知識點:
-
計算屬性的本質
-
計算屬性的快取原理
實現
接下來就開始實現 computed 計算屬性,。
_init
/src/index.js
/** * 初始化配置物件 * @param {*} options */ Vue.prototype._init = function (options) { // ... // 初始化 options.data // 代理 data 物件上的各個屬性到 Vue 例項 // 給 data 物件上的各個屬性設定響應式能力 initData(this) // 初始化 computed 選項,並將計算屬性代理到 Vue 例項上 // 結合 watcher 實現快取 initComputed(this) // 安裝執行時的渲染工具函式 renderHelper(this) // ... }
initComputed
/src/initComputed.js
/** * 初始化 computed 配置項 * 為每一項例項化一個 Watcher,並將其 computed 屬性代理到 Vue 例項上 * 結合 watcher.dirty 和 watcher.evalute 實現 computed 快取 * @param {*} vm Vue 例項 */ export default function initComputed(vm) { // 獲取 computed 配置項 const computed = vm.$options.computed // 記錄 watcher const watcher = vm._watcher = Object.create(null) // 遍歷 computed 物件 for (let key in computed) { // 例項化 Watcher,回撥函式預設懶執行 watcher[key] = new Watcher(computed[key], { lazy: true }, vm) // 將 computed 的屬性 key 代理到 Vue 例項上 defineComputed(vm, key) } }
defineComputed
/src/initComputed.js
/** * 將計算屬性代理到 Vue 例項上 * @param {*} vm Vue 例項 * @param {*} key computed 的計算屬性 */ function defineComputed(vm, key) { // 屬性描述符 const descriptor = { get: function () { const watcher = vm._watcher[key] if (watcher.dirty) { // 說明當前 computed 回撥函式在本次渲染週期內沒有被執行過 // 執行 evalute,通知 watcher 執行 computed 回撥函式,得到回撥函式返回值 watcher.evalute() } return watcher.value }, set: function () { console.log('no setter') } } // 將計算屬性代理到 Vue 例項上 Object.defineProperty(vm, key, descriptor) }
Watcher
/src/watcher.js
/**
* @param {*} cb 回撥函式,負責更新 DOM 的回撥函式
* @param {*} options watcher 的配置項
*/
export default function Watcher(cb, options = {}, vm = null) {
// 備份 cb 函式
this._cb = cb
// 回撥函式執行後的值
this.value = null
// computed 計算屬性實現快取的原理,標記當前回撥函式在本次渲染週期內是否已經被執行過
this.dirty = !!options.lazy
// Vue 例項
this.vm = vm
// 非懶執行時,直接執行 cb 函式,cb 函式中會發生 vm.xx 的屬性讀取,從而進行依賴收集
!options.lazy && this.get()
}
watcher.get
/src/watcher.js
/**
* 負責執行 Watcher 的 cb 函式
* 執行時進行依賴收集
*/
Watcher.prototype.get = function () {
pushTarget(this)
this.value = this._cb.apply(this.vm)
popTarget()
}
watcher.update
/src/watcher.js
/**
* 響應式資料更新時,dep 通知 watcher 執行 update 方法,
* 讓 update 方法執行 this._cb 函式更新 DOM
*/
Watcher.prototype.update = function () {
// 通過 Promise,將 this._cb 的執行放到 this.dirty = true 的後面
// 否則,在點選按鈕時,computed 屬性的第一次計算會無法執行,
// 因為 this._cb 執行的時候,會更新元件,獲取計算屬性的值的時候 this.dirty 依然是
// 上一次的 false,導致無法得到最新的的計算屬性的值
// 不過這個在有了非同步更新佇列之後就不需要了,當然,畢竟非同步更新物件的本質也是 Promise
Promise.resolve().then(() => {
this._cb()
})
// 執行完 _cb 函式,DOM 更新完畢,進入下一個渲染週期,所以將 dirty 置為 false
// 當再次獲取 計算屬性 時就可以重新執行 evalute 方法獲取最新的值了
this.dirty = true
}
watcher.evalute
/src/watcher.js
Watcher.prototype.evalute = function () {
// 執行 get,觸發計算函式 (cb) 的執行
this.get()
// 將 dirty 置為 false,實現一次重新整理週期內 computed 實現快取
this.dirty = false
}
pushTarget
/src/dep.js
// 儲存所有的 Dep.target
// 為什麼會有多個 Dep.target?
// 元件會產生一個渲染 Watcher,在渲染的過程中如果處理到使用者 Watcher,
// 比如 computed 計算屬性,這時候會執行 evalute -> get
// 假如直接賦值 Dep.target,那 Dep.target 的上一個值 —— 渲染 Watcher 就會丟失
// 造成在 computed 計算屬性之後渲染的響應式資料無法完成依賴收集
const targetStack = []
/**
* 備份本次傳遞進來的 Watcher,並將其賦值給 Dep.target
* @param {*} target Watcher 例項
*/
export function pushTarget(target) {
// 備份傳遞進來的 Watcher
targetStack.push(target)
Dep.target = target
}
popTarget
/src/dep.js
/**
* 將 Dep.target 重置為上一個 Watcher 或者 null
*/
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
結果
好了,到這裡,Vue computed 屬性實現就完成了,如果你能看到如下效果圖,則說明一切正常。
動圖地址:https://gitee.com/liyongning/typora-image-bed/raw/master/202203161832189.image
可以看到,頁面中的計算屬性已經正常顯示,而且也可以做到響應式更新,且具有快取的能力(通過控制檯檢視 computed 輸出)。
到這裡,手寫 Vue 系列就剩最後一部分內容了 —— 手寫 Vue 系列 之 非同步更新佇列。
連結
- 配套視訊,微信公眾號回覆:"精通 Vue 技術棧原始碼原理視訊版" 獲取
- 精通 Vue 技術棧原始碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
- github 倉庫 liyongning/Lyn-Vue-DOM 歡迎 Star
- github 倉庫 liyongning/Lyn-Vue-Template 歡迎 Star
感謝各位的:關注、點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 關注、 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。