# 11、React & Vue

React 只是专注于数据到视图的转换,而 Vue 则是典型的 MVVM,带有双向绑定。

# 事件差异

# React 事件机制

  1. React原生事件被包装成合成事件,通过事件代理,所有事件都冒泡到顶层document监听(17版本绑定到rootNode上),然后在这里合成事件下发,统一管理事件。基于这套,可以跨端使用事件机制,而不是和Web DOM强绑定。
  2. React组件上无事件,父子组件通信使用props

# Vue 事件机制

Vue 支持 2 种事件类型,原生DOM事件 和 自定义事件。

  1. Vue原生DOM事件使用标准Web事件:普通 DOM 元素和在组件上挂了.native 修饰符的事件。最终调用的还是原生的 addEventListener() 方法 ( 源码:target.addEventListener())。
  2. Vue自定义事件机制:只有组件节点才可以添加自定义事件,组件上的自定义事件会调用 Vue 原型上的$on$emit方法,实现父子组件通信。

# 提问

  • Q:React自己实现了一套合成事件机制,而Vue却没有实现自己的事件系统。那么既然React的合成事件,具有跨平台,抹平兼容性差异,统一管理事件,性能好等优点,那为什么Vue没有做这件事呢?
  • A:React 使用合成事件的背景,是 React 想要做跨平台,不仅仅是做 web。而 Vue 目前没看到那个野心。React合成事件的优点是非常明显,但同时也非常复杂,实现起来还不知道要改多少 bug 呢。软件开发也得考虑成本和时间。

# diff差异

浅谈 React/Vue/Inferno 在 DOM Diff 算法上的异同 (opens new window)

# 概述

  • Vue和React的diff算法,都是同级比较,深度优先
  • React是fiber架构,是个单链表,只能单向遍历,并且使用diff队列保存需要更新哪些DOM,得到patch树后,再统一操作批量更新DOM;而Vue Diff使用双向链表,双端遍历比较,边对比,边更新DOM。
  • Vue对比节点,当节点元素类型(tag)相同,但是属性不同,认为是不同类型元素,删除重建;而React会认为是同类型节点,只是修改节点属性
  • Vue的列表对比,采用从4指针双端比较,而React则采用从左到右单向遍历对比的方式。二者最不利情形都是序列倒序,需要N-1次移动,而对于末尾移到首位的情形,Vue只需一次移动,而React依然是最差的N-1次移动。总体上,Vue的对比方式相比于React更高效。

# React diff

通过 三大策略 进行优化,将O(n^3)复杂度 转化为 O(n)复杂度:

  • 1、策略一(tree diff),只同层级比较
  • 2、策略二(component diff),相同类的两个组件,生成相似的树形结构,不同类的两个组件,生成不同的树形结构
  • 3、策略三(element diff):对于同一层级的一组子节点,通过唯一key区分。

diffing算法复杂度O(n),按照同层比较,深度优先的顺序,遍历比较一遍,过程如下:

  • 深度优先比较
  • 不在同一层级,根本不比较
  • 同一层级下:
    • oldVnode不存在时,直接新增
    • newVnode不存在时,直接删除
    • key或type有一者不同,直接替换
    • key和type相同
      • oldVnode和newVnode相同,复用节点
      • oldVnode和newVnode不同,更新节点

# 列表diff

  • React 的 DOM diff过程,设定 Virtual DOM 的首个节点不执行移动(除非它要被移除),以该节点为原点,其它节点通过从左到右单向比较,都去寻找自己的新位置;
  • 对该算法最不利情形是序列倒序。比如从【A、B、C、D】转换为【D、C、B、A】,除了首个节点外,其它所有节点都需要移动,对于有 N 个节点的数组,总共 N-1次移动
  • 末尾移到首位,比如从【A、B、C、D】变成【D、A、B、C】,React 它会怎么做:
    1. 节点D是首个节点,不执行移动。
    2. 节点A移动到节点D后面:【B、C、D、A】;
    3. 节点B移动到节点A后面:【C、D、A、B】;
    4. 节点C移动到节点B后面:【D、A、B、C】。
    • 还是N-1次移动首个节点不执行移动这个特性,导致了只要把末尾节点移动到首位,就会引起 N-1次移动 这种最坏的 DOM Diff 过程,所以大家要尽可能避免这种重排序。
  • React 假定最大递增子序列从0开始,在末尾节点移动到首位的场景中会恶化;

# Vue diff

