Vue3 源码详解

504 阅读27分钟

Vue3 源码详解

1. 代理(Proxy)

Vue3 的响应式系统建立在 ES6 的 Proxy 基础上,彻底替代了 Vue2 中基于 Object.defineProperty 的响应式实现。让我们深入分析 Vue3 中的响应式实现源码。

核心实现:reactive 函数

首先,让我们看一下 Vue3 中 reactive 函数的简化版实现:

// packages/reactivity/src/reactive.ts

// 用于存储原始对象到代理对象的映射
export const reactiveMap = new WeakMap<Target, any>()

// reactive函数接收一个对象,返回该对象的代理
export function reactive(target: object) {
  // 如果尝试将一个已经是代理的对象再次代理,直接返回
  if (isProxy(target)) {
    return target
  }
  
  // 如果目标已经有对应的代理对象,则直接返回已存在的代理
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  // 只允许代理对象或数组
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`reactive() 只能用于对象或数组,而不是 ${typeof target}`)
    }
    return target
  }
  
  // 创建代理
  const proxy = new Proxy(
    target,
    mutableHandlers // 这个函数包含各种代理处理程序
  )
  
  // 缓存原始对象和代理对象的映射关系
  reactiveMap.set(target, proxy)
  
  return proxy
}

每一行的说明:

  1. export const reactiveMap = new WeakMap<Target, any>() - 创建一个 WeakMap 存储原始对象到其代理对象的映射,使用 WeakMap 可以避免内存泄漏。
  2. export function reactive(target: object) - 导出 reactive 函数,接收一个目标对象作为参数。
  3. if (isProxy(target)) return target - 如果目标已经是响应式对象(代理),则直接返回,避免重复代理。
  4. const existingProxy = reactiveMap.get(target) - 检查是否已经为该目标对象创建过代理。
  5. if (existingProxy) return existingProxy - 如果已存在代理,直接返回已存在的代理,确保同一个原始对象只有一个代理实例。
  6. if (!isObject(target)) {...} - 确保目标是对象或数组,如果不是,在开发模式下发出警告并返回原始值。
  7. const proxy = new Proxy(target, mutableHandlers) - 核心代码,使用 ES6 的 Proxy 创建代理对象。
  8. reactiveMap.set(target, proxy) - 缓存原始对象和代理的映射,便于后续检索。
  9. return proxy - 返回创建的代理对象。

mutableHandlers 详解

接下来,让我们看看 mutableHandlers 的实现,这是 Proxy 的处理程序,定义了拦截操作:

// packages/reactivity/src/baseHandlers.ts

export const mutableHandlers: ProxyHandler<object> = {
  get,        // 拦截对象属性的读取操作
  set,        // 拦截对象属性的设置操作
  deleteProperty, // 拦截删除对象属性的操作
  has,        // 拦截 in 操作符
  ownKeys     // 拦截 Object.keys, Object.getOwnPropertyNames 等
}

// get 处理程序实现
function get(target: Target, key: string | symbol, receiver: object) {
  // 检查是否是特殊的内部属性
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true
  }
  
  // 获取原始值
  const res = Reflect.get(target, key, receiver)
  
  // 依赖收集
  track(target, TrackOpTypes.GET, key)
  
  // 如果读取的值是对象,则返回该对象的代理
  if (isObject(res)) {
    return reactive(res)
  }
  
  return res
}

// set 处理程序实现
function set(
  target: Target,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  // 获取旧值
  const oldValue = (target as any)[key]
  
  // 设置新值
  const result = Reflect.set(target, key, value, receiver)
  
  // 如果值发生变化,触发响应
  if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  
  return result
}

// deleteProperty 处理程序实现
function deleteProperty(target: Target, key: string | symbol): boolean {
  // 检查属性是否存在且是否是目标对象自身的属性
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  
  // 执行删除操作
  const result = Reflect.deleteProperty(target, key)
  
  // 如果删除成功且属性确实存在,触发响应
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  
  return result
}

// has 处理程序实现
function has(target: Target, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  
  // 依赖收集
  track(target, TrackOpTypes.HAS, key)
  
  return result
}

// ownKeys 处理程序实现
function ownKeys(target: Target): (string | symbol)[] {
  // 依赖收集,使用特殊的 ITERATE_KEY 作为键
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  
  return Reflect.ownKeys(target)
}

每个处理程序的详细分析:

get 处理程序
  1. if (key === ReactiveFlags.IS_REACTIVE) return true - 检查是否请求特殊标志,用于判断对象是否是响应式的。
  2. const res = Reflect.get(target, key, receiver) - 使用 Reflect.get 获取原始属性值。
  3. track(target, TrackOpTypes.GET, key) - 执行依赖收集,记录当前副作用(effect)依赖于这个属性。
  4. if (isObject(res)) return reactive(res) - 如果获取的值是对象,则递归对其进行代理(深度响应性)。
  5. return res - 返回最终值。
set 处理程序
  1. const oldValue = (target as any)[key] - 获取旧值,用于后续比较。
  2. const result = Reflect.set(target, key, value, receiver) - 使用 Reflect.set 设置新值。
  3. if (hasChanged(value, oldValue)) - 检查值是否实际发生变化。
  4. trigger(target, TriggerOpTypes.SET, key, value, oldValue) - 如果值变化,触发响应更新。
  5. return result - 返回设置操作的结果(通常为 true)。
