vue2 与 vue3 对比及源码疑问

480 阅读7分钟

vue2的源码中,虚拟dom的对比更新是怎么实现的?

在 Vue 2 的源码中,虚拟 DOM 的对比更新是通过 diff 算法实现的。具体来说,当数据发生变化时,Vue 会先生成一个新的虚拟 DOM 树,然后通过比较新旧两棵虚拟 DOM 树的差异,得到需要更新的节点列表,最后才将这些节点更新到真实 DOM 树中。

Vue 2 中的 diff 算法采用了双端比较的策略,即从新旧两棵虚拟 DOM 树的头部和尾部同时开始比较,然后根据具体情况进行增、删、改、移等操作。在比较过程中,Vue 2 会对每个节点设置唯一的 key 值,并将其缓存到一个哈希表中,以便在比较过程中快速查找节点。

另外,在 Vue 2 中,由于需要递归比较整棵虚拟 DOM 树的所有节点,因此当数据量较大时,diff 算法的性能会受到影响。而在 Vue 3 中,采用了静态标记和动态标记的策略,可以避免对整棵虚拟 DOM 树进行递归比较,从而提高了 diff 算法的性能。

当触发数据更新时,Vue 会调用 updateComponent() 方法重新渲染组件。在 updateComponent() 方法中,Vue 会执行 patch() 方法进行虚拟 DOM 的更新。下面是 Vue 2 中的代码示例,可以看到其主要使用了 diff 算法实现虚拟 DOM 的对比更新。

function updateComponent() {
  // 调用渲染函数生成新的 VNode
  const vnode = vm._render()
  // 旧的 VNode
  const prevVnode = vm._prevVnode
  // 更新 vm._prevVnode
  vm._prevVnode = vnode
  // 执行 patch
  patch(prevVnode, vnode, hydrating, false /* removeOnly */)
}

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // 如果新旧节点相同,直接返回
  if (oldVnode === vnode) {
    return
  }
  // 如果旧节点不存在,说明是新节点,直接挂载
  if (!isRealElement(oldVnode)) {
    // ...
  } else {
    // 新旧节点都是真实 DOM,执行 diff 算法
    const oldElm = oldVnode.elm
    const parentElm = nodeOps.parentNode(oldElm)

    createElm(
      vnode,
      insertedVnodeQueue,
      parentElm,
      nodeOps.nextSibling(oldElm)
    )

    if (parentElm !== null) {
      removeVnodes(parentElm, [oldVnode], 0, 0)
    } else if (oldVnode.tag === vnode.tag) {
      // 如果没有父节点,说明是根节点,更新根节点
      invokeInsertHook(vnode, insertedVnodeQueue, true)
    }
  }
}

可以看到,Vue 2 中的 patch 方法是通过对比新旧节点的属性来判断是否需要进行更新。如果新旧节点不同,就会更新相应的属性;如果新节点不存在,就会直接挂载;如果旧节点不存在,说明是新节点,也会直接挂载。这种实现方式虽然能够达到性能优化的效果,但是在一些复杂的应用场景下可能会出现一些问题。

那么来了,在 Vue 2 中,使用 patch 方法对比新旧节点属性的方式虽然能够达到性能优化的效果,但是在一些复杂的应用场景下会出现一些问题,那是哪些问题呢?
  1. 比较耗时:由于对比新旧节点的属性需要耗费时间,如果节点层级比较多、属性比较复杂,可能会导致性能问题。
  2. 无法处理动态组件和异步组件:动态组件和异步组件无法预测其内部组件结构,无法使用静态优化,因此无法减少对比次数。
  3. 嵌套过深时出现性能问题:在嵌套组件层级比较深时,每次更新都需要递归比较所有子节点,这可能会导致性能问题。
  4. 无法对比非 VNode 节点:在一些场景下,需要对比非 VNode 节点,例如通过 innerHTML 方式动态添加内容的场景,这些节点无法被 patch 方法处理,可能会导致视图和数据不同步的问题。

为了解决这些问题,Vue 3 引入了基于静态标记和动态标记的编译器优化,使得 diff 算法的性能更加高效。

vue3的源码中,虚拟dom的对比更新是怎么实现的

在 Vue 3 中,虚拟 DOM 的对比更新主要是通过 diff 算法实现的。Vue 3 中的 diff 算法相比于 Vue 2 中的 diff 算法有很大的优化。

