说明
React 中的Virtual DOM(虚拟DOM)模式学习。
Virtual [ˈvɝtʃʊəl]
概念
什么是 “Virtual DOM”?
Virtual DOM 是一种由 Javascript 类库基于浏览器 API 实现的概念,一种模式。
在 React 的世界里,术语 “Virtual DOM” 通常与 React 元素关联在一起,因为它们都是代表了用户界面的对象。而 React 也使用一个名为 “fibers” 的内部对象来存放组件树的附加信息。上述二者也被认为是 React 中 “Virtual DOM” 实现的一部分。
什么是 “React Fiber”?
Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染。
官方文档《Virtual DOM 及内核》
react虚拟DOM原理
在某一时间节点调用 React 的 render()
方法,会创建一棵由 React 元素组成的树。
在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。
React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM。
详细查看我的文章《React中的diffing算法》
React是如何生成虚拟DOM、并渲染成真实DOM的?
推荐阅读《从零开始实现一个React(一):JSX和虚拟DOM》
1. jsx
const element = <h1>Hello, world!</h1>;
它被称为 JSX,是一个 JavaScript 的语法扩展。
Babel 会把 JSX 转译成一个名为 React.createElement()
的普通 JS 函数调用,并且对其取值后得到 JavaScript 对象。
2. 理解React.createElement()
生成虚拟DOM
React.createElement(type, [props], [...children])
创建并返回指定类型的新 React 元素。
- 第一个类型参数既可以是标签名字符串(如 'div' 或 'span'),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。
- 第二个参数是一个对象,里面包含了所有的属性,可能包含了className,id等等
- 从第三个参数开始,就是它的子节点。
3. jsx 被 babel 转换成普通js代码的过程(React.createElement()
)
// JSX
const Name = function (props) {
return <div>{props.name}</div>;
};
const a = (
<div className="container">
<Name name="caicai"></Name>
<h1 title="hello" className="a">
hello world
</h1>
<p>world</p>
</div>
);
jsx是语法糖,上面这段代码会被 babel 转换成如下代码:
const Name = function (props) {
return React.createElement("div", null, props.name);
};
const a = React.createElement(
"div",
{
className: "container",
},
React.createElement(Name, {
name: "caicai",
}),
React.createElement(
"h1",
{
title: "hello",
className: "a",
},
"hello world"
),
React.createElement("p", null, "world")
);
而 React.createElement()
实际上它创建了一个这样的对象(也就是我们说的虚拟DOM~):
// 注意:这是简化过的结构
const element = {
type: "div",
props: {
className: "container",
children: [
{
type: "h1",
props: { title: "hello", className: "a", children: "hello world" },
},
{ type: "p", props: { children: "world" } },
],
},
};
这些通过React.createElement()
方法生成返回的对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容(即记录了这个DOM节点所有的信息)。React 通过读取这些对象,然后使用它们来构建真实的 DOM 以及保持随时更新。而这个记录信息的对象我们称之为虚拟DOM。
3. React 如何将虚拟DOM渲染为真实的DOM:通过ReactDOM.render()
想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render()
,页面上会展示出 “Hello, world”:
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
// 经过转换,这段代码变成了这样
ReactDOM.render(
React.createElement( 'h1', null, 'Hello, world!' ),
document.getElementById('root')
);
render的第一个参数实际上接受的是createElement返回的对象,也就是虚拟DOM
而第二个参数则是挂载的目标DOM。总而言之,render()
方法的作用就是将虚拟DOM渲染成真实的DOM(递归创建type节点、设置节点属性、创建子节点等)
- 当组件的
state
、props
发生改变时,会重新执行组件中的render()
函数,然后react更新DOM,用户就看到新的效果; - 当父组件的
render()
函数执行时,它的子组件的render()
函数也都将被重新执行一次。(不论是否有传数据给字组件。)
假如没有react时,我们如何实现上面的渲染?
实现方法1【不好】
- 准备 state 数据
- 准备 JSX 模版
- 用
render()
把数据+模版结合,生成真实的 DOM,再把DOM挂载在页面上显示。 - 当state 发生改变
- 再用
render()
把数据+模版结合,生成新的真实的 D0M,替换原始的 D0M 【非常耗性能】
缺陷
第一次生成了一个完整的DOM片段
第二次生成了一个完整的DOM片段
第二次的DOM替换第一次的DOM,是非常耗性能的!
实现方法2【有缺陷】
- 准备 state 数据
- 准备 JSX 模版
- 用
render()
把数据+模版结合,生成真实的 DOM,再把DOM挂载在页面上显示。 - 当state 发生改变
- 数据+模版结合,生成真实的 D0M,但并不直接替换原始的 D0M
新的 D0M(即js底层的
DoucumentFragment
,叫文档碎片)和原始的 DOM 做比对,找出差异【DOM对比消耗性能】- DOM的比较很耗性能!(但是js中比较js对象不太耗性能)
- 比如找出了 input 框发生了变化
- 只用新的 D0M 中的 input 元素,替换掉老的 DOM 中的 input 元素——局部更新【节约了性能】
缺陷
性能的提升并不明显(又消耗、又节约)
实现方法3【虚拟DOM,react的实现~】
- 准备 state 数据
- 准备 JSX 模版
用
render()
把数据+模版结合(JSX),JSX通过React.createElement()
生成虚拟 D0M- 【损耗了性能(极小)】
['div',{id: 'abc'},['span',{},'hello world']
- 格式:
(component, props, ...children)
即[DOM标签,属性集,内容或子节点] - 损耗极小的性能,因为用js生成js对象,比js生成DOM元素代价小太多了。
用虚拟DOM的结构生成真实的 DOM,再把DOM挂载在页面上显示。
- 比如
<div id='abc'><span>hello world</span></div>
- 比如
- 当state 发生改变
用
render()
把数据+模版结合再生成新的虚拟 D0M- 【相比方法2中的第5步,现在极大的提升了性能】
['div',{id: 'abc'},['span',{},'bye bye']
比较新的虚拟 D0M 和原始虚拟 D0M 的区别 ——【diffing算法】(点击阅读文章)
- 即比较两个js对象,非常不耗性能【所以极大的提升性能】
- 找到区别是 span 中内容不同了
- 直接操作 DOM,改变 span 中的内容
优点
- 性能提升了
- 它使得跨端应用得以实现。如React Native写原生应用——得益于虚拟DOM的存在。
理解:如果没有虚拟dom,渲染DOM在浏览器上是没问题的,但在移动端的原生应用里(如安卓、ios等)是没有DOM的概念的,则无法被使用,只能被运行再浏览器里。但是虚拟DOM是js对象,在浏览器、原生里都可以被识别(如在原生里可以让虚拟DOM生成可以识别的组件)。
流程导图:
JSX -> 虚拟DOM(JS对象) -> 真实的DOM -> 挂载渲染
js创建dom元素,底层调用的是web application
小结【牢记】
1.什么是虚拟D0M?
就是一个JS 对象,用它来描述真实 DOM。
2.虚拟DOM为什么能提高性能?
因为减少了对真实DOM的创建和对比,取而代之,创建的都是js对象,对比的也都是js对象,所以 React 底层实现了极大的飞跃。
3. JSX与真实DOM的关系?或:react 生成虚拟DOM并渲染为真实DOM的过程?【牢记】
- 我们在代码里编写JSX(不是真实的DOM)
- Babel 会把 JSX 转译成一个名为
React.createElement(type, [props], [...children])
的普通 JS 函数调用; React.createElement()
创建并返回指定类型的新 React 元素,即生成了虚拟DOM(其实就是JS对象)。【递归】- 执行
ReactDOM.render()
函数:将虚拟DOM(JS对象)转化生成真实的DOM(html)并挂载。【递归】 - 当组件的
state
、props
发生改变时,会重新执行组件中的render()
函数,生成新的虚拟DOM,然后diff,再react更新真实DOM,用户就看到新的效果; - 当父组件的
render()
函数执行时,它的子组件的render()
函数也都将被重新执行一次。(不论是否有传数据给字组件。)
所以实际上,JSX 仅仅只是 React.createElement(component, props, ...children)
函数的语法糖。
例子如下:
// jsx
const title = <h1 className="title">Hello</h1>;
ReactDOM.render(title, document.getElementById("root"));
// jsx被babel转换成:
const title = React.createElement("h1", { className: "title" }, "hello");
ReactDOM.render(
React.createElement("h1", { className: "title" }, "hello"),
document.getElementById("root")
);
// React.createElement() 返回生成的虚拟DOM 为:
title = {
type: "h1",
props: { className: "title", children: "hello" },
};
对比
- vue和react都是虚拟DOM的机制(完全一致的),angularjs是脏值检测方式,虚拟DOM比脏检测要好很多。
- vue使用的是
Object.defineProperty()
进行数据劫持。react是以对象的形式创建虚拟dom然后用diff算法进行比较。这两种效率都很高的,当然相对而言vue更简单。Angular没了解过
更多学习:我的文章《React 虚拟DOM中的Diffing算法》