deleteProperty 处理程序
  1. const hadKey = hasOwn(target, key) - 检查属性是否存在于目标对象上。
  2. const oldValue = (target as any)[key] - 获取旧值,用于在触发更新时提供。
  3. const result = Reflect.deleteProperty(target, key) - 执行删除操作。
  4. if (result && hadKey) - 如果删除成功且属性确实存在。
  5. trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) - 触发响应更新。
  6. return result - 返回删除操作的结果。
has 处理程序
  1. const result = Reflect.has(target, key) - 使用 Reflect.has 检查属性是否在目标对象上。
  2. track(target, TrackOpTypes.HAS, key) - 执行依赖收集,这样当属性添加或删除时,使用 in 操作符的代码可以响应。
  3. return result - 返回检查结果。
ownKeys 处理程序
  1. track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) - 对迭代操作收集依赖。
  2. return Reflect.ownKeys(target) - 返回目标对象的所有自有属性键。

依赖收集与触发更新

Vue3 响应式系统的核心在于依赖收集(track)和触发更新(trigger):

// packages/reactivity/src/effect.ts

// 活动的 effect
let activeEffect: ReactiveEffect | undefined

// 依赖收集函数
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果没有活动的 effect,直接返回
  if (!activeEffect) return
  
  // 获取目标对象的依赖映射
  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()))
  }
  
  // 将当前活动 effect 添加到依赖集合
  trackEffects(dep)
}

// 将 effect 添加到依赖集合
export function trackEffects(dep: Dep) {
  // 如果依赖已经被收集,则跳过
  if (!dep.has(activeEffect!)) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

// 触发更新函数
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key: unknown,
  newValue?: unknown,
  oldValue?: unknown
) {
  // 获取目标对象的依赖映射
  const depsMap = targetMap.get(target)
  if (!depsMap) return // 没有依赖,直接返回
  
  // 要触发的 effect 集合
  const effects = new Set<ReactiveEffect>()
  
  // 添加相关依赖到 effects 集合
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  
  // 1. 处理特定 key 的依赖
  if (key !== void 0) {
    add(depsMap.get(key))
  }
  
  // 2. 处理数组长度变化
  if (type === TriggerOpTypes.ADD && isArray(target)) {
    add(depsMap.get('length'))
  }
  
  // 3. 处理对象属性添加/删除
  if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
    add(depsMap.get(ITERATE_KEY))
  }
  
  // 触发收集的 effects
  effects.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  })
}

依赖收集(track)分析:

  1. if (!activeEffect) return - 如果没有活动的 effect(例如,不在 computed 或 watch 中),则不需要收集依赖。
  2. let depsMap = targetMap.get(target) - 获取与目标对象关联的依赖映射。
  3. if (!depsMap) targetMap.set(target, (depsMap = new Map())) - 如果依赖映射不存在,则创建一个新的。
  4. let dep = depsMap.get(key) - 获取与特定键关联的依赖集合。
  5. if (!dep) depsMap.set(key, (dep = createDep())) - 如果依赖集合不存在,则创建一个新的。
  6. trackEffects(dep) - 将当前活动的 effect 添加到依赖集合。

触发更新(trigger)分析:

  1. const depsMap = targetMap.get(target) - 获取目标对象的依赖映射。
  2. if (!depsMap) return - 如果没有依赖映射,说明该对象没有被观察,直接返回。
  3. const effects = new Set<ReactiveEffect>() - 创建一个 Set 来存储需要触发的 effect。
  4. const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {...} - 辅助函数,用于将 effect 添加到集合。
  5. if (key !== void 0) add(depsMap.get(key)) - 添加特定键的依赖。
  6. 处理数组长度变化和对象迭代相关的依赖。
  7. effects.forEach(effect => {...}) - 触发所有收集的 effect。

2. 虚拟DOM(Virtual DOM)

Vue3 的虚拟 DOM 系统是其渲染机制的核心。虚拟 DOM 本质上是真实 DOM 的 JavaScript 对象表示。

VNode 数据结构

首先,让我们看看 VNode(虚拟节点)的数据结构定义:

// packages/runtime-core/src/vnode.ts

export interface VNode {
  __v_isVNode: true             // 标识这是一个 VNode
  type: VNodeTypes               // 节点类型(组件、HTML 标签等)
  props: VNodeProps | null       // 节点属性
  key: string | number | null    // 节点的 key
  ref: VNodeRef | null           // 节点的 ref
  children: VNodeNormalizedChildren // 子节点
  component: ComponentInternalInstance | null // 组件实例(只对组件节点有效)
  el: HostNode | null           // 对应的真实 DOM 节点
  anchor: HostNode | null       // 用于 Fragment 的锚点节点
  target: HostElement | null    // Teleport 目标
  targetAnchor: HostNode | null // Teleport 目标锚点
  // 其他属性...
}

// createVNode 函数
export function createVNode(
  type: VNodeTypes,
  props: VNodeProps | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps = null,
  isBlockNode = false
): VNode {
  // class 和 style 标准化
  if (props) {
    // 对 class 进行标准化处理
    if ('class' in props) {
      props.class = normalizeClass(props.class)
    }
    
    // 对 style 进行标准化处理
    if ('style' in props) {
      props.style = normalizeStyle(props.style)
    }
  }
  
  // 确定节点标志(判断节点类型)
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT // HTML 标签
    : isObject(type)
      ? ShapeFlags.COMPONENT // 组件
      : 0
  
  // 创建 VNode 对象
  const vnode: VNode = {
    __v_isVNode: true,
    type,
    props,
    key: props?.key ?? null,
    ref: props?.ref ?? null,
    children: null,
    component: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    shapeFlag,
    patchFlag,
    dynamicProps,
    // 其他属性初始化...
  }
  
  // 标准化子节点
  normalizeChildren(vnode, children)
  
  return vnode
}

// 标准化子节点
function normalizeChildren(vnode: VNode, children: unknown) {
  let type = 0
  const { shapeFlag } = vnode
  
  if (children == null) {
    children = null
  } else if (isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'object') {
    // 单个 VNode 或者插槽
    if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
      // 将单个 VNode 转换为数组
      type = ShapeFlags.ARRAY_CHILDREN
      children = [children as VNode]
    } else {
      // 插槽
      type = ShapeFlags.SLOTS_CHILDREN
    }
  } else if (isFunction(children)) {
    // 函数子节点(渲染函数)
    type = ShapeFlags.SLOTS_CHILDREN
    children = { default: children }
  } else {
    // 字符串或数字,转换为文本节点
    children = String(children)
    type = ShapeFlags.TEXT_CHILDREN
  }
  
  vnode.children = children
  vnode.shapeFlag |= type // 使用位运算合并节点类型标志
}

VNode 和 createVNode 函数的分析:

  1. export interface VNode {...} - 定义 VNode 接口,表示虚拟 DOM 节点的属性。
  2. __v_isVNode: true - 标识这是一个 VNode 对象。
  3. type: VNodeTypes - 节点类型,可以是字符串(HTML 标签名)、组件对象或特殊的符号(Fragment, Teleport 等)。
  4. props: VNodeProps | null - 节点的属性,如 id, class, style 等。
  5. key: string | number | null - 节点的唯一标识,用于优化 diff 算法。
  6. children: VNodeNormalizedChildren - 子节点,可以是文本、VNode 数组或插槽对象。
  7. el: HostNode | null - 对应的真实 DOM 节点,初始为 null,在挂载时赋值。

createVNode 函数的关键步骤:

  1. if (props) {...} - 标准化 class 和 style 属性。
  2. const shapeFlag = ... - 确定节点的类型标志。
  3. const vnode: VNode = {...} - 创建 VNode 对象,初始化各种属性。
  4. normalizeChildren(vnode, children) - 标准化子节点。
  5. return vnode - 返回创建的 VNode。

子节点标准化(normalizeChildren)的关键步骤:

  1. 根据子节点的类型(数组、对象、函数或原始值)确定 shapeFlag。
  2. 对不同类型的子节点进行适当的转换(如将单个 VNode 转换为数组)。
  3. 设置 vnode.children 和 vnode.shapeFlag。

渲染流程

Vue3 的渲染流程涉及创建 VNode 树、挂载和更新:

// packages/runtime-core/src/renderer.ts