Vue 3 的 diff 算法主要是通过两个阶段来实现:

  1. 标记阶段:在标记阶段,算法会遍历新旧虚拟节点,判断它们的关系,然后给它们打上标记,标记它们需要执行哪种更新操作(比如创建新节点、删除节点、移动节点等)。
  2. 应用阶段:在应用阶段,算法会根据标记对节点进行具体的更新操作。

在标记阶段,Vue 3 的 diff 算法主要是通过两个指针来实现的,一个指针指向新节点,一个指针指向旧节点。算法会从两个指针所指向的节点开始比较,如果节点相同,则直接跳到下一个节点;如果节点不同,则从两端向中间靠拢,尽可能地复用旧节点或创建新节点,直到两个指针相遇。

function patchChildren(
  n1: VNode | null,
  n2: VNode | null,
  container: RendererElement,
  anchor?: RendererNode,
  parentComponent?: ComponentInternalInstance,
  parentSuspense?: SuspenseBoundary
) {
  // 如果新节点的 children 为空,则直接移除旧节点的所有子节点
  if (n2.shapeFlag & ShapeFlags.ARRAY_CHILDREN && n2.children.length === 0) {
    unmountChildren(n1, parentComponent, parentSuspense, true);
  } else {
    // 如果旧节点的 children 为空,则直接添加新节点的所有子节点
    if (n1 == null || n1.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(n2, container, anchor, parentComponent, parentSuspense);
    } else {
      // 如果旧节点不是数组形式的 children,则直接移除旧节点的子节点,然后添加新节点的所有子节点
      unmountChildren(n1, parentComponent, parentSuspense);
      mountChildren(n2, container, anchor, parentComponent, parentSuspense);
    }
  }
}

function mountChildren(
  children: VNodeArrayChildren,
  container: RendererElement,
  anchor?: RendererNode,
  parentComponent?: ComponentInternalInstance,
  parentSuspense?: SuspenseBoundary,
  start = 0
) {
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = normalizeVNode(children[i]));
    patch(null, child, container, anchor, parentComponent, parentSuspense);
  }
}

Vue 3 中,采用了静态标记和动态标记的策略,可以避免对整棵虚拟 DOM 树进行递归比较,从而提高了 diff 算法的性能。那静态标记和动态标记是如何实现的呢?

在 Vue 3 中,静态标记和动态标记是通过编译阶段进行的,即在模板编译时会进行静态分析,将模板中的节点分为静态节点和动态节点。

静态节点是指不包含动态数据的节点,其内容不会发生变化,因此只需要在初次渲染时被渲染一次即可。Vue 3 使用了 Patch Flag 的方式来标记静态节点,在更新时跳过对静态节点的比较,从而提高 diff 算法的效率。

动态节点是指包含动态数据的节点,其内容会发生变化,需要在每次更新时被重新渲染。为了提高动态节点的更新效率,Vue 3 使用了一种名为“基于 Proxy 的观察者机制”,这种机制可以精确地追踪数据的变化,从而避免了对整个虚拟 DOM 树进行递归比较的问题。

通过静态标记和动态标记的策略,Vue 3 在渲染和更新时可以跳过对静态节点的比较,以及避免对整个虚拟 DOM 树进行递归比较的问题,从而大大提高了 diff 算法的性能。

vue2 和vue3 的虚拟dom更新机制不同的地方

  1. Proxy 代替 Object.defineProperty

Vue2 中使用了 Object.defineProperty 实现数据响应式,但是其存在一些限制,比如不能监听数组下标变化。Vue3 引入了 ES6 的 Proxy,完美地解决了这个问题,同时还能够监听对象和数组的新增和删除操作,使得 Vue3 的响应式更加高效和灵活。

  1. 静态提升

Vue3 引入了静态提升(Static Hoisting)机制,将组件中的静态节点在编译时提升为常量,从而减少了虚拟 DOM 的生成和对比操作。这一机制在性能优化上起到了重要的作用。

  1. Fragments

Vue3 中支持了 Fragments(片段),可以在一个组件中返回多个根节点。这一特性在开发中非常方便,减少了不必要的嵌套。

  1. Teleport

Vue3 中新增了 Teleport(瞬移)组件,可以将组件的内容挂载到指定的 DOM 节点上,这在一些场景下也能够提高开发效率。

  1. 缓存策略的优化

Vue3 在虚拟 DOM 更新机制上做了一些优化,比如可以缓存上一次的渲染结果,在下一次渲染时先和当前结果进行比较,以减少对比和更新操作,从而提高性能。

总的来说,Vue3 在虚拟 DOM 更新机制上做了不少优化,使得响应式和渲染性能更加高效。