vue3源码解析(二)

134 阅读9分钟

vue3源码解析(一)

3.render

创建完vnode节点后,我们开始进入render也就是渲染阶段,也就是app实例里的render方法

- vnode : 要渲染的虚拟节点
- container : 挂载的DOM容器
- namespace : 命名空间(用于SVG/MathMLconst render: RootRenderFunction = (vnode, container, namespace) => {
  if (vnode == null) {
      //当 vnode 为null时,表示要卸载当前容器中的内容,调用 unmount 函数
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
  //当 vnode 存在时,调用 patch 函数进行更新或挂载
    patch(
      container._vnode || null,
      vnode,
      container,
      null,
      null,
      null,
      namespace,
    )
  }
  //通过 isFlushing 标志防止重复刷新
  if (!isFlushing) {
    isFlushing = true
    //调用 flushPreFlushCbs() 和 flushPostFlushCbs() 处理预刷新和后刷新回调
    flushPreFlushCbs()
    flushPostFlushCbs()
    isFlushing = false
  }
  container._vnode = vnode
}

1.patch

optimized 标志控制是否使用Block Tree优化

switch各节点的type来进行对应处理

相同节点快速返回避免不必要计算

const patch: PatchFn = (
  n1,  // 旧VNode (null表示首次挂载)
  n2,  // 新VNode
  container,  // 挂载容器
  anchor = null,  // 锚点位置
  parentComponent = null,  // 父组件实例
  parentSuspense = null,  // Suspense边界
  namespace = undefined,  // 命名空间(svg/mathml)
  slotScopeIds = null,  // 插槽作用域ID
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 优化模式
) => {
  // 1. 相同节点快速返回
  if (n1 === n2) return

  // 2. 节点类型不同时卸载旧节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }

  // 3. 处理BAIL标记(强制全量diff)
  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    n2.dynamicChildren = null
  }

  // 4. 根据节点类型分发处理
  const { type, ref, shapeFlag } = n2
  switch (type) {
      //文本/注释节点:直接DOM操作
    case Text:
      processText(n1, n2, container, anchor)
      break
    case Comment:
      processCommentNode(n1, n2, container, anchor) 
      break
      //  静态节点:开发环境HMR更新
    case Static:
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, namespace)
      } else if (__DEV__) {
        patchStaticNode(n1, n2, container, namespace)
      }
      break
      //Fragment:处理多根节点
    case Fragment:
      processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
      break
    default:
      // 5. 处理元素
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
      //组件
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
      //Teleport:跨DOM树渲染
        ;(type as typeof TeleportImpl).process(n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals)
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
      // Suspense:异步组件处理
        ;(type as typeof SuspenseImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals)
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }

  // 6. 处理ref引用
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

1. processText 文本节点处理

非空断言 n1.el! 表明更新路径必有已挂载节点 hostCreateText / hostSetText 是跨平台抽象接口,在浏览器环境下对应 document.createTextNodenode.nodeValue 设置

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null) {  // 首次挂载
    hostInsert(  // 创建并插入新文本节点
      (n2.el = hostCreateText(n2.children as string)), // 创建DOM文本节点并赋值给vnode.el
      container,
      anchor,
    )
  } else {  // 更新
    const el = (n2.el = n1.el!)  // 复用已有DOM节点
    if (n2.children !== n1.children) {  // 文本内容变化时才更新
      hostSetText(el, n2.children as string)  // 高效更新文本内容
    }
  }
}

2.processCommentNode 注释节点

  • hostCreateComment 是跨平台抽象接口
  • 在浏览器环境下对应 document.createComment
