Vue课代表,今天复习响应式(二)

前景回顾

各位看官,上回书说到,Object.definePropertyProxy在Vue的生命中都是非常重要的一员,即使到了Vue3也是难舍难分,各有好处,也不知道尤大是怎么找到的

不如我们深入源码,再看看这块田里,咱们能挖到什么。

依赖收集和依赖分发

参考文件:packages/reactivity/src/reactiveEffect.ts

发布 - 订阅模式

发布 - 订阅模式是一种设计模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当主题对象的状态发生变化时,它会通知所有观察者对象,使它们能够自动更新自己。

Vue3的响应式原理就是基于发布 - 订阅模式实现的,它通过Proxy对象来代理原始数据对象,拦截其读取和修改的操作,从而实现对数据的依赖收集和依赖分发。

依赖收集

依赖收集的目的是为了建立数据和副作用函数之间的映射关系,让数据知道哪些副作用函数依赖于它,当数据变化时,能够通知这些副作用函数进行更新。

什么是副作用函数(effect)

副作用函数是Vue3中一个非常重要的概念,它是指那些依赖于响应式数据变化而执行的函数,比如:

  • 渲染函数:负责将数据渲染到视图层
  • 计算属性:根据数据计算出一个新的值
  • 侦听器:监听数据的变化并执行一些操作
  • 自定义函数:用户自定义的需要响应数据变化的函数

track函数

track函数是Vue3响应式系统的核心部分之一,它的主要作用是跟踪对响应式对象属性的访问。

当你访问一个响应式对象的属性时,track函数会被调用。它会检查当前是否有一个活动的effect(副作用函数)。如果有,那么这个effect就会被添加到这个属性的依赖列表中。这样,当这个属性的值发生变化时,所有依赖于这个属性的effect就会被重新执行(trigger函数)。

这是Vue3实现数据响应式的关键机制。当你在组件中使用refreactive创建响应式数据,然后在Template或Computed中使用这些数据时,Vue就会自动跟踪这些依赖关系。当数据发生变化时,Vue知道需要重新渲染哪些组件重新计算哪些计算属性

track函数的实现如下:

