深入分析虚拟DOM的渲染原理和特性|博客系列
# 导读
React的虚拟DOM和Diff算法是React的非常重要的核心特性,这部分源码也非常复杂,理解这部分知识的原理对更深入的掌握React是非常必要的。
本来想将虚拟DOM和Diff算法放到一篇文章,写完虚拟DOM发现文章已经很长了,所以本篇只分析虚拟DOM。
本篇文章从源码出发,分析虚拟DOM的核心渲染原理(首次渲染),以及React对它做的性能优化点。
说实话React源码真的很难读😅,如果本篇文章帮助到了你,那么请给个赞👍支持一下吧。
# 开发中的常见问题
- 为何必须引用
React - 自定义的
React组件为何必须大写 React如何防止XSSReact的Diff算法和其他的Diff算法有何区别key在React中的作用- 如何写出高性能的
React组件
如果你对上面几个问题还存在疑问,说明你对React的虚拟DOM以及Diff算法实现原理还有所欠缺,那么请好好阅读本篇文章吧。
首先我们来看看到底什么是虚拟DOM:
# 虚拟DOM

在原生的JavaScript程序中,我们直接对DOM进行创建和更改,而DOM元素通过我们监听的事件和我们的应用程序进行通讯。
而React会先将你的代码转换成一个JavaScript对象,然后这个JavaScript对象再转换成真实DOM。这个JavaScript对象就是所谓的虚拟DOM。
比如下面一段html代码:
<div class="title">
<span>Hello ConardLi</span>
<ul>
<li>苹果</li>
<li>橘子</li>
</ul>
</div>
在React可能存储为这样的JS代码:
const VitrualDom = {
type: 'div',
props: { class: 'title' },
children: [
{
type: 'span',
children: 'Hello ConardLi'
},
{
type: 'ul',
children: [
{ type: 'ul', children: '苹果' },
{ type: 'ul', children: '橘子' }
]
}
]
}
当我们需要创建或更新元素时,React首先会让这个VitrualDom对象进行创建和更改,然后再将VitrualDom对象渲染成真实DOM;
当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。
# 为何使用虚拟DOM
React为何采用VitrualDom这种方案呢?
# 提高开发效率
使用JavaScript,我们在编写应用程序时的关注点在于如何更新DOM。
使用React,你只需要告诉React你想让视图处于什么状态,React则通过VitrualDom确保DOM与该状态相匹配。你不必自己去完成属性操作、事件处理、DOM更新,React会替你完成这一切。
这让我们更关注我们的业务逻辑而非DOM操作,这一点即可大大提升我们的开发效率。
# 关于提升性能
很多文章说VitrualDom可以提升性能,这一说法实际上是很片面的。
直接操作DOM是非常耗费性能的,这一点毋庸置疑。但是React使用VitrualDom也是无法避免操作DOM的。
如果是首次渲染,VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。
VitrualDom的优势在于React的Diff算法和批处理策略,React在页面更新之前,提前计算好了如何进行更新和渲染DOM。实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。所以,在这个过程中React帮助我们"提升了性能"。
所以,我更倾向于说,VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。
如果您对本部分的分析有什么不同见解,欢迎在评论区拍砖。
# JSX和createElement
我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写:
class Hello extends Component {
render() {
return <div>Hello ConardLi</div>;
}
}
第二种是直接使用React.createElement编写:
class Hello extends Component {
render() {
return React.createElement('div', null, `Hello ConardLi`);
}
}
实际上,上面两种写法是等价的,JSX只是为 React.createElement(component, props, ...children) 方法提供的语法糖。也就是说所有的JSX 代码最后都会转换成React.createElement(...) ,Babel帮助我们完成了这个转换的过程。
如下面的JSX
<div>
<img src="avatar.png" className="profile" />
<Hello />
</div>;
将会被Babel转换为
React.createElement("div", null, React.createElement("img", {
src: "avatar.png",
className: "profile"
}), React.createElement(Hello, null));
注意,babel在编译时会判断JSX中组件的首字母,当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象;
另外,由于JSX提前要被Babel编译,所以JSX是不能在运行时动态选择类型的,比如下面的代码:
function Story(props) {
// Wrong! JSX type can't be an expression.
return <components[props.storyType] story={props.story} />;
}
需要变成下面的写法:
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
所以,使用JSX你需要安装Babel插件babel-plugin-transform-react-jsx
{
"plugins": [
["transform-react-jsx", {
"pragma": "React.createElement"
}]
]
}
# 创建虚拟DOM
下面我们来看看虚拟DOM的真实模样,将下面的JSX代码在控制台打印出来:
<div className="title">
<span>Hello ConardLi</span>
<ul>
<li>苹果</li>
<li>橘子</li>
</ul>
</div>

这个结构和我们上面自己描绘的结构很像,那么React是如何将我们的代码转换成这个结构的呢,下面我们来看看createElement函数的具体实现(文中的源码经过精简)。

createElement函数内部做的操作很简单,将props和子元素进行处理后返回一个ReactElement对象,下面我们来逐一分析:
(1).处理props:

- 1.将特殊属性
ref、key从config中取出并赋值 - 2.将特殊属性
self、source从config中取出并赋值 - 3.将除特殊属性的其他属性取出并赋值给
props
后面的文章会详细介绍这些特殊属性的作用。
(2).获取子元素

- 1.获取子元素的个数 —— 第二个参数后面的所有参数
- 2.若只有一个子元素,赋值给
props.children - 3.若有多个子元素,将子元素填充为一个数组赋值给
props.children
(3).处理默认props

- 将组件的静态属性
defaultProps定义的默认props进行赋值
ReactElement
ReactElement将传入的几个属性进行组合,并返回。
type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)key:组件的唯一标识,用于Diff算法,下面会详细介绍ref:用于访问原生dom节点props:传入组件的propsowner:当前正在构建的Component所属的Component
$$typeof:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE:
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
可见,$$typeof是一个Symbol类型的变量,这个变量可以防止XSS。
如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能会成为一个问题:
// JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* put your exploit here */'
},
},
};
let message = { text: expectedTextButGotJSON };
<p>
{message.text}
</p>
JSON中不能存储Symbol类型的变量。
ReactElement.isValidElement函数用来判断一个React组件是否是有效的,下面是它的具体实现。
ReactElement.isValidElement = function (object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
};
可见React渲染时会把没有$$typeof标识,以及规则校验不通过的组件过滤掉。
当你的环境不支持Symbol时,$$typeof被赋值为0xeac7,至于为什么,React开发者给出了答案:
0xeac7看起来有点像React。



