说实话React源码真的很难读,很枯燥。React虚拟DOM的特性很多人都知道,但相信很多人并没那么清楚它究竟是什么,React的虚拟 DOM和 Diff算法是 React的非常重要的核心特性,这部分源码也非常复杂,理解这部分知识的原理对更深入的掌握 React是非常必要的
在原生的 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这种方案呢?
1.提高开发效率
使用 JavaScript,我们在编写应用程序时的关注点在于如何更新 DOM,而且操作dom代价很大。
使用 React,只需要告诉 React我们想让视图处于什么状态, React则通过 VitrualDom确保DOM与该状态相匹配。你不必自己去完成属性操作、事件处理、 DOM更新, React会完成这一切。
这让我们更关注我们的业务逻辑而非 DOM操作,这一点即可大大提升我们的开发效率。
2.关于提升性能
关于VitrualDom可以提升性能,在很多技术文章中都有提到,然而也很多人提出这一说法实际上是很片面的。
直接操作 DOM是非常耗费性能的,这一点毋庸置疑。但是 React使用 VitrualDom也是无法避免操作 DOM的。
如果是首次渲染, VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。
VitrualDom的优势在于 React的 Diff算法和批处理策略, React在页面更新之前,提前计算好了如何进行更新和渲染 DOM。实际上,这个计算过程我们在直接操作 DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如 React好的。所以,在这个过程中 React帮助我们"提升了性能"。
所以,我更倾向于这个说法: “VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比 DOM操作更快。”
跨浏览器兼容
React基于 VitrualDom自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题
跨平台兼容
VitrualDom为 React带来了跨平台渲染的能力。以 ReactNative为例子。React根据 VitrualDom画出相应平台的 ui层,只不过不同平台画的姿势不同罢了
虚拟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>
ReactElement
ReactElement将传入的几个属性进行组合,并返回。
type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或 class)
key:组件的唯一标识,用于 Diff算法,下面会详细介绍
ref:用于访问原生 dom节点
props:传入组件的 props
owner:当前正在构建的 Component所属的Component
$$typeof:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE:
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
可见,$$typeof是一个Symbol类型的变量,这个变量可以防止XSS。
虚拟DOM转换为真实DOM
当任何一个组件使用setState时,React都会认为该组件变‘脏’了,于是触发组件本身的重新渲染,同时因其始终同时维护两套虚拟的DOM,其中一套是更新后的虚拟DOM;另一套是前一个状态的虚拟DOM,通过对这两套虚拟DOM运行diff算法,找到需要变化是最小单元集,然后把这个最小单元集应用在真实的DOM中。
而这个diff算法是将两棵DOM树之间的diff复杂度缩减到线性函数级别O(N)
而React的这个神奇的Diff算法是基于两大假设:
1.DOM节点的跨层级移动忽略不计
2.拥有相同类的两个组件生产相似的树形结构,拥有不同类的两个组件生成不同的树形结构。
根据这些假设,React采取的策略如下
1.React对组件进行分层比较,两棵树只会对同一层级的节点进行比较。
2.当对同一层级的节点进行比较时,对于不同的组件类型,直接将整个组件替换为新类型组件
网友评论