Vue 3.0 —— Watch 與 Reactivity 代碼走讀

前言

本篇文章同步發表在我的博客 Vue 3.0 —— Watch 與 Reactivity 代碼走讀react

若是對源碼查看沒有頭緒的能夠先參見參考文章數組

本篇文章爲梳理 scheduler、 effect、scheduler 與 proxy 之間的關係緩存

本篇文章以一個很簡單小例子打斷點入口開始分析,狀況很單一,僅僅是一個簡單的 object,沒有涉及到組件實例,目的也很簡單:搞清楚三者之間的工做流程、同時熟悉一些概念。app

例子代碼:函數

const { reactive, watch } = Vue
const a = reactive({ name: 'ym' })

watch(() => a.name, (val) => {
  console.log(val)
}, { lazy: true })

setTimeout(() => {
  a.name = 'cjh'
}, 1000)
複製代碼

代碼走讀

咱們將 demo 代碼分爲 3個部分:post

  • 初始化 reactive
  • 初始化 watch
  • 賦值屬性

因此代碼走讀也分爲三個部分,來分別參數這三個過程。性能

第一部分

先用 reactive 初始化了對象 a,因此咱們看看 reactive 初始化過程ui

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
複製代碼

reactive 中有2個對象是須要理解的spa

  • rawToReactive = new WeakMap<any, any>() 普通對象與reactivity對象的映射
  • reactiveToRaw = new WeakMap<any, any>() reactivity對象與普通對象的映射

利用 WeakMap 初始化的弱引用對象,弱引用對象在這裏的好處:調試

  • 避免內存泄漏,即不用手動清除依賴對象的引用
  • 鍵名能夠直接使用對象、減小遍歷查找操做

例如:

const wm = new WeakMap()
let arr = new Array(1024 * 1024)
wm.set(arr, 1)
// 這裏只用將arr的引用去除,而不用再將 wm 所引用的 arr 刪除
arr = null
複製代碼

這麼作是爲了緩存提升查找性能,由於對於一個嵌套對象,是須要遞歸遍歷每個屬性的。

reactive 本質是對 createReactiveObject 的包裹,其中傳入了 mutableHandlers,mutableHandlers 用來定義一個對象的屬性描述符,和 defineProperty 相似,這裏咱們只看 get 和 set。

// get 是由 createGetter 函數建立
function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    // 避免循環引用
    const res = Reflect.get(target, key, receiver)
    // 排除關鍵字
    if (typeof key === 'symbol' && builtInSymbols.has(key)) {
      return res
    }
    // 自動 unwrap 屬性,因此對於一個 reactivity 對象的屬性,咱們直接 obj.property 便可
    if (isRef(res)) {
      return res.value
    }
    // 此處很是關鍵,屬於收集依賴的入口
    track(target, OperationTypes.GET, key)
    // 遞歸處理
    return isObject(res) ? reactive(res) : res
  }
}

// set 
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean {
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // 當前僅當 target 和 調用對象相同時才作處理
  // 關於 receiver 這裏能夠查看個人另一篇文章:熟悉 Proxy
  if (target === toRaw(receiver)) {
    if (!hadKey) {
      trigger(target, OperationTypes.ADD, key)
    } else if (value !== oldValue) {
      // 觸發收集的依賴
      trigger(target, OperationTypes.SET, key)
    }
  }
  return result
}
複製代碼

初始化一個對象時,惟一值得說的就是遞歸對象,爲每個屬性都添加上 proxy,由於 proxy 的層級只有一層。

第二部分

一樣,咱們發現全部的 Api 入口函數都只是內部函數的一個包裝,這樣利於邏輯的單一且反作用分隔。

