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)。

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

目录
相关文章
|
1月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
483 139
|
30天前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
199 1
|
6月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
902 5
|
2月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
359 11
|
1月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
239 0
|
3月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
420 1
|
3月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
234 0
|
4月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
135 0
|
6月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
578 17
|
7月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点