// 创建渲染器
export function createRenderer(options: RendererOptions) {
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    // 其他平台相关的函数...
  } = options
  
  // 挂载组件
  const mountComponent = (
    initialVNode: VNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    // 创建组件实例
    const instance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
    
    // 设置组件实例
    setupComponent(instance)
    
    // 设置并运行渲染效果
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }
  
  // 设置渲染效果
  const setupRenderEffect = (
    instance: ComponentInternalInstance,
    initialVNode: VNode,
    container: HostElement,
    anchor: HostNode | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    // 创建响应式效果
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        // 初始渲染
        const subTree = (instance.subTree = renderComponentRoot(instance))
        
        // 挂载子树
        patch(
          null,
          subTree,
          container,
          anchor,
          instance,
          parentSuspense,
          isSVG
        )
        
        initialVNode.el = subTree.el
        instance.isMounted = true
      } else {
        // 更新
        const prevTree = instance.subTree
        const nextTree = renderComponentRoot(instance)
        
        instance.subTree = nextTree
        
        // 更新子树
        patch(
          prevTree,
          nextTree,
          hostParentNode(prevTree.el!)!,
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )
        
        initialVNode.el = nextTree.el
      }
    }, prodEffectOptions)
  }
  
  // patch 函数 - 核心的 DOM 操作函数
  const patch = (
    n1: VNode | null, // 旧 VNode
    n2: VNode,        // 新 VNode
    container: HostElement,
    anchor: HostNode | null = null,
    parentComponent: ComponentInternalInstance | null = null,
    parentSuspense: SuspenseBoundary | null = null,
    isSVG: boolean = false,
    optimized: boolean = false
  ) => {
    // 如果新旧节点类型不同,卸载旧节点
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }
    
    // 根据节点类型执行不同的处理
    const { type, shapeFlag } = n2
    switch (type) {
      case Text:
        // 处理文本节点
        processText(n1, n2, container, anchor)
        break
      case Comment:
        // 处理注释节点
        processCommentNode(n1, n2, container, anchor)
        break
      case Fragment:
        // 处理 Fragment
        processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // 处理普通元素
          processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 处理组件
          processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          // 处理 Teleport
          type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals)
        } // ... 其他类型的处理
    }
  }
  
  // 处理元素
  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      // 挂载元素
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      // 更新元素
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  }
  
  // 挂载元素
  const mountElement = (
    vnode: VNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    let el: HostElement
    const { type, props, shapeFlag } = vnode
    
    // 创建元素
    el = vnode.el = hostCreateElement(
      type as string,
      isSVG,
      props?.is,
      props
    )
    
    // 设置文本内容
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 挂载子节点数组
      mountChildren(
        vnode.children as VNode[],
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG && type !== 'foreignObject',
        optimized || !!vnode.dynamicChildren
      )
    }
    
    // 设置属性
    if (props) {
      for (const key in props) {
        if (!isReservedProp(key)) {
          hostPatchProp(
            el,
            key,
            null,
            props[key],
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
    }
    
    // 触发 'beforeMount' 钩子
    onBeforeMount(vnode, parentComponent)
    
    // 插入到 DOM
    hostInsert(el, container, anchor)
    
    // 触发 'mounted' 钩子
    queuePostRenderEffect(
      () => onMounted(vnode, parentComponent),
      parentSuspense
    )
  }
  
  // 返回渲染器 API
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

渲染流程分析:

  1. createRenderer 函数创建一个渲染器,接收平台相关的 DOM 操作函数。

  2. mountComponent 函数挂载组件:

    • 创建组件实例
    • 设置组件(处理 props, slots 等)
    • 设置渲染效果
  3. setupRenderEffect 函数设置组件的渲染效果:

    • 创建一个响应式效果,当依赖变化时自动重新渲染
    • 处理初始渲染和后续更新
    • 调用 patch 函数进行 DOM 操作
  4. patch 函数是核心的 DOM 操作函数:

    • 比较新旧 VNode
    • 根据节点类型(文本、元素、组件等)分发给不同的处理函数
    • 处理 VNode 的挂载和更新
  5. processElement 函数处理普通 DOM 元素:

    • 如果没有旧节点,调用 mountElement 挂载新元素
    • 如果有旧节点,调用 patchElement 更新元素
  6. mountElement 函数挂载一个元素:

    • 创建 DOM
// packages/runtime-core/src/renderer.ts

const mountElement = (
  vnode: VNode,
  container: HostElement,
  anchor: HostNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean
) => {
  let el: HostElement
  const { type, props, shapeFlag, transition, dirs } = vnode
  
  // 创建元素
  el = vnode.el = hostCreateElement(
    type as string,
    isSVG,
    props?.is,
    props
  )
  
  // 设置元素内容
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 如果子节点是文本,直接设置文本内容
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 如果子节点是数组,递归挂载子节点
    mountChildren(
      vnode.children as VNode[],
      el,
      null,
      parentComponent,
      parentSuspense,
      isSVG && type !== 'foreignObject',
      optimized
    )
  }
  
  // 处理指令
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'created')
  }
  
  // 处理 scopeId
  setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
  
  // 设置元素属性
  if (props) {
    for (const key in props) {
      if (!isReservedProp(key)) {
        hostPatchProp(
          el,
          key,
          null,
          props[key],
          isSVG,
          vnode.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }
    // 如果有 onVnodeBeforeMount 钩子,执行它
    if ((vnodeHook = props.onVnodeBeforeMount)) {
      invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }
  }
  
  // 指令 beforeMount 钩子
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  }
  
  // 插入到 DOM
  hostInsert(el, container, anchor)
  
  // 处理挂载后逻辑
  const { transition } = vnode
  if (shapeFlag & ShapeFlags.COMPONENT && transition && !transition.persisted) {
    transition.beforeEnter(el)
  }
  
  // onMounted 钩子
  if ((vnodeHook = props && props.onVnodeMounted) || (shapeFlag & ShapeFlags.COMPONENT && transition) || dirs) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
      transition && !transition.persisted && transition.enter(el)
      dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
    }, parentSuspense)
  }
}

详细解析每一行:

  1. const { type, props, shapeFlag, transition, dirs } = vnode - 从 vnode 解构出需要的属性。
  2. el = vnode.el = hostCreateElement(...) - 创建真实 DOM 元素并保存到 vnode.el。
  3. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {...} - 如果子节点是文本,直接设置元素的文本内容。
  4. else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {...} - 如果子节点是数组,调用 mountChildren 递归挂载。
  5. if (dirs) {...} - 如果有指令,调用指令的 created 钩子。
  6. setScopeId(...) - 设置 scoped CSS 的 ID,用于样式隔离。
  7. if (props) {...} - 遍历 props 设置元素属性,忽略保留的属性。
  8. if ((vnodeHook = props.onVnodeBeforeMount)) {...} - 如果有 onVnodeBeforeMount 钩子,执行它。
  9. if (dirs) {...} - 执行指令的 beforeMount 钩子。
  10. hostInsert(el, container, anchor) - 将元素插入到 DOM 中。
  11. if (shapeFlag & ShapeFlags.COMPONENT && transition && !transition.persisted) {...} - 处理组件的过渡效果。
  12. if ((vnodeHook = props && props.onVnodeMounted) || ...) - 将 onVnodeMounted 钩子等加入渲染后队列。

