企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 一、监听数据变化的实现原理不同 ***** - Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能 - React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 VDOM 的重新渲染 # 二、数据流的不同 ***** ![](https://img.kancloud.cn/17/fa/17fabbfa1c7385a862ebb5c3463b19aa_922x453.png =500x) 在 Vue1.0 中我们可以实现两种双向绑定(这里的双向绑定指的是数据流而不是 View 与 Model): 1. 父子组件之间,props 可以双向绑定 2. 组件与 DOM 之间可以通过 v-model 双向绑定 在 Vue2.x 中去掉了第一种,也就是父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改),并且 Vue2.x 已经不鼓励组件对自己的 props 进行任何修改了。 所以现在我们只有组件 DOM 之间的双向绑定这一种。 然而 React 从诞生之初就不支持双向绑定,React 一直提倡的是单向数据流,他称之为 onChange/setState() 模式。 不过由于我们一般都会用 Vuex 以及 Redux 等单向数据流的状态管理框架,因此很多时候我们感受不到这一点的区别了。 # 三、组件通信的区别 ***** 单纯地看父子组件通信: - Vue 中父组件可以通过 props 向子组件传递数据和回调,但是我们一般都只传数据,通过事件 `$emit` 的机制来处理子组件向父组件的通信。 - React 也可以向子组件传递数据和回调,在 React 中子组件向父组件通信都是采用回调的方式进行的。 # 四、Vuex 和 Redux 的区别 ***** 在 Vuex 中,store 被直接注入到了所有的组件实例中,因此可以比较灵活的使用: - 使用 dispatch 和 commit 提交更新 - 通过 mapState 或者直接通过 this.$store 来读取数据 在 Redux 中,我们每一个组件都需要显式地用 connect 把需要的 props 和 dispatch 连接起来。 另外 Vuex 更加灵活一些,组件中既可以 dispatch action 也可以 commit updates,而 Redux 中只能进行 dispatch,并不能直接调用 reducer 进行修改。 从实现原理上来说,最大的区别是两点: * Redux 使用的是不可变数据,而 Vuex 的数据是可变的。Redux 每次都是用新的 state 替换旧的 state,而 Vuex 是直接修改 * Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而 Vuex 其实和 Vue 的原理一样,是通过 getter/setter 来比较的(如果看 Vuex 源码会知道,其实他内部直接创建一个 Vue 实例用来跟踪数据变化) 而这两点的区别,其实也是因为 React 和 Vue 的设计理念上的区别。React 更偏向于构建稳定大型的应用,非常的科班化。相比之下,Vue 更偏向于简单迅速的解决问题,更灵活,不那么严格遵循条条框框。因此也会给人一种大型项目用 React,小型项目用 Vue 的感觉。 # 五、css 管理的区别 ***** Vue 中我们直接给 \<style> 加上 scoped 属性就能保证它的样式只作用于当前组件: ```html <style scoped> .example { color: red; } </style> <template> <div class="example">hi</div> </template> ``` ```html <style> .example[data-v-5558831a] { color: red; } </style> <template> <div class="example" data-v-5558831a>hi</div> </template> ``` PostCSS 给一个组件中的所有 dom 添加了<span style="color: red">一个独一无二的动态属性</span>,然后,给 CSS 选择器额外添加一个对应的属性选择器来选择该组件中 dom,这种做法使得样式只作用于含有该属性的 dom——组件内部 dom >为什么 Vue 在开发环境下我们能看到原本的样式?而 React 使用 Styled-Component 看到的就直接是哈希后的样式?能做转换吗? 这使得我们写一个 Vue 组件只需要一个 .vue 文件即可,而 React 中有多种样式管理方案,比如直接写 css 文件 import 导入,使用 styled-component 等,文件结构看起来就不那么整洁了。 # 六、diff 算法的差异 ## 传统 diff 算法 ![](https://img.kancloud.cn/d4/fa/d4fa369fa849f51a8f9c939af5b36b6a_1000x440.png =500x) 计算两颗树形结构差异并进行转换,传统 diff 算法是这样做的:循环递归每一个节点 比如左侧树 a 节点依次进行如下对比,左侧树节点 b、c、d、e 亦是与右侧树每个节点对比 算法复杂度能达到 O(n^2),n 代表节点的个数 ```js a->e、a->d、a->b、a->c、a->a ``` 查找完差异后还需计算最小转换方式,最终达到的算法复杂度是 O(n^3) ## React 的 diff 策略 React 的 diff 策略将时间复杂度优化到了 O(n),这一壮举是通过以下三条策略来实现的: * Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计,所以 React 的 diff 是同层级比较 * 拥有相同类型的两个组件将会生成相似的树形结构,不同类型的两个组件将会生成不同的树形结构 * 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分 这三个策略分别对于 tree diff、component diff 和 element diff ### tree diff 既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 只会对相同层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。 ![](https://img.kancloud.cn/10/d4/10d4ca61b2debe4a05ab7202912dac12_918x492.png =400x) 这个策略的前提是操作 DOM 时跨层级操作较少,那么如果发生了跨层级操作应该如何处理呢? ![](https://img.kancloud.cn/dc/14/dc145d9c99650747b5657b28aa37a79e_710x415.png =400x) 这一过程可以用下图来描述: ![](https://img.kancloud.cn/b6/34/b634dc3fd6f1d1ecb7042bd771228b13_1289x270.png) A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单地考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,diff 的执行情况:create A → create B → create C → delete A。 由此可以发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的整个树被重新创建。这是一种影响 React 性能的操作,因此官方建议不要进行 DOM 节点跨层级的操作。 ### component diff React 是基于组件构建应用的,对于组件间的比较所采取的策略也是非常简洁、高效的: * 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树即可 * 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点 * 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff 算法分析,但是如果调用了 forceUpdate 方法,shouldComponentUpdate 则失效 接下来我们看下面这个例子是如何实现转换的: ![](https://img.kancloud.cn/b2/29/b2290d26c07c3f4997be48204b22fb1a_745x232.png =500x) 转换流程如下: ![](https://img.kancloud.cn/1d/fa/1dfa56cb64b6d96c3e01d3fef382a20c_1115x241.png =800x) 当组件 D 变为组件 G 时,即使这两个组件结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除组件 D,重新创建组件 G 及其子节点。虽然当两个组件是不同类型但结构相似时,diff 会影响性能,但正如 React 官方博客所言:不同类型的组件很少存在相似 DOM 树的情况,因此这种极端因素很难在实际开发过程中造成重大的影响。 ### element diff 当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT\_MARKUP (插入)、MOVE\_EXISTING (移动)和 REMOVE\_NODE (删除)。 * INSERT\_MARKUP :新的组件类型不在旧集合里,即全新的节点,需要对新节点执行插入操作。 * MOVE\_EXISTING :旧集合中有新组件类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent ,这种情况下 prevChild = nextChild ,就需要做移动操作,可以复用以前的 DOM 节点。 * REMOVE\_NODE :旧组件类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。 我们可以忽略上面的说明直接来看例子 (-.-) 当一个组件包含多个子组件的情况: ```html <ul> <TodoItem text="First" completed={false} /> <TodoItem text="Second" completed={false} /> </ul> // 更新为 <ul> <TodoItem text="Zero" completed={false} /> <TodoItem text="First" completed={false} /> <TodoItem text="Second" completed={false} /> </ul> ``` 直观上看,只需要创建一个新组件,更新之前的两个组件;但是实际情况并不是这样的,React 并没有找出两个序列的精确差别,而是直接挨个比较每个子组件。 在上面的新的 TodoItem 实例插入在第一位的例子中,React 会首先认为把 text 为 First 的 TodoItem 组件实例的 text 改成了 Zero,text 为 Second 的 TodoItem 组件实例的 text 改成了 First,在最后面多出了一个 TodoItem 组件实例。这样的操作的后果就是,现存的两个实例的 text 属性被改变了,强迫它们完成了一个更新过程,创造出来的新的 TodoItem 实例用来显示 Second。 我们可以看到,理想情况下只需要增加一个 TodoItem 组件,但实际上其还强制引发了其他组件实例的更新。 假设有 100 个组件实例,那么就会引发 100 次更新,这明显是一个浪费;所以就需要开发人员在写代码的时候提供一点小小的帮助,这就是接下来要讲的 key 的作用 ```html <ul> <TodoItem key={1} text="First" completed={false} /> <TodoItem key={2} text="Second" completed={false} /> </ul> // 新增一个 TodoItem 实例 <ul> <TodoItem key={0} text="Zero" completed={false} /> <TodoItem key={1} text="First" completed={false} /> <TodoItem key={2} text="Second" completed={false} /> </ul> ``` React 根据 key 值,就可以知道现在的第二个和第三个 TodoItem 实例其实就是之前的第一个和第二个实例,所以 React 就会把新创建的 TodoItem 实例插在第一位,对于原有的两个 TodoItem 实例只用原有的 props 来启动更新过程,这样 shouldComponentUpdate 就会发生作用,避免无谓的更新操作; 了解了这些之后,我们就知道 key 值应该是**唯一**且**稳定不变的** 比如用数组下标值作为 key 就是一个典型的 “错误”,看起来 key 值是唯一的,但是却不是稳定不变的(但是一般用数组下标就行了) 比如:\[a, b, c\] 值与下标的对应关系:a: 0 b:1 c:2 删除a -> \[b, c\] 值与下标的对应关系 b:0 c:1 无法用 key 值来确定比对关系(新的 b 应该与旧的 b 比,如果按 key 值则是与 a 比) ![](https://box.kancloud.cn/843f670bb548fca2492bf347a2d0c3f6_501x135.png) >需要注意,虽然 key 是一个 prop,但是接受 key 的组件并不能读取到 key 的值,因为 key 和 ref 是 React 保留的两个特殊 prop,并没有预期让组件直接访问 ## Vue 的 diff 策略 首先与传统 diff 策略相比,Vue 也采用了同层节点对比的方式。 接下来就是与 React 的策略的第一个区别了:通知更新的方式 Vue 通过 Watcher 来监听数据变化实现视图更新,而 React 显然没有这些监听器,至于 React 是怎么通知组件更新的我没看过源码,我猜是 setState 或其他操作造成 state 或 prop 改变时触发某些函数吧。 下面看下 Vue 的 dff 流程图: ![](https://img.kancloud.cn/54/23/5423ccebb441c247066578eabc5a8999_668x606.png) 可以大致地梳理下其流程: 首先要知道 VNode 大致是怎样的: ```js // body下的 <div id="v" class="classA"><div> 对应的 oldVnode 就是 { el: div // 对真实的节点的引用,本例中就是document.querySelector('#id.classA') tagName: 'DIV', // 节点的标签 sel: 'div#v.classA' // 节点的选择器 data: null, // 一个存储节点属性的对象,对应节点的 el[prop] 属性,例如 onclick , style children: [], // 存储子节点的数组,每个子节点也是 vnode 结构 text: null, // 如果是文本节点,对应文本节点的 textContent,否则为 null } ``` ### patch 然后看下`patch`是如何判断新旧 Vnode 是否相同的: ```js function patch (oldVnode, vnode) { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oEl = oldVnode.el let parentEle = api.parentNode(oEl) createEle(vnode) if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) api.removeChild(parentEle, oldVnode.el) oldVnode = null } } return vnode } ``` patch 函数内第一个`if`判断`sameVnode(oldVnode, vnode)`就是判断这两个节点是否为同一类型节点,以下是它的实现: ```js function sameVnode(oldVnode, vnode){ // 两节点 key 值相同,并且 sel 属性值相同,即认为两节点属同一类型,可进行下一步比较 return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel } ``` 也就是说,即便同一个节点元素比如 div,他的`className`不同,Vue 就认为是两个不同类型的节点,执行删除旧节点、插入新节点操作。这与 React diff 的实现是不同的,React 对于同一个元素节点认为是同一类型节点,只更新其节点上的属性。 ### patchVnode 之后就看`patchVnode`,对于同类型节点调用`patchVnode(oldVnode, vnode)`进一步比较,伪代码如下: ```js patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el // 让 vnode.el 引用到现在的真实 dom,当 el 修改时,vnode.el 会同步变化 let i, oldCh = oldVnode.children, ch = vnode.children if (oldVnode === vnode) return // 新旧节点引用一致,认为没有变化 // 1.文本节点的比较 if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { api.setTextContent(el, vnode.text) }else { updateEle(el, vnode, oldVnode) // 2.对于拥有子节点(两者的子节点不同)的两个节点,调用 updateChildren if (oldCh && ch && oldCh !== ch) { updateChildren(el, oldCh, ch) }else if (ch){ // 3.只有新节点有子节点,添加新的子节点 createEle(vnode) //create el's children dom }else if (oldCh){ // 4.只有旧节点内存在子节点,执行删除子节点操作 api.removeChildren(el) } } } ``` 其中第二种情况是一种比较常见的情况,执行`updateChildren`函数 ### updateChildren 源码就不贴出来了,其思路大致如下: ![](https://img.kancloud.cn/1a/f7/1af729ed569c5ad77f801e7443f90ba9_1000x728.png =400x) oldCh 和 newCh 各有两个头尾的变量 StartIdx 和 EndIdx,它们的 2 个变量相互比较,一共有 4 种比较方式。如果 4 种比较都没匹配,则以遍历的方式作比较;如果设置了 key,就会用 key 进行比较,在比较的过程中,变量会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。 这一具体过程可以参考这篇文章:[https://www.cnblogs.com/wind-lanyan/p/9061684.html](https://www.cnblogs.com/wind-lanyan/p/9061684.html) 有空的话我把他的图重画一下...... 同样的条件下,React 采取的比较方式是从左至右一一比较(如果没有 key),而 Vue 采取的是这种指针匹配的形式,这也是其 diff 策略的一个不同之处。 # 参考链接 [https://juejin.im/post/5b8b56e3f265da434c1f5f76#heading-0](https://juejin.im/post/5b8b56e3f265da434c1f5f76#heading-0) [https://segmentfault.com/a/1190000017508285](https://segmentfault.com/a/1190000017508285) [https://segmentfault.com/a/1190000018914249?utm\_source=tag-newest](https://segmentfault.com/a/1190000018914249?utm_source=tag-newest) [https://www.jianshu.com/p/398e63dc1969](https://www.jianshu.com/p/398e63dc1969) [https://www.cnblogs.com/wind-lanyan/p/9061684.html](https://www.cnblogs.com/wind-lanyan/p/9061684.html)