【React】393 深入了解React 渲染原理及性能优化
如今的前端,框架横行,出去面试问到框架是常有的事。
我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。
React是一个专注于构建用户界面的 Javascript Library.
React做了什么?
Virtual Dom模型
生命周期管理
setState机制
Diff算法
React patch、事件系统
React的 Virtual Dom模型
virtual dom 实际上是对实际Dom的一个抽象,是一个js对象。react所有的表层操作实际上是在操作Virtual dom。
经过 Diff 算法会计算出 Virtual DOM 的差异,然后将这些差异进行实际的DOM操作更新页面。
React 总体架构
几点要了解的知识
JSX 如何生成Element
Element 如何生成DOM
先看一个例子, Counter :
App.js 就做了一件事情,就是把 Counter 组件挂在 #root 上.
Counter 组件里面定义了自己的 state, 这是个默认的 property ,还有一个 handleclick 事件和 一个 render 函数。
看到 render 这个函数里,竟然在 JS 里面写了 html !
这是一种 JSX 语法。React 为了方便 View 层组件化,承载了构建 html 结构化页面的职责。
这里也简单的举个例子:
将 html 语法直接加入到 javascript 代码中,再通过翻译器转换到纯 javascript 后由浏览器执行。
这里调用了 React 和 createElement 方法,这个方法就是用于创建虚拟元素 Virtual Dom 的。
React 把真实的 DOM 树转换成 Javascript 对象树,也就是 Virtual Dom。
每次数据更新后,重新计算 Virtual Dom ,并和上一次生成的 virtual dom 做对比,对发生变化的部分做批量更新。
而 React 是通过创建与更新虚拟元素 Virtual Element 来管理整个Virtual Dom 的。
虚拟元素可以理解为真实元素的对应,它的构建与更新都是在内存中完成的,并不会真正渲染到 dom 中去。
回到我们的计数器 counter 组件:
注意下 a 标签 createElement 的返回结果, 这里 CreateElement 只是做了简单的参数修正,返回一个 ReactElemet 实例对象。
Virtual element 彼此嵌套和混合,就得到了一颗 virtual dom 的树:
现在我们有了由 ReactElement 组成的 Virtual Dom 树,接下来我们要怎么我们构建好的 Virtual dom tree 渲染到真正的 DOM 里面呢?
这时可以利用 ReactDOM.render 方法,传入一个 reactElement 和一个 作为容器的 DOM 节点。
看进去 ReactDOM.render 的源码,里面有两个比较关键的步骤:
第一步是 instantiateReactComponent。
这个函数创建一个 ReactComponent 的实例并返回,也可以看到 ReactDOM.render 最后返回的也是这个实例。
instantiateReactComponent 方法是初始化组件的入口函数,它通过判断 node 的类型来区分不同组件的入口。
当 node 为空的时候,初始化空组件。
当 node 为对象,类型 type 字段标记为是字符串,初始化 DOM 标签。否则初始化自定义组件。
当 node 为字符串或者数字时,初始化文本组件。
虽然 Component 有多种类型,但是它们具有基本的数据结构:ReactComponent 类。
注意到这里的 setState, 这也是重点之一。
创建了 Component 实例后,调用 component 的 mountComponent 方法,注意到这里是会被批量 mount 的,这样组件就开始进入渲染到 DOM 的流程了。
React生命周期
React 组件基本由三个部分组成,
属性 props
状态 state
生命周期方法
React 生命周期的全局图
首次挂载组件时,按顺序执行
componentWillMount、
render
componentDidMount
卸载组件时,执行 componentDidUnmount
当组件接收到更新状态,重新渲染组件时,执行
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
更新策略
通过 updateComponent 更新组件,首先判读上下文是否改变,前后元素是否一致,如果不一致且组件的 componentWillReceiveProps 存在,则执行。然后进行 state 的合并。
调用 shouldComponentUpdate 判断是否需要进行组件更新,如果存在 componentWillUpdate 则执行。
后面的流程跟 mountComponent 相似,这里就不赘述了。
setState机制
为避免篇幅过长,这部分可移步我的另一篇文章:
[第10期] 深入了解 React setState 运行机制
Diff算法
Diff算法用于计算出两个virtual dom的差异,是React中开销最大的地方。
传统diff算法通过循环递归对比差异,算法复杂度为 O(n3)。
React diff算法制定了三条策略,将算法复杂度从 O(n3)降低到O(n)。
1. UI中的DOM节点跨节点的操作特别少,可以忽略不计。
2. 拥有相同类的组件会拥有相似的DOM结构。拥有不同类的组件会生成不同的DOM结构。
3. 同一层级的子节点,可以根据唯一的
ID来区分。
对于策略一,React 对树进行了分层比较,两棵树只会对同一层次的节点进行比较。
只会对相同层级的 DOM 节点进行比较,当发现节点已经不存在时,则该节点及其子节点会被完全删除,不会用于进一步的比较。
如果出现了 DOM 节点跨层级的移动操作。
如上图这样,A节点就会被直接销毁了。
Diif 的执行情况是:create A -> create C -> create D -> delete A
当节点处于同一层级时,diff 提供了 3 种节点操作:插入、移动和删除。
对于同一层的同组子节点添加唯一 key 进行区分。
通过 diff 对比后,发现新旧集合的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置更新为新集合中节点的位置.
原理解析
几个概念
对新集合中的节点进行循环遍历,新旧集合中是否存在相同节点
nextIndex: 新集合中当前节点的位置
lastIndex: 访问过的节点在旧集合中最右的位置(最大位置)
If (child._mountIndex < lastIndex)
对新集合中的节点进行循环遍历,通过 key 值判断,新旧集合中是否存在相同节点,如果存在,则进行移动操作。
在移动操作的过程中,有两个指针需要注意,
一个是 nextIndex,表示新集合中当前节点的位置,也就是遍历新集合时当前节点的坐标。
另一个是 lastIndex,表示访问过的节点在旧集合中最右的位置,
更新流程:
( 如果新集合中当前访问的节点比 lastIndex 大,证明当前访问节点在旧集合中比上一个节点的位置靠后,则该节点不会影响其他节点的位置,即不进行移动操作。只有当前访问节点比 lastIndex 小的时候,才需要进行移动操作。)
首先,我们开遍历新集合中的节点, 当前 lastIndex = 0, nextIndex = 0,拿到了 B.
此时在旧集合中也发现了 B,B 在旧集合中的 mountIndex 为 1 , 比当前 lastIndex 0 要大,不满足 child._mountIndex < lastIndex,对 B 不进行移动操作,更新 lastIndex = 1, 访问过的节点在旧集合中最右的位置,也就是 B 在旧集合中的位置,nextIndex++ 进入下一步。
当前 lastIndex = 1, nextIndex = 1,拿到了 A,在旧集合中也发现了 A,A 在旧集合中的 mountIndex 为 0 , 比当前 lastIndex 1 要小,满足 child._mountIndex < lastIndex,对 A 进行移动操作,此时 lastIndex 依然 = 1, A 的 _mountIndex 更新为 nextIndex = 1, nextIndex++, 进入下一步.
这里,A 变成了蓝色,表示对 A 进行了移动操作。
当前 lastIndex = 1, nextIndex = 2,拿到了 D,在旧集合中也发现了 D,D 在旧集合中的 mountIndex 为 3 , 比当前 lastIndex 1 要大,不满足 child._mountIndex < lastIndex,不进行移动操作,此时 lastIndex = 3, D 的 _mountIndex 更新为 nextIndex = 2, nextIndex++, 进入下一步.
当前 lastIndex = 3, nextIndex = 3,拿到了 C,在旧集合中也发现了 C,C 在旧集合中的 mountIndex 为 2 , 比当前 lastIndex 3 要小,满足 child._mountIndex < lastIndex,要进行移动,此时 lastIndex不变,为 3, C 的 _mountIndex 更新为 nextIndex = 3.
由于 C 已经是最后一个节点,因此 diff 操作完成.
这样最后,要进行移动操作的只有 A C。
刚刚说的例子是新旧集合中都是相同节点但是位置不同。
那如果新集合中有新加入的节点且旧集合存在需要删除的节点,
那 diff 又是怎么进行的呢?比如:
1
首先,依旧,我们开遍历新集合中的节点, 当前 lastIndex = 0, nextIndex = 0,拿到了 B,此时在旧集合中也发现了 B,B 在旧集合中的 mountIndex 为 1 , 比当前 lastIndex 0 要大,不满足 child._mountIndex < lastIndex,对 B 不进行移动操作,更新 lastIndex = 1, 访问过的节点在旧集合中最右的位置,也就是 B 在旧集合中的位置,nextIndex++ 进入下一步。
2
当前 lastIndex = 1, nextIndex = 1,拿到了 E,发现旧集合中并不存在 E,此时创建新节点 E,nextIndex++,进入下一步
3
当前 lastIndex = 1, nextIndex = 2,拿到了 C,在旧集合中也发现了 C,C 在旧集合中的 mountIndex 为 2 , 比当前 lastIndex 1 要大,不满足 child._mountIndex < lastIndex,不进行移动,此时 lastIndex 更新为 2, nextIndex++ ,进入下一步
4
当前 lastIndex = 2, nextIndex = 3,拿到了 A,在旧集合中也发现了 A,A 在旧集合中的 mountIndex 为 0 , 比当前 lastIndex 2 要小,不满足 child._mountIndex < lastIndex,进行移动,此时 lastIndex 不变, nextIndex++ ,进入下一步
5
当完成新集合中所有节点的差异化对比后,还需要对旧集合进行循环遍历,判断是否勋在新集合中没有但旧集合中存在的节点。
此时发现了 D 满足这样的情况,因此删除 D。
Diff 操作完成。
整个过程还是很繁琐的, 明白过程即可。
二、性能优化
由于react中性能主要耗费在于update阶段的diff算法,因此性能优化也主要针对diff算法。
减少diff算法触发次数实际上就是减少update流程的次数。
正常进入update流程有三种方式:
1.setState
setState机制在正常运行时,由于批更新策略,已经降低了update过程的触发次数。
因此,setState优化主要在于非批更新阶段中(timeout/Promise回调),减少setState的触发次数。
常见的业务场景即处理接口回调时,无论数据处理多么复杂,保证最后只调用一次setState。
2.父组件render
父组件的render必然会触发子组件进入update阶段(无论props是否更新)。此时最常用的优化方案即为shouldComponentUpdate方法。
最常见的方式为进行this.props和this.state的浅比较来判断组件是否需要更新。或者直接使用PureComponent,原理一致。
需要注意的是,父组件的render函数如果写的不规范,将会导致上述的策略失效。
// Bad case
// 每次父组件触发render 将导致传入的handleClick参数都是一个全新的匿名函数引用。
// 如果this.list 一直都是undefined,每次传入的默认值[]都是一个全新的Array。
// hitSlop的属性值每次render都会生成一个新对象
class Father extends Component {
    onClick() {}
    render() {
        return <Child handleClick={() => this.onClick()} list={this.list || []} hitSlop={{ top: 10, left: 10}}/>
    }
}
// Good case
// 在构造函数中绑定函数,给变量赋值
// render中用到的常量提取成模块变量或静态成员
const hitSlop = {top: 10, left: 10};
class Father extends Component {
    constructor(props) {
        super(props);
        this.onClick = this.onClick.bind(this);
        this.list = [];
    }
    onClick() {}
    render() {
        return <Child handleClick={this.onClick} list={this.list} hitSlop={hitSlop} />
    }
}
3. forceUpdate
forceUpdate方法调用后将会直接进入componentWillUpdate阶段,无法拦截,因此在实际项目中应该弃用。
其他优化策略
1. shouldComponentUpdate
使用shouldComponentUpdate钩子,根据具体的业务状态,减少不必要的props变化导致的渲染。如一个不用于渲染的props导致的update。
2. 合理设计state,不需要渲染的state,尽量使用实例成员变量。
不需要渲染的 props,合理使用 context机制,或公共模块(比如一个单例服务)变量来替换。
不使用跨层级移动节点的操作。
对于条件渲染多个节点时,尽量采用隐藏等方式切换节点,而不是替换节点。
尽量避免将后面的子节点移动到前面的操作,当节点数量较多时,会产生一定的性能问题。
这时一个 List 组件,里面有标题,包含 ListItem 子组件的members列表,和一个按钮,绑定了一个 onclick 事件.
然后我加了一个插件,可以显示出各个组件的渲染情况。
现在我们来点击改变标题, 看看会发生些什么。
奇怪的事情发生了,为什么我只改了标题, 为什么不相关的 ListItem 组件也会重新渲染呢?
我们可以回到组件生命周期看看为什么。
还记得这个组件更新的生命周期流程图嘛,这里的重点在于这个 shouldComponentUpdate。
只有这个方法返回 true 的时候,才会进行更新组件的操作。我们进步一来看看源码。
可以看到这里,原来如果组件没有定义 shouldComponentUpdate 方法,也是默认认为需要更新的。
当然,我们的 ListItem 组件是没有定义这个 shouldComponentUpdate 方法的。
然后我们使用PureComponent :
其原理为重新实现了 shouldComponentUpdate 生命周期方法,让当前传入的 props 和 state 之前做浅比较,如果返回 false ,那么组件就不会更新了。
这里也放上一张官网的例图:
根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。
如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq)。
如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;
如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。
相似的APi还有React.memo:
回到组件
再次回到我们的组件中, 这次点击按钮, 把第二条数据换掉:
奇怪的事情发生了,为什么我只改了第二个 listItem, 还是全部 10 个都重新渲染了呢?
原因在于 shallow compare , 浅比较。
前面说到,我们不能直接修改 this.state 的值,所以我们把
this.state.members 拷贝出来再修改第二个人的信息。
很明显,因为对象的比较是引用地址,显然是不相等的。
因此 shoudComponentUpdate 方法都返回了 false, 组件就进行了更新。
那么我们怎么能避免这种情况的发生呢?
其中一个方法是做深比较,但是如果对象或数组层级比较深和复制,那么这个代价就太昂贵了。
我们就可以用到 Immutable.js 来解决这个问题,进一步提高组件的渲染性能。
Immutable Data 就是一旦被创建,就是不能再更改的数据。
首先,我们定义了一个 Immutable 的 List 对象,List 对应于原生 JS 的 Array,对 Immutable 对象进行修改、添加或删除操作,都会返回一个新的 Immutable 对象,所以这里 bbb 不等于 aaa。
但是同时为了避免深拷贝吧所有节点都复制一遍带来的性能消耗,Immutable 使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其他节点则进行共享。
结果也是我们预期的那样。
用好火焰图, 该优化的时候再优化。
回复“加群”与大佬们一起交流学习~
【React】393 深入了解React 渲染原理及性能优化的更多相关文章
- JavaScript 如何工作:渲染引擎和性能优化技巧
		