patchElement 函数详解

当更新一个已存在的元素时,Vue 使用 patchElement 函数:

// packages/runtime-core/src/renderer.ts

const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean
) => {
  const el = (n2.el = n1.el!)
  const oldProps = n1.props || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ
  
  // 处理指令
  const dirs = n2.dirs
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
  }
  
  // 如果有 onVnodeBeforeUpdate 钩子,执行它
  if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
    invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
  }
  
  // 更新子节点
  if (n2.dynamicChildren) {
    // 如果有动态子节点(编译器优化),只更新动态子节点
    patchBlockChildren(
      n1.dynamicChildren!,
      n2.dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      optimized
    )
  } else if (!optimized) {
    // 全量 diff 更新子节点
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG
    )
  }
  
  // 更新属性
  if (oldProps !== newProps) {
    for (const key in newProps) {
      // 忽略保留属性
      if (!isReservedProp(key) && newProps[key] !== undefined) {
        hostPatchProp(
          el,
          key,
          oldProps[key],
          newProps[key],
          isSVG,
          n1.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }
    
    // 删除不再存在的属性
    if (oldProps !== EMPTY_OBJ) {
      for (const key in oldProps) {
        if (!isReservedProp(key) && !(key in newProps)) {
          hostPatchProp(
            el,
            key,
            oldProps[key],
            null,
            isSVG,
            n1.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
    }
  }
  
  // 处理更新后逻辑
  if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
      dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    }, parentSuspense)
  }
}

patchElement 函数的详细解析:

  1. const el = (n2.el = n1.el!) - 重用旧元素的 DOM 节点。
  2. const oldProps = n1.props || EMPTY_OBJ - 获取旧属性或使用空对象。
  3. const newProps = n2.props || EMPTY_OBJ - 获取新属性或使用空对象。
  4. if (dirs) {...} - 如果有指令,调用指令的 beforeUpdate 钩子。
  5. if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {...} - 如果有 onVnodeBeforeUpdate 钩子,执行它。
  6. if (n2.dynamicChildren) {...} else if (!optimized) {...} - 更新子节点,优先使用优化路径。
  7. if (oldProps !== newProps) {...} - 如果属性发生变化,更新属性。
  8. for (const key in newProps) {...} - 设置新属性或更新变化的属性。
  9. if (oldProps !== EMPTY_OBJ) {...} - 删除不再存在的属性。
  10. if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {...} - 将 onVnodeUpdated 钩子等加入渲染后队列。

mountChildren 和 patchChildren

处理子节点的两个关键函数:

// packages/runtime-core/src/renderer.ts

// 挂载子节点
const mountChildren = (
  children: VNode[],
  container: HostElement,
  anchor: HostNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean,
  slotScopeIds: string[] | null = null,
  start = 0
) => {
  for (let i = start; i < children.length; i++) {
    const child = normalizeVNode(children[i])
    patch(
      null,
      child,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized,
      slotScopeIds
    )
  }
}

// 更新子节点
const patchChildren = (
  n1: VNode | null,
  n2: VNode,
  container: HostElement,
  anchor: HostNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null = null,
  optimized = false
) => {
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children
  const { shapeFlag } = n2
  
  // 子节点更新的四种情况
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 1. 新子节点是文本
    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) {
      // 2. 旧子节点是数组
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 新子节点也是数组,执行 diff 算法
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // 新子节点不是数组,卸载旧子节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // 3. 旧子节点是文本或不存在
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        // 旧子节点是文本,清空文本
        hostSetElementText(container, '')
      }
      // 4. 新子节点是数组,挂载新子节点
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
    }
  }
}

mountChildren 和 patchChildren 的详细解析:

  1. mountChildren 函数遍历子节点数组,对每个子节点调用 patch 函数,传入 null 作为旧节点(表示挂载)。

  2. patchChildren 函数根据新旧子节点的类型(文本或数组)进行不同的处理:

    • 新子节点是文本:
      • 如果旧子节点是数组,卸载旧子节点
      • 设置容器的文本内容
    • 新子节点是数组:
      • 如果旧子节点也是数组,执行 diff 算法
      • 如果旧子节点是文本,清空文本并挂载新子节点
      • 如果旧子节点不存在,直接挂载新子节点

3. Diff 算法

Vue3 的 diff 算法主要实现在 patchKeyedChildren 函数中,这是 Vue3 中最复杂也是最核心的部分之一:

// packages/runtime-core/src/renderer.ts

const patchKeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: HostElement,
  parentAnchor: HostNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1 // 旧子节点的结束索引
  let e2 = l2 - 1 // 新子节点的结束索引
  
  // 1. 从头开始同步
  // (a b) c
  // (a b) d e
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = normalizeVNode(c2[i])
    if (isSameVNodeType(n1, n2)) {
      // 如果节点类型相同,递归更新
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      // 遇到不同的节点,跳出循环
      break
    }
    i++
  }
  
  // 2. 从尾开始同步
  // a (b c)
  // d e (b c)
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = normalizeVNode(c2[e2])
    if (isSameVNodeType(n1, n2)) {
      // 如果节点类型相同,递归更新
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      // 遇到不同的节点,跳出循环
      break
    }
    e1--
    e2--
  }
  
  // 3. 处理公共序列外的节点
  if (i > e1) {
    // 旧子节点已处理完,但新子节点还有剩余
    // 挂载剩余的新子节点
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
      while (i <= e2) {
        patch(
          null,
          normalizeVNode(c2[i]),
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        i++
      }
    }
  } else if (i > e2) {
    // 新子节点已处理完,但旧子节点还有剩余
    // 卸载剩余的旧子节点
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  } else {
    // 4. 处理中间部分未匹配的节点
    const s1 = i // 旧子节点开始索引
    const s2 = i // 新子节点开始索引
    
    // 4.1 建立新子节点的 key 到索引的映射
    const keyToNewIndexMap = new Map<string | number | symbol, number>()
    for (i = s2; i <= e2; i++) {
      const nextChild = normalizeVNode(c2[i])
      if (nextChild.key != null) {
        keyToNewIndexMap.set(nextChild.key, i)
      }
    }
    
    // 4.2 更新和移除
    let j
    let patched = 0 // 已处理的新子节点数量
    const toBePatched = e2 - s2 + 1 // 需要处理的新子节点总数
    let moved = false // 是否需要移动节点
    let maxNewIndexSoFar = 0 // 最大新索引
    
    // 用于存储新子节点在旧子节点中的位置
    const newIndexToOldIndexMap = new Array(toBePatched)
    for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
    
    // 遍历旧子节点,在新子节点中找到匹配项并更新
    for (i = s1; i <= e1; i++) {
      const prevChild = c1[i]
      
      // 如果已处理的新子节点数量等于总数,直接卸载剩余的旧子节点
      if (patched >= toBePatched) {
        unmount(prevChild, parentComponent, parentSuspense, true)
        continue
      }
      
      let newIndex
      if (prevChild.key != null) {
        // 如果旧子节点有 key,从映射中找到匹配的新子节点
        newIndex = keyToNewIndexMap.get(prevChild.key)
      } else {
        // 如果旧子节点没有 key,在未处理的新子节点中遍历查找匹配的节点
        for (j = s2; j <= e2; j++) {
          if (
            newIndexToOldIndexMap[j - s2] === 0 &&
            isSameVNodeType(prevChild, c2[j] as VNode)
          ) {
            newIndex = j
            break
          }
        }
      }
      
      if (newIndex === undefined) {
        // 如果未找到匹配的新子节点,卸载旧子节点
        unmount(prevChild, parentComponent, parentSuspense, true)
      } else {
        // 标记新子节点对应的旧子节点索引
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        
        // 检查是否需要移动节点
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex
        } else {
          moved = true
        }
        
        // 更新匹配的节点
        patch(
          prevChild,
          c2[newIndex] as VNode,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        patched++
      }
    }
    
    // 4.3 移动和挂载
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : EMPTY_ARR
    j = increasingNewIndexSequence.length - 1
    
    // 倒序处理以便正确移动节点
    for (i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = s2 + i
      const nextChild = c2[nextIndex] as VNode
      const anchor =
        nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
      
      if (newIndexToOldIndexMap[i] === 0) {
        // 挂载新子节点
        patch(
          null,
          nextChild,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (moved) {
        // 移动节点
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor, MoveType.REORDER)
        } else {
          j--
        }
      }
    }
  }
}

Vue3 diff 算法的详细分析:

1. 从头开始同步

// 1. 从头开始同步
while (i <= e1 && i <= e2) {
  const n1 = c1[i]
  const n2 = normalizeVNode(c2[i])
  if (isSameVNodeType(n1, n2)) {
    // 如果节点类型相同,递归更新
    patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
  } else {
    // 遇到不同的节点,跳出循环
    break
  }
  i++
}

这一步从头开始比较新旧子节点列表,直到遇到不同类型的节点。对于类型相同的节点,调用 patch 函数递归更新内容。

2. 从尾开始同步

// 2. 从尾开始同步
while (i <= e1 && i <= e2) {
  const n1 = c1[e1]
  const n2 = normalizeVNode(c2[e2])
  if (isSameVNodeType(n1, n2)) {
    // 如果节点类型相同,递归更新
    patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
  } else {
    // 遇到不同的节点,跳出循环
    break
  }
  e1--
  e2--
}

这一步从尾开始比较新旧子节点列表,直到遇到不同类型的节点。同样,对于类型相同的节点,调用 patch 函数递归更新内容。

3. 处理公共序列外的节点

