浅谈对 Vue、React 数据更新的理解

| 1.5k字 | 5分钟

说两者之前要先简单讲一下前端的 “刀耕火种” 的年代,从后端渲染模板 → jQuery 直接操作 DOM → mvvm 的虚拟 DOM → 工程化。

Vue1.0 的解决方案,采用的是响应式,初始化的时候,Watcher 监听了数据的每个属性,这样数据发生变化的时候,我们就能精确地知道数据的哪个 key 变了,然后再去针对性修改对应的 DOM 即可,这个 DOM 是真实的 DOM,而非虚拟 DOM。好处就是,响应式的数据监听,不需要人为的去操纵数据。但坏处就是,Vue 中每个真实的元素都要绑定一个 Watcher,元素一旦过多,Watcher 的数量就比较大,致使数据在更新、渲染 DOM 的时候,时间更多,对性能产生影响。

所以 Vue2.0 借鉴了 React 虚拟 DOM 思想,在 Vue2.0 中引入了虚拟 DOM 的概念,那么何为虚拟 DOM 呢,其实就是一个对象,这个对象完整的描述了对象的树形结构,这样数据有变化的时候,就会生成一份新的虚拟 DOM 数据,然后再对之前的虚拟 DOM 进行计算,算出需要修改的 DOM,再去页面进行操作,这就是 「数据驱动页面」的方式,这样规避了 jQuery 和 Vue1.0 那种直接操作 DOM 对性能的影响。浏览器操作 DOM 一直都是性能杀手,而虚拟 DOM 的 Diff 的逻辑,又能够确保尽可能少的操作 DOM,这也是虚拟 DOM 驱动的框架性能一直比较优秀的原因之一。

// 虚拟 DOM,其实就是一个对象,也就是React中通过createElement创建的.
// ReactElement通过createElement创建,调用该方法需要传入三个参数:type、config、children
{
  "tag": "div",
  "attrs": {
    "id": "app"
  },
  "children": [
    {
      "tag": "p",
      "attrs": {
        "className": "item"
      },
      "children": ["Item1"]
    },
    {
      "tag": "div",
      "attrs": {
        "className": "item"
      },
      "children": ["Item2"]
    }
  ]
}

Vue 和 React 对数据改变后通知页面进行更新,采用的是两种不同的机制。

Vue框架下,如果数据发生了改变,框架会主动告诉你修改了哪些数据「响应式/双向绑定模式」;React的数据发生变化后,只能通过新老数据的计算Diff来得知数据的改变;

这两个方案都解决了数据变化后,如何通知页面更新的问题,但是都有对应的弊端。

上面开始的时候,有提到过,对于 Vue 来说,它的核心就是 “响应式” 也就是数据变化后,会主动通知我们,响应式数据新建 Watcher 监听,本身就比较耗费性能,项目大了之后每个数据都有一个 Watcher,会更影响性能。

对于 React 的虚拟 DOM 的 Diff 比对逻辑来说,如果虚拟 DOM 数过于庞大,使得计算时间大于 16.6ms「1s 渲染 60 次,1 帧 ⇒ 1s/60fps = 16.6ms」在这 16.6ms 里,浏览器自己的渲染更新任务执行后,会有一部分的空闲时间,这段时间就是用来计算 Diff 的,所以,一旦时间超过 16.6ms,就可能会造成性能的卡顿,为了解决这个卡顿,之后 React 做了新的架构 Fiber,这个后面会提到。

为了解决性能瓶颈,Vue 和 React 走了不同的道路:
在 React 中 由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了 16ms,用户交互就会卡顿。

React 为了突破性能瓶颈,借鉴了操作系统时间分片的概念,引入了新的 Fiber 架构,将其虚拟 DOM 从树形 → 链表,利用浏览器的空闲时间计算,进行 相应的 Diff 操作,就是一旦浏览器有任务,我们就先把没计算完的任务放在一边,把主进程控制权交还给浏览器,等待浏览器空闲了,再把新的任务交给浏览器进行计算,使得任务可中断、可暂停、可分片….巧妙的利用了浏览器的空闲时间,解决了卡顿的问题。

那么链表具体是如何对数据进行比对的呢,其实是遍历通过 child → sibling → parent 元素的逻辑,随时可以中断和恢复 Diff 的计算过程。

Vue1.0 的问题在于响应式的数据过多,会带来内存占用过多的问题,所以 Vue2.0 引入了虚拟 DOM 来解决响应式数据过多的问题。

响应式数据是主动推送变化,虚拟 DOM 是被动计算数据的Diff,所以Vue2.0 采用了组件级别的划分,就是每个组件都被独立的Watcher监听。

那么问题来了,Vue为什么不需要React的Fiber呢?

答:是因为Vue将虚拟 DOM 控制在组件级, 每次变化影响的只是一颗子树, 相对范围小

  1. Vue3 把虚拟 Dom 控制在组件级别,组件之间使用响应式,这就让 Vue3 的虚拟 Dom 不会过于庞大
  2. Vue3 虚拟 Dom 的静态标记和自动缓存功能,让静态的节点和属性可以直接绕过 Diff 逻辑,也大大减少了虚拟 Dom 的 Diff 事件
  3. 时间切片也会带来额外的系统复杂性

这就是为什么 Vue 没有采用 Fiber 的原因,本质还是数据更新通知机制不一样,Vue 是主动推送,而且 Watcher 对应的是组件级。这跟 React Diff 算法是不一样的,React Diff 是被动计算数据的 Diff。