Vue 响应式源码 1

972 阅读13分钟

Vue 响应式机制使用了观察者模式,数据源作为发布者,更新后需要通知自身的观察者(deps),触发相应的事件。

实现方式上,Vue 2 使用 Object.defineProperty (重写对象属性的 getter 和 setter 方法),Vue 3 使用的是 Proxy,都是针对对象。JavaScript 并不能直接检测到变量的变化,只能劫持对象操作。Object.defineProperty 最大的问题是,代理的是对象上的属性,例如 obj.a 的读取和设置,不能拦截到 obj 的新增或删除属性。Proxy 中做了更完整的代理,常用的有 get(获取属性)、set(设置属性,包括新增和修改)、deleteProperty(删除属性)。

Proxy 基本使用:

let temp = 1
temp = 2 // js 没有提供机制检测这种修改let val = "aa"
const obj1 = {
  a: val,
};
// 代理 obj1 中属性 a
Object.defineProperty(obj1, "a", {
  get() {
    getterHandler() // do something
    // 不能直接获取 obj1.a
    // 会再次触发 getter
    return val;
  },
  set(newValue) {
   if(val !== newValue){
     setterHandler() // do something
     // 直接设置 obj1.a
     // 会重复触发 setter
     val = newValue
   };
  },
  enumerable: true,
  configurable: true,
});
​
const obj2 = {
  a: "aa",
};
​
const proxyObj = new Proxy(obj, {
  get(target, key) {
    console.log('getter', key)
    getterHandler() // do something
    return Reflect.get(target, key);
  },
  // 即使新增 key 也能检测到
  // 代理针对的是对象
  set(target, key, value) {
    // 严格模式下 return falsish 会报错
    setterHandler() // do something
    return Reflect.set(target, key, value);
  },
});

使用 Object.defineProperty,我们操作的就是原数据,为了避免重复触发 getter 和 setter,每个属性需要映射一个值。而 proxy 会返回一个代理对象,开发者操作修改的是代理对象。代理对象本身并不存储值,只是拦截操作做额外处理,最后使用 Reflect 将操作反射到 target 上。上面注释的 do something 就是可以实现响应式的地方,响应式数据需要做额外处理,性能上有损耗,实践中不能无脑使用响应式。这样我们的工作就涉及几步:

  • 分析响应式使用的场景。
  • 设置响应式数据源。
  • 设置响应式数据改变后的回调。

后面两点,实现上要比示例复杂。

先来看一段代码:

const test = ref(22)
​
console.log('set up', test.value)
​
setTimeout(() => {
  test.value = 33
  console.log('value changed', test.value)
}, 1000)
​
testChange() // 调用方法,
function testChange() {
  console.log('inner test', test.value)
}
// 最终结果 
// set up 22
// inner test 22
// value changed 33
// 没有调用 testChange

上述代码,定义了一个响应式数据 test。1 秒后会修改 test 的值,响应式数据变更触发更新。接来下调用 testChange 方法,成功打印结果,方法并没有问题。1 秒后 test 更新,没有再次触发 testChange。这里缺少了让响应式数据和更新操作产生联系的关键步骤。

开发角度考虑,将定义响应式和更新操作收集解耦,是很重要的。功能开发,经常是迭代累加的。一个基础数据,后续可能添加各种功能。这里就需要一种机制,检测到当前需要收集的更新操作。

如何做到这一点,Vue 官网有基础示例:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 获取位置,追踪依赖
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      // 修改时触发更新
      trigger(target, key)
    }
  })
}
​
function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

响应式变量,一定有使用的地方,使用就会触发 getter,Vue 会在 getter 中追踪依赖。随后再次设置值,就会触发 setter,Vue 会在这里触发响应式操作更新页面状态(Vue 称之为副作用,effect)。

流程:1. 运行 effect 方法,方法会使用响应式数据,触发 getter 收集 deps(实际是订阅者,subscriber),就是当前运行的 effect。2. 修改响应数据时,会触发 setter,setter 中通知 deps 主动触发更新操作。3. 更新操作会再次获取数据,触发 getter 重新收集 deps。三步循环执行。

第三步重新收集 dep 很重要,不要以为这和第一步重复了。假设有这样一个 effect:

// 示例伪代码
const effect = () => {
  if (dep1) {
    do something with dep2
  }
}

effect 使用到两个响应式数据 dep1 和 dep2。第一次 dep1 是 true,运算需要使用 dep2 ,此时 effect 同时依赖于 dep1 和 dep2。接着 dep1 变成 false,执行 effect 没有问题。这一次没有用到 dep2,后续再修改 dep2,都不应通知 effect。下一次 dep2 修改时,会重新收集订阅,effect 不在其中,就不应该执行 effect,同时要将 effect 从订阅列表移除。

