Vue.js設計與實現學習總結(第四章7)偵聽器

瑪拉_以琳發表於2023-02-16

前置知識點:

執行排程 https://segmentfault.com/a/11...
計算屬性 https://segmentfault.com/a/11...

正文

在上一篇介紹了計算屬性的實現原理, 這篇是 Vue 中時常和計算屬性做比較的偵聽器的原理實現的簡介.
所謂的偵聽器watch本質上就是觀測響應式資料是否發生變化, 當資料發生變化時通知並執行相應的回撥函式:

watch (obj, () => {
    console.log('資料變化了')
})

// 修改資料導致響應式資料變化
obj.foo++

本質上是利用了副作用函式effect以及排程選項option.scheduler:

effect(() => {
  console.log(obj.foo)
},
// options
{
  scheduler () {
    // obj.foo 變化時, 執行 scheduler 排程函式
  }
})

如果副作用函式存在scheduler選項, 當響應式資料發生變化時會觸發scheduler排程函式執行, 而不是直接觸發副作用函式利用這點可以實現最簡單的watch函式:

function watch (source, cb) {
  effect(
    // 觸發讀取操作, 從而建立聯絡
    () => source.foo,
    {
      scheduler () {
        // obj.foo 變化時, 執行 scheduler 排程函式
        cb()
      }
    }
  )
}

但是這個太基本了, 還只能監聽obj.foo這個屬性的變化, 因此需要封裝一個通用的讀取操作, 使watch具有通用性:

function watch (source, cb) {
  effect(
    // 觸發讀取操作, 從而建立聯絡
    // 呼叫函式遞迴讀取將每一個資料都建立聯絡
    () => traverse(source),
    {
       scheduler () {
        // obj.foo 變化時, 執行 scheduler 排程函式
        cb()
      }
    }
  )
}

function traverse (value, seen = new Set()) {
  // 如果該值是原始資料型別, 或者已被讀取過來就什麼都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  // 將資料新增到 seen 中, 代表遍歷過了 避免引起死迴圈
  seen.add(value)
  // 假設 value就是就是一個物件, 暫時不考慮陣列等情況
  for (const k in value) {
    // 遞迴呼叫 traverse
    traverse(value[k], seen)
  }
  return value
}

這樣就可以讀取物件上的任意屬性, 從而當任意屬性發生變化時都能觸發回撥函式執行, watch不僅僅可以觀測響應函式還可以接受getter函式:

watch (
  // getter 函式
  () => obj.foo,
  // 回撥函式
  () => console.log('obj.foo 的值改變了')
)

getter函式的內部可以指定改watch依賴哪些響應式資料, 只有當這些資料變化時才會觸發回撥函式執行:

function watch (source, cn) {
  // 定義 getter
  let getter
  // 如果 source 是函式, 說明使用者 傳遞的是 getter
  if (typeof source === 'function') {
    getter = source
  } else {
    // 否則按照原來的呼叫 traverse 遞迴讀取
    getter = () => traverse(source)
  }

  effect(
    // 執行 getter 獲取值
    () => getter(),
    {
      scheduler () {
        // obj.foo 變化時, 執行 scheduler 排程函式
        cb()
      }
    }
  )
}

這時功能已經比價完善了, 不過目前還少了一點就是不能夠得到變化前後的值, 但是在 Vue.js 中是可以的:

watch (
  // getter 函式
  () => obj.foo,
  // 回撥函式
  (newVal, oldVal) => console.log(newVal, oldVal)
)

因此需要獲取新值與舊值, 這時可以利用effectlazy選項:

關於 lazy 請參看: https://segmentfault.com/a/11...
function watch (source, cn) {
  // 定義 getter
  let getter
  // 如果 source 是函式, 說明使用者 傳遞的是 getter
  if (typeof source === 'function') {
    getter = source
  } else {
    // 否則按照原來的呼叫 traverse 遞迴讀取
    getter = () => traverse(source)
  }

  let oldValue, newValue
  // 使用 effect 註冊副作用函式時開啟 lazy 選項, 並把返回的值儲存到 effectFn 中以便後續手動呼叫
  const effectFn = effect(
    // 執行 getter 獲取值
    () => getter(),
    {
      lazy: true,
      scheduler () {
        // 重新執行 effectFn 得到的是新值
        newValue = effectFn()
        // 將新舊值作為回撥函式的引數
        // obj.foo 變化時, 執行 scheduler 排程函式
        cb(newValue, oldValue)
        // 更新舊值, 不然下一次就會得到錯誤的舊值
        oldValue = newValue
      }
    }
  )

  // 手動呼叫副作用函式拿到的就是舊值
  oldValue = effectFn()
}

在程式碼的最下面, 手動呼叫 effectFn 函式返回的值得到的就是舊值也就是第一次執行得到的值, 當觸發 scheduler 排程函式時會重新呼叫 effectFn 得到新值.

相關文章