Vue3 响应式原理(三)

简介: Vue3 响应式原理(三)

effect.ts 文件

等把 effect.ts 文件讲解完,响应式模块基本上差不多结束了。

effect()

effect() 主要和响应式的对象结合使用。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果已经是 effect 函数,取得原来的 fn
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  // 如果 lazy 为 false,马上执行一次
  // 计算属性的 lazy 为 true
  if (!options.lazy) {
    effect()
  }
  return effect
}

真正创建 effect 的是 createReactiveEffect() 函数。

let uid = 0
function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // reactiveEffect() 返回一个新的 effect,这个新的 effect 执行后
  // 会将自己设为 activeEffect,然后再执行 fn 函数,如果在 fn 函数里对响应式属性进行读取
  // 会触发响应式属性 get 操作,从而收集依赖,而收集的这个依赖函数就是 activeEffect
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    // 为了避免递归循环,所以要检测一下
    if (!effectStack.includes(effect)) {
      // 清空依赖
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn(...args)
      } finally {
        // track 将依赖函数 activeEffect 添加到对应的 dep 中,然后在 finally 中将 activeEffect
        // 重置为上一个 effect 的值
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true // 用于判断当前 effect 是否激活,有一个 stop() 来将它设为 false
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

其中 cleanup(effect) 的作用是让 effect 关联下的所有 dep 实例清空 effect,即清除这个依赖函数。

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

从代码中可以看出来,真正的依赖函数是 activeEffect。执行 track() 收集的依赖就是 activeEffect。

趁热打铁,现在我们再来看一下 track()trigger() 函数。

track()

// 依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect 为空,代表没有依赖,直接返回
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap 依赖管理中心,用于收集依赖和触发依赖
  let depsMap = targetMap.get(target)
  // targetMap 为每个 target 建立一个 map
  // 每个 target 的 key 对应着一个 dep
  // 然后用 dep 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数
  // 类似于这样
  // targetMap(weakmap) = {
  //     target1(map): {
  //       key1(dep): (fn1,fn2,fn3...)
  //       key2(dep): (fn1,fn2,fn3...)
  //     },
  //     target2(map): {
  //       key1(dep): (fn1,fn2,fn3...)
  //       key2(dep): (fn1,fn2,fn3...)
  //     },
  // }
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    // 开发环境下会触发 onTrack 事件
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

targetMap 是一个 WeakMap 实例。

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

弱引用是什么意思呢?

let obj = { a: 1 }
const map = new WeakMap()
map.set(obj, '测试')
obj = null

当 obj 置为空后,对于 { a: 1 } 的引用已经为零了,下一次垃圾回收时就会把 weakmap 中的对象回收。

但如果把 weakmap 换成 map 数据结构,即使把 obj 置空,{ a: 1 } 依然不会被回收,因为 map 数据结构是强引用,它现在还被 map 引用着。

trigger()

// 触发依赖
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
  }
  // 对收集的依赖进行分类,分为普通的依赖或计算属性依赖
  // effects 收集的是普通的依赖 computedRunners 收集的是计算属性的依赖
  // 两个队列都是 set 结构,为了避免重复收集依赖
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // effect !== activeEffect 避免重复收集依赖
        if (effect !== activeEffect || !shouldTrack) {
          // 计算属性
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        } else {
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop
        }
      })
    }
  }
  // 在值被清空前,往相应的队列添加 target 所有的依赖
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { // 当数组的 length 属性变化时触发
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // 如果不符合以上两个 if 条件,并且 key !== undefined,往相应的队列添加依赖
    if (key !== void 0) {
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      // 如果 scheduler 存在则调用 scheduler,计算属性拥有 scheduler
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // 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)
}

对依赖函数进行分类后,需要先运行计算属性的依赖,因为其他普通的依赖函数可能包含了计算属性。先执行计算属性的依赖能保证普通依赖执行时能得到最新的计算属性的值。

track() 和 trigger() 中的 type 有什么用?

这个 type 取值范围就定义在 operations.ts 文件中:

// track 的类型
export const enum TrackOpTypes {
  GET = 'get', // get 操作
  HAS = 'has', // has 操作
  ITERATE = 'iterate' // ownKeys 操作
}
// trigger 的类型
export const enum TriggerOpTypes {
  SET = 'set', // 设置操作,将旧值设置为新值
  ADD = 'add', // 新增操作,添加一个新的值 例如给对象新增一个值 数组的 push 操作
  DELETE = 'delete', // 删除操作 例如对象的 delete 操作,数组的 pop 操作
  CLEAR = 'clear' // 用于 Map 和 Set 的 clear 操作。
}

type 主要用于标识 track()trigger() 的类型。

trigger() 中的连续判断代码

if (key !== void 0) {
  add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
  type === TriggerOpTypes.ADD ||
  (type === TriggerOpTypes.DELETE && !isArray(target))
if (
  isAddOrDelete ||
  (type === TriggerOpTypes.SET && target instanceof Map)
) {
  add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
  add(depsMap.get(MAP_KEY_ITERATE_KEY))
}

trigger() 中有这么一段连续判断的代码,它们作用是什么呢?其实它们是用于判断数组/集合这种数据结构比较特别的操作。

看个示例:

let dummy
const counter = reactive([])
effect(() => (dummy = counter.join()))
counter.push(1)

effect(() => (dummy = counter.join())) 生成一个依赖,并且自执行一次。

在执行函数里的代码 counter.join() 时,会访问数组的多个属性,分别是 joinlength,同时触发 track() 收集依赖。也就是说,数组的 joinlength 属性都收集了一个依赖。

当执行 counter.push(1) 这段代码时,实际上是将数组的索引 0 对应的值设为 1。这一点,可以通过打 debugger 从上下文环境看出来,其中 key 为 0,即数组的索引,值为 1。

设置值后,由于是新增操作,执行 trigger(target, TriggerOpTypes.ADD, key, value)。但由上文可知,只有数组的 key 为 joinlength 时,才有依赖,key 为 0 是没有依赖的。

从上面两个图可以看出来,只有 joinlength 属性才有对应的依赖。

这个时候,trigger() 的一连串 if 语句就起作用了,其中有一个 if 语句是这样的:

if (
  isAddOrDelete ||
  (type === TriggerOpTypes.SET && target instanceof Map)
) {
  add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}

如果 target 是一个数组,就添加 length 属性对应的依赖到队列中。也就是说 key 为 0 的情况下使用 length 对应的依赖。

另外,还有一个巧妙的地方。待执行依赖的队列是一个 set 数据结构。如果 key 为 0 有对应的依赖,同时 length 也有对应的依赖,就会添加两次依赖,但由于队列是 set,具有自动去重的效果,避免了重复执行。

示例

仅看代码和文字,是很难理解响应式数据和 track()trigger() 是怎么配合的。所以我们要配合示例来理解:

let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy == 0)
counter.num = 7
console.log(dummy == 7)

上述代码执行过程如下:

  1. { num: 0 } 进行监听,返回一个 proxy 实例,即 counter。
  2. effect(fn) 创建一个依赖,并且在创建时会执行一次 fn
  3. fn() 读取 num 的值,并赋值给 dummy。
  4. 读取属性这个操作会触发 proxy 的属性读取拦截操作,在拦截操作里会去收集依赖,这个依赖是步骤 2 产生的。
  5. counter.num = 7 这个操作会触发 proxy 的属性设置拦截操作,在这个拦截操作里,除了把新的值返回,还会触发刚才收集的依赖。在这个依赖里把 counter.num 赋值给 dummy(num 的值已经变为 7)。

用图来表示,大概这样的:

目录
相关文章
|
2天前
|
JavaScript 前端开发 CDN
vue3速览
vue3速览
11 0
|
2天前
|
设计模式 JavaScript 前端开发
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
|
2天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(中)
Vue3 官方文档速通(中)
9 0
|
2天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(上)
Vue3 官方文档速通(上)
9 0
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
7 1
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之三:vue-router 的安装和使用
Vue3+Vite+Pinia+Naive后台管理系统搭建之三:vue-router 的安装和使用
6 0
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之二:scss 的安装和使用
Vue3+Vite+Pinia+Naive后台管理系统搭建之二:scss 的安装和使用
6 0
|
2天前
|
JavaScript 前端开发 API
Vue3 系列:从0开始学习vue3.0
Vue3 系列:从0开始学习vue3.0
8 1
|
2天前
|
网络架构
Vue3 系列:vue-router
Vue3 系列:vue-router
8 2
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之一:基础项目构建
Vue3+Vite+Pinia+Naive后台管理系统搭建之一:基础项目构建
7 1