React渲染原理
React渲染原理
看了公众号的文章
,做一个简单的摘抄。侵权即删
文中有一句话我是很赞同的
其实渲染原理决定着性能优化的方法,只有在了解原理之后,才能完全理解为什么这样做可以优化性能。正所谓:知其然,然后知其所以然。
按照原文结构,本文也分为四个部分。
JSX如何生成element
element如何生成真实DOM节点
性能优化
React16异步渲染方案
1. JSX如何生成element
先来看一下render
里的jsx
经过babel
编译后的React.createElement
;
# render jsx
return (
<div className='cn'>
<Header> hello </Header>
<div> start </div>
Right Reserve
</div>
)
# babel React.createElement
return (
React.createElement(
'div',
{
className : 'cn'
},
React.createElement(
Header,
null,
'hello'
),
React.createElement(
'div',
null,
'start'
),
'Right Reserve'
)
)
- 什么是
createElement
?
生成element
。element
在React
里是组成虚拟Dom树
的节点。这个表达式会在render
被调用的时候执行并返回一个element
。它有三个参数: - type -> 标签
- attributes -> 标签属性,若无则为null
- children -> 标签的子节点
element
的结构(不一定是Object
类型)
{
type : 'div',
props: {
className : 'cn',
children : [
{
type : function Header,
props : {
children : 'hello'
}
},
{
type : 'div',
props : {
children : 'start'
}
},
'Right Reserve'
]
}
}
children
类型string
- 原生
dom
节点 React Component
- 自定义组件false
、null
、undefined
、number
- 数组 - 使用
map
方法的时候
Element
如何生成真实节点
初始化element
规则如下: * 判断是否为Object
类型 * yes -> 判断type
是否为原生DOM
标签 * yes -> 创建ReactDomComponent
的实例对象。 * no -> 使用ReactCompositeCompontWrapper
* no -> 判断类型 * string
number
-> 使用ReactDomTextComponent
* null
false
-> 使用ReactDomEmptyComponent
这里的ReactDOMComponent
、ReactCompositeComponentWrapper
是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(domElment);
return domElement;
}
这个类的mountComponent方法会自己操作浏览器DOM元素
ReactCompositeComponentWrapper
这个类的mountComponent
方法作用是:实例化自定义组件,最后是通过递归调用到ReactDOMComponent
的mountComponent
方法来得到真实DOM
。他自己是不直接生成DOM节点
的。
首次渲染(讲述递归过程)
假设我们有一个Example的组件,它返回<div>hello world</div>
这样一个标签。 初次渲染过程如下图
- 从
React render
开始render
函数被调用的时候会返回一个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
方法,从而形成了一个递归。
由图可知,在第一步得到instance
对象之后,就会去看instance.componentWillMount
是否有被定义,有的话调用,而在整个渲染过程结束之后调用componentDidMount
。
2. element
=> 真实DOM节点
触发组件的更新有两种更新方式:props
以及state
改变带来的更新。本次主要解析state
改变带来的更新。整个过程流程图如下:
1、一般改变state
,都是从setState
开始,这个函数被调用之后,会将我们传入的state
放进pendingState
的数组里存起来,然后判断当前流程是否处于批量更新,如果是,则将当前组件的instance
放进dirtyComponent
里,当这个更新流程中所有需要更新的组件收集完毕之后(这里面涉及到事务的概念,感兴趣的可以自己去了解一下)就会遍历dirtyComponent
这个数组,调用他们的uptateComponent
对组件进行更新。当然,如果当前不处于批量更新的状态,会直接去遍历dirtyComponent
进行更新。
2、在我们这个例子中,由于Example
是自定义组件,所以调用的是ReactCompositeComponentWrapper
这个类的updateComponent
方法,这个方法做三件事。
- 计算出
nextState
render()
得到nextRenderElement
- 与
prevElement
进行Diff
比较(这个过程后面会介绍),更新节点
最后这个需要去更新节点的时候,跟首次渲染一样,也需要调用ReactDOMComponent
的updateComponent
来更新。其中第二步render
得到的也是自定义组件的话, 会形成递归调用。
接下来,还是上次的问题:那么更新过程中的生命周期函数,shouldComponentUpdate
,componentWillUpdate
跟componentDidUpdate
在哪被调用呢?
由图可知,shouldComponentUpdate
在第一步调用得到nextState
之后调用,因为nextState
也是它的其中一个参数嘛~这个函数很重要,它是我们性能优化的一个很关键的点:由图可以看到,当shouldComponentUpdate
返回false
的时候,下面的一大块都不会被去执行,包括已经被优化的diff算法。
当shouldComponentUpdate
返回true
的时候,会先调用componentWillUpdate
,在整个更新过程结束之后调用componentDidUpdate
。
以上就是更新渲染的过程。
Diff
算法
React
基于两个假设:
- 两个相同的组件产生类似的
DOM结构
,不同组件产生不同DOM结构
- 对于同一层次的一组子节点,它们可以通过唯一的id区分
发明了一种叫Diff
的算法来比较两棵DOM tree
,它极大的优化了这个比较的过程,将算法复杂度从O(n^3)
降低到O(n)
。
同时,基于第一点假设,我们可以推论出,Diff
算法只会对同层的节点进行比较。如图,它只会对颜色相同的节点进行比较。
也就是说如果父节点不同,React
将不会在去对比子节点。因为不同的组件DOM结构
会不相同,所以就没有必要在去对比子节点了。这也提高了对比的效率。
下面,我们具体看下Diff
算法是怎么做的,这里分为三种情况考虑
- 节点类型不同
- 节点类型相同
- 子节点比较
- 不同节点类型
对于不同的节点类型,react
会基于第一条假设,直接删去旧的节点,新建一个新的节点。
比如:
<A>
<C/>
</A>
// change
<B>
<C/>
</B>
React
会直接删掉A节点
(包括它所有的子节点),然后新建一个B节点
插入
为了验证这一点,我打印出了从shape1
到shape2
节点的生命周期
最后终端输出的结果是:
Shape1 :
A is created
A render
C is created
C render
C componentDidMount
A componentDidMountShape2 :
A componentWillUnmount
C componentWillUnmount
B is created
B render
C is created
C render
C componentDidMount
B componentDidMount
由此可以看出,A
与其子节点C
会被直接删除,然后重新建一个B
,C
插入。这样就给我们的性能优化提供了一个思路,就是我们要保持DOM标签
的稳定性
。
打个比方,如果写了一个<div><List /></div>
(List
是一个有几千个节点的组件),切换的时候变成了<section><List /></section>
,,此时即使List
的内容不变,它也会先被卸载在创建,其实是很浪费的。
相同节点类型
当对比相同的节点类型比较简单,这里分为两种情况,一种是DOM元素类型
,对应html
直接支持的元素类型:div
,span
和p
,还有一种是自定义组件。
DOM元素类型
react
会对比它们的属性,只改变需要改变的属性 比如:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
这两个div
中,react
会只更新className
的值
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
这两个div
中,react
只会去更新color
的值
自定义组件类型
由于React
此时并不知道如何去更新DOM
树,因为这些逻辑都在React
组件里面,所以它能做的就是根据新节点的props
去更新原来根节点的组件实例,触发一个更新的过程,最后在对所有的child
节点在进行diff
的递归比较更新。
- shouldComponentUpdate
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
子节点比较
<div>
<A />
<B />
</div>
// 列表一到列表二<div>
<A />
<C />
<B />
</div>
因为React
在没有key
的情况下对比节点的时候,是一个一个按着顺序对比的。从列表一到列表二,只是在中间插入了一个C
,但是如果没有key
的时候,react
会把B
删去,新建一个C
放在B
的位置,然后重新建一个节点B
放在尾部。
列表一:
A is created
A render
B is created
B render
A componentDidMount
B componentDidMount列表二:
A render
B componentWillUnmount
C is created
C render
B is created
B render
A componentDidUpdate
C componentDidMount
B componentDidMount
当节点很多的时候,这样做是非常低效的。有两种方法可以解决这个问题:
1、保持DOM
结构的稳定性,我们来看这个变化,由两个子节点变成了三个,其实是一个不稳定的DOM
结构,我们可以通过通过加一个null
,保持DOM
结构的稳定。这样按照顺序对比的时候,B
就不会被卸载又重建回来。
<div>
<A />
{null} <B />
</div>
// 列表一到列表二<div>
<A />
<C />
<B />
</div>
2、key
通过给节点配置key
,让React
可以识别节点是否存在。
配上key
之后,在跑一遍代码看看。
A render
C is created
C render
B render
A componentDidUpdate
C componentDidMount
B componentDidUpdate
果然,配上key
之后,列表二的生命周期就如我所愿,只在指定的位置创建C节点
插入。
这里要注意的一点是,key
值必须是稳定(所以我们不能用Math.random()
去创建key
),可预测,并且唯一的。
这里给我们性能优化也提供了两个非常重要的依据:
保持DOM
结构的稳定性
map
的时候,加key
未完待续