# 11、React & Vue
React 只是专注于数据到视图的转换,而 Vue 则是典型的 MVVM,带有双向绑定。
# 事件差异
# React 事件机制
- React原生事件被包装成合成事件,通过事件代理,所有事件都冒泡到顶层document监听(17版本绑定到rootNode上),然后在这里合成事件下发,统一管理事件。基于这套,可以跨端使用事件机制,而不是和Web DOM强绑定。
- React组件上无事件,父子组件通信使用props
# Vue 事件机制
Vue 支持 2 种事件类型,原生DOM事件 和 自定义事件。
- Vue原生DOM事件使用标准Web事件:
普通 DOM 元素
和在组件上挂了.native
修饰符的事件。最终调用的还是原生的addEventListener()
方法 ( 源码:target.addEventListener())。 - 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 它会怎么做:- 节点D是首个节点,不执行移动。
- 节点A移动到节点D后面:【B、C、D、A】;
- 节点B移动到节点A后面:【C、D、A、B】;
- 节点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。