手寫 Vue2 系列 之 computed

當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注點贊、收藏評論。

新視頻和文章會第一時間在微信公眾號發(fā)送,歡迎關(guān)注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

封面

image

前言

上一篇文章 手寫 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 系列 之 異步更新隊列。

鏈接

感謝各位的:關(guān)注、點贊收藏評論,我們下期見。


當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注點贊、收藏評論。

新視頻和文章會第一時間在微信公眾號發(fā)送,歡迎關(guān)注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注、點贊、收藏和評論。 新視頻和文章會第一時間在微信公眾號發(fā)...
    李永寧_lyn閱讀 639評論 0 0
  • 當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注、點贊、收藏和評論。 新視頻和文章會第一時間在微信公眾號發(fā)...
    李永寧_lyn閱讀 179評論 0 0
  • 當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注、點贊、收藏和評論。 新視頻和文章會第一時間在微信公眾號發(fā)...
    李永寧_lyn閱讀 391評論 0 0
  • 當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 點贊、收藏和評論。 新視頻和文章會第一時間在微信公眾號發(fā)送,歡...
    李永寧_lyn閱讀 194評論 0 0
  • 當學(xué)習(xí)成為了習(xí)慣,知識也就變成了常識。 感謝各位的 關(guān)注、點贊、收藏和評論。 新視頻和文章會第一時間在微信公眾號發(fā)...
    李永寧_lyn閱讀 340評論 0 0

友情鏈接更多精彩內(nèi)容