typescript 复制代码
/**
 * 跟踪对响应式属性的访问。
 *
 * 这将检查当前正在运行的效果,并将其记录为dep,
 * dep记录了所有依赖于响应式属性的效果。
 *
 * @param target - 持有响应式属性的对象。
 * @param type - 定义对响应式属性的访问类型。
 * @param key - 要跟踪的响应式属性的标识符。
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // shouldTrack和activeEffect都为真时,才进行跟踪
  if (shouldTrack && activeEffect) {
    // 从targetMap中获取目标对象的依赖映射
    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(() => depsMap!.delete(key))))
    }
    // 跟踪效果
    // activeEffect - 当前活动的效果
    // dep - 属性的依赖
    // __DEV__ ? {target, type, key} : void 0 - 在开发模式下,传递额外的调试信息
    trackEffect(
      activeEffect,
      dep,
      __DEV__
        ? {
            target,
            type,
            key,
          }
        : void 0,
    )
  }
}

依赖分发

依赖分发的目的是为了在数据发生变化时,触发依赖于该数据的副作用函数进行更新,从而实现数据和视图的同步。

如何做的?

trigger函数在Vue3的响应式系统中起着关键的作用,它负责在数据发生变化时触发更新。以下是trigger函数的详细工作流程:

  1. 获取依赖映射 :首先,trigger函数会从targetMap中获取目标对象的依赖映射。targetMap是一个全局的WeakMap,它存储了所有响应式对象及其对应的依赖映射。
  2. 确定需要触发的依赖 :然后,trigger函数会根据操作类型和键名来确定需要触发哪些依赖。例如,如果操作类型是CLEAR,那么就需要触发目标对象的所有依赖。如果目标是数组且键名是length,那么就需要触发长度变化的依赖。对于SETADDDELETE操作,还需要根据键名来触发相应的依赖。
  3. 触发依赖的效果 :最后,trigger函数会遍历所有需要触发的依赖,并执行其中存储的效果。这些效果通常是重新计算计算属性的值或重新渲染组件。

通过这种方式,trigger函数实现了Vue3的数据响应式机制,使得当数据发生变化时,所有依赖于该数据的计算属性和组件都能自动更新。

trigger函数的实现如下:

typescript 复制代码
/**
 * 找到与目标(或特定属性)相关的所有依赖,并触发其中存储的效果。
 *
 * @param target - 响应式对象。
 * @param type - 定义需要触发效果的操作类型。
 * @param key - 可用于定位目标对象中的特定响应式属性。
 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
  // 从targetMap中获取目标对象的依赖映射
  const depsMap = targetMap.get(target)
  // 如果目标对象没有依赖映射,说明它从未被跟踪过,直接返回
  if (!depsMap) {
    return
  }

  let deps: (Dep | undefined)[] = []
  // 如果操作类型是CLEAR,说明集合正在被清空,触发目标的所有效果
  if (type === TriggerOpTypes.CLEAR) {
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 如果目标是数组且键名是'length',则触发长度变化的效果
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
        deps.push(dep)
      }
    })
  } else {
    // 对于SET、ADD、DELETE操作,触发相应的效果
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // 对于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)) {
          // 数组中添加了新索引 -> 长度变化
          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
    }
  }

  // 暂停调度
  pauseScheduling()
  // 触发所有依赖的效果
  for (const dep of deps) {
    if (dep) {
      triggerEffects(
        dep,
        DirtyLevels.Dirty,
        __DEV__
          ? {
              target,
              type,
              key,
              newValue,
              oldValue,
              oldTarget,
            }
          : void 0,
      )
    }
  }
  // 重置调度
  resetScheduling()
}

这样,我们就完成了trigger函数的解析,它的作用是在数据发生变化时,触发依赖于该数据的副作用函数进行更新,从而实现数据和视图的同步。

本节参考

第二节总结

本章呢,主要聊了一下源码中track函数和trigger函数,基本讲了一下他们是干什么的,他们在响应式中做了什么事情。

内容如有不妥,欢迎指正。

课后问题

批量异步更新 :在Vue的响应式系统中,当数据发生变化时,trigger函数会立即触发所有依赖于该数据的effect。但是,如果在一个事件循环中,同一个数据被多次修改,那么同一个effect就会被多次触发,这可能会导致不必要的计算和渲染。Vue是如何解决这个问题的?如果你要设计一个批量异步更新的机制,你会如何设计?

相关推荐
&白帝&4 小时前
vue右键显示菜单
前端·javascript·vue.js
Wannaer4 小时前
从 Vue3 回望 Vue2:事件总线的前世今生
前端·javascript·vue.js
光影少年5 小时前
vue中,created和mounted两个钩子之间调用时差值受什么影响
前端·javascript·vue.js
cdcdhj6 小时前
vue用通过npm的webpack打包编译,这样更适合灵活配置的项目
vue.js·webpack·npm
运维@小兵9 小时前
vue使用路由技术实现登录成功后跳转到首页
前端·javascript·vue.js
能来帮帮蒟蒻吗10 小时前
VUE3 -综合实践(Mock+Axios+ElementPlus)
前端·javascript·vue.js·笔记·学习·ajax·typescript
Java&Develop12 小时前
Vue ElementUI原生upload修改字体大小和区域宽度
vue.js
郭尘帅66613 小时前
vue3基础学习(上) [简单标签] (vscode)
前端·vue.js·学习
st紫月14 小时前
用vue和go实现登录加密
前端·vue.js·golang
岁岁岁平安14 小时前
Vue3学习(组合式API——计算属性computed详解)
前端·javascript·vue.js·学习·computed·计算属性