const processCommentNode: ProcessTextOrCommentFn = (
  n1,  // 旧VNode (null表示首次挂载)
  n2,  // 新VNode
  container,  // 挂载容器
  anchor,  // 锚点位置
) => {
  if (n1 == null) {  // 首次挂载路径
    hostInsert(  // 创建并插入新注释节点
      (n2.el = hostCreateComment((n2.children as string) || '')), // 创建DOM注释节点并赋值给vnode.el
      container,
      anchor,
    )
  } else {  // 更新路径
    // 不支持动态更新注释内容
    n2.el = n1.el  // 直接复用已有DOM节点
  }
}

3.mountStaticNode 静态节点

  • 专门用于处理编译器生成的静态节点
  • 通过 hostInsertStaticContent 平台方法批量插入
const mountStaticNode = (
  n2: VNode,  // 要挂载的静态VNode
  container: RendererElement,  // 容器元素
  anchor: RendererNode | null,  // 锚点位置
  namespace: ElementNamespace,  // 命名空间(svg/mathml)
) => {
  // 静态节点仅在compiler-dom/runtime-dom中使用
  // 这保证了hostInsertStaticContent的存在
  ;[n2.el, n2.anchor] = hostInsertStaticContent!(
    n2.children as string,  // 静态内容字符串
    container,
    anchor,
    namespace,
    n2.el,  // 可选:已有开始节点
    n2.anchor,  // 可选:已有结束节点
  )
}

hostInsertStaticContent函数

  • 缓存路径:当已有节点可复用时,直接克隆插入(性能优化)
  • 全新插入:使用template元素批量处理HTML字符串
insertStaticContent(content, parent, anchor, namespace, start, end) {
 // 1. 确定插入位置参考节点
 const before = anchor ? anchor.previousSibling : parent.lastChild
 
 // 2. 缓存路径判断条件:
 // - 单根节点
 // - 兄弟节点信息可用
 if (start && (start === end || start.nextSibling)) {
   // 3. 缓存路径:克隆现有节点
   while (true) {
     parent.insertBefore(start!.cloneNode(true), anchor)
     if (start === end || !(start = start!.nextSibling)) break
   }
 } else {
   // 4. 全新插入路径
   templateContainer.innerHTML = 
     namespace === 'svg' ? `<svg>${content}</svg>` :
     namespace === 'mathml' ? `<math>${content}</math>` : 
     content
   
   const template = templateContainer.content
   // 5. 处理SVG/MathML命名空间
   if (namespace === 'svg' || namespace === 'mathml') {
     const wrapper = template.firstChild!
     while (wrapper.firstChild) {
       template.appendChild(wrapper.firstChild)
     }
     template.removeChild(wrapper)
   }
   parent.insertBefore(template, anchor)
 }
 
 // 6. 返回插入的节点范围
 return [
   before ? before.nextSibling! : parent.firstChild!,
   anchor ? anchor.previousSibling! : parent.lastChild!
 ]
}

4.processFragment 处理多根节点

const processFragment = (
 n1,  // 旧VNode (null表示首次挂载)
 n2,  // 新VNode
 container,  // 容器元素
 anchor,  // 锚点位置
 parentComponent,  // 父组件实例
 parentSuspense,  // Suspense边界
 namespace,  // 命名空间(svg/mathml)
 slotScopeIds,  // 插槽作用域ID
 optimized,  // 优化模式
) => {
 // 1. 创建/复用Fragment锚点节点
 const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
 const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

 // 2. 处理开发环境特殊情况
 let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
 if (__DEV__ && (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)) {
   patchFlag = 0
   optimized = false
   dynamicChildren = null
 }

 // 3. 合并插槽作用域ID
 if (fragmentSlotScopeIds) {
   slotScopeIds = slotScopeIds 
     ? slotScopeIds.concat(fragmentSlotScopeIds)
     : fragmentSlotScopeIds
 }

 // 4. 分路径处理
 if (n1 == null) {
   // 4.1 首次挂载路径
   hostInsert(fragmentStartAnchor, container, anchor)
   hostInsert(fragmentEndAnchor, container, anchor)
   //递归children执行patch
   mountChildren(
     (n2.children || []) as VNodeArrayChildren,  // 处理空Fragment情况
     container,
     fragmentEndAnchor,
     parentComponent,
     parentSuspense,
     namespace,
     slotScopeIds,
     optimized,
   )
 } else {
   // 4.2 更新路径
   if (patchFlag > 0 && 
       patchFlag & PatchFlags.STABLE_FRAGMENT && 
       dynamicChildren &&
       n1.dynamicChildren) {
     // 4.2.1 稳定Fragment优化路径
     patchBlockChildren(
       n1.dynamicChildren,
       dynamicChildren,
       container,
       parentComponent,
       parentSuspense,
       namespace,
       slotScopeIds,
     )
     // 开发环境HMR处理
     if (__DEV__) {
       traverseStaticChildren(n1, n2)
     } else if (n2.key != null || 
               (parentComponent && n2 === parentComponent.subTree)) {
       traverseStaticChildren(n1, n2, true /* shallow */)
     }
   } else {
     // 4.2.2 常规更新路径
     patchChildren(
       n1,
       n2,
       container,
       fragmentEndAnchor,
       parentComponent,
       parentSuspense,
       namespace,
       slotScopeIds,
       optimized,
     )
   }
 }
}

