https://mp.weixin.qq.com/s/aM-SkTsQrgruuf5wy3xVmQ   原文件地址

【第1392期】React从渲染原理到性能优化(二)-- 更新渲染

黄琼 前端早读课 今天

前言

没去2018 React Conf的童鞋,别错误今天的。今日早读文章由腾讯IMWeb@黄琼授权分享。

@黄琼,腾讯前端工程师,IMWeb团队成员,目前负责企鹅辅导

正文从这开始~~

很多人都使用过React,但是很少人能说出它内部的渲染原理。有人会说,会用就行了,知道渲染原理有必要么?其实渲染原理决定着性能优化的方法,只有在了解原理之后,才能完全理解为什么这样做可以优化性能。正所谓:知其然,然后知其所以然。

废话不多说,下面我们就开始吧~

本篇文章,将会分为四部分介绍:

JSX如何生成element

当我们写下一段JSX代码的时候,react是如何根据我们的JSX代码来生成虚拟DOM的组成元素element的。

element如何生成真实DOM节点

再生成elment之后,react又如何将其转成浏览器的真实节点。这里会通过介绍首次渲染以及更新渲染的流程来帮助大家理解这个渲染流程,

性能优化

结合渲染原理,通过实际例子,看看如何优化组件。

React 16异步渲染方案

到目前为止,这些优化组件的方法还不能解决什么问题,所以我们需要引入异步渲染,以及异步渲染的原理是什么。

二、element如何生成真实DOM节点

【第1386期】React从渲染原理到性能优化(一)介绍了首次渲染的过程,接下来我们来看更新渲染的过程。

触发组件的更新有两种更新方式: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

由图可知,shouldComponentUpdate在第一步调用得到nextState之后调用,因为nextState也是它的其中一个参数嘛~这个函数很重要,它是我们性能优化的一个很关键的点:由图可以看到,当shouldComponentUpdate返回false的时候,下面的一大块都不会被去执行,包括已经被优化的diff算法。

当shouldComponentUpdate返回true的时候,会先调用componentWillUpdate,在整个更新过程结束之后调用componentDidUpdate。

以上就是更新渲染的过程。

下面我们重点再来介绍这个过程中的Diff算法。

Diff算法

React基于两个假设:

  • 两个相同的组件产生类似的DOM结构,不同组件产生不同DOM结构

  • 对于同一层次的一组子节点,它们可以通过唯一的id区分

发明了一种叫Diff的算法来比较两棵DOM tree,它极大的优化了这个比较的过程,将算法复杂度从O(n^3)降低到O(n)。

同时,基于第一点假设,我们可以推论出,Diff算法只会对同层的节点进行比较。如图,它只会对颜色相同的节点进行比较。

也就是说如果父节点不同,React将不会在去对比子节点。因为不同的组件DOM结构会不相同,所以就没有必要在去对比子节点了。这也提高了对比的效率。

下面,我们具体看下Diff算法是怎么做的,这里分为三种情况考虑

  • 节点类型不同

  • 节点类型相同

  • 子节点比较

  • 不同节点类型

对于不同的节点类型,react会基于第一条假设,直接删去旧的节点,新建一个新的节点。

比如:

<A>
 <C/>
</A>
// 由shape1到shape2<B>
 <C/>
</B>

React会直接删掉A节点(包括它所有的子节点),然后新建一个B节点插入

为了验证这一点,我打印出了从shape1到shape2节点的生命周期,链接如下:

https://codesandbox.io/s/lyop4w9x9mlyop4w9x9m - CodeSandboxlyop4w9x9m - CodeSandbox

最后终端输出的结果是:

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放在尾部。

你说什么就是什么咯?!不信的话,我们还是跑一边代码,看看生命周期验证一下,连接地址为:lpl52wy9vl - CodeSandbox

列表一:
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

参考文档:

https://facebook.github.io/react/docs/reconciliation.htm

关于本文

作者:@黄琼
原文:
https://zhuanlan.zhihu.com/p/43566956

