Skip to content

Vue 3 响应式渲染原理与实现

1. 响应式系统原理

1.1 核心概念

Vue 3 的响应式系统基于 ES6 的 ProxyReflect API,相比 Vue 2 的 Object.defineProperty 有以下优势:

  • 能够监听对象的新增属性
  • 能够监听数组的索引和长度变化
  • 支持 Map、Set、WeakMap、WeakSet 等集合类型
  • 性能更好,特别是在大型对象上

1.2 实现原理

Vue 3 的响应式系统由以下核心部分组成:

  1. Proxy 代理:创建对象的代理,拦截各种操作
  2. ReactiveEffect:响应式副作用,用于追踪依赖
  3. WeakMap:存储原始对象到代理对象的映射
  4. Map:存储对象属性到依赖集合的映射

1.2.1 核心实现代码

javascript
// 创建响应式对象
function reactive(target) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

// 创建代理对象
function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers
) {
  // 目标不是对象,直接返回
  if (!isObject(target)) {
    return target
  }
  // 目标已经是代理对象,直接返回
  if (target.__v_isReactive && !isReadonly) {
    return target
  }
  // 目标已经是只读代理,直接返回
  if (target.__v_isReadonly && isReadonly) {
    return target
  }
  // 检查是否有缓存的代理对象
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 创建新的代理对象
  const proxy = new Proxy(
    target,
    isCollectionType(target) ? collectionHandlers : baseHandlers
  )
  // 缓存代理对象
  proxyMap.set(target, proxy)
  return proxy
}

1.3 依赖追踪与触发

1.3.1 依赖追踪

当访问响应式对象的属性时,会触发 get 拦截器,此时会收集当前正在执行的副作用函数作为依赖:

javascript
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // 非只读时进行依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 处理嵌套对象
    if (isObject(res) && !shallow) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

// 依赖收集
function track(target, type, key) {
  if (!isTracking()) {
    return
  }
  // 获取 target 对应的依赖映射
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取 key 对应的依赖集合
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }
  // 将当前副作用添加到依赖集合
  trackEffects(dep)
}

1.3.2 依赖触发

当修改响应式对象的属性时,会触发 set 拦截器,此时会通知所有依赖该属性的副作用函数重新执行:

javascript
function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标是原型链上的属性,不触发更新
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 新增属性
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (
        !shallow &&
        oldValue !== value &&
        (oldValue === oldValue || value === value)
      ) {
        // 修改属性
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

// 依赖触发
function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  // 收集所有需要触发的依赖
  const effects = new Set()
  const add = effectsToAdd => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => effects.add(effect))
    }
  }
  // 处理不同类型的操作
  if (type === TriggerOpTypes.CLEAR) {
    // 清除操作,触发所有依赖
    depsMap.forEach(add)
  } else if (type === TriggerOpTypes.RESET) {
    // 重置操作,触发所有依赖
    depsMap.forEach(add)
  } else if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
    // 新增或删除属性,触发对应 key 的依赖以及长度相关的依赖(数组)
    add(depsMap.get(key))
    if (isArray(target)) {
      if (key === 'length') {
        // 长度变化,触发所有索引大于等于新长度的依赖
        depsMap.forEach((dep, key) => {
          if (key >= newValue) {
            add(dep)
          }
        })
      } else {
        // 索引变化,触发长度的依赖
        add(depsMap.get('length'))
      }
    }
  } else if (type === TriggerOpTypes.SET) {
    // 修改属性,触发对应 key 的依赖
    add(depsMap.get(key))
    // 数组特殊处理
    if (isArray(target) && isIntegerKey(key)) {
      add(depsMap.get('length'))
    }
  }
  // 触发所有依赖
  effects.forEach(effect => {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  })
}

2. 虚拟 DOM 原理

2.1 核心概念

虚拟 DOM(Virtual DOM)是对真实 DOM 的抽象表示,是一个普通的 JavaScript 对象,包含了描述真实 DOM 所需的信息。