mountChildren其实就是递归children执行patch

patchBlockChildren

批量更新block节点

跳过常规的diff算法,直接按索引对比

const patchBlockChildren: PatchBlockChildrenFn = (
  oldChildren,  // 旧子节点数组
  newChildren,  // 新子节点数组
  fallbackContainer,  // 备用容器
  parentComponent,  // 父组件实例
  parentSuspense,  // Suspense边界
  namespace: ElementNamespace,  // 命名空间
  slotScopeIds,  // 插槽作用域ID
) => {
  // 1. 遍历新子节点数组
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i]
    const newVNode = newChildren[i]
    
    // 2. 确定patch容器
    const container =
      // 2.1 处理特殊情况:
      // - 异步组件错误
      // - Fragment节点
      // - 不同类型节点
      // - 组件或Teleport节点
      oldVNode.el &&
      (oldVNode.type === Fragment ||
       !isSameVNodeType(oldVNode, newVNode) ||
       oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
        ? hostParentNode(oldVNode.el)!  // 获取实际父节点
        : fallbackContainer  // 使用备用容器
    
    // 3. 执行patch
    patch(
      oldVNode,
      newVNode,
      container,
      null,  // anchor设为null
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      true,  // 强制优化模式
    )
  }
}

5.子节点diff算法patchChildren 常规更新子元素

子节点diff算法核心算法

  • KEYED_FRAGMENT 标记:使用 patchKeyedChildren 处理带key的子节点,采用双端diff算法
  • UNKEYED_FRAGMENT 标记:使用 patchUnkeyedChildren 处理无key子节点,按索引顺序比对

子节点类型处理矩阵 :

旧子节点类型新子节点类型处理方式
数组文本卸载旧数组 + 设置新文本
文本数组清空文本 + 挂载新数组
数组数组完整diff算法
文本/空文本/空直接更新文本
const patchChildren: PatchChildrenFn = (
  n1,  // 旧VNode
  n2,  // 新VNode 
  container,  // 容器元素
  anchor,  // 锚点位置
  parentComponent,  // 父组件实例
  parentSuspense,  // Suspense边界
  namespace: ElementNamespace,  // 命名空间
  slotScopeIds,  // 插槽作用域ID
  optimized = false,  // 优化模式
) => {
  // 1. 获取新旧子节点和shapeFlag
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children

  const { patchFlag, shapeFlag } = n2
  
  // 2. 大于0即为动态节点
  if (patchFlag > 0) {
    // 2.1 处理带key的子节点
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      patchKeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
      return
    } 
    // 2.2 处理不带key的子节点
    else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      patchUnkeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
      return
    }
  }

  // 新节点是文本节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 如果旧子节点是数组时,执行递归卸载
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    //如果新旧不同则重新创建
    if (c2 !== c1) {
      hostSetElementText(container, c2 as string)
    }
  } else {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 3.2 旧子节点是数组
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 新旧都是数组,执行完整diff
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      } else {
        // 新子节点不是数组,卸载旧子节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // 3.3 旧子节点是文本或null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, '')
      }
      // 新子节点是数组则挂载
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      }
    }
  }
}
1.patchKeyedChildren 处理带key的子节点

