Skip to content

响应式基础 API 之 reactive

本篇文章主要介绍一个 API 的实现:reactive。 对于响应式系统里,最主要的就是两个 API:reactiveeffect, 分别对应代理包装添加响应式副作用的用途。 其他的主要 APIs(readonly, shallow 等功能)也都是基于着两个 API 实现的。

reactiveeffect也是两个互相关联的 API,但在这篇文章中主要先通过reactive看看基本的响应式流程,effect里相关的依赖收集触发副作用的具体细节咱们在下一篇介绍effect中再说。

基本介绍

开始之前还是先简单看看如何使用的,除了文档,或许看测试用例也是个不错的选择。

一句话概括就是响应式对象的键值更新可以自动触发依赖这个键值的副作用(就是一个函数的执行)

ts
it('should handle multiple effects', () => {
  let dummy1, dummy2
  const counter = reactive({ num: 0 })
  effect(() => (dummy1 = counter.num))
  effect(() => (dummy2 = counter.num))

  expect(dummy1).toBe(0)
  expect(dummy2).toBe(0)
  counter.num++
  expect(dummy1).toBe(1)
  expect(dummy2).toBe(1)
})
it('should handle multiple effects', () => {
  let dummy1, dummy2
  const counter = reactive({ num: 0 })
  effect(() => (dummy1 = counter.num))
  effect(() => (dummy2 = counter.num))

  expect(dummy1).toBe(0)
  expect(dummy2).toBe(0)
  counter.num++
  expect(dummy1).toBe(1)
  expect(dummy2).toBe(1)
})

上面的用例测试了当counter对象num键的值改变时,两个 effects 就会响应式地执行。

在这里咱们明确一些名词表述:

  • 依赖:收集依赖时的依赖指的是 effect 函数里依赖的响应式对象对应的键值,上面用例中,两个 effects 都有一个counter.num依赖。
  • 副作用:即每个 effect 函数里的回调,上面用例中counter.num有两个副作用。

要点概括

响应式基本流程结构

首先来看看基本的响应式流程结构:

null
  • 首先通过reactive将传入的 obj 对象进行代理,后续更改的都是返回的 proxied 对象。
  • 触发对象更新set会找到对应依赖的副作用并执行。
  • 当使用effect方法时会立即首次执行回调,通过get同步此effect回调里依赖的相互关系,这样下次触发对象更新时就会执行此effect了。
  • 每次执行effect回调之前都会将当前effect赋值为一个全局activeEffect上,在track中同步依赖和副作用的关系。

在以上的流程里,我们不免会有一些问题:

  • track 过程如何同步依赖和副作用的关系?
  • trigger 过程如何找到对应依赖的副作用?
  • 循环触发响应副作用怎么处理?
  • 如何自定义控制副作用在依赖更新后的执行时机(scheduler)?

这些问题我们都会在源码中找到答案。

具体实现

createReactiveObject

ts
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
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
  )
}
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
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
  )
}

以上就是具体实现,done, 非常简洁 😝

好吧,主要就是来看createReactiveObject函数的实现

ts
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,因为readonly就是在proxied对象基础之上处理的
  // target[ReactiveFlags.RAW]存的就是original 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 specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
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,因为readonly就是在proxied对象基础之上处理的
  // target[ReactiveFlags.RAW]存的就是original 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 specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject函数主要做了一下几件事:

  • 限制只有对象类型的数据才能被代理成响应式
  • 如果已经是响应式对象,则直接返回原对象
  • 如果对同一个对象重复创建响应式,则直接从对应的 proxyMap 中获取对应的 proxied 对象
  • 如果不是可代理的数据类型,则直接返回原对象

target flags

其中原数据对象target是一个可能有以下属性的对象,通过这些 flag 属性就可以来判断当前target对象的状态。

ts
export interface Target {
  [ReactiveFlags.SKIP]?: boolean // 跳过响应式代理处理
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.IS_SHALLOW]?: boolean
  [ReactiveFlags.RAW]?: any // 原始对象
}
export interface Target {
  [ReactiveFlags.SKIP]?: boolean // 跳过响应式代理处理
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.IS_SHALLOW]?: boolean
  [ReactiveFlags.RAW]?: any // 原始对象
}

可代理数据类型

