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 时找到对应 fnconst 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.tsexport 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.tsexport const mutableHandlers: ProxyHandler<object> = {  get,  set,  deleteProperty,  has,  ownKeys}

看下 get 和 set

// packages/reactivity/src/baseHandlers.tsconst 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.tsconst 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.tsexport 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.tsexport 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))    }  }}// 执行 effectexport 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.tsexport 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 算法,如果有趣味的话别忘了关注我呀,咱们一起学习、提高。