优化到O(n):

  • 只比较同一层级,不跨级比较
  • tag不相同,则认为是不同节点,直接删掉重建,不再深度比较
  • tag和key两者都相同,则认为是相同节点,不再深度比较

过程:

  • 首先进行树级别比较patch()同层比较,深度优先,可能有三种情况:增删改
    • new VNode不存在就删;
    • old VNode不存在就增;
    • 都存在就改,执行diff更新:递归更新节点patchVnode()
      • 执行patchVnode,一定是判断为两个相同的节点,即sameVnode():key相同、tag相同
      • sameVnode(vnode1, vnode2),如果没有key(即key为undefined),那么tag相同时,key都是undefined也相同,则就是相同节点
      • 比较两个VNode,包括三种类型操作:属性更新、文本更新、子节点更新
        • 属性更新:xxxxx
        • text文本和children子节点是互斥的:
          • 1、当新老节点都无子节点,只是文本的替换。
          • 2、当新节点有子节点而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点。
          • 3、当新节点没有子节点而老节点有子节点,则移除该节点的所有子节点。
          • 4、新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren重排子节点

# 列表diff

  • Vue 的 DOM Diff 过程就是一个查找排序的过程(使用了双向遍历的方式,加速了遍历的速度),遍历 Virtual DOM 的节点,在 Real DOM 中找到对应的节点,并移动到新的位置上。
  • 对该算法最不利情形也是序列倒序。比如从【A、B、C、D】转换为【D、C、B、A】,算法将执行N-1次移动,与 React 相同,并没有更坏。
  • 末尾移到首位,序列【A、B、C、D】转换为【D、A、B、C】,看一下 Vue 的算法表现如何:
    • 在第一轮比较中,Real DOM 的末尾节点D与 Virtual DOM 的首节点D相同,那么就把节点D移动到首位,变成【D、A、B、C】,直接一步到位,高效完成了转换。
  • Vue 利用双端比较遍历排序的方法,有可能不是最优解,但与最优解十分逼近。

因此,在实际的业务开发中,无论是Vue还是React,序列倒序都是最应该被避免的场景。而对于 React 还应注意末尾节点的问题。除此之外,没有什么特别需要担心的。

# 变化侦测的方式差异

既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异? 「pull、push」

现代前端框架有两种方式进行「变化侦测」,一种是pull,一种是push。

# pull

  • React 的 setState 和(早期)Angular 的脏检查都是 pull:即 系统不知道数据是否已改变,需要进行 pull。
  • 以React为例,用 setState API显式更新,然后React会进行一层层的 VDOM Diff操作找出差异,之后Patch到DOM上。也就是说,React从一开始就不知道到底是哪发生了变化,只是知道 “有变化了”,然后再进行比较暴力的Diff操作查找 “哪发生变化了”。

# push

  • 相比之下 push 在数据变动时会立刻知道哪些数据改变,但这里还有个粒度问题。如果 pull 是完全粗粒度的,那么 push 可以进行更细粒度的更新。push 方式 要做到 粒度掌控,就需要付出 相应内存开销、建立依赖追踪开销的代价。
  • Vue为例,当 Vue 初始化时,就会对数据data进行依赖收集,一但数据发生变化,响应式系统就会立刻知道 “在哪发生变化了”。而Vue的每一次数据绑定,就需要一个Watcher,如果粒度过细,就会有太多的Watcher,开销太大,但如果粒度过粗,又无法精准侦测变化。
  • 因此,Vue 选择的是中间的 「混合式:push + pull:Vue在组件级别选择 push方式,每个组件都是 Watcher,一旦某个组件发生变化Vue立刻就能知道;而在组件内部选择 pull方式,使用 VDOM Diff 进行比较。

# Vue为什么没有类似于React中shouldComponentUpdate的生命周期?

承接上文:

  • 首先,React是以 pull 的方式侦测变化的,因为不能知道到底哪里发生了变化,所以会进行大量 VDOM Diff 差异检测,为了提升性能,就可以使用 SCU(shouldComponentUpdate)来避免对那些肯定不会变化的组件进行Diff检测。
  • 而Vue在组件级是push方式,在push阶段能够自动判断,无需手动控制Diff。在组件内是pull的方式,理论上是可以引入类似于 SCU的钩子来让开发者控制的,但是,通常合理大小的组件不会有过量的Diff 检测,手动优化的价值有限。因此Vue没有引入SCU。
Last Updated: 10/9/2020, 5:51:45 PM