对于可代理的数据类型主要有三类:

  • COMMON: 普通对象和数组
  • COLLECTION: Map, Set, WeakMap 和 WeakSet
  • INVALID: 不可代理的数据类型

其中标记了ReactiveFlags.SKIP不能扩展属性的对象也属于INVALID类型。

ts
function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}
function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

mutableHandlers

createReactiveObject最主要的部分其实是传入的baseHandlerscollectionHandlers,即 proxy 的配置函数,包括get/set方法的实现。

vue3里目前的 handlers:mutableHandlers, readonlyHandlersshallowReactiveHandlers,以及mutableCollectionHandlers, shallowCollectionHandlers, readonlyCollectionHandlersshallowReadonlyCollectionHandlers

对于reactive方法,传入的是mutableHandlersmutableCollectionHandlers

这里我们主要看看mutableHandlersget/set的实现。

get

ts
get(target: Target, key: string | symbol, receiver: object) {
  const isReadonly = this._isReadonly,
    shallow = this._shallow
  // 处理标记的键值ReactiveFlags
  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 (
    // 获取原始值,此时target就是原始值,直接从对应的Map获取返回
    key === ReactiveFlags.RAW &&
    receiver ===
      (isReadonly
        ? shallow
          ? shallowReadonlyMap
          : readonlyMap
        : shallow
          ? shallowReactiveMap
          : reactiveMap
      ).get(target)
  ) {
    return target
  }

  const targetIsArray = isArray(target)

  if (!isReadonly) {
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 键值为数组方法时,从我们自己patched的数组方法中获取对应的方法
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // edge case
    if (key === 'hasOwnProperty') {
      return hasOwnProperty
    }
  }

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

  // 内置Symbol和一些不能track(不应该响应式)的键值,不进行track,也不需要递归添加响应式
  // isNonTrackableKeys: __proto__, __v_isRef, __isVue
  if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
    return res
  }

  // readonly 不用track,因为其本身就不能被更新,所以也不会有后续的trigger。
  if (!isReadonly) {
    track(target, TrackOpTypes.GET, key)
  }

  // shallow不需要递归添加响应式
  if (shallow) {
    return res
  }

  // 对于ref自动解构,除了数组元素和整数key
  if (isRef(res)) {
    // ref unwrapping - skip unwrap for Array + integer key.
    return targetIsArray && isIntegerKey(key) ? res : res.value
  }

  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.
    // 在一开始new Proxy时,代理只是顶层对象,对于深层对象是没有执行响应式的(Proxy不会deep),比如proxyMap中是找不到的;
    // 所以我们需要lazily将深层对象reactive/readonly一下
    return isReadonly ? readonly(res) : reactive(res)
  }

  return res
}
get(target: Target, key: string | symbol, receiver: object) {
  const isReadonly = this._isReadonly,
    shallow = this._shallow
  // 处理标记的键值ReactiveFlags
  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 (
    // 获取原始值,此时target就是原始值,直接从对应的Map获取返回
    key === ReactiveFlags.RAW &&
    receiver ===
      (isReadonly
        ? shallow
          ? shallowReadonlyMap
          : readonlyMap
        : shallow
          ? shallowReactiveMap
          : reactiveMap
      ).get(target)
  ) {
    return target
  }

  const targetIsArray = isArray(target)

  if (!isReadonly) {
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 键值为数组方法时,从我们自己patched的数组方法中获取对应的方法
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // edge case
    if (key === 'hasOwnProperty') {
      return hasOwnProperty
    }
  }

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

  // 内置Symbol和一些不能track(不应该响应式)的键值,不进行track,也不需要递归添加响应式
  // isNonTrackableKeys: __proto__, __v_isRef, __isVue
  if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
    return res
  }

  // readonly 不用track,因为其本身就不能被更新,所以也不会有后续的trigger。
  if (!isReadonly) {
    track(target, TrackOpTypes.GET, key)
  }

  // shallow不需要递归添加响应式
  if (shallow) {
    return res
  }

  // 对于ref自动解构,除了数组元素和整数key
  if (isRef(res)) {
    // ref unwrapping - skip unwrap for Array + integer key.
    return targetIsArray && isIntegerKey(key) ? res : res.value
  }

  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.
    // 在一开始new Proxy时,代理只是顶层对象,对于深层对象是没有执行响应式的(Proxy不会deep),比如proxyMap中是找不到的;
    // 所以我们需要lazily将深层对象reactive/readonly一下
    return isReadonly ? readonly(res) : reactive(res)
  }

  return res
}