ref 和 reactive

Vue 中常用的响应式 API 包括 ref 和 reactive 两种,直接看源码实现。

ref

export function ref(value?: unknown) {
  return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
  // 重复创建拦截
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}
// Ref class
class RefImpl<T> {
  private _value: T // 保存的值
  private _rawValue: T // 保存的原始值,响应式变量的 target
​
  public dep?: Dep = undefined // 订阅列表
  public readonly __v_isRef = true
​
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // Object 会转成 reactive
    this._value = __v_isShallow ? value : toReactive(value)
  }
  // get 关键字定义 getter
  get value() {
    // 收集依赖
    trackRefValue(this)
    // 返回值
    return this._value
  }
  // set 关键字定义 setter
  // value 是属性名
  set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    // 使用 Object.is 判断是否修改值
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      // 判断是否 shallowRef,shallowRef 不做深层递归代理
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      // 触发更新
      triggerRefValue(this, newVal)
    }
  }
}
​
export function trackRefValue(ref: RefBase<any>) {
  // 当前变量是否需要收集,是否有运行中的 effect
  // 高阶组件中,有父子通讯,可能会造成嵌套
  // 使用 shouldTrack 手动阻止
  if (shouldTrack && activeEffect) {
    // TODO 返回原始值
    // 判断是否有 __v_raw,ref 没有看到这个属性
    // 后续看为什么这里需要 toRaw
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      // 默认创建一个新的 dep
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}
​
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

ref 整体功能和示例代码差不多,setter 增加 lazy 处理。track 和 trigger 是对 trackEffectstriggerEffects 的封装处理,通用方法后续看。

reactive

// 会将生成的 reactive 全部用 WeakMap 保存起来
// key 是 target 被代理对象
// proxy 可以获取到当前访问的 target
// 找到对应的 reactive 对象
export const reactiveMap = new WeakMap<Target, any>()
​
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    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>
) {
  // 简单类型不能做响应式处理
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  // 判断是否为合法的响应式数据源类型
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    // 判断数据源类型,适用不同的 handlers
    // Object Array 使用 baseHandlers
    // Map Set WeakMap WeakSet 使用 collectionHandlers
    // 这两类数据访问自身属性形式不一样,做了区分
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 保存 map
  proxyMap.set(target, proxy)
  return proxy
}

接下来是重点,代理对象的 getter,setter 方法。Object 和 Array 可以直接设置、添加属性,例如 arr[0] = 1,需要拦截 getttersetter。Map 和 Set 不一样,操作都是调用的实例方法,例如 map.delete(key)map.set(key, value),实例方法都会触发 getter,通过方法名进行拦截。先看最常用的 Object 和 Array:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
// has 和 ownKeys 比较简单,就是代理操作,同时追踪收集依赖
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    // track 后面两个参数用于 dev 展示调试信息
    // 整个 track 方法后面再看
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}
​
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}
// deleteProperty 删除属性,需要触发更新
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

接下来看 getter,getter 是 createGetter 返回的一个闭包方法:

// 所有 reacrive 用的都是同一个 get 方法
// 避免每个对象新建一个闭包占用内存
// 通过闭包也保存了共有的 isReadonly shallow 信息
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 特殊属性的代理
    // 体现 proxy 特点,可以访问 target 中没有的属性
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
​
    const targetIsArray = isArray(target)
    // 数组方法重写
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
​
    const res = Reflect.get(target, key, receiver)
    // 不需要代理的属性,有一些 builtin 方法,也需要获取属性对比
    // 这部分不涉及视图更新
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
​
    if (!isReadonly) {
      // 追踪数据
      track(target, TrackOpTypes.GET, key)
    }
​
    if (shallow) {
      // 浅层代理,直接返回,无需递归
      return res
    }
​
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
​
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      // 默认将内部访问数据,也会转换成响应式
      // 也就是获取时再转换,定义时不会转换
      // reactive 中没有使用的属性不会转换成响应式
      return isReadonly ? readonly(res) : reactive(res)
    }
​
    return res
  }
}
​
// 重写的数组方法
function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  // 查找时,可能会有响应式数据,在原数据中是查找不到的
  // 需要做额外处理
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '')
      }
      // we run the method using the original args first (which may be reactive)
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)
  // 改变数组长度的方法,内部可能会获取数组长度,形成 infinite loops
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as  const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 暂停依赖收集
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      // 恢复
      resetTracking()
      return res
    }
  })
  return instrumentations
}

