深入 Vue3 源碼,學習響應式原理

Vue2 響應式原理

學過 Vue2 的話應該知道響應式原理是由 Object.defineProperty 對數據進行劫持,再加上訂閱發佈,實現數據的響應的。

Object.defineProperty 存在以下幾個方面的缺點。

  1. 初始化的時候需要遍歷對象的所有屬性進行劫持,如果對象存在嵌套還需要進行遞歸。導致初始化的時候需要消耗一些資源用於遞歸遍歷。

  2. 從上面可以推導出 Vue2 對於新增、刪減對象屬性是無法進行劫持,需要通過 Vue.set、Vue.delete 進行操作。

  3. 每個調用者會生成一個 Watcher,造成內存佔用。

  4. 無法劫持 Set、Map 對象。

Vue3 響應式原理

針對以上問題,Vue3 改用了 ES6 原生的 Proxy 對數據進行代理。

Proxy 基本用法如下:

const reactive = (target) => {
  return new Proxy(target, {
    get(target, key) {
      console.log("get: ", key);
      // return Reflect.get(target, key);
      return target[key];
    },

    set(target, key, value) {
      console.log("set: ", key, " = ", value);
      // Reflect.set(target, key, value);
      target[key] = value;
      return value;
    },
  });
};

var a = reactive({ count: 1 });
console.log(a.count);

a.count = 2;
console.log(a.count);

// log 輸出
// get:  count
// 1
// set:  count  =  2
// get:  count
// 2

如此便可檢測到數據的變化。接下來只需在 get 進行收集依賴,set 通知依賴更新。

接下來還需藉助 effect、track 和 trigger 方法。

effect 函數傳入一個回調函數,回調函數會立即執行,並自動與響應式數據建立依賴關係。

track 在 proxy get 中執行,建立依賴關係。

trigger 響應式數據發生變化時,根據依賴關係找到對應函數進行執行。

代碼實現如下:

const reactive = (target) => {
  return new Proxy(target, {
    get(target, key) {
      console.log("[proxy get] ", key);
      track(target, key);
      // return Reflect.get(target, key);
      return target[key];
    },

    set(target, key, value) {
      console.log("[proxy set]  ", key, " = ", value);
      // Reflect.set(target, key, value);
      target[key] = value;
      trigger(target, key);
      return value;
    },
  });
};

// 用於存放 effect 傳入的 fn,便於 track 時找到對應 fn
const effectStack = [];

// 用於保存 響應式對象 和 fn 的關係
// {
//   target: {
//     key: [fn, fn];
//   }
// }
const targetMap = {};

const track = (target, key) => {
  let depsMap = targetMap[target];
  if (!depsMap) {
    targetMap[target] = depsMap = {};
  }
  let dep = depsMap[key];
  if (!dep) {
    depsMap[key] = dep = [];
  }

  // 建立依賴關係
  const activeEffect = effectStack[effectStack.length - 1];
  dep.push(activeEffect);
};

const trigger = (target, key) => {
  const depsMap = targetMap[target];
  if (!depsMap) return;
  const deps = depsMap[key];
  // 根據依賴關係,找出 fn 並重新執行
  deps.map(fn => {
    fn();
  });
};

const effect = (fn) => {
  try {
    effectStack.push(fn);
    fn();
  } catch (error) {
    effectStack.pop(fn);
  }
};

var a = reactive({ count: 1 });

effect(() => {
  console.log("[effect] ", a.count);
});

a.count = 2;

// log 輸出
// [proxy get]  count
// [effect]  1
// [proxy set]   count  =  2
// [proxy get]  count
// [effect]  2

以上代碼並不是 Vue3 的源碼,而是 Vue3 響應式的原理,相比起 Vue2 要更加簡單。

執行順序爲

  1. 調用 reactive 代理響應式對象;
  2. 調用 effect ,會將 fn 保存至 effectStack,在執行 fn 時會觸發 Proxy 的 get;
  3. 從 Proxy 的 get 觸發 track,將數據與 fn 建立關係;
  4. 修改響應式數據,觸發 Proxy 的 set;
  5. 從 Proxy 的 set 觸發 trigger,從而找出對應的 fn 並執行。

弄清楚原理再去看源碼會簡單很多,下面我們一起去看下源碼。

Vue3 響應式源碼

Vue3 的響應式是一個獨立的模塊,不依賴框架,甚至可以在 React、Angular 中使用。

reactive 函數位於 packages/reactivity/src/reactive.ts

// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
    // ...
  const proxy = new Proxy(
    target,
    // 對 Set、Map 的集合使用 collectionHandlers(mutableCollectionHandlers)
    // 普通對象使用 baseHandlers(mutableHandlers)
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // ...
  return proxy
}

