[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)
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直线、矩形、多边形
- Part2-曲线图形
- Part3-线条操作
- Part4-文本操作
- Part5-图像操作
- Part6-变形操作
- Part7-像素操作
- Part8-渐变与阴影
- Part9-路径与状态
- Part10-物理动画
- Part11-边界检测
- Part12-碰撞检测
- Part13-用户交互
- Part14-高级动画
- CSS
- SCSS
- codePen
- 速查表
- 面试题
- 《CSS Secrets》
- SVG
- 移动端适配
- 滤镜(filter)的使用
- JS
- 基础概念
- 作用域、作用域链、闭包
- this
- 原型与继承
- 数组、字符串、Map、Set方法整理
- 垃圾回收机制
- DOM
- BOM
- 事件循环
- 严格模式
- 正则表达式
- ES6部分
- 设计模式
- AJAX
- 模块化
- 读冴羽博客笔记
- 第一部分总结-深入JS系列
- 第二部分总结-专题系列
- 第三部分总结-ES6系列
- 网络请求中的数据类型
- 事件
- 表单
- 函数式编程
- Tips
- JS-Coding
- Framework
- Vue
- 书写规范
- 基础
- vue-router & vuex
- 深入浅出 Vue
- 响应式原理及其他
- new Vue 发生了什么
- 组件化
- 编译流程
- Vue Router
- Vuex
- 前端路由的简单实现
- React
- 基础
- 书写规范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 与 Hook
- 《深入浅出React和Redux》笔记
- 前半部分
- 后半部分
- react-transition-group
- Vue 与 React 的对比
- 工程化与架构
- Hybird
- React Native
- 新手上路
- 内置组件
- 常用插件
- 问题记录
- Echarts
- 基础
- Electron
- 序言
- 配置 Electron 开发环境 & 基础概念
- React + TypeScript 仿 Antd
- TypeScript 基础
- 样式设计
- 组件测试
- 图标解决方案
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- Other
- markdown 书写
- Git
- LaTex 常用命令