vue3源码解析(四)

87 阅读6分钟

vue3源码解析(三)

渲染函数 setupRenderEffect

当setupComponent初始化组件结束后,进入同步渲染阶段

setupRenderEffect是Vue组件渲染的核心部分,负责创建响应式的副作用函数来处理组件的挂载和更新

其中ReactiveEffect 是 @vue/reactivity 核心类

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  namespace,
  optimized,
) => {
  // 组件更新函数
   const componentUpdateFn = () => {
   ...
   }
  // 创建响应式副作用函数
  const effect = (instance.effect = // 创建响应式副作用
    new ReactiveEffect(
      componentUpdateFn,  // 组件更新函数
      NOOP,               // 清理函数
      () => queueJob(update),  // 调度器
      instance.scope      // 作用域
    )
  )
  
  const update: SchedulerJob = (instance.update = () => {
    //执行副作用
    if (effect.dirty) effect.run()
  })
  update()
}

组件更新函数componentUpdateFn

我们先看看componentUpdaten里都是在做了什么

componentUpdateFn的函数是Vue 3组件渲染的核心逻辑,处理了组件的挂载、更新、生命周期钩子、服务端渲染激活、异步组件加载以及开发者工具集成等多个方面。通过响应式系统和虚拟DOM的patch过程,实现了高效的组件渲染和更新

在挂载阶段(if (!instance.isMounted)),代码处理了beforeMount生命周期钩子,调用invokeArrayFns(bm)执行所有beforeMount钩子函数。接着处理onVnodeBeforeMount事件,如果存在则调用

接下来,检查是否需要 hydration(服务端渲染时的客户端激活)。如果有el且存在hydrateNode函数,则执行hydration逻辑。这里区分了异步组件包装器(isAsyncWrapperVNode)的情况,如果是异步组件,则等待其加载完成后再执行hydration。否则直接调用hydrateSubTree进行激活。对于非hydration情况,则执行常规的渲染和patch过程,生成子树并挂载到容器中

挂载完成后,处理mounted生命周期钩子和onVnodeMounted事件,同样考虑了兼容性钩子hook:mounted。此外,处理了KeepAlive组件的activated钩子,确保在适当时候触发。

在更新阶段(else部分),处理beforeUpdate钩子和onVnodeBeforeUpdate事件。然后重新渲染组件生成新的子树nextTree,并与旧的子树prevTree进行对比和patch。更新完成后,触发updated钩子和onVnodeUpdated事件,同样处理兼容性钩子hook:updated

graph TD
A[组件更新入口] --> B{已挂载?}
B -->|否| C[挂载阶段]
B -->|是| D[更新阶段]
C --> C1[执行beforeMount钩子]
C1 --> C2[处理服务端渲染激活]
C2 --> C3[生成子树并patch]
C3 --> C4[触发mounted钩子]
D --> D1[执行beforeUpdate钩子]
D1 --> D2[重新渲染生成新子树]
D2 --> D3[patch新旧子树]
D3 --> D4[触发updated钩子]

代码如下

const componentUpdateFn = () => {
  // 挂载/更新逻辑...
  if (!instance.isMounted) {
      // 1. beforeMount生命周期处理
      if (bm) invokeArrayFns(bm) 

      // 2. 服务端渲染激活逻辑
      if (el && hydrateNode) {
        const hydrateSubTree = () => {
          instance.subTree = renderComponentRoot(instance)
          hydrateNode(el, instance.subTree, ...)
        }
        // 异步组件处理
        if (isAsyncWrapperVNode) {
          initialVNode.type.__asyncLoader().then(hydrateSubTree)
        }
      }

      // 3. 常规渲染路径
      const subTree = renderComponentRoot(instance)
      patch(null, subTree, container, ...)

      // 4. 挂载后处理 通过queuePostRenderEffect实现批量更新
      queuePostRenderEffect(m) // mounted钩子
      instance.isMounted = true
    }else{
       // 1. beforeUpdate处理
      if (bu) invokeArrayFns(bu)

      // 2. 重新渲染
      const nextTree = renderComponentRoot(instance)

      // 3. 差异比对
      patch(prevTree, nextTree, ...)

      // 4. 更新后处理
      queuePostRenderEffect(u) // updated钩子
      updateHOCHostEl() // 高阶组件处理
    }
  
}

renderComponentRoot 生成组件的虚拟DOM树

函数的结构分为状态组件和函数式组件的处理。状态组件使用proxy或withProxy来调用render函数,而函数式组件则根据参数长度来处理

graph TD
A[开始渲染] --> B{组件类型}
B -->|状态组件| C[创建渲染代理]
B -->|函数式组件| D[处理函数式渲染]
C --> E[执行render函数]
D --> F[处理函数参数]
E --> G[属性合并处理]
F --> G
G --> H[错误边界处理]
H --> I[根节点处理]
I --> J[指令继承]
J --> K[过渡效果处理]
K --> L[返回最终VNode]
export function renderComponentRoot(
  instance: ComponentInternalInstance,
): VNode {
  // ...核心实现...
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      //状态组件 
      const proxyToUse = withProxy || proxy
      result = normalizeVNode(
          render!.call(thisProxy, ...)
      )
  }else{
      //函数组件 根据参数长度自动判断渲染参数格式
      result = normalizeVNode(
  render.length > 1 
    ? render(props, { attrs, slots, emit })
    : render(props)
)
  }
  //属性合并机制
  if (fallthroughAttrs && inheritAttrs !== false) {
      root = cloneVNode(root, fallthroughAttrs, false, true)
  }
}

renderComponentRoot生成组件的虚拟DOM树后,在执行patch函数进行新旧树diff比较