实现了双端diff算法

同步头部时,函数通过while循环从前往后比较节点,直到遇到不同的节点

同步尾部,类似地从后往前比较,直到节点不同

当旧节点遍历完而新节点还有剩余时,说明有新增节点,需要挂载到正确的位置

当新节点遍历完而旧节点还有剩余时,说明有节点需要卸载,调用unmount函数进行移除

其余的需要构建新旧节点的key映射,查找可复用的节点,并处理节点的移动和新增

const patchKeyedChildren = (
  c1: VNode[],          // 旧子节点数组
  c2: VNodeArrayChildren, // 新子节点数组
  container: RendererElement, 
  parentAnchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // 核心算法分5个阶段:
  // 1. 头部同步(从前向后比对)
  while (i <= e1 && i <= e2) {
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container, ...)
    } else break
    i++
  }

  // 2. 尾部同步(从后向前比对)
  while (i <= e1 && i <= e2) {
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container, ...)
    } else break
    e1--; e2--;
  }

  // 3. 新增节点挂载
  if (i > e1) {
    while (i <= e2) {
      patch(null, c2[i], container, anchor)
      i++
    }
  }

  // 4. 旧节点卸载 
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i])
      i++
    }
  }

  // 5. 未知序列处理(核心diff逻辑)
  else {
    // 5.1 构建key到新索引的映射表
    const keyToNewIndexMap = new Map()
    for (i = s2; i <= e2; i++) {
      keyToNewIndexMap.set(c2[i].key, i)
    }

    // 5.2 新旧节点匹配
    for (i = s1; i <= e1; i++) {
      const prevChild = c1[i]
      const newIndex = keyToNewIndexMap.get(prevChild.key)
      if (newIndex === undefined) {
        unmount(prevChild)
      } else {
        // 记录新旧索引映射
        newIndexToOldIndexMap[newIndex - s2] = i + 1
      }
    }

    // 5.3 最长递增子序列优化移动
    const sequence = getSequence(newIndexToOldIndexMap)
    let j = sequence.length - 1
    for (i = toBePatched - 1; i >= 0; i--) {
      if (newIndexToOldIndexMap[i] === 0) {
        // 挂载新节点
        patch(null, nextChild, ...)
      } else if (moved) {
        // 根据LIS决定移动策略
        if (j < 0 || i !== sequence[j]) {
          move(nextChild, ...)
        } else j--
      }
    }
  }
}
2.patchUnkeyedChildren 无key diff

仅按索引顺序比较新旧子节点

const patchUnkeyedChildren = (
  c1: VNode[],          // 旧子节点数组
  c2: VNodeArrayChildren, // 新子节点数组
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  // 1. 初始化处理
  c1 = c1 || EMPTY_ARR  // 确保旧数组非空
  c2 = c2 || EMPTY_ARR  // 确保新数组非空
  const oldLength = c1.length
  const newLength = c2.length
  const commonLength = Math.min(oldLength, newLength) // 计算公共长度
  
  // 2. 公共部分patch
  let i
  for (i = 0; i < commonLength; i++) {
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)  // 优化模式克隆节点
      : normalizeVNode(c2[i]))          // 普通模式标准化节点
    patch(
      c1[i],
      nextChild,
      container,
      null,  // anchor设为null
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized,
    )
  }

  // 3. 处理剩余部分
  if (oldLength > newLength) {
    // 3.1 卸载旧节点
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,    // 执行remove操作
      false,   // 不跳过ref卸载
      commonLength,  // 起始索引
    )
  } else {
    // 3.2 挂载新节点
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized,
      commonLength,  // 起始索引
    )
  }
}
1.卸载函数 unmountChildren

