You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
当我们写下一段 JSX 代码的时候,react 是如何根据我们的 JSX 代码来生成虚拟 DOM 的组成元素 element 的。
return(<divclassName="cn"><Header> Hello, This is React </Header><div>Start to learn right now!</div>
Right Reserve.
</div>);
中间过程经过 babel 编译, createElement 的参数有三个,其一: type -> 标签类型,其二 :attributes -> 标签属性,没有的话,可以为 null,其三: children -> 标签的子节点
returnReact.createElement('div',{className: 'cn'},React.createElement(Header,null,'Hello, This is React'),React.createElement('div',null,'Start to learn right now!'),'Right Reserve');
简化版本的 dom 树
{
"div": {
"attributes": [{ "name": "classname", "value": "cn" }],
"childNodes": [
{ "nodeName": "#text", "nodeType": 3, "nodeValue": "↵" },
{
"nodeName": "HEADER",
"nodeType": 1,
"nodeValue": null,
"innerText": "Hello, This is React"
},
{
"nodeName": "#text",
"nodeType": 3,
"nodeValue": "↵"
},
{
"nodeName": "DIV",
"nodeType": 1,
"nodeValue": null,
"innerText": "Start to learn right now!"
}
],
"children": [
{ "tagName": "HEADER", "textContent": " Hello, This is React " },
{ "tagName": "DIV", "textContent": "Start to learn right now!" }
],
"tagName": "DIV",
"innerHTML": "↵ <header> Hello, This is React </header>↵ <div>Start to learn right now!</div>↵ Right Reserve.↵ "
}
}
对比 render 函数被调用的时候,会返回的 element 对象
{type: 'div',props: {className: 'cn',children: [{type: functionHeader,props: {children: 'Hello, This is React'}},{type: 'div',props: {children: 'start to learn right now!'}},'Right Reserve']}}
前言
相信很多人都用过 React ,那么大家是否遇到过海量 DOM render 卡顿的问题? React 16 对渲染机制做了大改动,很大的提升了交互体验,背后的原理又是什么? 实践出真知,本次分享深入挖掘 React 的渲染机制,同时结合实例来解决实践中遇到的性能问题,从而写出高性能的 React 应用。
JSX 如何生成 element
中间过程经过 babel 编译, createElement 的参数有三个,其一: type -> 标签类型,其二 :attributes -> 标签属性,没有的话,可以为 null,其三: children -> 标签的子节点
简化版本的 dom 树
对比 render 函数被调用的时候,会返回的 element 对象
我们来观察一下这个对象的 children,现在有三种类型:
1、string
2、原生 DOM 节点
3、React Component - 自定义组件
除了这三种,还有两种类型:
4、false ,null, undefined, number
5、数组 - 使用 map 方法的时候
这里需要记住一个点:element 不一定是 Object 类型。
二、element 如何生成真实节点
转化的规则如下:
ReactDOMComponent, ReactCompositeComponentWrapper 是 React 自己使用私有类,不会暴露给用户,常用方法:mountComponent, updateComponent 等是私有类的方法由于涉及创建,更新操作生命周期方法被暴露给用户使用
ReactDOMComponent
核心关键是 ReactMComponent 的 mountComponent 方法(直接操作浏览器 DOM 元素),作用是 将 element 转成真实 DOM 节点,并且插入到相应的 container 里 ,然后返回 markup(realDOM),最后暴露生命周期钩子给用户
ReactCompositeComponentWrapper
mountComponent: 实例化自定义组件,不直接生成 DOM 节点,最后是通过递归调用到 ReactDOMComponent 的 mountComponent 方法来得到真实 DOM
假设我们有一个 Example 的组件,它返回
首次渲染
过程如下:
首先从 React.render 开始, render 函数被调用的时候会返回一个 element
由于这个 type 是一个自定义组件类,此时要初始化的类是 ReactCompositeComponentWrapper,接着调用它的 mountComponent 方法。这里面会做四件事情,详情可以看上图。其中,第二步的 render 的得到的 element 为
由图可知,在第一步得到 instance 对象之后,就会去看 instance.componentWillMount 是否有被定义,有的话调用,而在整个渲染过程结束之后调用 componentDidMount。
以上,就是渲染原理的部分,让我们来总结以下:
React v15
渲染更新
触发组件的更新有两种更新方式: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
由图可知,shouldComponentUpdate 在第一步调用得到 nextState 之后调用,因为 nextState 也是它的其中一个参数嘛~这个函数很重要,它是我们性能优化的一个很关键的点:由图可以看到,当 shouldComponentUpdate 返回 false 的时候,下面的一大块都不会被去执行,包括已经被优化的 diff 算法。
当 shouldComponentUpdate 返回 true 的时候,会先调用 componentWillUpdate,在整个更新过程结束之后调用 componentDidUpdate。
以上就是更新渲染的过程。
Diff 算法
React 基于两个假设:
发明了一种叫 Diff 的算法来比较两棵 DOM tree,它极大的优化了这个比较的过程,将算法复杂度从 O(n^3)降低到 O(n)。
同时,基于第一点假设,我们可以推论出,Diff 算法只会对同层的节点进行比较。如图,它只会对颜色相同的节点进行比较
也就是说如果父节点不同,React 将不会在去对比子节点。因为不同的组件 DOM 结构会不相同,所以就没有必要在去对比子节点了。这也提高了对比的效率。
下面,我们具体看下 Diff 算法是怎么做的,这里分为三种情况考虑
不同节点类型
对于不同的节点类型,react 会基于第一条假设,直接删去旧的节点,新建一个新的节点。
比如:
生命周期打印结果:
由此可以看出,A 与其子节点 C 会被删除,然后重新建一个 B,C 插入。这样就给我们的性能优化提供了一个思路,就是我们要保持 DOM 标签的稳定性。
打个比方,如果写了一个
<div><List /></div>
(List 是一个有几千个节点的组件),切换的时候变成了<section><List /></section>
,此时即使 List 的内容不变,它也会先被卸载在创建,其实是很浪费的。相同节点类型
当对比相同的节点类型比较简单,这里分为两种情况,一种是 DOM 元素类型,对应 html 直接支持的元素类型:div,span 和 p,还有一种是自定义组件。
react 会对比它们的属性,只改变需要改变的属性
由于 React 此时并不知道如何去更新 DOM 树,因为这些逻辑都在 React 组件里面,所以它能做的就是根据新节点的 props 去更新原来根节点的组件实例,触发一个更新的过程,最后在对所有的 child 节点在进行 diff 的递归比较更新。
子节点比较
因为 React 在没有 key 的情况下对比节点的时候,是一个一个按着顺序对比的。从列表一到列表二,只是在中间插入了一个 C,但是如果没有 key 的时候,react 会把 B 删去,新建一个 C 放在 B 的位置,然后重新建一个节点 B 放在尾部。
生命周期打印结果:
当节点很多的时候,这样做是非常低效的。有两种方法可以解决这个问题:
1、保持 DOM 结构的稳定性,我们来看这个变化,由两个子节点变成了三个,其实是一个不稳定的 DOM 结构,我们可以通过通过加一个 null,保持 DOM 结构的稳定。这样按照顺序对比的时候,B 就不会被卸载又重建回来。
更新时的打印结果:
2、key
通过给节点配置 key,让 React 可以识别节点是否存在。
配上 key 之后,再跑一遍的打印结果。
果然,配上 key 之后,列表二的生命周期就如我所愿,只在指定的位置创建 C 节点插入。
这里要注意的一点是,key 值必须是稳定(所以我们不能用 Math.random()去创建 key),可预测,并且唯一的。
这里给我们性能优化也提供了两个非常重要的依据:
性能优化
1、Mount/Unmount
<div> -> <section>
2、避免重复渲染
3、使用 Pure Functional Component recompose
目前 react 性能优化的点主要集中在防止重复渲染,DOM 稳定性的方面:
但是大家看一个问题,如例子中所展示,点击改变计数按钮后,开始有大量组件重新渲染,但比较阶段不可被打断,input 输入框不可使用。
更新机制:一边对比一边更新,操作 dom 结构同步,从上到下是不间断的,主线程用于大批量更新时会被卡住,导致其他的用户操作无法响应,体验很差
现在 React 16 将异步渲染方案分为了两个阶段,第一阶段专注比较,第二阶段专注更新
工具
React 16 异步渲染方案
React 16 改动
1、比较阶段 – 可被打断
2、commit 阶段 – 不可被打断
主线程不间断使用(同步比较 + 同步更新) =》 自由释放主线程(可打断的比较 + 异步更新)
由于 React 16 异步方案的引入,异步 render 函数之前的函数可能被打断,调用多次,所以 render 之前的函数变得不安全,从而新增了
getDerivedStateFromProps
API 代替 componentWillMount,componentWillReceiveProps,componentWillUpdate,在此静态方法里面专门做 state 的更新初始化操作
参考链接
拓展阅读
The text was updated successfully, but these errors were encountered: