从渲染原理到性能优化(一)

83次阅读

共计 3983 个字符,预计需要花费 10 分钟才能阅读完成。

前言
以下,是我在 2018 React Conf 的分享内容,希望对大家有所帮助。可以先在官网下载我的 ppt 对照看,效果更佳哦~。
很多人都使用过 React,但是很少人能说出它内部的渲染原理。有人会说,会用就行了,知道渲染原理有必要么?其实渲染原理决定着性能优化的方法,只有在了解原理之后,才能完全理解为什么这样做可以优化性能。正所谓:知其然,然后知其所以然。废话不多说,下面我们就开始吧~
本篇文章,将会分为四部分介绍:
JSX 如何生成 element 当我们写下一段 JSX 代码的时候,react 是如何根据我们的 JSX 代码来生成虚拟 DOM 的组成元素 element 的。
element 如何生成真实 DOM 节点再生成 elment 之后,react 又如何将其转成浏览器的真实节点。这里会通过介绍首次渲染以及更新渲染的流程来帮助大家理解这个渲染流程。
性能优化结合渲染原理,通过实际例子,看看如何优化组件。
React 16 异步渲染方案到目前为止,这些优化组件的方法还不能解决什么问题,所以我们需要引入异步渲染,以及异步渲染的原理是什么。
一、JSX 如何生成 element
这里是一段写在 render 里的 jsx 代码。
return (
<div className=”cn”>
<Header> Hello, This is React </Header>
<div>Start to learn right now!</div>
Right Reserve.
</div>
)
首先,它会经过 babel 编译成 React.createElement 的表达式。
return (
React.createElement(
‘div’,
{className: ‘cn’},
React.createElement(
Header,
null,
‘Hello, This is React’
),
React.createElement(
‘div’,
null,
‘Start to learn right now!’
),
‘Right Reserve’
)
)

这个 createElement 方法是做什么的呢?
其实从它的名字就可以看出,这是用来生成 element 的。element 在 React 里,其实就是组成虚拟 DOM 树的节点,它用来描述你想要在浏览器上看到什么。它的参数有三个:1、type -> 标签 2、attributes -> 标签属性,没有的话,可以为 null3、children -> 标签的子节点
这个 React.createElement 的表达式会在 render 函数被调用的时候执行,换句话说,当 render 函数被调用的时候,会返回一个 element。说了那么久 element,这个 element 究竟长什么样呢?其实,它就是一个对象,如下:
{
type: ‘div’,
props: {
className: ‘cn’,
children: [
{
type: function Header,
props: {
children: ‘Hello, This is React’
}
},
{
type: ‘div’,
props: {
children: ‘start to learn right now!’
}
},
‘Right Reserve’
]
}
}

我们来观察一下这个对象的 children,现在有三种类型:1、string2、原生 DOM 节点 3、React Component – 自定义组件
除了这三种,还有两种类型:4、fale ,null, undefined,number5、数组 – 使用 map 方法的时候
这里需要记住一个点:element 不一定是 Object 类型。
二、element 如何生成真实节点
顺利得到 element 之后,我们再来看看 React 是如何把 element 转化成真实 DOM 节点的。首先,需要去初始化 element, 初始化的规则如下:以第一条为例:先判断是否为 Object 类型,是的话,看它的 type 是否是原生 DOM 标签,是的话,给它创建 ReactDOMComponent 的实例对象,其他同理。
这时候有的人可能会有所疑问:这些个 ReactDOMComponent, ReactCompositeComponentWrapper 怎么开发的时候都没有见过?
其实这些都是 React 的私有类,React 自己使用,不会暴露给用户的。它们的常用方法有:mountComponent,updateComponent 等。其中 mountComponent 用于创建组件,而 updateComponent 用于用户更新组件。而我们自定义组件的生命周期函数以及 render 函数都是在这些私有类的方法里被调用的。既然这些私有类的方法那么重要我们就先来简单了解一下吧~
ReactDOMComponent
首先是 ReactMComponent 的 mountComponent 方法,这个方法的作用是:将 element 转成真实 DOM 节点,并且插入到相应的 container 里,然后返回 markup(realDOM)。由此可知 ReactDOMComponent 的 mountComponent 是 element 生成真实节点的关键。下面看个栗子它是怎么做到的吧。
假设有这样一个 type 类型是原生 DOM 的 element:
{
type: ‘div’,
props: {
className: ‘cn’,
children: ‘Hello world’,
}
}

