當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注、點贊、收藏和評論。
新視頻和文章會第一時間在微信公眾號發(fā)送,歡迎關(guān)注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。
封面

前言
上一篇文章 手寫 Vue2 系列 之 patch —— diff 實現(xiàn)了 DOM diff 過程,完成頁面響應(yīng)式數(shù)據(jù)的更新。
目標
本篇的目標是實現(xiàn) computed 計算屬性,完成模版中計算屬性的展示。涉及的知識點:
計算屬性的本質(zhì)
計算屬性的緩存原理
實現(xiàn)
接下來就開始實現(xiàn) computed 計算屬性,。
_init
/src/index.js
/**
* 初始化配置對象
* @param {*} options
*/
Vue.prototype._init = function (options) {
// ...
// 初始化 options.data
// 代理 data 對象上的各個屬性到 Vue 實例
// 給 data 對象上的各個屬性設(shè)置響應(yīng)式能力
initData(this)
// 初始化 computed 選項,并將計算屬性代理到 Vue 實例上
// 結(jié)合 watcher 實現(xiàn)緩存
initComputed(this)
// 安裝運行時的渲染工具函數(shù)
renderHelper(this)
// ...
}
initComputed
/src/initComputed.js
/**
* 初始化 computed 配置項
* 為每一項實例化一個 Watcher,并將其 computed 屬性代理到 Vue 實例上
* 結(jié)合 watcher.dirty 和 watcher.evalute 實現(xiàn) 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,回調(diào)函數(shù)默認懶執(zhí)行
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 回調(diào)函數(shù)在本次渲染周期內(nèi)沒有被執(zhí)行過
// 執(zhí)行 evalute,通知 watcher 執(zhí)行 computed 回調(diào)函數(shù),得到回調(diào)函數(shù)返回值
watcher.evalute()
}
return watcher.value
},
set: function () {
console.log('no setter')
}
}
// 將計算屬性代理到 Vue 實例上
Object.defineProperty(vm, key, descriptor)
}
Watcher
/src/watcher.js
/**
* @param {*} cb 回調(diào)函數(shù),負責(zé)更新 DOM 的回調(diào)函數(shù)
* @param {*} options watcher 的配置項
*/
export default function Watcher(cb, options = {}, vm = null) {
// 備份 cb 函數(shù)
this._cb = cb
// 回調(diào)函數(shù)執(zhí)行后的值
this.value = null
// computed 計算屬性實現(xiàn)緩存的原理,標記當前回調(diào)函數(shù)在本次渲染周期內(nèi)是否已經(jīng)被執(zhí)行過
this.dirty = !!options.lazy
// Vue 實例
this.vm = vm
// 非懶執(zhí)行時,直接執(zhí)行 cb 函數(shù),cb 函數(shù)中會發(fā)生 vm.xx 的屬性讀取,從而進行依賴收集
!options.lazy && this.get()
}
watcher.get
/src/watcher.js
/**
* 負責(zé)執(zhí)行 Watcher 的 cb 函數(shù)
* 執(zhí)行時進行依賴收集
*/
Watcher.prototype.get = function () {
pushTarget(this)
this.value = this._cb.apply(this.vm)
popTarget()
}
watcher.update
/src/watcher.js
/**
* 響應(yīng)式數(shù)據(jù)更新時,dep 通知 watcher 執(zhí)行 update 方法,
* 讓 update 方法執(zhí)行 this._cb 函數(shù)更新 DOM
*/
Watcher.prototype.update = function () {
// 通過 Promise,將 this._cb 的執(zhí)行放到 this.dirty = true 的后面
// 否則,在點擊按鈕時,computed 屬性的第一次計算會無法執(zhí)行,
// 因為 this._cb 執(zhí)行的時候,會更新組件,獲取計算屬性的值的時候 this.dirty 依然是
// 上一次的 false,導(dǎo)致無法得到最新的的計算屬性的值
// 不過這個在有了異步更新隊列之后就不需要了,當然,畢竟異步更新對象的本質(zhì)也是 Promise
Promise.resolve().then(() => {
this._cb()
})
// 執(zhí)行完 _cb 函數(shù),DOM 更新完畢,進入下一個渲染周期,所以將 dirty 置為 false
// 當再次獲取 計算屬性 時就可以重新執(zhí)行 evalute 方法獲取最新的值了
this.dirty = true
}
watcher.evalute
/src/watcher.js
Watcher.prototype.evalute = function () {
// 執(zhí)行 get,觸發(fā)計算函數(shù) (cb) 的執(zhí)行
this.get()
// 將 dirty 置為 false,實現(xiàn)一次刷新周期內(nèi) computed 實現(xiàn)緩存
this.dirty = false
}
pushTarget
/src/dep.js
// 存儲所有的 Dep.target
// 為什么會有多個 Dep.target?
// 組件會產(chǎn)生一個渲染 Watcher,在渲染的過程中如果處理到用戶 Watcher,
// 比如 computed 計算屬性,這時候會執(zhí)行 evalute -> get
// 假如直接賦值 Dep.target,那 Dep.target 的上一個值 —— 渲染 Watcher 就會丟失
// 造成在 computed 計算屬性之后渲染的響應(yīng)式數(shù)據(jù)無法完成依賴收集
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]
}
結(jié)果
好了,到這里,Vue computed 屬性實現(xiàn)就完成了,如果你能看到如下效果圖,則說明一切正常。
動圖地址:https://gitee.com/liyongning/typora-image-bed/raw/master/202203161832189.image
[圖片上傳失敗...(image-8414ff-1647830959206)]
可以看到,頁面中的計算屬性已經(jīng)正常顯示,而且也可以做到響應(yīng)式更新,且具有緩存的能力(通過控制臺查看 computed 輸出)。
到這里,手寫 Vue 系列就剩最后一部分內(nèi)容了 —— 手寫 Vue 系列 之 異步更新隊列。
鏈接
- 配套視頻,微信公眾號回復(fù):"精通 Vue 技術(shù)棧源碼原理視頻版" 獲取
- 精通 Vue 技術(shù)棧源碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
- github 倉庫 liyongning/Lyn-Vue-DOM 歡迎 Star
- github 倉庫 liyongning/Lyn-Vue-Template 歡迎 Star
感謝各位的:關(guān)注、點贊、收藏和評論,我們下期見。
當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注、 點贊、收藏和評論。
新視頻和文章會第一時間在微信公眾號發(fā)送,歡迎關(guān)注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。