// 3. 处理公共序列外的节点
if (i > e1) {
  // 旧子节点已处理完,但新子节点还有剩余,挂载剩余的新子节点
  if (i <= e2) {
    const nextPos = e2 + 1
    const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
    while (i <= e2) {
      patch(null, normalizeVNode(c2[i]), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
      i++
    }
  }
} else if (i > e2) {
  // 新子节点已处理完,但旧子节点还有剩余,卸载剩余的旧子节点
  while (i <= e1) {
    unmount(c1[i], parentComponent, parentSuspense, true)
    i++
  }
}

这一步处理两种情况:

  1. 如果新子节点比旧子节点多,挂载剩余的新子节点。
  2. 如果旧子节点比新子节点多,卸载剩余的旧子节点。

4. 处理中间部分未匹配的节点

else {
  // 4. 处理中间部分未匹配的节点
  const s1 = i // 旧子节点开始索引
  const s2 = i // 新子节点开始索引
  
  // 4.1 建立新子节点的 key 到索引的映射
  const keyToNewIndexMap = new Map<string | number | symbol, number>()
  for (i = s2; i <= e2; i++) {
    const nextChild = normalizeVNode(c2[i])
    if (nextChild.key != null) {
      keyToNewIndexMap.set(nextChild.key, i)
    }
  }
  
  // 4.2 更新和移除
  let j
  let patched = 0 // 已处理的新子节点数量
  const toBePatched = e2 - s2 + 1 // 需要处理的新子节点总数
  let moved = false // 是否需要移动节点
  let maxNewIndexSoFar = 0 // 最大新索引
  
  // 用于存储新子节点在旧子节点中的位置
  const newIndexToOldIndexMap = new Array(toBePatched)
  for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
  
  // 遍历旧子节点
  for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    
    if (patched >= toBePatched) {
      // 如果已处理的新子节点数量等于总数,直接卸载剩余的旧子节点
      unmount(prevChild, parentComponent, parentSuspense, true)
      continue
    }
    
    let newIndex
    if (prevChild.key != null) {
      // 如果旧子节点有 key,从映射中找到匹配的新子节点
      newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
      // 如果旧子节点没有 key,在未处理的新子节点中遍历查找匹配的节点
      for (j = s2; j <= e2; j++) {
        if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode)) {
          newIndex = j
          break
        }
      }
    }
    
    if (newIndex === undefined) {
      // 如果未找到匹配的新子节点,卸载旧子节点
      unmount(prevChild, parentComponent, parentSuspense, true)
    } else {
      // 标记新子节点对应的旧子节点索引
      newIndexToOldIndexMap[newIndex - s2] = i + 1
      
      // 检查是否需要移动节点
      if (newIndex >= maxNewIndexSoFar) {
        maxNewIndexSoFar = newIndex
      } else {
        moved = true
      }
      
      // 更新匹配的节点
      patch(prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
      patched++
    }
  }
  
  // 4.3 移动和挂载
  const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR
  j = increasingNewIndexSequence.length - 1
  
  // 倒序处理以便正确移动节点
  for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex] as VNode
    const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el
// 倒序处理以便正确移动节点
for (i = toBePatched - 1; i >= 0; i--) {
  const nextIndex = s2 + i
  const nextChild = c2[nextIndex] as VNode
  const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
  
  if (newIndexToOldIndexMap[i] === 0) {
    // 挂载新子节点
    patch(
      null,
      nextChild,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else if (moved) {
    // 移动节点
    if (j < 0 || i !== increasingNewIndexSequence[j]) {
      move(nextChild, container, anchor, MoveType.REORDER)
    } else {
      j--
    }
  }
}

这部分代码是 diff 算法的最后一步,处理节点的移动和挂载。让我们详细分析每一行:

  1. for (i = toBePatched - 1; i >= 0; i--) - 倒序遍历需要处理的新子节点,这样可以确保在移动节点时锚点位置是正确的。

  2. const nextIndex = s2 + i - 计算当前处理的新子节点在原数组中的索引。

  3. const nextChild = c2[nextIndex] as VNode - 获取当前处理的新子节点。

  4. const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor - 确定插入位置的锚点,使用下一个节点的 DOM 元素或父容器的锚点。

  5. if (newIndexToOldIndexMap[i] === 0) - 检查当前新子节点是否在旧子节点中存在。值为 0 表示这是一个全新的节点。

  6. 如果是新节点,调用 patch(null, nextChild, ...) 挂载它。

  7. else if (moved) - 如果需要移动节点(之前的处理中检测到了顺序变化)。

  8. if (j < 0 || i !== increasingNewIndexSequence[j]) - 检查当前节点是否需要移动:

    • j < 0 表示已经处理完所有最长递增子序列中的节点
    • i !== increasingNewIndexSequence[j] 表示当前节点不在最长递增子序列中
  9. 如果需要移动,调用 move(nextChild, container, anchor, MoveType.REORDER) 移动节点。

  10. 如果不需要移动(即当前节点在最长递增子序列中),则 j-- 继续处理前一个最长递增子序列中的节点。

最长递增子序列算法

Vue3 的 diff 算法使用了最长递增子序列(Longest Increasing Subsequence,简称 LIS)算法来优化节点移动。这个算法的实现如下:

// 获取最长递增子序列的索引
function getSequence(arr: number[]): number[] {
  const p = arr.slice() // 用于记录前驱节点的索引
  const result = [0] // 结果数组,初始包含第一个元素的索引
  let i, j, u, v, c
  const len = arr.length
  
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    
    // 跳过值为 0 的元素(表示这是新增的节点)
    if (arrI !== 0) {
      j = result[result.length - 1]
      
      // 如果当前元素大于结果数组中最后一个元素对应的值,直接添加到结果数组
      if (arr[j] < arrI) {
        p[i] = j // 记录前驱节点
        result.push(i)
        continue
      }
      
      // 二分查找,找到第一个大于等于 arrI 的位置
      u = 0
      v = result.length - 1
      while (u < v) {
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      
      // 更新结果数组
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1] // 记录前驱节点
        }
        result[u] = i
      }
    }
  }
  
  // 回溯构建最终序列
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  
  return result
}