Vue 3 的虚拟 DOM 相比 Vue 2 有以下改进:

  • 更轻量:移除了一些不必要的属性
  • 更高效:优化了创建和更新的流程
  • 更好的 TypeScript 支持

2.2 VNode 结构

Vue 3 的 VNode 结构如下:

typescript
interface VNode {
  __v_isVNode: true
  type: VNodeTypes // 元素类型、组件、Fragment 等
  props: VNodeProps | null // 属性
  key: string | number | null // 唯一标识,用于 diff 算法
  ref: VNodeRef | null // 引用
  children: VNodeNormalizedChildren // 子节点
  el: HostNode | null // 对应的真实 DOM 节点
  // 其他内部属性
}

2.3 创建 VNode

javascript
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  // 标准化 props
  props = props || null

  // 创建 VNode 对象
  const vnode = {
    __v_isVNode: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    children: null,
    el: null,
    patchFlag,
    dynamicProps,
    appContext: null
  }

  // 标准化子节点
  normalizeChildren(vnode, children)

  // 设置 appContext
  if (currentApp) {
    vnode.appContext = currentApp._context
  }

  return vnode
}

3. Diff 算法原理与实现

3.1 核心概念

Diff 算法是虚拟 DOM 的核心,用于比较新旧 VNode 树的差异,并最小化 DOM 操作。Vue 3 的 Diff 算法相比 Vue 2 有以下改进:

  • 静态标记:通过 patchFlag 标记动态内容,减少比较次数
  • 区块化:将 VNode 树划分为区块,只比较动态区块
  • 快速路径:针对常见场景优化,如数组尾部添加元素

3.2 实现原理

Vue 3 的 Diff 算法主要分为以下几个步骤:

  1. 同类型节点比较:如果新旧 VNode 类型相同,进行精细化比较
  2. 不同类型节点替换:如果新旧 VNode 类型不同,直接替换整个节点
  3. 子节点比较:对于有子节点的节点,使用双端比较算法

3.3 核心实现代码

3.3.1 节点比较

javascript
function processComponent(n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
  if (n1 == null) {
    // 首次渲染
    mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  } else {
    // 更新组件
    updateComponent(n1, n2, optimized)
  }
}

function processElement(n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
  if (n1 == null) {
    // 首次渲染
    mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  } else {
    // 更新元素
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  }
}

3.3.2 子节点比较

javascript
function patchChildren(n1: VNode, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
  const c1 = n1 && n1.children
  const c2 = n2.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const nextShapeFlag = n2.shapeFlag

  // 处理文本节点
  if (nextShapeFlag & 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) {
      if (nextShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 新旧节点都是数组,使用 diff 算法
        patchKeyedChildren(c1 as VNode[], c2 as VNode[], container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      } else {
        // 旧节点是数组,新节点不是,清空旧节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
    } else {
      if (nextShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 旧节点不是数组,新节点是数组,挂载新节点
        mountChildren(c2 as VNode[], container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      // 新旧节点都不是数组,无需处理
    }
  }
}

3.3.3 双端比较算法

javascript
function patchKeyedChildren(c1: VNode[], c2: VNode[], container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1
  let e2 = l2 - 1

  // 1. 从头部开始比较
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    if (isSameVNodeType(n1, n2)) {
      // 类型相同,递归比较
      patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, optimized)
    } else {
      break
    }
    i++
  }

  // 2. 从尾部开始比较
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      // 类型相同,递归比较
      patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, optimized)
    } else {
      break
    }
    e1--
    e2--
  }

  // 3. 新节点比旧节点多,挂载新节点
  if (i > e1) {
    if (i <= e2) {
      const nextAnchor = e2 + 1 < l2 ? c2[e2 + 1].el : anchor
      while (i <= e2) {
        patch(null, c2[i], container, nextAnchor, parentComponent, parentSuspense, isSVG, optimized)
        i++
      }
    }
  }
  // 4. 旧节点比新节点多,卸载旧节点
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }
  // 5. 中间部分,使用 key 进行比较
  else {
    const s1 = i
    const s2 = i

    // 构建新节点的 key 到索引的映射
    const keyToNewIndexMap = new Map()
    for (let i = s2; i <= e2; i++) {
      const nextChild = c2[i]
      keyToNewIndexMap.set(nextChild.key, i)
    }

    const toBePatched = e2 - s2 + 1
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0)

    // 遍历旧节点,进行更新或卸载
    for (let i = s1; i <= e1; i++) {
      const prevChild = c1[i]
      let newIndex = keyToNewIndexMap.get(prevChild.key)
      if (newIndex === undefined) {
        // 旧节点在新节点中不存在,卸载
        unmount(prevChild, parentComponent, parentSuspense, true)
      } else {
        // 旧节点在新节点中存在,更新
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)
      }
    }

    // 遍历新节点,挂载或移动
    const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
    let j = increasingNewIndexSequence.length - 1
    for (let i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = s2 + i
      const nextChild = c2[nextIndex]
      const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : anchor
      if (newIndexToOldIndexMap[i] === 0) {
        // 新节点在旧节点中不存在,挂载
        patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      } else {
        // 新节点在旧节点中存在,检查是否需要移动
        if (i !== increasingNewIndexSequence[j]) {
          // 需要移动
          hostInsert(nextChild.el!, container, anchor)
        } else {
          // 不需要移动,更新 j
          j--
        }
      }
    }
  }
}