get方法中核心的地方就是track函数的调用。后面我们将 track 和set里的trigger结合起来一起看。

嵌套对象 lazily 响应式
ts
if (isObject(res)) {
  return isReadonly ? readonly(res) : reactive(res)
}
if (isObject(res)) {
  return isReadonly ? readonly(res) : reactive(res)
}

对于以上代码的原因,我们需要知道 proxy 是不会 deep 将嵌套对象进行拦截的。

ts
const obj1 = { name: 'leo' }
const obj2 = { o: obj1 }

const proxy = new Proxy(obj2, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
})

console.log(proxy.o.name) // 会触发obj2的get, 因为在访问o.name是得先访问proxy.o
proxy.o.name = 'git' // 不会触发obj1/2的set, 因为修改的是obj1.name的值,而obj1没有被代理。
const obj1 = { name: 'leo' }
const obj2 = { o: obj1 }

const proxy = new Proxy(obj2, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
})

console.log(proxy.o.name) // 会触发obj2的get, 因为在访问o.name是得先访问proxy.o
proxy.o.name = 'git' // 不会触发obj1/2的set, 因为修改的是obj1.name的值,而obj1没有被代理。

如果我们想要在proxy.o.name = 'git'时也触发set,则需要对 obj1 也进行代理

ts
const obj1 = { name: 'leo' }
const obj2 = { o: obj1 }

const proxy = new Proxy(obj2, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver) // res 就是 obj1
    return new Proxy(res, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        return Reflect.set(target, key, value, receiver)
      }
    })
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
})

console.log(proxy.o.name) // obj1/2的get都会触发
proxy.o.name = 'git' // 只会触发obj1的set, 因为修改的是obj1.name的值,而这条语句我们需要先访问proxy.o, 触发obj2的get, 返回的是代理的obj1。
const obj1 = { name: 'leo' }
const obj2 = { o: obj1 }

const proxy = new Proxy(obj2, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver) // res 就是 obj1
    return new Proxy(res, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        return Reflect.set(target, key, value, receiver)
      }
    })
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
})

console.log(proxy.o.name) // obj1/2的get都会触发
proxy.o.name = 'git' // 只会触发obj1的set, 因为修改的是obj1.name的值,而这条语句我们需要先访问proxy.o, 触发obj2的get, 返回的是代理的obj1。

最后一条语句proxy.o.name = 'git'执行的过程中已经将obj1代理了,因为我们需要先访问proxy.o, 触发obj2get, 所以返回的是代理的obj1

这也是为什么会有以下这条用例

ts
test('setting a property with an unobserved value should wrap with reactive', () => {
  const observed = reactive<{ foo?: object }>({})
  const raw = {}
  observed.foo = raw
  expect(observed.foo).not.toBe(raw) // 访问了observed, 返回了reactive(raw)
  expect(toRaw(observed.foo)).toBe(raw)
  expect(isReactive(observed.foo)).toBe(true)
})
test('setting a property with an unobserved value should wrap with reactive', () => {
  const observed = reactive<{ foo?: object }>({})
  const raw = {}
  observed.foo = raw
  expect(observed.foo).not.toBe(raw) // 访问了observed, 返回了reactive(raw)
  expect(toRaw(observed.foo)).toBe(raw)
  expect(isReactive(observed.foo)).toBe(true)
})
arrayInstrumentations

arrayInstrumentations是重写了的数组方法 Record,包括includes, indexOf, lastIndexOf, push, pop, shift, unshiftslice.

为什么要重写呢?proxy 不是已经可以监听数组的变化了吗?

  • 无法正确对比对象元素(只读方法)
ts
const obj = {}
const arr = [reactive(obj)]

console.log(arr.includes(obj)) // false
console.log(arr.indexOf(obj)) // -1
const obj = {}
const arr = [reactive(obj)]

console.log(arr.includes(obj)) // false
console.log(arr.indexOf(obj)) // -1

可以看到以上会将原始对象的引用和代理对象的应用进行比较,结果肯定是 false。 我们需要重写需要对比元素的方法,进而可以使用原始对象来对比。主要做的其实就是加了一层响应式解构,获取原始对象再进行比较。

ts
;(['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++) {
      // 为每个index收集依赖
      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
    }
  }
})
;(['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++) {
      // 为每个index收集依赖
      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
    }
  }
})
  • 改变数组时 length 的改变可能导致死循环副作用#2138
ts
const arr = reactive([])

watchEffect(() => {
  arr.push(1)
})

watchEffect(() => {
  arr.push(2)
})
const arr = reactive([])

watchEffect(() => {
  arr.push(1)
})

watchEffect(() => {
  arr.push(2)
})

第一次watchEffect回调执行时会收集length依赖,即之后length的改变会触发第一个watchEffect的回调; 接着第二个watchEffect回调执行也会收集length依赖,但在最后改变length时会触发第一个watchEffect的回调,改变length,然后又触发第二个watchEffect的回调,无穷匮也。

watchEffect 是实际提供给用户使用的 API,effect主要是内部使用的更加灵活的 API,具体区别在这里先不赘述了。

ts
;(['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
  }
})
;(['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
  }
})

我们只要在执行这些 mutable 方法时暂停依赖收集即可,因为这些方法是修改数组,并没用用到对应的值(get),所以本就不应该收集依赖。 至于如何暂停收集依赖,后面会在effect方法的实现中体现。

这里我们简单总结一下get做的事情:

  • 处理标记的键值 ReactiveFlags:当获取这些键值时,我们直接返回对应的值即可,不需要 track;注意这些键值是不存在代理对象上的,只是用户访问时我们在get中拦截返回对应的值
  • 对于数组,当键值是数组的某些方法名时,从 patched 的arrayInstrumentations中获取对应的方法。
  • 对于Symbol类型键值,一些 builtInSymbols(arguments, caller)以及不能 track 的键值是需要跳过 track 的。
  • 对于readonly,是不需要 track 的,因为其本身就不能被更新,所以也不会有后续的 trigger。
  • 对于shallow,是不需要递归添加响应式。
  • 对于获取值是ref的,自动解构再返回值,除了数组元素和整数 key。
  • 对于获取值就是个非响应式对象,为其添加响应式/readonly 再返回。

set

ts
set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  let oldValue = (target as any)[key]
  // oldValue: readonly & ref, newValue: non-ref, 不允许更新值
  // 比如 1. 当newValue是ref时,不管oldValue是否是readonly都是允许更新的,这样对应键值的readonly就被取消了
  // 2. 当然,如果newValue和oldValue都是ref, 且oldValue是readonly,则不允许了
  if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
    return false
  }
  if (!this._shallow) { // 这个是在createReactiveObject是设置的值,如果是shallow mode则为true
    // 在非shallow mode,newValue不是shallow也不是readonly时,需要先将new/old value变成原始值
    if (!isShallow(value) && !isReadonly(value)) {
      oldValue = toRaw(oldValue)
      value = toRaw(value)
    }
    // 不是数组响应式对象时,如果oldValue是ref,且newValue不是ref,直接更新oldValue的值即可
    // 因为oldValue的ref会自动触发更新
    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
  }

  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
  // 这里target !== toRaw(receiver)的情况我暂时没有想出来。。。
  if (target === toRaw(receiver)) {
    // 这了新增和更新的触发需要进行区分,因为新增我们需要处理一些iteration key的effects,具体后面trigger时会分析。
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
  }
  return result
}
set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  let oldValue = (target as any)[key]
  // oldValue: readonly & ref, newValue: non-ref, 不允许更新值
  // 比如 1. 当newValue是ref时,不管oldValue是否是readonly都是允许更新的,这样对应键值的readonly就被取消了
  // 2. 当然,如果newValue和oldValue都是ref, 且oldValue是readonly,则不允许了
  if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
    return false
  }
  if (!this._shallow) { // 这个是在createReactiveObject是设置的值,如果是shallow mode则为true
    // 在非shallow mode,newValue不是shallow也不是readonly时,需要先将new/old value变成原始值
    if (!isShallow(value) && !isReadonly(value)) {
      oldValue = toRaw(oldValue)
      value = toRaw(value)
    }
    // 不是数组响应式对象时,如果oldValue是ref,且newValue不是ref,直接更新oldValue的值即可
    // 因为oldValue的ref会自动触发更新
    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
  }

  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
  // 这里target !== toRaw(receiver)的情况我暂时没有想出来。。。
  if (target === toRaw(receiver)) {
    // 这了新增和更新的触发需要进行区分,因为新增我们需要处理一些iteration key的effects,具体后面trigger时会分析。
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
  }
  return result
}

