一:虚拟DOM
1. 为什么要用虚拟DOM
真实DOM的操作十分耗费性能,操作过于频繁会带来非常大的性能损耗。
2. 原理
用js的对象结构表示DOM树结构,然后利用这个js对象构建 一个真正的DOM树,插入到文档当中。相当于在JS和真实DOM中间加了一个缓存。编码时基本上只需要操作虚拟DOM,React会将虚拟DOM变化转为真实DOM变化,然后更新界面,大大减少了DOM操作。
3. 图解
4. 用jsx手动实现虚拟DOM
基本思路:
- 创建一个变量存储dom
- 手动实现一个render方法,将虚拟dom对象转化为真实dom
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
// 不引入react要解析jsx,必须进行以下操作
// @jsx是babel的自执行指令,指定babel执行自定义的createElement
/*@jsx createElement*/
/*
此方法会递归的将html转为对象的形式
nodename:节点名字
attr:节点属性
args:后代节点
*/
function createElement(nodeName, attr, ...args) {
return { nodeName, attr, children: [].concat(...args) }
}
let users = [
{ name: "张三" },
{ name: "李四" },
{ name: "王五" }
];
let vDom = (<div id="app" name="app">
hello world!
<ul>
{
users.map((item, index) => {
return (
<li key={index}>{item.name}</li>
)
})
}
</ul>
</div>);
/* 自定义render方法 */
function render(vnode) {
if (vnode.split) { // 如果是文本,创建文本节点
return document.createTextNode(vnode);
}
// 如果不是文本,创建根节点
let node = document.createElement(vnode.nodeName);
// 添加属性
let attr = vnode.attr || {};
// 返回attr自身可枚举属性,并组成一个数组,然后遍历
Object.keys(attr).forEach((k) => {
node.setAttribute(k, attr[k]);
});
// 添加子节点
(vnode.children || []).forEach((n) => {
node.appendChild(render(n))
});
return node;
}
let dom = render(vDom);
document.body.appendChild(dom);
</script>
二:DOM Diff算法
1. 为什么要用DOM Diff
React利用虚拟DOM,可以避免频繁操作真实DOM。但是虚拟DOM最终都会被React转化为真实DOM,DOM操作耗费性能,如果每次更新状态都重新渲染整个应用或者整个组件,将会损耗非常大的性能。为了减少DOM更新,我们需要找到渲染前后真正变化的部分,然后更新这部分。而对比变化,找出需要更新部分的算法,我们称之为Diff算法。
2. 原理
- 初始化时render()函数创建了一颗虚拟DOM树,在下一次更新时,render()函数创建一颗新的虚拟DOM树,然后用Diff算法对两颗树进行对比。
- 传统的Diff算法的事件复杂度为o(n^3),React通过大胆的策略,将复杂度将为o(n)。
- 两个不同类型的元素会产生不同的树
- 对于同一层级的一组子节点,它们可以通过key值进行区分
- 基于以上两个前提策略,React分别对tree diff、component diff、element diff三种diff方法进行算法优化
1. Tree Diff
概念:将新旧两颗虚拟DOM树,按照层级对应的关系,从头到尾遍历一遍。
- 只会对相同颜色框(同级)的DOM节点进行比较,即同一父节点下的所有子节点。
- 我们以为的操作:
A.parent.remove(A);
D.append(A);- React中的操作
A.destroy();
A= new A();
A.append(new B());
A.append(new C());
D.append(A);- 当发现该节点已经不存在,则该节点及其子节点将会被完全删除掉,不会用于进一步比较(这也是算法的时间复杂度能够降低到o(n)的原因)
2. Component Diff
将同一位置上的组件进行对比,这种对比方法其实比较的就是类型
- 如果类型相同,暂不更新
- 如果类型不相同,就需要更新(删除旧的组件,再创建一个新的组件,插入到删除组件的那个位置)
3. Element Diff
在类型相同的组件内,再继续对比组件内部的元素,查看其是否相同。找到不同的元素,进行针对性修改,这就是Element Diff
三种节点操作:
- 插入:新的Component类型不在旧集合内,执行插入操作
- 移动:旧的集合包含新的Component,执行移动操作,复用之前的DOM节点(有key值才会进行移动,否则只是简单粗暴的插入和删除)
- 删除:旧的Component不再新的集合里,或者旧的Component的类型相同但是Element不同,不能复用,执行删除操作。
4. 总结
- DOM Diff通过Tree Diff,Component Diff,Element Diff逐层进行节点比较,极大的优化了传统的Diff算法。
- 在实现自己的组件时,保持稳定的DOM结构,有助于性能的提升。例如:可以通过CSS隐藏或者显示某些节点,而不是真的移除或者添加DOM节点。
- 在进行Component和Element对比的时候,为了提高对比效率,React推荐我们为每个for循环创建出来的元素或者组件,提供一个唯一的key。