// 針對 demo 的例子,咱們傳入 doWatch 的有三個參數,恰好對上
function doWatch(source, cb, WatchOptions): StopHandle {
  let getter: Function
  if (isArray(source)) {
    // 保證 getter 拿到的始終是普通對象
    getter = () =>
      source.map(
        s =>
          // 這裏能夠發現 watch 數組時,也會自動 unwrap
          isRef(s)
            ? s.value
            : callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      )
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (cb) {
    // getter with cb
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    // no cb -> simple effect
    getter = () => {
      if (instance && instance.isUnmounted) {
        return
      }
      if (cleanup) {
        cleanup()
      }
      return callWithErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [registerCleanup]
      )
    }
  }

  // 以上咱們能夠看到 watch 的對象有3種
  // 數組
  // ref包裹的對象
  // 回調函數
  // 最後的 else 實際上是錯誤處理
  // callWithErrorHandling 是一個取值包裝函數,用來包裹取值時的錯誤處理

  let oldValue = isArray(source) ? [] : undefined
  // 包裹回調
  // applyCb 是依賴更新後觸發的真正函數
  const applyCb = cb
    ? () => {
        const newValue = runner()
        if (deep || newValue !== oldValue) {
          // cleanup before running cb again
          if (cleanup) {
            cleanup()
          }
          callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
            newValue,
            oldValue,
            registerCleanup
          ])
          oldValue = newValue
        }
      }
    : void 0

  // 定義 scheduler,默認是值更新後再觸發
  // 時機是 nextTick 後即下一個 task 隊列執行以前
  let scheduler: (job: () => any) => void
  scheduler = job => {
    queuePostRenderEffect(job, suspense)
  }

  // 初始化 effect
  const runner = effect(getter, {
    lazy: true,
    // so it runs before component update effects in pre flush mode
    computed: true,
    onTrack,
    onTrigger,
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  })

  // 緩存舊值
  oldValue = runner()

  // 返回中止 watch 的句柄
  return () => {
    stop(runner)
  }
}
複製代碼

watch 的初始化作了 2 件事

  • 定義 getter,獲取真正的值
  • 初始化 effect,同時注入 scheduler

第三部分

a.name = 'cjh' 的賦值,此時會觸發 set 的 trigger

trigger(target, OperationTypes.SET, key)
複製代碼

咱們先來看看 track 收集依賴的函數,由於 trigger 一定是依賴 track 收集後的數據的

export function track(target, type, key) {
  // 初始化 reactive 時觸發 track
  // activeReactiveEffectStack 是不會有值的,那麼這個依賴是何時注入的呢?
  // 思考下,確定是在 watch 初始化的時候
  // 咱們回到 watch,初始化舊值時,咱們初始化了 effect
  // 在 run 函數中,activeReactiveEffectStack.push(effect)
  // 因此這裏的依賴是存在的
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
  if (effect) {
    // targetMap 是proxy遞歸時的用來存放的那層單一對象的鍵值對
    // 其中 key 就是這個對象,值初始化空的 Map
    // depsMap 是一個空的 Map
    let depsMap = targetMap.get(target)
    // dep 是一個 Set
    let dep = depsMap.get(key!)
    if (dep === void 0) {
      depsMap.set(key!, (dep = new Set()))
    }
    if (!dep.has(effect)) {
      // dep 用來存放全部對 watch 的 getter
      dep.add(effect)
      // ⚠️這一步不知道緣由???
      // ️️⚠️包括 targetMap 什麼時候被 set ???否則的話 depsMap 永遠是個空的 Map
      effect.deps.push(dep)
    }
  }
}
複製代碼

咱們再來看看 trigger 函數

export function trigger(target, type, key, extraInfo) {
  // 由 trigger 添加的 dep 依賴的 Set
  const depsMap = targetMap.get(target)
  // 空的 Set
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  
  // schedule runs for SET | ADD | DELETE
  if (key !== void 0) {
    addRunners(effects, computedRunners, depsMap.get(key))
  }
  // also run for iteration key on ADD | DELETE
  // 這是針對數組的 Proxy, push時會觸發屢次的 hack:一次是下標賦值,一次是 length 賦值
  if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
    const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
    addRunners(effects, computedRunners, depsMap.get(iterationKey))
  }

  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  effects.forEach(run)
}
複製代碼