接下來看下 mutableHandlers

// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

看下 get 和 set

// packages/reactivity/src/baseHandlers.ts
const get = /*#__PURE__*/ createGetter()
// ...
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      // 調用 track 建立依賴關係
      track(target, TrackOpTypes.GET, key)
    }

    // ...
    return res
  }
}
// packages/reactivity/src/baseHandlers.ts
const set = /*#__PURE__*/ createSetter()
// ...
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    // ...
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 調用 trigger 通知依賴重新執行
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 調用 trigger 通知依賴重新執行
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

接下來再看下 track

// packages/reactivity/src/effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined

  trackEffects(dep, eventInfo)
}


export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
    // ...
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

上半部分與我們自己實現的邏輯很類似,先找出 dep 如果不存在則創建,只不過 Vue 使用的是 Map 和 Set(createDep 返回值爲 Set)。

然後是 trackEffects,關鍵代碼就是 dep 和 activeEffect 互相保存,我們的做法只是將 activeEffect 存入 dep 。

接下來看看 set 中調用的 trigger。

// packages/reactivity/src/effect.ts
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    // 沒有被 track 收集到,直接返回
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // 清空依賴,需要觸發與 target 關聯的所有 effect
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 修改數組的 length 時對應的處理
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // 修改、新增、刪除屬性時執行
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    // 往 deps 中添加迭代器屬性的 effect
    switch (type) {
      // ...
    }
  }
  
  // 以上操作則是爲了取出 deps (targetMap[target][key])

  // 下面的操作則是將 deps 中的 effect 取出並執行
  // 開發時還會傳入 eventInfo
  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

// 執行 effect
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  }
}

trigger 函數看似很長,其實可以簡化成我們的例子進行理解,無非就是取出對應的 deps ,遍歷出 deps 中的 effect 並執行。

接下來就該看看 effect 函數的實現了。

// packages/reactivity/src/effect.ts
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  // 調用 ReactiveEffect 對進行封裝
  const _effect = new ReactiveEffect(fn)
  // ...
  // 判斷是否有 options.lazy
  // lazy 爲 true 不會立即執行
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}


export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []

  // can be attached after creation
  computed?: boolean
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        // 執行時將當前的 effect 存入 effectStack
        // 並賦值給 activeEffect
        // 在 track 時獲取
        effectStack.push((activeEffect = this))
        enableTracking()
        // ...
        return this.fn()
      } finally {
        // ...
        resetTracking()
        effectStack.pop()
        const n = effectStack.length
        // 從 effectStack 繼續取出上一個的 activeEffect 繼續執行
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }

  stop() {
    // ...
  }
}

我們在使用 effect 時,會將我們傳入的函數經過 ReactiveEffect 封裝,如果我們沒傳入 { lazy: true } 則會立即執行 run 函數。

run 函數就是先賦值 activeEffect 並存入 effectStack,然後執行我們傳入的回調函數。

執行回調函數的過程會觸發 Proxy 的 get,get 又會觸發 track 進行依賴收集。

執行完成後將 activeEffect 從 effectStack pop出去,並取出上一個 activeEffect 繼續執行。

爲什麼要用 effectStack ?

假如我們在 effect 中使用了 computed,Vue 需要先執行計算出 computed。

computed 內部也會調用 ReactiveEffect,所以需要將 computed 的 effect 存入 effectStack ,當 computed 計算完成之後,則從 effectStack pop 出去,繼續執行我們的 effect。

如此便完成依賴收集,當響應式數據發生變化時則會觸發 trigger,重新執行我們在 effect 中傳入的回調函數。

修改響應式數據爲什麼頁面會自動更新?還記得上篇文章<深入 Vue3 源碼,學習初始化流程>介紹的 setupRenderEffect 嗎?

這個方法也是利用了 ReactiveEffect,在 mount 的時候會觸發 setupRenderEffect 執行進而觸發 patchpatch 的過程中會使用響應式數據,從而建立依賴關係,當響應式數據發生變化時會重新執行 setupRenderEffect,後面就進入 diff 了,下篇文章在詳細展開 diff。

結語

以上便是 Vue3 的響應式原理,只要瞭解了原理,能用自己的語言清晰的描述出來,面試肯定能增加成功率。

好了,這篇文章就水到這裏吧。如有錯誤的地方,希望還能在評論區指出,感謝!

下篇文章將解析 Vue3 的 diff 算法,如果有興趣的話別忘了關注我呀,我們一起學習、進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章