翻译自:How JavaScript works: the rendering engine and tips to optimize its performance 这是探索 JavaScript ...
 - 详解react/redux的服务端渲染:页面性能与SEO
		
亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染) react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...
 - 【redux】详解react/redux的服务端渲染:页面性能与SEO
		
亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染) react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...
 - 正式学习 React(三)番外篇 reactjs性能优化之shouldComponentUpdate
		
性能优化 每当开发者选择将React用在真实项目中时都会先问一个问题:使用react是否会让项目速度更快,更灵活,更容易维护.此外每次状态数据发生改变时都会进行重新渲染界面的处理做法会不会造成性能瓶颈 ...
 - 【转】Oracle Freelist和HWM原理及性能优化
		
文章转自:http://www.wzsky.net/html/Program/DataBase/74799.html 近期来,FreeList的重要作用逐渐为Oracle DBA所认识,网上也出现一些 ...
 - How Javascript works (Javascript工作原理) (十一) 渲染引擎及性能优化小技巧
		
个人总结:读完这篇文章需要20分钟,这篇文章主要讲解了浏览器中引擎的渲染机制. DOMtree ----| |----> RenderTree CSSOMtree ----| ...
 - JavaScript 工作原理之十一-渲染引擎及性能优化小技巧
		
原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland. 本系列持续更新中,Github 地址请查阅这里. 这是 JavaScript 工作原理的第十一章. 迄 ...
 - react渲染原理深度解析
		
