# 虚拟dom与diff算法
## 虚拟dom
本质上是使用js对象结构表示dom树的结构。然后使用这个树构建真正的dom结构。虚拟dom使用diff算法完成了没必要的dom操作,从而优化了效率。
在这里先看一下一个简单的实现dom的方式。
一般情况下被称为转译,本质上是一个携带特定参数的函数调用过程。大多数情况是以ReactElement的形式存在,
~~~
class VNode {
constructor(tagName, props = {}, children = []) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
render() {
let createdElement = document.createElement(this.tagName);
for (let prop in this.props) {
createdElement.setAttribute(prop, this.props[prop]);
}
if (this.children) {
this.children.forEach(child => {
createdElement.appendChild(child instanceof VNode ? child.render() : document.createTextNode(item));
})
}
return createdElement;
}
}
~~~
在使用时
~~~
const h = (tag, props, children) => new VNode(tag, props, children);
~~~
在写jsx时,其实所有的jsx都使用了React.creatElement()来生成dom。
在正式react中,经常调用的api有:
1. React.creatElement 通过读入节点的type attributes以及children来进行react的绘制。在使用自定义组件例如<Table xxxxx />时,在浏览器端,调用方式是React.creatElement(Table,{xxxxx}); 这里的Table是作为一个引用被我们使用的。
2. 在构建虚拟DOM对象完成之后,ReactDOM.render将会按下面的原则,尝试将其转换为浏览器可以识别和展示的DOM节点:
如果type包含一个带有String类型的标签名称(tag name)—— 创建一个标签,附带上props下所有attributes。
如果type是一个函数(function)或者类(class),调用它,并对结果递归地重复这个过程。
如果props下有children属性 —— 在父节点下,针对每个child重复以上过程。
## diff算法
> 1.通过比较key来优化效率,比较出具体哪个dom发生了变化。
> 2.合并操作,当进行setState操作时,React将其标记为脏值,当事件循环结束时,React检查脏值,并重新绘制component。
> 3. 想dom树拆解,只比较同级dom。
如果没有key,比如A节点下有B C两个节点,我们想交换这两个节点时,会先将两个节点unmount 再重新生成这两个节点。
但是有key的话就不一样。 可以直接update这两个节点。
## 如何优化react的效率呢?
1. 使用shouldComponentUpdate
2. 使用key
### diff算法解析
三个基本原则:
1. DOM中跨层级的操作很少,可以忽略不计。
2. 拥有同类型的两个组件生产相似的树形结构,不同的组件生成不同的树形结构。
3. 同一层的子节点,可以通过唯一ID进行区别。
**tree diff**
基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。
既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同 层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
**component diff**
1. 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
2. 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
3. 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
**element diff**
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
**INSERT_MARKUP**,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
**MOVE_EXISTING**,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
**REMOVE_NODE**,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
在elementDiff中涉及到key的问题。
> 首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。
>
**大致流程**
> 1. 有一个全局变量updateDepth来标识递归的深度,进行diff算法之前+1,diff算法结束之后-1。当其重新变为0的时候表示整个diff算法结束了,可以拿更新队列diffQueue来更新DOM了。
> 2. Diff算法只对同一个父元素的同级子元素进行对比。如果元素的type和key(如果有的话)相同,视为同一个元素,进行更新;否则替换掉。
> 3. Diff使用了一个局部变量:lastIndex——记录已经处理的旧列表中最靠后的元素。当元素的._mountIndex大于lastIndex的时候,不需要移动元素。因为移动元素不会对前面对元素产生任何影响,因此可以省略这个动作。由于很多时候大部分元素处于这种情况下,因此这个局部变量提升了性能(有时候很明显)。
## 参考
[diff](https://zhuanlan.zhihu.com/p/20346379)
- html
- 冒泡/捕获/委托
- 前端路由
- Dom
- 创建节点API
- 页面修改型API
- 节点查询型API
- 节点关系型API
- 元素属性API
- DOM事件
- classList
- 性能优化
- 节流防抖
- localStorage sessionStorage
- BOM
- meta
- data属性
- js实现拖拽
- html5
- 关于meta
- 轮播图
- js实现拖放
- 电话号inputFormater
- js
- es6
- promise
- iterator
- generator
- async
- proxy
- Set
- Map
- Object的扩展
- String
- Iterator
- Symbol
- 解构赋值
- 函数式编程
- module
- 基本数据类型
- 数组相关codings
- for of/for in
- this
- call bind apply
- 闭包
- 作用域
- prototype与继承
- 深浅拷贝
- setTimeOut/setInterval
- 垃圾处理机制
- 设计模式
- 观察者模式
- 单例模式
- 策略模式
- RegExp
- with
- 其他玩意
- Error/Stack Trace
- 面向对象
- css
- 回流重绘
- %取值
- 属性继承/属性优先级
- flex
- BFC
- 盒模型
- 设置css的方法
- 定位机制
- 块级/行内元素
- hack和一些别的玩意
- css动画
- 几个布局
- 画图形
- css3
- animation对比transform
- 点击不同图片区域跳转不同
- css选择器性能问题
- vh rem em
- css选择器
- 伪类伪元素
- css匹配原理
- 数据结构与算法
- 数据结构
- 树
- 链表
- 栈和队列
- 排序
- 归并排序
- 插入排序
- 选择排序
- 冒泡排序
- 快速排序
- 递归
- 回溯法
- 搜索算法
- 动态规划
- http
- 跨域问题
- CORS
- GET/POST
- ajax
- ajax上传文件
- http缓存
- https
- TCP/UDP
- cookie/session
- http2.0
- spdy
- websocket
- React
- redux
- 生命周期
- 虚拟dom与diff
- 双向数据绑定
- mvvm
- setState
- contextApi props reudx
- 高阶组件
- react-redux
- Fiber
- react-router
- 受控/非受控组件
- 待整理
- webpack
- loader实现
- 前端安全
- 移动端适配
- Vue
- 传值
- 其他