react渲染原理深度解析的更多相关文章

  1. java8Stream原理深度解析

    Java8 Stream原理深度解析 Author:Dorae Date:2017年11月2日19:10:39 转载请注明出处 上一篇文章中简要介绍了Java8的函数式编程,而在Java8中另外一个比 ...

  2. mysql索引原理深度解析

    mysql索引原理深度解析 一.总结 一句话总结: mysql索引是b+树,因为b+树在范围查找.节点查找等方面优化 hash索引,完全平衡二叉树,b树等 1.数据库中最常见的慢查询优化方式是什么? ...

  3. 【React】393 深入了解React 渲染原理及性能优化

    如今的前端,框架横行,出去面试问到框架是常有的事. 我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家. React 是什么 React是一个专注 ...

  4. React源码深度解析视频 某课网(完整版)

    <ignore_js_op> [课程介绍]:        React毫无疑问是前端界主流的框架,而框架本身就是热点.课程以讲解React实现原理为主,并在实现过程中讲解这么做的原因,带来 ...

  5. jsonp协议原理深度解析

    前言 今天在开发联调的过程中,需要跨域的获取数据,因为使用的jquery,当然使用dataType:'jsonp'就能够很easy的解决了.但是因为当时后端没有支持jsonp来访问,后来他在实现这个功 ...

  6. SQL注入原理深度解析

    本文转自:http://www.iii-soft.com/forum.php?mod=viewthread&tid=1613&extra=page%3D1 对于Web应用来说,注射式攻 ...

  7. 第1课:SQL注入原理深度解析

    对于Web应用来说,注射式攻击由来已久,攻击方式也五花八门,常见的攻击方式有SQL注射.命令注射以及新近才出现的XPath注射等等.本文将以SQL注射为例,在源码级对其攻击原理进行深入的讲解. 一.注 ...

  8. Vue双向数据绑定原理深度解析

    首先,什么是双向数据绑定?Vue是三大MVVM框架之一,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化. 在分析其原理和代码的时候,大家首先了解如下几个j ...

  9. 关于laravel5.5控制器方法参数依赖注入原理深度解析及问题修复

    在laravel5.5中,可以根据控制器方法的参数类型,自动注入一个实例化对象,极大提升了编程的效率,但是相比较与Java的SpringMVC框架,功能还是有所欠缺,使用起来还是不太方便,主要体现在方 ...

随机推荐

  1. JSTL标签总结备用

    前言 ========================================================================= JSTL标签库,是日常开发经常使用的,也是众多 ...

  2. Halcon标定与自标定

    Halcon标定:https://blog.csdn.net/niyintang/article/details/78752585 Halcon自标定:https://www.cnblogs.com/ ...

  3. TangoAreaDescriptionMetaData区域描述元数据

    TangoAreaDescriptionMetaData com.google.atap.tangoservice Class TangoAreaDescriptionMetaData java.la ...

  4. T4模板调用反射

    <#@ template debug="false" hostspecific="true" language="C#" #> ...

  5. Middleware / 中间件

    中间件格式 module.exports = options => { return (ctx, next) => { // do something } } 中间件格式为一个高阶函数,外 ...

  6. 16、Semantic-UI之模态窗口

    16.1 定义模态窗口 示例:定义基础的模态窗口 <!DOCTYPE html> <html lang="en"> <head> <met ...

  7. java学习(三)数组

    一维数组的定义格式: int[] a;  //定义一个int类型的数组a变量 int a[];  //定义一个int类型的a数组变量 初始化一个int类型的数组 int[]   arr = new i ...

  8. [LeetCode 题解]:Candy

    There are N children standing in a line. Each child is assigned a rating value. You are giving candi ...

  9. [LeetCode 题解]: First Missing Positive

    Given an unsorted integer array, find the first missing positive integer. For example,Given [1,2,0]  ...

  10. RobotFramework与Jenkins集成后失败用例重跑

    Jenkins的执行Windows批处理命令填写如下: call pybot.bat -i 1adsInterface 01_测试用例\接口测试用例\adsInterface.txt call pyb ...