https://mp.weixin.qq.com/s/aM-SkTsQrgruuf5wy3xVmQ 原文件地址 [第1392期]React从渲染原理到性能优化(二)-- 更新渲染 黄琼 前端早读课 ...
 - react+redux渲染性能优化原理
		
大家都知道,react的一个痛点就是非父子关系的组件之间的通信,其官方文档对此也并不避讳: For communication between two components that don't ha ...
 
随机推荐
- 力扣(LeetCode)旋转字符串 个人题解
			
给定两个字符串, A 和 B. A 的旋转操作就是将 A 最左边的字符移动到最右边. 例如, 若 A = 'abcde',在移动一次之后结果就是'bcdea' .如果在若干次旋转操作之后,A 能变成B ...
 - 在VMware通过挂载系统光盘搭建本地yum仓库
			
1.首先需要有一个VMware虚拟机: 2.进去虚拟机(这里用Linux下deCentOS进行演示): 3.用root账号进行登录,否则在根目录下没有一些操作权限: 4.打开终端: 5,输入命令“cd ...
 - 四 linuk常用命令   2. 权限管理命令
			
一 权限管理命令chmod 所有者u 所属组g 其他人o 所有人a 所有者和root超级用户可以更改该权限 普通更改权限是不会改变子目录的权限的,要想改变用递归修改 useradd增加用户 目录的r和 ...
 - Java描述设计模式(23):访问者模式
			
