解析Vue 3中 `trigger` 函数是如何触发 DOM 更新的

解析 Vue 3 中 trigger 函数是如何触发 DOM 更新的整个流程。

1. trigger 函数的实现

typescript:packages/reactivity/src/effect.ts 复制代码
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) return

  // 收集需要触发的 effects
  const effects = new Set<ReactiveEffect>()
  
  // 添加 effects 到集合中
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // 避免自触发
        if (effect !== activeEffect) {
          effects.add(effect)
        }
      })
    }
  }

  // 1. 处理特定属性的依赖
  if (key !== void 0) {
    add(depsMap.get(key))
  }

  // 2. 处理数组长度变化
  if (type === TriggerOpTypes.ADD && isArray(target)) {
    add(depsMap.get('length'))
  }

  // 3. 执行收集到的 effects
  effects.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  })
}

2. DOM 更新的完整流程

第一阶段:触发响应式更新

  1. 数据变更触发 trigger
typescript 复制代码
// 当响应式数据发生变化时
state.count = 2
  1. 收集相关 effects
typescript 复制代码
const effects = new Set<ReactiveEffect>()
// 从 targetMap 中找到相关的依赖并添加到 effects 中
add(depsMap.get(key))

第二阶段:调度更新

  1. 调度器介入
typescript 复制代码
effects.forEach(effect => {
  if (effect.scheduler) {
    // 组件更新会走调度器
    effect.scheduler()
  } else {
    // 普通 effect 直接运行
    effect.run()
  }
})
  1. 进入调度队列
typescript:packages/runtime-core/src/scheduler.ts 复制代码
function queueJob(job: SchedulerJob) {
  if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
    // 将更新任务加入队列
    queue.push(job)
    queueFlush()
  }
}

第三阶段:组件更新

  1. 组件渲染 effect 执行
typescript:packages/runtime-core/src/renderer.ts 复制代码
const setupRenderEffect = (
  instance: ComponentInternalInstance,
  initialVNode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentSuspense: null | SuspenseBoundary,
  namespace: ElementNamespace,
  optimized: boolean
) => {
  // 创建渲染 effect
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 首次挂载
      const subTree = (instance.subTree = renderComponentRoot(instance))
      patch(null, subTree, container, anchor, instance, parentSuspense, namespace)
      instance.isMounted = true
    } else {
      // 更新
      let { next, vnode } = instance
      
      // 更新组件实例
      if (next) {
        updateComponentPreRender(instance, next, optimized)
      }
      
      // 重新渲染
      const nextTree = renderComponentRoot(instance)
      const prevTree = instance.subTree
      instance.subTree = nextTree
      
      // 执行 patch 更新 DOM
      patch(
        prevTree,
        nextTree,
        hostParentNode(prevTree.el!)!,
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        namespace
      )
    }
  }
}

第四阶段:DOM 更新

  1. patch 过程
typescript:packages/runtime-core/src/renderer.ts 复制代码
const patch = (
  n1: VNode | null,  // 旧 vnode
  n2: VNode,         // 新 vnode
  container: RendererElement,
  anchor: RendererNode | null = null,
  parentComponent: ComponentInternalInstance | null = null,
  parentSuspense: SuspenseBoundary | null = null,
  namespace: ElementNamespace = SVGNamespace,
  optimized = false
) => {
  // 1. 处理不同类型的节点
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    case Fragment:
      processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, optimized)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, optimized)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, optimized)
      }
  }
}
  1. 实际 DOM 操作
typescript 复制代码
const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  optimized: boolean
) => {
  if (n1 === null) {
    // 挂载新元素
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace,
      optimized
    )
  } else {
    // 更新已有元素
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      namespace,
      optimized
    )
  }
}

总结:完整的更新链路

  1. 触发阶段

    • 响应式数据变更
    • trigger 函数被调用
    • 收集相关的副作用函数
  2. 调度阶段

    • 更新任务进入调度队列
    • 通过 Promise.then 等待下一个微任务
    • 批量处理更新任务
  3. 渲染阶段

    • 执行组件的更新函数
    • 生成新的虚拟 DOM 树
    • 对比新旧虚拟 DOM
  4. 提交阶段

    • patch 过程对比差异
    • 执行实际的 DOM 操作
    • 更新完成

这个过程是 Vue 3 响应式系统和渲染系统协同工作的结果,通过精心设计的调度系统,确保了更新的高效性和可控性。

相关推荐
前端爆冲9 分钟前
基于vue和flex实现页面可配置组件顺序
前端·javascript·vue.js
正在脱发中18 分钟前
vue-cropper 遇到的坑 Failed to execute 'getComputedStyle' on 'Window': parameter
前端·vue.js
浪裡遊1 小时前
前端热门面试题day1
前端·javascript·vue.js·前端框架
hemoo1 小时前
Element UI 日期区间定制
vue.js·element
bilibilibiu灬2 小时前
Vue 3 `setupRenderEffect` 函数解析
vue.js
樊小肆2 小时前
Vue3 在线 PDF 编辑 2.0 撤回、反撤回
前端·vue.js·开源
雪中何以赠君别3 小时前
【组件开发】基于 Vue 3 和 Element Plus 的花名册表头设置组件(el-table动态设置表头组件开发)
vue.js
iceprosurface3 小时前
来试试用 react 的写法写 vue
vue.js
weixin_516875653 小时前
Vue 3 Watch 监听 Props 的踩坑记录
前端·javascript·vue.js