trigger 函數作了 1 件事:添加 addRunners runners ,再調用它們。

接下來再看看 addRunners

function addRunners(effects, computedRunners, effectsToAdd) {
  if (effectsToAdd !== void 0) {
    effectsToAdd.forEach(effect => {
      // effect 就是 trigger 裏的 dep 數組

      if (effect.computed) {
        // 這裏應該是用來區分 computed 函數初始化的依賴
        computedRunners.add(effect)
      } else {
        // 這是普通的 watch 依賴數組
        effects.add(effect)
      }
    })
  }
}
複製代碼

addRunners 區分 computed 分別爲 2 個數組 push 值。咱們這裏的 demo 沒有 computed,因此最終就是 forEach 數組調用 scheduleRun。

scheduleRun 就是調用 watch 初始化時的 applyCb

effect.scheduler(effect)
複製代碼

而初始化時

effect.scheduler = job => {
  queuePostRenderEffect(job, suspense)
}
複製代碼

咱們看看 queuePostRenderEffect 函數,本質是調用的 queuePostFlushCb

export function queuePostFlushCb(cb: Function | Function[]) {
  if (Array.isArray(cb)) {
    // 這種寫法比 concat 優雅。。。
    postFlushCbs.push.apply(postFlushCbs, cb)
  } else {
    postFlushCbs.push(cb)
  }
  if (!isFlushing) {
    nextTick(flushJobs)
  }
}
複製代碼

queuePostFlushCb 函數也比較簡單,收集回調函數,再 nextTick 後 flushJobs。

咱們能夠發現 scheduler 中有 2 個隊列:

  • queue
  • postFlushCbs

對應的添加函數

  • queueJob
  • queuePostFlushCb

很顯然,這是對應的 2 種更新時機的回調,而觸發這些回調都是由 flushJobs 完成:

function flushJobs(seenJobs?: JobCountMap) {
  isFlushing = true
  let job
  while ((job = queue.shift())) {
    try {
      // queueJob
      job()
    } catch (err) {
      handleError(err, null, ErrorCodes.SCHEDULER)
    }
  }
  flushPostFlushCbs()
  isFlushing = false
  // some postFlushCb queued jobs!
  // keep flushing until it drains.
  if (queue.length) {
    flushJobs(seenJobs)
  }
}
複製代碼

最後咱們回到回調函數 applyCb

() => {
  // 獲取最新值
  const newValue = runner()
  // 若是值發生了改變
  if (deep || newValue !== oldValue) {
    // 觸發回調函數
    // 能夠看到回調函數也能夠是一個 Promise
    callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
      newValue,
      oldValue,
      registerCleanup
    ])
    oldValue = newValue
  }
}
複製代碼

總結

再回過頭來看這三個部分及它們的做用

  • reactivity 的做用在於處理對象的 proxy,在每一個取值操做的地方 track。track 有多種來源:一種是普通的取值,一種是依賴取值,依賴取值時會在 activeReactiveEffectStack 數組中 push 依賴 effect。這其實就完成了初始化。
  • watch 的巧妙之處在於取舊值添加依賴,因此能明白爲何第一個參數只能傳回調函數了。建立 effect 的同時,對回調進行 scheduler 處理,scheduler 顯然是根據 flush 時機來區分的。
  • scheduler 相對簡單了,目前來看只是對回調的收集分類與觸發作了處理。

Vue 3中還有 ref 和 computed ,我以爲熟悉完 reactivity 和 watch 後基本就能理解所有了。

固然其中還有不少細節沒有說到也不知道,由於必須有相應的場景你才能明白它這麼寫的做用,若是你連應用的場景都考慮不到或者說都沒用過,強行去理解就沒有太大意義了。

未完待續。

參考文章

相關文章
相關標籤/搜索