1. 程式人生 > 其它 >手寫 Vue2 系列 之 computed

手寫 Vue2 系列 之 computed

前言

上一篇文章 手寫 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 系列 之 非同步更新佇列

連結

感謝各位的:關注點贊收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識。 感謝各位的 關注點贊收藏評論

新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn

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