React源码系列(二):虚拟DOM与其价值
前言
这是React源码系列专栏的第二篇文章,预计写10篇左右,之前的文章请查看文末,通过本专栏的学习,相信大家可以快速掌握React源码的相关概念以及核心思想,向成为大佬的道路上更近一步;
为什么我们需要虚拟 DOM?想必很多人都会有这样疑问,这个问题常见的答案就是:“DOM 操作是很慢的,而 JS 却可以很快,直接操作 DOM 可能会导致频繁的回流与重绘,JS 不存在这些问题。因此虚拟 DOM 比原生 DOM 更快”。事实上真的是这样吗?带着这个问题来进入本文;
原生DOM所带来的问题
我们来看一段代码:
<body>
<ul id="wrap"></ul>
<button id="btn">改变</button>
</body>
<script>
const person = [
{
name: "xiao ming",
age: 25,
},
{
name: "xiao zhang",
age: 23,
},
];
const btn = document.getElementById("btn");
const wrap = document.getElementById("wrap");
btn.addEventListener("click",function(){
person[0].name = 'xiao li'
render(person)
})
function render(params) {
wrap.innerHTML = ''
params.forEach((item) => {
let cell = document.createElement("li");
cell.innerHTML = `${item.name}-${item.age}`;
wrap.appendChild(cell);
});
}
render(person);
</script>
当我们点击按钮时,我们改变了xiao ming的名称,但是dom帮我们把整个div的结构都重新渲染了一遍。如果我们在一个很大的工程中重复的做类似的操作,就会使我们的项目运行的很卡。所以就此引出了虚拟DOM的概念,我们只关心改变的那个DOM其他的不用去管。
我们来看一个创建div的操作:
<script>
var div = document.createElement('div')
var item,result = ''
for(item in div){
result += ' | ' + item ;
}
console.log(result)
</script>
从上图中可以看出,我们每一次创建DOM都会创建出这么多属性来,如果多次创建,可以想象浏览器的性能。
基于以上,我们总结出下面几点:
- DOM操作是“昂贵”的,js运行的效率高
- 尽量减少DOM操作,而不是“推到重来”
- 项目越复杂,影响就越严重
- vdom即可解决这个问题
- 将DOM对比操作放在js层,提高效率
虚拟DOM是什么
虚拟 DOM(Virtual DOM)本质上是JS 和 DOM 之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象。
- 虚拟 DOM 是 JS 对象
- 虚拟 DOM 是对真实 DOM 的描述
虚拟DOM大致工作流程
- 挂载阶段,React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线);
- 更新阶段,页面的变化在作用于真实 DOM 之前,会先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM。
虚拟 DOM 是如何解决问题的
数据 + 模板 = 虚拟 DOM----->真实DOM--->挂载到页面上。 从上面过程可以看出,DOM操作多出了一层虚拟 DOM 作为缓冲层。这个缓冲层带来的利好是:当 DOM 操作(渲染更新)比较频繁时,它会先将前后两次的虚拟 DOM 树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,最后只把“补丁”打在需要更新的那部分真实 DOM 上,实现精准的“差量更新”。
- 原始操作DOM:注销旧 DOM -> 数据 + 模板 => 新的一套HTML 代码 -> 挂载新 DOM
- 使用虚拟DOM:数据 + 模板 = 虚拟 DOM -> diff 新旧两套虚拟 DOM 的差异,得到补丁集 -> 把“补丁”打到真实 DOM 上
虚拟 DOM真的能带来更好的性能吗?
虚拟 DOM 并不一定会带来更好的性能,它的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。 模板渲染和虚拟 DOM 在性能开销上的差异?
- 原始:构建新的真实DOM ------> 旧的DOM被新的DOM替换
- 虚拟DOM:构建新的虚拟DOM -----> 通过diff对比新旧两棵树差异------->差量更新DOM
乍一看好像差量更新一定比全量更新高效,但你需要考虑这样一种情况:数据内容变化非常大(或者说整个发生了改变),促使差量更新计算出来的结果和全量更新极为接近(或者说完全一样)。此时 DOM 更新的工作量基本一致,而虚拟 DOM 却伴随着开销更大的 JS 计算。那么虚拟 DOM 大概率不敌模板渲染。但只要两者在最终 DOM 操作量上拉开那么一点点的差距,虚拟 DOM 就将具备战胜模板渲染的底气。因为虚拟 DOM 的劣势主要在于 JS 计算的耗时,而 DOM 操作的能耗和 JS 计算的能耗根本不在一个量级,极少量的 DOM 操作耗费的性能足以支撑大量的 JS 计算。
在实际的开发中,更加高频的场景是修改少量的数据。在这种场景下虚拟 DOM 将在性能上具备绝对的优势。 所以虚拟 DOM 的价值不在性能。
虚拟 DOM 的价值到底是什么
虚拟DOM解决的关键问题:
- 研发体验/研发效率的问题:虚拟 DOM 的出现,使得前端开发能够基于函数式 UI 的编程方式实现高效的声明式编程。
- 跨平台的问题:虚拟 DOM 是对真实渲染内容的一层抽象。若没有这一层抽象,那么视图层将和渲染平台紧密耦合在一起。
除了差量更新以外,“批量更新”也是虚拟 DOM 在性能方面做的一个亮点,“批量更新”是由 batch 函数来处理的。在差量更新速度非常快的情况下(比如极短的时间里多次操作同一个 DOM),用户实际上只能看到最后一次更新的效果。这种场景下,前面几次的更新动作虽然意义不大,但都会触发重渲染流程,带来大量不必要的高耗能操作。 这时就需要请 batch 来帮忙了,batch 的作用是缓冲每次生成的补丁集,它会把收集到的多个补丁集暂存到队列中,再将最终的结果交给渲染函数,最终实现集中化的 DOM 批量更新。
虚拟DOM用在哪里
react中用JSX语法描述视图(View),react17之前是通过babel-loader转译后它们变为React.createElement(...)形式,该函数将生成vdom来描述真实dom。将来如果状态变化,vdom将作出相应变化,再通过diff算法对比新老vdom区别从而做出最终dom操作。使用 babel 来展示一下效果;
简单实现虚拟DOM案例
这里我们借助了一个虚拟dom的库 snabbdom 简单实现了解一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vdom</title>
<style>
li {
list-style: none;
}
</style>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-class.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-props.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-style.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/h.js"></script>
<script>
var snabbdom = window.snabbdom;
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners,
]);
var h = snabbdom.h;
var container = document.getElementById("container");
var vnode = h("ul#list", {}, [
h("li.item", {}, "Item 1"),
h("li.item", {}, "Item 2"),
]);
patch(container, vnode);
const btn = document.getElementById("btn-change");
btn.addEventListener("click", function () {
var newvnode = h("ul#list", {}, [
h("li.item", {}, "Item 1"),
h("li.item", {}, "Item 3"),
h("li.item", {}, "Item 4"),
]);
patch(vnode, newvnode);
});
</script>
</body>
</html>
核心API:h函数、patch函数;
可以自己打开浏览器运行一下案例,DOM呈现的效果不是整块重新渲染,它只是对修改的模块重新渲染;
参考链接
React源码系列
转载自:https://juejin.cn/post/7199865975277453370