这个算法的详细分析:

  1. const p = arr.slice() - 创建一个数组 p 用于记录前驱节点的索引。

  2. const result = [0] - 初始化结果数组,包含第一个元素的索引。

  3. 主循环遍历输入数组:

    • 跳过值为 0 的元素(表示新增节点)
    • 如果当前元素大于结果数组最后一个元素对应的值,直接添加到结果数组
    • 否则,使用二分查找找到结果数组中第一个大于等于当前元素的位置,并更新该位置
  4. 最后通过回溯构建最终的最长递增子序列。

move 函数

当需要移动节点时,Vue3 使用 move 函数:

const move = (
  vnode: VNode,
  container: HostElement,
  anchor: HostNode | null,
  moveType: MoveType,
  parentSuspense: SuspenseBoundary | null = null
) => {
  const { el, type, transition, children, shapeFlag } = vnode
  
  // 处理 Fragment
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    // KeepAlive 组件特殊处理
    (vnode.component as any).move(vnode, container, anchor, moveType)
    return
  }
  
  if (shapeFlag & ShapeFlags.TELEPORT) {
    // Teleport 组件特殊处理
    (type as typeof TeleportImpl).move(vnode, container, anchor, internals)
    return
  }
  
  if (type === Fragment) {
    // Fragment 节点,移动所有子节点
    hostInsert(el!, container, anchor)
    for (let i = 0; i < (children as VNode[]).length; i++) {
      move((children as VNode[])[i], container, anchor, moveType)
    }
    hostInsert(vnode.anchor!, container, anchor)
    return
  }
  
  // 处理静态节点
  if (type === Static) {
    moveStaticNode(vnode, container, anchor)
    return
  }
  
  // 处理普通节点
  const needTransition = moveType !== MoveType.REORDER &&
    shapeFlag & ShapeFlags.ELEMENT &&
    transition
  
  if (needTransition) {
    // 如果需要过渡效果
    if (moveType === MoveType.ENTER) {
      transition!.beforeEnter(el!)
      hostInsert(el!, container, anchor)
      queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
    } else {
      const { leave, delayLeave, afterLeave } = transition!
      const remove = () => hostInsert(el!, container, anchor)
      const performLeave = () => {
        leave(el!, () => {
          remove()
          afterLeave && afterLeave()
        })
      }
      if (delayLeave) {
        delayLeave(el!, remove, performLeave)
      } else {
        performLeave()
      }
    }
  } else {
    // 直接移动节点
    hostInsert(el!, container, anchor)
  }
}

move 函数的详细分析:

  1. 首先检查节点的类型,对不同类型的节点进行特殊处理:

    • KeepAlive 组件
    • Teleport 组件
    • Fragment 节点
    • 静态节点
  2. 对于普通节点,检查是否需要过渡效果:

    • 如果需要过渡效果,根据移动类型(进入或离开)应用相应的过渡
    • 如果不需要过渡效果,直接调用 hostInsert 插入节点

Vue3 Diff 算法的优化

Vue3 的 diff 算法相比 Vue2 有以下几个主要优化:

  1. 静态提升(Static Hoisting)

    • 将静态节点提升到渲染函数之外,避免每次渲染时重新创建
    • 在编译时完成,不在运行时的 diff 算法中
  2. 静态树提升(Static Tree Hoisting)

    • 将整个静态子树提升,减少虚拟 DOM 的创建和比对
    • 同样在编译时完成
  3. 块树(Block Tree)

    • 将模板基于动态节点指令切割为嵌套的区块
    • 每个区块内部的节点结构是固定的
    • 每个区块只需要追踪自身包含的动态节点
  4. 更高效的 Diff 算法

    • 双端比较优化
    • 最长递增子序列算法优化节点移动
  5. PatchFlags 标记

    • 在编译时为动态节点添加标记,指示节点的哪些属性是动态的
    • 运行时只需要检查和更新这些动态属性,跳过静态部分

总结

通过深入分析 Vue3 的源码,我们了解了其三个核心部分:

  1. 响应式系统:基于 ES6 Proxy 实现,相比 Vue2 的 Object.defineProperty 有更好的性能和功能。

  2. 虚拟 DOM:使用 JavaScript 对象表示 DOM 结构,通过比较新旧虚拟 DOM 树的差异来最小化 DOM 操作。

  3. Diff 算法:Vue3 的 diff 算法采用了多种优化策略,包括:

    • 从头尾同步处理相同节点
    • 使用 key 快速定位节点
    • 最长递增子序列算法优化节点移动
    • 编译时优化(静态提升、块树等)

Vue3 的这些核心实现使其在性能和开发体验上都有了显著提升。理解这些实现细节不仅有助于更好地使用 Vue3,也能帮助我们在构建自己的应用时做出更优的设计决策。

Vue3 的源码设计非常精巧,每一部分都经过精心设计和优化。通过学习这些源码,我们可以学到很多现代前端框架的设计思想和实现技巧,这对提升我们自身的编程能力非常有帮助。