setter:

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    // readonly 属性修改拦截,readonly 数据不能直接修改
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow && !isReadonly(value)) {
      // 展平数据
      if (!isShallow(value)) {
        value = toRaw(value)
        oldValue = toRaw(oldValue)
      }
      // 子元素是响应式数据,触发自身的更新
      // shallow 数据无需继续递归
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    // 是否有对应 key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, 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(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 存在为修改
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

以上就是 Vue 在属性劫持中做的额外处理。

Map、Set 对应的方法在 collectionHandlers.ts 中,只劫持了 getter,getter 中对调用方法做了处理,值得注意的是 keys()values() 等方法,需要手动创建迭代器。

track 和 trigger 逻辑

ref 中使用的是 trackEffectstriggerEffects,reactive 则使用了 tracktrigger 方法。来到 effect.ts 文件中,会发现 tracktrigger 是对 trackEffectstriggerEffects 方法的二次封装:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // shouldTrack 标识是否需要追踪,如上面所示
  // 有些情况需要阻止响应式,比如,array 一些 builtin 方法
  // 本身需要获取 length,可能会触发 loop
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      // Dep 本身是一个 Set 增加了两个标识位
      // w 标识是否已被追踪
      // n 标识新增的 dep
      depsMap.set(key, (dep = createDep()))
    }
​
    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined
​
    trackEffects(dep, eventInfo)
  }
}
​
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
    return
  }
​
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    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
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
​
  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) {
        // dep 本质是一个 Set,保存 effects
        // 展开后就是 effects
        // w & n 的标志已经没有用了
        // 这里的一定是有更新的
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

再看一下基础的方法:

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 默认 false 重复拦截
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      // 新增的依赖需要加入当前 effect
      // 如果已经添加过,即 wasTracked 无需多次添加
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }
​
  if (shouldTrack) {
    // 添加依赖
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}
​
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()
      }
    }
  }
}

总结

整理一下,reactive 和 ref 的响应式逻辑。

调用方法,首先会创建一个代理对象,代理对数据的操作。ref 和 reactive 转换方式略有区别,ref 使用 Vue 自己封装的 RefImpl 类做代理,其中实现了 gettersetter。ref 通常适用于简单数据类型,没有做额外的嵌套判断,如果传入对象,会转成 reactive 处理。reactive 中维护了一个 WeakMap 对象 reactiveMap,用来避免嵌套造成的 infinity loop(和深拷贝实现类似)。

在 reactive 中,有几个属性(ReactiveFlags)体现了 proxy 的特点。如果打印 reactive 返回结果,就是一个普通的 proxy 对象,只有 target、handlers 两个属性,ReactiveFlags 对应的属性是通过拦截计算得到的。

reactive 在 track 处理中,会创建 target -> key -> dep 的链条,dep 中有保存相关的 effects。这里有两种 map,KeyToDepMap 保存 target 属性到 dep 的映射关系,targetMap 存放 target 到 KeyToDepMap 的映射。targetMap 使用 WeakMap,某个 target 被 GC 清理后,WeakMap 中的键引用将不存在,相应值(KeyToDepMap)没有额外引用,也会被清理。dep 是一个 Set,track 过程会将当前正在运行的 effect(activeEffect)添加进去。trigger 时,会依次调用 dep 中存放的 effect。ref 的 track 处理更加简单,dep 是在 RefImpl 实例内部维护的,track 时直接将 activeEffect 添加进 dep 即可。

trigger 过程最终就是要执行 effect。ref 调用保存在 dep 中 effects 即可,reactive 需要根据之前存储的 target -> key -> dep 查找,找到所有的 effects 执行即可。

这个就是典型的观察者模式,每个响应式变量是一个主题,内部通过 deps 维护一个观察者(effects)列表,每次发生变化,通知 effects(调用 effect 的 run 方法)。

WeakMap、WeakSet 使用的是弱引用,不会阻止对象被 GC 清理,合法的数据只能是对象或者符号(Symbol)。WeakMap 的键被清理后,如果值没有别的引用,也会被清理。

const weakTest = new WeakMap()
​
let obj = { text: 'pick a place' }
​
weakTest.set(obj, true) // 此时 weakTest 有一个 obj 为 key 的键值对console.log(weakTest);
// 清除原有对象的引用
// 刚清除引用,不会有变化
// 等到下一轮 GC 完成,对象被清除后
// 会发现 weakTest 没有元素了
// chrome 可以在 performance 手动触发 GC
obj = null 

到这里响应式数据定义已经很清晰了,还有一个问题就是 Effect 做了什么。可以看到收集订阅时都判断了,是否有激活的 effect。