3.3.4 最长递增子序列算法

为了最小化 DOM 移动操作,Vue 3 使用了最长递增子序列算法来找出不需要移动的节点:

javascript
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]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      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
}

4. 响应式渲染流程

4.1 整体流程

Vue 3 的响应式渲染流程如下:

  1. 初始化:创建组件实例,初始化响应式数据
  2. 渲染:执行 render 函数,生成虚拟 DOM
  3. 挂载:将虚拟 DOM 转换为真实 DOM
  4. 更新:当响应式数据变化时,触发依赖更新,重新渲染
  5. 卸载:组件销毁时,清理依赖和 DOM

4.2 渲染过程

javascript
function setupRenderEffect(instance: ComponentInternalInstance, initialVNode: VNode, container: RendererElement, anchor: RendererNode | null, parentSuspense: SuspenseBoundary | null) {
  // 创建响应式副作用
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 首次渲染
      const subTree = (instance.subTree = renderComponentRoot(instance))
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG(instance.vnode.type))
      initialVNode.el = subTree.el
      instance.isMounted = true
    } else {
      // 更新渲染
      let { next, vnode } = instance
      if (next) {
        vnode = next
        next = null
      }
      const prevTree = instance.subTree
      const nextTree = renderComponentRoot(instance)
      instance.subTree = nextTree
      patch(prevTree, nextTree, container, anchor, instance, parentSuspense, isSVG(vnode.type))
    }
  }

  // 创建副作用
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope
  ))

  // 执行副作用
  const update = (instance.update = () => effect.run())
  update()
}

5. 性能优化

5.1 编译优化

Vue 3 编译器会对模板进行静态分析,生成优化后的代码:

  • 静态提升:将静态节点提升到渲染函数外部
  • Patch Flag:为动态节点添加标记,减少比较次数
  • 区块化:将 VNode 树划分为区块,只比较动态区块

5.2 运行时优化

  • 缓存事件处理函数:避免每次渲染都创建新的函数
  • 合理使用 key:帮助 Diff 算法快速定位节点
  • 避免不必要的响应式:对于不需要响应式的数据,使用 shallowRefmarkRaw
  • 使用 v-memo:缓存计算结果,避免重复计算

6. 总结

Vue 3 的响应式系统和虚拟 DOM 实现相比 Vue 2 有了显著的改进:

  • 响应式系统:基于 Proxy,支持更多类型,性能更好
  • 虚拟 DOM:更轻量,更高效
  • Diff 算法:优化了比较策略,减少了 DOM 操作
  • 编译优化:生成更高效的渲染代码

这些改进使得 Vue 3 在大型应用中表现更加出色,同时保持了 Vue 一贯的易用性和灵活性。

基于 VitePress 的本地知识库