看完了组件更新函数componentUpdateFn后,我们大概知道了这是在组件挂在或者更新时,进行对应的VNode创建或者更新,进行diff比较,生成虚拟dom树

接着我们来看下ReactiveEffect副作用类的实现

ReactiveEffect类

ReactiveEffect是Vue响应式系统的核心类,用于管理副作用函数

类中有多个内部状态变量,如_dirtyLevel、_trackId等,这些用于跟踪副作用的依赖和状态。构造函数接收一个函数fn、触发器、调度器和作用域

run方法是执行副作用函数的地方。在运行前,会暂停跟踪,清理旧依赖,然后执行fn。完成后,恢复之前的跟踪状态

stop方法用于停止副作用,清理相关依赖并触发onStop回调

graph TD
A[创建Effect实例] --> B[管理依赖状态]
B --> C{状态判断}
C -->|Dirty| D[执行副作用函数]
C -->|NotDirty| E[跳过执行]
D --> F[收集新依赖]
F --> G[清理旧依赖]
G --> H[触发回调]
export class ReactiveEffect<T = any> {
  // ...完整类实现...
  _dirtyLevel = DirtyLevels.Dirty  // 脏检查状态
  _trackId = 0                    // 依赖追踪版本号
  _depsLength = 0                 // 有效依赖数量
  
  run() {
      // 状态重置
      this._dirtyLevel = DirtyLevels.NotDirty
      // 激活状态检查
      if (!this.active) return this.fn() 
      // 依赖追踪上下文管理
      let lastShouldTrack = shouldTrack
      let lastEffect = activeEffect
      try {
        // 执行前准备
        shouldTrack = true
        activeEffect = this
        this._runnings++
        preCleanupEffect(this)
        // 执行副作用函数
        return this.fn()
      } finally {
        // 执行后清理
        postCleanupEffect(this)
        this._runnings--
        activeEffect = lastEffect
        shouldTrack = lastShouldTrack
      }
    }
}

这样我们就大概知道了,vue在渲染组件的整个流程,如果是一次渲染,则创建组件实例,初始化组件,执行副作用函数

那么如果节点是组件类型,我们怎么判断是否要更新呢,vue3提供了shouldUpdateComponent关键决策函数函数

shouldUpdateComponent 关键决策函数

用来决定组件是否需要更新。

函数接收新旧VNode、优化标志等参数,返回一个布尔值

patchFlag分级检测 :

  • 动态插槽 > 全属性 > 部分属性 的优先级判断
  • 利用位运算快速判断更新类型
graph TD
A[开始判断] --> B{热更新场景?}
B -->|是| C[强制更新]
B -->|否| D{新VNode有指令/过渡?}
D -->|是| C
D -->|否| E{启用优化模式?}
E -->|是| F[基于patchFlag判断]
E -->|否| G[全量比较子节点和props]
F --> H{动态插槽?}
H -->|是| C
F --> I{全属性变化?}
I -->|是| J[比较所有props]
F --> K{部分属性变化?}
K -->|是| L[比较动态props]
G --> M{子节点不稳定?}
M -->|是| C
G --> N[比较新旧props]
export function shouldUpdateComponent(
  prevVNode: VNode,
  nextVNode: VNode,
  optimized?: boolean,
): boolean {
  // ...完整实现...
  if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {
      return true // 强制更新热替换组件
    }
   if (nextVNode.dirs || nextVNode.transition) {
      return true // 运行时指令或过渡效果需要强制更新
    } 
    if (optimized && patchFlag >= 0) {
      // 动态插槽更新(如v-for产生的动态slot)
      if (patchFlag & PatchFlags.DYNAMIC_SLOTS) return true

      // 全属性更新判断
      if (patchFlag & PatchFlags.FULL_PROPS) {
        return hasPropsChanged(prevProps, nextProps!, emits)
      }

      // 部分属性更新判断
      else if (patchFlag & PatchFlags.PROPS) {
        for (const key of dynamicProps) {
          if (props值变化 && 非emit事件) return true
        }
      }
    }else{
        if (prevChildren || nextChildren) {
          if (!nextChildren?.$stable) return true // 子节点不稳定时强制更新
        }
         // 全量props比较
        return hasPropsChanged(prevProps, nextProps, emits)
        
    }
}

updateComponent 组件更新

当执行patch函数,发现是组件阶段且非首次的时候,要调用该函数

函数接收两个VNode(n1和n2)以及一个优化标志。首先,它把n1的组件实例赋给n2,确保实例复用。然后通过shouldUpdateComponent判断是否需要更新组件

如果不需要更新,直接复制属性;否则,处理异步组件的情况。对于异步组件,如果还在加载中,只更新props和slots,避免重复渲染。否则,将新VNode赋值给实例的next属性,并触发更新

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  //组件实例复用
  const instance = (n2.component = n1.component)!
  if (shouldUpdateComponent(n1, n2, optimized)) {
  // 更新逻辑...
  //异步组件 未结束
      if (__FEATURE_SUSPENSE__ && instance.asyncDep && !instance.asyncResolved) {
          updateComponentPreRender(instance, n2, optimized) // 仅更新props/slots
          return
        }else{ 
            instance.next = n2 // 缓存新VNode
            invalidateJob(instance.update) // 防止重复入队
            instance.effect.dirty = true // 标记为脏状态
            instance.update() // 触发响应式更新
        }
    } else {
      // 跳过更新,直接同步属性
      n2.el = n1.el
      instance.vnode = n2
    }
}

至此,整个render函数后续根据节点类型做不同处理的逻辑都看了一遍