简单 mountComponent 的实现:
mountComponent(container) {
const domElement = document.createElement(this._currentElement.type);
const textNode = document.createTextNode(this._currentElement.props.children);

domElement.appendChild(textNode);
container.appendChild(domElement);
return domElement;
}

其实实现的过程很简单,就是根据 type 生成 domElement, 再将子节点 append 进来返回。当然,真实的 mountComponent 没有那么简单,感兴趣的可以自己去看源码啦。这里需要记住的一个点是:这个类的 mountComponent 方法会自己操作浏览器 DOM 元素。讲完 ReactDOMComponent,再来看看 ReactCompositeComponentWrapper。
ReactCompositeComponentWrapper
这个类的 mountComponent 方法作用是:实例化自定义组件,最后是通过递归调用到 ReactDOMComponent 的 mountComponent 方法来得到真实 DOM。注意:也就是说他自己是不直接生成 DOM 节点的。那这个递归是一个怎样的过程呢?我们通过首次渲染来看下。
首次渲染
假设我们有一个 Example 的组件,它返回 <div>hello world</div> 这样一个标签。首次渲染的过程如下:
首先从 React.render 开始,由于我们刚刚说,render 函数被调用的时候会返回一个 element,所以此时返回给我们的 element 是:
{
type: function Example,
props: {
children: null
}
}

由于这个 type 是一个自定义组件类,此时要初始化的类是 ReactCompositeComponentWrapper, 接着调用它的 mountComponent 方法。这里面会做四件事情,详情可以看上图。其中,第二步的 render 的得到的 element 为:
{
type: ‘div’,
props: {
children: ‘Hello World’
}
}

由于这个 type 是一个原生 DOM 标签,此时要初始化的类是 ReactDOMComponent。接下来它的 mountComponent 方法就可以帮我们生成对应的 DOM 节点放在浏览器里啦。这时候有人可能会有疑问,如果第二步 render 出来的 element 类型也是自定义组件呢?这时候它就会去调用 ReactCompositeComponentWrapper 的 mountComponent 方法,从而形成了一个递归。不管你的自定义组件嵌套多少层,最后总会生成原生 dom 类型的 element,所以最后一定能调用到 ReactDOMComponent 的 mountComponent 方法。感兴趣的可以自己在打断点看下这个递归的过程。由我打的断点图可以看出在 ReactCompositeComponent 的 mountComponent 被调用多次之后,最后调用到了 ReactDOMComponent 的 mountComponent 方法。
到这里,首次渲染的过程就基本讲完了:-D。但是还有一个问题:前面我们说自定义组件的生命周期跟 render 函数都是在私有类的方法里被调用的,现在只看到 render 函数被调用了,那么首次渲染时候生命周期函数 componentWillMount 跟 componentDidMount 在哪被调用呢?
由图可知,在第一步得到 instance 对象之后,就会去看 instance.componentWillMount 是否有被定义,有的话调用,而在整个渲染过程结束之后调用 componentDidMount。以上,就是渲染原理的部分,让我们来总结以下:

JSX 代码经过 babel 编译之后变成 React.createElement 的表达式,这个表达式在 render 函数被调用的时候执行生成一个 element。
在首次渲染的时候,先去按照规则初始化 element,接着 ReactComponentComponentWrapper 通过递归,最终调用 ReactDOMComponent 的 mountComponent 方法来帮助生成真实 DOM 节点。

正文完
 0