本文源码:GitHub·点这里 || GitEE·点这里 一.生活场景 1.场景描述 电竞是游戏比赛达到"竞技"层面的体育项目.利用电子设备作为运动器械进行的.人与人之间的智力对抗 ...
 - Springboot 系列(十六)你真的了解 Swagger 文档吗?
			
前言 目前来说,在 Java 领域使用 Springboot 构建微服务是比较流行的,在构建微服务时,我们大多数会选择暴漏一个 REST API 以供调用.又或者公司采用前后端分离的开发模式,让前端和 ...
 - python3之递归实例
			
一.利用递归求: 1+2+3+4+5...+n的前n项和 def recursion_sum_1(n): #当n = 1:和为1 #否则,n的和等同于 n + (n -1) if n == 1: re ...
 - Flow入门初识
			
Flow是facebook出品的JavaScript静态类型检查工具. 由于JavaScript是动态类型语言,它的灵活性也会造成一些代码隐患,使用Flow可以在编译期尽早发现由类型错误引起的bug, ...
 - css三大特效之继承性
			
css三大特效之继承性
 - python中return和print的区别
			
之前遇到这个问题,就试着对比几种不同的结果,总结啦一下return和print的区别. 总结: return的作用之一是返回计算的值print的作用是输出数据到控制端 在第一个结果中什么都没有输出:在 ...
 - 如何平滑优雅地在Rancher 2.x中升级cert-manager?
			
作者: Nassos Michas丨European Dynamics SA, CTO 如果你正在使用由Rancher提供的Helm Chart在Rancher管理的Kubernetes集群中安装ce ...