set核心部分当然就是trigger方法的调用,同样后面结合track一起来看。咱们先简单总结下set所做的事情:

  • 判断一些不需要 trigger 和更新值的边缘情况
    • 当 newValue 是 ref 时,不管 oldValue 是否是 readonly 都是允许更新的,这样对应键值的 readonly 就被取消了
    • 如果 newValue 和 oldValue 都是 ref, 且 oldValue 是 readonly,则不允许更新,毕竟 oldValue 就是应该是个 readonly ref
    • 不是数组响应式对象时,如果 oldValue 是 ref,且 newValue 不是 ref,直接更新 oldValue 的值即可,因为 oldValue 的 ref 会自动触发更新
  • 防止 target 为原型链上的对象时的 trigger:anybody provides some cases?

此外mutableHandlers还有一些方法的实现:deleteProperty, has and ownKeys,这些方法都类似地通过调用track或者trigger来处理响应式,实现比较类似,就不在这里赘述了。

trigger

咱们终于要来看最主要的两个部分,track and trigger。不过天色已晚,下次再继续分析吧。

after a week...

o ha yo u, 咱们趁热打铁,先来看看trigger的实现。

ts
/**
 * Finds all deps associated with the target (or a specific property) and
 * triggers the effects stored within.
 *
 * @param target - The reactive object.
 * @param type - Defines the type of the operation that needs to trigger effects.
 * @param key - Can be used to target a specific reactive property in the target object.
 */
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
  }

  // 添加需要处理的deps (deps就是effects set)
  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)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      // 当数组的length变小,之前大于当前length的索引键值的effects都要被添加,相当于removed trigger
      // 新增索引的effects在下面TriggerOpTypes.ADD case下添加了
      if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 提供了key就把对于key的deps添加上
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    // 因为新增或者删除,都会影响对应target的遍历结果
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // MAP_KEY_ITERATE_KEY是在只是遍历keys时才会被对应effect收集的依赖。
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 新增索引时,length的effects也需要被trigger
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        // 数组元素的删除操作会trigger上面key === 'length' && isArray(target)的effects,所以这里不需要处理
        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 {
    // 如果有多个deps,需要把所有deps里的effects gather到一起,重写组成一个deps再处理
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}
/**
 * Finds all deps associated with the target (or a specific property) and
 * triggers the effects stored within.
 *
 * @param target - The reactive object.
 * @param type - Defines the type of the operation that needs to trigger effects.
 * @param key - Can be used to target a specific reactive property in the target object.
 */
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
  }

  // 添加需要处理的deps (deps就是effects set)
  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)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      // 当数组的length变小,之前大于当前length的索引键值的effects都要被添加,相当于removed trigger
      // 新增索引的effects在下面TriggerOpTypes.ADD case下添加了
      if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 提供了key就把对于key的deps添加上
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    // 因为新增或者删除,都会影响对应target的遍历结果
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // MAP_KEY_ITERATE_KEY是在只是遍历keys时才会被对应effect收集的依赖。
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 新增索引时,length的effects也需要被trigger
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        // 数组元素的删除操作会trigger上面key === 'length' && isArray(target)的effects,所以这里不需要处理
        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 {
    // 如果有多个deps,需要把所有deps里的effects gather到一起,重写组成一个deps再处理
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

void 0 or undefined

trigger方法做的事情注释已经说的很清楚了:Finds all deps associated with the target (or a specific property) and triggers the effects stored within.

主要就是找到所有的 effects,因为trigger不一定只是触发对应的 key(如果提供了参数)的 deps,像数组,collection 类型对象,可能会需要触发lengthITERATE_KEY

  • collection类型被clear了,直接触发所有 key 的effects
  • key 是数组的length时,除了length本身的effects,还要触发被删除的索引 key 的effects(如果 length 变小)。
  • 对于剩下的其他情况,首先将provided keyeffects添加上,然后处理是否需要添加额外的iteration key
    • TriggerOpTypes.ADD: 如果是新增 key,对于collection类型添加ITERATE_KEYeffectsMap类型还需加MAP_KEY_ITERATE_KEYeffects;数组的话需要添加lengtheffect
    • TriggerOpTypes.DELETE: 如果是删除 key,对于collection类型的处理和新增 key 时一样;此时不需要处理数组类型被删索引 key 的effects了,因为会在处理lengthkey 时将删索引 key 的effects加上。
    • TriggerOpTypes.SET: Mapset操作,只需要再添加ITERATE_KEYeffects就行了,因为 key 没有变化,所以不需要MAP_KEY_ITERATE_KEYeffects

对于触发 effects就非常简单了。对于需要触发多个depseffects,将所有 deps 里的 effects gather 到一起,重写组成一个 deps 再处理即可,然后交给triggerEffects方法处理。

ts
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

triggerEffects方法做的事情就是将computed effects非computed effects分离开触发,且保证前者先触发完。至于computed effects的来源和用法,咱们暂时不去深究,目测是和computed相关。

那现在就剩最后的triggerEffect方法了,用来真正的 run 一下对应effect的回调。

ts
function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 防止递归执行
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}
function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 防止递归执行
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

首先是防止递归的判断:effect !== activeEffect || effect.allowRecurse。 通过effect !== activeEffect可以防止以下情况。

ts
const obj = reactive({ name: 'leo' })
effect(() => {
  // 收集了依赖
  console.log(obj.name)
  // 紧接着就触发了,导致循环
  obj.name = 'pit'
})
const obj = reactive({ name: 'leo' })
effect(() => {
  // 收集了依赖
  console.log(obj.name)
  // 紧接着就触发了,导致循环
  obj.name = 'pit'
})

其次就是判断是否有scheduler这东西,有就执行scheduler,否则就触发对应回调。

scheduler就是咱们用来自定义effects执行时机的,因为我们可以知道scheduler回调的调用就是effect本应该被执行的时候,但我就吊着你,先不执行。

那我们就可以 schedule 一下例如触发实际渲染前多次修改响应式值的情况了,防止短时间内的重复渲染。

track

OK,现在知道了触发过程,咱们就得看看那些effects是如果被关联到对应 key 的deps中的。

我们知道track函数的调用是在对应 key 的代理被get时,所以在执行某个effect回调时,只要get了一下对应的 key,那么就会调用track

ts
/**
 * Tracks access to a reactive property.
 *
 * This will check which effect is running at the moment and record it as dep
 * which records all effects that depend on the reactive property.
 *
 * @param target - Object holding the reactive property.
 * @param type - Defines the type of access to the reactive property.
 * @param key - Identifier of the reactive property to track.
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 当然只有activeEffect存在才需要track
  // shouldTrack就是用来控制之前提到的暂停收集的标识,具体怎么设置还得后面再说
  // shouldTrack && activeEffect都是effect.js的闭包变量
  if (shouldTrack && activeEffect) {
    // 就是获取/初始化dep(Set<effects>)的过程,
    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)
  }
}
/**
 * Tracks access to a reactive property.
 *
 * This will check which effect is running at the moment and record it as dep
 * which records all effects that depend on the reactive property.
 *
 * @param target - Object holding the reactive property.
 * @param type - Defines the type of access to the reactive property.
 * @param key - Identifier of the reactive property to track.
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 当然只有activeEffect存在才需要track
  // shouldTrack就是用来控制之前提到的暂停收集的标识,具体怎么设置还得后面再说
  // shouldTrack && activeEffect都是effect.js的闭包变量
  if (shouldTrack && activeEffect) {
    // 就是获取/初始化dep(Set<effects>)的过程,
    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)
  }
}

activeEffect是个Effect对象,里面有其对应的回调函数,以及其它一些 meta 信息,用来存放deps以及实现后续的其他功能等。

主要来看看trackEffects,里面已经开始出现effect方法涉及的相关逻辑了,但咱们先忽略,后面再结合effect来分析。

ts
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  // effectTrackDepth & maxMarkerBits都是effect.js的闭包变量
  // 用来记录最大effect递归层数,这里先提一嘴,具体后面再说
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 这里就是effect和其对应依赖关系的形成之处!
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  // effectTrackDepth & maxMarkerBits都是effect.js的闭包变量
  // 用来记录最大effect递归层数,这里先提一嘴,具体后面再说
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 这里就是effect和其对应依赖关系的形成之处!
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}

可以看到,对应的depSet 会把activeEffect添加进去,而activeEffect会把对应的dep加进自己的deps数组里。

接下来,我们就得看看effect方法的实现以及其和以上我们的trigger&track的关系了。