递归调用unmount进行卸载节点

const unmount: UnmountFn = (
vnode,          // 要卸载的虚拟节点
parentComponent, // 父组件实例
parentSuspense,  // Suspense边界
doRemove = false, // 是否执行DOM移除
optimized = false // 优化模式
) => {
// 核心逻辑分为六个阶段:

// 1. 引用清理
if (ref != null) {
  setRef(ref, null, parentSuspense, vnode, true)
}

// 2. KeepAlive组件特殊处理
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
  ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
  return
}

//通过位运算检查当前节点是否为DOM元素且包含指令
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
//排除异步组件包装器
const shouldInvokeVnodeHook = !isAsyncWrapper(vnode)
  
// 3. 生命周期钩子触发
// 从props中获取 onVnodeBeforeUnmount 钩子 ,并执行其函数

if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeBeforeUnmount)) {
  invokeVNodeHook(vnodeHook, parentComponent, vnode)
}

// 4. 组件卸载核心逻辑
if (shapeFlag & ShapeFlags.COMPONENT) {
  unmountComponent(vnode.component!, parentSuspense, doRemove)
} else {
  // 处理Suspense/Teleport等特殊节点
  if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
    vnode.suspense!.unmount(parentSuspense, doRemove)
    return
  }

  // 5. 动态子节点快速卸载
  if (dynamicChildren && (type !== Fragment || ...)) {
    unmountChildren(dynamicChildren, parentComponent, parentSuspense, false, true)
  }

  // 6. DOM元素最终移除
  if (doRemove) {
    remove(vnode)
  }
}

// 7. 异步清理钩子
queuePostRenderEffect(() => {
  // 触发unmounted生命周期
}, parentSuspense)
}
2.组件卸载 unmountComponent

该函数是组件卸载核心函数,确保组件卸载时的资源释放和状态管理

graph TD
A[beforeUnmount] --> B[Vue2 beforeDestroy]
B --> C[停止响应式]
C --> D[卸载子树]
D --> E[queue unmounted]
const unmountComponent = (
instance: ComponentInternalInstance,  // 要卸载的组件实例
parentSuspense: SuspenseBoundary | null, // 父级Suspense边界
doRemove?: boolean                    // 是否执行DOM移除
) => {

// 1. HMR注销(开发模式专用)
if (__DEV__ && instance.type.__hmrId) {
  unregisterHMR(instance)  // 注销热更新
}

// 2. 提取关键实例属性
const { bum, scope, update, subTree, um } = instance

// 3. beforeUnmount生命周期
if (bum) { // beforeUnmount钩子数组
  invokeArrayFns(bum) // 顺序执行所有beforeUnmount钩子
}

// 4. Vue 2兼容处理
if (__COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)) {
  instance.emit('hook:beforeDestroy') // Vue 2风格的beforeDestroy事件
}

// 5. 停止响应式追踪
scope.stop() // 停止组件作用域内的所有响应式effect

// 6. 卸载子组件树
if (update) { // 渲染effect存在时
  update.active = false // 禁用更新队列
  unmount(subTree, instance, parentSuspense, doRemove) // 递归卸载虚拟DOM树
}

// 7. 异步执行unmounted钩子
if (um) { // unmounted钩子数组
  queuePostRenderEffect(um, parentSuspense) // 加入后处理队列
}

// 8. Suspense相关处理
if (__FEATURE_SUSPENSE__ && parentSuspense && ...) {
  parentSuspense.deps--  // 减少Suspense依赖计数
  if (parentSuspense.deps === 0) {
    parentSuspense.resolve() // 若所有依赖完成则resolve
  }
}
}

vue3源码解析(三)