什么是虚拟DOM
const ele = (
<div
className='myDiv'
style={{
width:100,
height: 100,
background: 'red'
}}
>
<span>virtual dom</span>
</div>
)
将上面的ele打印出来,如下所示:
这就是虚拟DOM在React中的形态,是JS和DOM之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象。
为什么需要虚拟DOM
-
提升开发者的体验和效率
引用
React官网中的一段话:"Virtual DOM赋予了React声明式的API:您告诉React希望让UI是什么状态,React就确保DOM匹配该状态。这使您可以从属性操作、事件处理和手动DOM更新这些在构建应用程序时必要的操作中解放出来。" 从原生
JS到JQuery,开发者除了关心数据之外,还需要关心DOM的操作;接着出现了模板引擎,虽然实现了用户界面与业务数据的分离,但是它涉及到大量的字符串的拼接,开发效率也比较低下。为了解决开发体验和开发效率与DOM操作之间的矛盾,虚拟DOM应运而生。 -
解决跨平台不兼容的问题
虚拟
DOM是对真实渲染内容的一层抽象,将视图层和渲染平台解耦了,它可以是对Web页面真实DOM的描述,也可以是对IOS界面、安卓界面和小程序等的描述,实现"一套代码,多端运行" -
性能问题?
操作真实的
DOM耗时(JS引擎和渲染引擎互斥,上下文切换耗时;可能导致重排和重绘),但是引入虚拟 DOM 也并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式的同时,仍然保持一个还不错的性能。引用尤大大的一句话:“框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。” 性能是一个比较复杂复杂的问题,需要在一个特定的场景下谈论才有意义,举一个极端的例子,如果数据内容整个发生了改变,此时
DOM更新的工作量是一致的,而虚拟DOM却会产生开销更大的JS计算,因此此时虚拟DOM的优势是发挥不出来的。
React中的虚拟DOM是怎么产生的
虚拟DOM的产生需要借助于Babel
可以看到JSX标签都被编译成了React.createElement,也就是说JSX本质其实是React.createElement这个JS的语法糖,nameReact.createElement是怎样转化成虚拟DOM的?
createElement的源码如下:
/**
@param {String} type: 同于标识节点的类型,可以是html标签比如div等,也可以是React组件或者React Fragment类型
@param {Object} config: 组件的所有属性,比如className、style等
@param {Object} children: 存储的是组件标签之间嵌套的内容,也就是所谓的子节点、子元素
*/
function createElement(type, config, children) {
var propName; // 用于存储后面需要用到的元素属性
var props = {}; // 用于存储元素属性的键值对集合
// 定义React元素的属性key、ref、self和source
var key = null;
var ref = null;
var self = null;
var source = null;
if (config != null) {
// 根据入参config依次给ref、key、self和source赋值
if (hasValidRef(config)) {
ref = config.ref;
{
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object
// 遍历config中的属性,将符合条件的属性存储到props中
for (propName in config) {
if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
} // Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
// 获取当前元素子节点的个数
var childrenLength = arguments.length - 2;
// 单个节点 ==> 直接赋值给props.children
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// 多个子节点 ==> 声明一个子节点数组,将子节点放进该数组中,然后赋值给props.children
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
{
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
} // Resolve default props
// 处理defaultProps
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
... // 处理key和ref
// 返回reactElelemnt方法
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
从上面的源码可以看出来,createElement的主要逻辑就是格式化数据,将开发者传入的参数转换成符合ReactElement方法的参数。ReactElement的源码如下:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 父组件
_owner: owner,
};
...// 针对 __DEV__ 环境下的一些处理逻辑
}
return element;
};
从源码可以看出,ReactElelemt的主要逻辑就是把入参分装进element对象,然后将其返回,这个element对象就是一开始我们打印的那个虚拟DOM。
现在可以得出的结论是,JSX最终被转化成了一个element对象即虚拟DOM。
diff算法
将前后两次的虚拟DOM树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,这就是diff算法。diff算法基于以下三个规律:
①、若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树结构
②、DOM 节点之间的跨层级操作并不多,主要是同层级操作。
③、处于同一层级的一组子节点,可用通过设置 key 作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性
Diff算法的核心要点主要是三个:
①、分层对比
基于“DOM 节点之间的跨层级操作并不多,同层级操作是主流”这一规律,React的 Diff过程直接放弃了跨层级的节点比较,它只针对相同层级的节点作对比。
如果存在跨层级的操作,React则直接判断移出子树那一层的组件消失了,对应子树需要被销毁;而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。销毁 + 重建的代价是昂贵的,因此React官方也建议开发者不要做跨层级的操作,尽量保持 DOM 结构的稳定性。
②、只有类型相同的元素才有Diff的必要
基于“若两个组件属于同一个类型,那么它们将拥有相同的DOM树形结构”这一规律,React认为,只有同类型的组件,才有进一步对比的必要性;若参与Diff的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点,如下图所示。只有确认组件类型相同后,React才会在保留组件对应 DOM 树(或子树)的基础上,尝试向更深层次去Diff。
③、属性key可以提高节点的复用性
React官方对key的定义是:key是用来帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果key值发生了变更,React 则会触发 UI 的重渲染。这是一个非常有用的特性。
所以,key想解决的是同一层级中节点的重用问题。如下图所示,如果想在组件A的两个子节点B和D之间插入一个新的节点C
如果没加key,两棵树之间的 Diff 过程应该是这样的:
首先对比位于第 1 层的节点,发现两棵树的节点类型是一致的(都是A),于是进一步 Diff;
开始对比位于第 2 层的节点,第 1 个接受比较的是B这个位置,对比下来发现两棵树这个位置上的节点都是B,继续下个节点的diff
第 2 个接受比较的是 D 这个位置,对比 D 和 C,发现前后的类型不一致,直接删掉 D 重建 C;
第 3 个接受比较的是 E 这个位置,对比 E 和 D,发现前后的类型不一致,直接删掉E重建 D;
最后接受比较的是树 2 的 E 节点这个位置,这个位置在树 1 里是空的,也就是说树 2 的E 是一个新增节点,所以新增一个 E。
如果有key,如下图所示:
那么 key 就可以充当每个节点的 唯一标识,有了这个标识之后,当 C 被插入到 B 和D之间时,React 会通过识别 ID,意识到D和 E 并没有发生变化,只是被调整了顺序而已。接着,React 便能够轻松地重用它“追踪”到旧的节点,将 D 和 E 转移到新的位置,并完成对 C 的插入。这样一来,同层级下元素的操作成本便大大降低。
作者:wangmiaolin