# 一、开始

Vue和React的对比是老生常谈的话题。本质的区别是数据驱动的区别,我这里只是记录简单的几项,感兴趣的可以查看文章最后的相关资料。

# 二、对比

# 1. 核心思想

React是函数式编程的思想,组件多使用JSX的语法,HTML、CSS全都融进JS中,JSX语法相对比较灵活。

Vue组件多用SFC单文件(single-file components)编写,很像HTML+JS+CSS,Vue提供了v-for、v-if、v-model等指令方便开发,开发Vue应用就像开发经典的Web应用,结构、表现、行为分离。

上述差异的一个重要原因是React背靠Facebook,提出一个新的理念、改变用户习惯、向外推广都不是大问题。而Vue一开始是由尤大一个人开发的,需要足够易用、简单、灵活才能够有更多人用。

# 2. 数据驱动

数据驱动上,Vue的响应式是Object.defineProperty/Proxy加观察者模式实现的,数据发生变化时,不会像React一样比较整棵树,而是更新状态变化了的组件实例。

React中如果某个组件的状态发生改变,React会把此组件和此组件之后的所有后代组件重新渲染。React会通过Diff算法比较两次的虚拟DOM,最后patch到真实的DOM上。如果组件树过大,Diff算法开销大,页面会出现卡顿。为此,React通过Fiber优化Diff算法,此外,还可以通过shouldComponentUpdate、pureComponent提升性能。

# 3. Diff算法

这里说的Diff指的是新旧节点类型相同的情况,对于类型不同的节点会直接替换。

React的Diff算法本质上是暴力比较法,会经历两轮遍历,第一轮遍历:处理更新的节点。第二轮遍历:处理剩下的不属于更新的节点。

Vue2的Diff算法采用双端比较,就是头头比较、尾尾比较、头尾比价、尾头比较,找出可以复用的元素,在对比的过程中指针会向内靠拢。

Vue3的Diff算法用了LIS(最长递增子序列)的思想,也就是找到不需要移动的节点,原地复用,对于新增/删除/移动的节点进行操作。

# 4. 主要优化方向

这里只是自己的理解,Vue在数据变化时已经能控制组件更新的最小范围,所以要做的是在组件中提取更细的范围,也就是编译优化,包括静态节点提升、标记动态节点。

React的主要优化方向在Diff算法中,也就是现在的Fiber,因为更新范围大,性能瓶颈更明显。

# 5. 组件化

组件化上,Vue多用SFC,React多用JSX语法。

# 6. 生态

构建工具上,Vue -> Vue-CLI,React -> Create-React-App。还有一些其他生态上的区别:

类型 React Vue
路由 react-router、react-router-dom vue-router
状态管理 redux vuex
UI库 antd elementUI、iview
脚手架 Create React APP vue-cli、vite
SSR工具 next.js nuxt
跨端框架 react-native、taro uni-app、weex
移动端UI antd-mobile mint-ui、vux、Vonic、cube-ui

还有一些生命周期函数、数据传递的差异,比较简单,就不贴了。

其实对比React和Vue的意义不大,还不如单独深入某一个框架的某一个部分来得深刻。另外,它们在生产环境下的页面性能差别不大,想用什么框架纯粹是个人喜好问题。

# 三、手写Diff算法

如果是暴力的Diff算法,遍历两次树,然后标记出需要更新的节点,再操作dom,则时间复杂度为O(n^3)。Vue是边比较边更新dom,时间复杂度为O(n)。

需要注意的一点是,el.insertBefore在插入新节点的同时,也会在原来的位置删除。下面的代码已经上传到这个仓库 (opens new window)

# 1. Vue2

function domDiff(el, oldChildren, newChildren) {
  // 老的开始索引
  let oldStartIndex = 0;
  // 老的开始节点
  let oldStartVnode = oldChildren[0];
  // 老的结束索引
  let oldEndIndex = oldChildren.length - 1;
  // 老的结束节点
  let oldEndVnode = oldChildren[oldEndIndex];

  // 新的开始索引
  let newStartIndex = 0;
  // 新的开始节点
  let newStartVnode = newChildren[0];
  // 新的结束索引
  let newEndIndex = newChildren.length - 1;
  // 新的结束节点
  let newEndVnode = newChildren[newEndIndex];

  // 根据老的节点,构造一个map,其value是oldChildren的下标
  let oldNodeMap = oldChildren.reduce((acc, item, index) => {
    acc[item.key] = index;
    return acc;
  }, {});

  // 双指针对比,从两端向中间遍历,当指针交叉的时候,就是对比完成了
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 指针移动的时候,可能元素已经被移走了,那就跳过这一项
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIndex];
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIndex];
    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 头头比较,如果相同就移动头指针
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 尾尾比较,如果相同,移动尾指针
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];
    } else if (isSameVnode(oldStartVnode, newEndVnode)) {
      /**
       * 头尾比较
       * 
       * a b
       * b a
       * 
       * 将旧的头节点(a)的真实DOM,插入到旧的尾节点(b)的下一个节点之前
       */
      el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      oldStartVnode = oldChildren[++oldStartIndex];
      newEndVnode = newChildren[--newEndIndex];
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      /**
       * 尾头比较
       * 
       * a b c d
       * d b a c
       * 
       * 将旧的尾节点(d) 插入到 旧的头节点(a)的前面
       */
      el.insertBefore(oldEndVnode.el, oldStartVnode.el);
      oldEndVnode = oldChildren[--oldEndIndex];
      newStartVnode = newChildren[++newStartIndex];
    } else {
      /**
       * 头头、尾尾、头尾、尾头都不相等
       */
      let moveIndex = oldNodeMap[newStartVnode.key];
      if (moveIndex === undefined) {
        /**
         * a b c
         * d
         * 
         * 找不到索引, 是新的节点,直接在旧的头节点前面插入创建的节点
         */
        // 
        el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
      } else {
        /**
         * a b c d
         * b d a c
         * 
         * 找到了索引,将找到的节点(b) 移动到旧的头节点(a)之前,同时将找到的位置置空
         */
        // 
        let moveVnode = oldChildren[moveIndex];
        el.insertBefore(moveVnode.el, oldStartVnode.el);
        // 将已经移动的节点标记为undefine
        oldChildren[moveIndex] = undefined;
      }
      newStartVnode = newChildren[++newStartIndex];
    }
  }

  if (oldStartIndex > oldEndIndex) {
    /**
     * 新的节点多,那么就将多的插入进去即可
     * 
     * a b c
     * d e f a b c
     * 
     * 一直是尾尾相同,尾节点都一直减1,直到 oldStartIndex=0,oldEndIndex=-1,newStartIndex=0,newEndIndex=2
     * 将多出的节点def(下标0-2),不断添加到旧节点的前面
     */
    let anchor =
      newChildren[newEndIndex + 1] === null
        ? null
        : newChildren[newEndIndex + 1].el;

    for (let i = newStartIndex; i <= newEndIndex; i++) {
      el.insertBefore(createElm(newChildren[i]), anchor);
    }
  }

  if (newStartIndex > newEndIndex) {
    /**
     * 老的节点多,直接删除
     * 
     * a b c d e f
     * d e f
     * 
     * 一直都是尾尾相同,直到 newStartIndex=0,newEndIndex=-1, oldStartIndex=0, oldEndIndex=2
     * 将多出的节点abc(下标0-2)删除
     */
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      let child = oldChildren[i];
      child && el.removeChild(child.el);
    }
  }
}

# 2. React

function reactDiff(parent, prevChildren, nextChildren) {
  let lastIndex = 0;
  for (let i = 0; i < nextChildren.length; i++) {
    const nextChild = nextChildren[i];
    let find = false;
    for (let j = 0; j < prevChildren.length; j++) {
      const prevChild = prevChildren[j];
      if (nextChild.key === prevChild.key) {
        find = true;
        // patch(prevChild, nextChild, parent)
        if (j < lastIndex) {
          // 移动到前一个节点的后面
          const refNode = nextChildren[i - 1].el.nextSibling;
          parent.insertBefore(nextChild.el, refNode);
        } else {
          // 不需要移动节点,记录当前位置,与之后的节点进行对比
          lastIndex = j;
        }
        break;
      }
    }
    if (!find) {
      /**
       * 遍历过程中,如果无法复用,插入新节点
       *
       * a b d
       * a b c d
       */
      const refNode = i <= 0
        ? prevChildren[0].el
        : nextChildren[i - 1].el.nextSibling;

      parent.insertBefore(createElm(nextChild), refNode);
      // mount(nextChild, parent, refNode);
    }
  }
  for (let i = 0; i < prevChildren.length; i++) {
    const prevChild = prevChildren[i];
    const { key } = prevChild;
    const has = nextChildren.find(item => item.key === key);
    /**
     * a b c d
     * a b d
     *
     * 第一轮遍历结束后,对于多余的节点,直接删除
     */
    if (!has) parent.removeChild(prevChild.el);
  }
}

# 四、相关资料

  1. vue与react的个人体会 (opens new window)
  2. 关于Vue和React的一些对比及个人思考(上) (opens new window)
  3. 关于Vue和React的一些对比及个人思考(中) (opens new window)
  4. React、Vue2、Vue3的三种Diff算法 (opens new window)
  5. Vue 和 React 的优点分别是什么? (opens new window)
  6. Vue3 Compiler 优化细节,如何手写高性能渲染函数 (opens new window)
  7. React Fiber很难?六个问题助你理解 React Fiber (opens new window)
  8. React、Vue2、Vue3的三种Diff算法 (opens new window)
  9. 手写简化版的 vue3 diff 算法 (opens new window)
  10. react和vue diff算法解析与对比 (opens new window)
  11. React、Vue2、Vue3的三种Diff算法 (opens new window)
  12. 深入浅出虚拟 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别 (opens new window)
  13. label语句 (opens new window)
  14. vue diff算法 (opens new window)