相信很多人跟我之前一样,看到源码
两个字觉得触不可及,觉得离自己还很遥远,是需要非常多年的工作经验的大佬才能触及到的领域。就在去年我改变了这个想法,当时被react
的几个生命周期执行顺序弄的睡不着觉,为什么有些时候生命周期
的执行事与愿违?又为什么数组
中必须要加上key
属性?为啥在render
中不能写setState
等等问题......在一系列的问题中,我终于还是打开了那份久违的源码,并且Ctrl + F
慢慢探索了起来。
直到今天,趁着二季度业务结束忙里偷闲总结出这份不看源码也能让你看懂的渲染原理
。因为有些地方需要承上启下,所以文本分为两大部分讲解,一部分是首次挂载渲染
原理,另一部分是更新和卸载
原理,很多地方非常抽象,希望大家仔细阅读,不然容易脱节。废话不多话,开车!!
在开始之前,需要一些前置知识才能帮助我们更好的理解整个渲染过程。首先就是生命周期(16版本之后)
,为什么要讲一下生命周期?跟渲染原理有关系吗?当然有,如果你不理解渲染原理的话,更新一个嵌套很深的组件你甚至连父与子
生命周期执行的先后顺序都不知道。本文直接对照16
版本之后的新生命周期
进行讲解,就不讲解老版本了。
顾名思义,跟人生一样,生命周期
就是一个组件从诞生
到销毁
的过程。React
在组件的生命周期
中注册了一系列的钩子函数
,支持开发者在其中注入代码,并在适当的时机运行。这里指的生命周期
仅针对于类组件
中的钩子函数
。因为生命周期
不是本文的重点,所以Hooks
中的新增的钩子函数
在本文中均不涉及,可以以后出个Hooks
原理篇。
从图中可以看到,我把生命周期
分为了挂载阶段
、更新阶段
、卸载阶段
三个阶段。同时,在挂载阶段
和更新阶段
都会运行getDerivedStateFromProps
和render
,卸载阶段很好理解,只有一个componentWillUnMount
,在卸载组件之前做一些事情,通常用来清除定时器等副作用操作
。那么挂载阶段
和更新阶段
中的生命周期我们来逐一看下每个运行点及作用。
在同一个类组件对象只会运行一次。所以经常来做一些初始化
的操作。同一个组件对象被多次创建,它们的construcotr
互不干扰。
注意:在construcotr
中要尽量避免(最好禁止)使用setState
。 我们都知道使用setState
会造成页面的重新渲染,但是在初始化
阶段,页面都还没有将真实DOM
挂载到页面上,那么重新渲染的又有什么意义呢。除异步
的情况,比如setInterval
中使用setState
是没问题的,因为在执行的时候页面早已渲染完成
。但也最好不要,容易一些引起奇怪的问题。
constructor(props) {
super(props);
this.state = {
num: 1
};
//不可以,直接Warning
this.setState({
num: this.state.num + 1
});
//可以使用,但不建议
setInterval(()=>{
this.setState({
num: this.state.num + 1
});
}, 1000);
}
复制代码
截屏2022-07-13 下午2.09.07.png
该方法是一个静态属性
,在16
版本之前不存在,在新版生命周期
中主要用来取代componentWillMount
和componentWillReceiveProps
,因为这两个老生命周期
方法在一些开发者不规范的使用下极容易产生一些反模式
的bug。因为是静态方法
,所以你在其中根本拿不到this
,更不可能调用setState
。
该方法在挂载阶段
和更新阶段
都会运行。它有两个参数props
和state
当前的属性值
和状态
。它的返回值会合并掉当前的状态(state)
。如果返回了非Object
的值,那么它啥都不会做,如果返回的是Object
,那么它将会跟当前的状态合并,可以理解为Object.assign[1]。通常情况下,几乎不怎么使用该方法。
/**
* 静态方法,首次挂载和更新渲染都会运行该方法
* @param {*} props 当前属性
* @param {*} state 当前状态
*/
static getDerivedStateFromProps(props, state){
// return 1; //没用
return {
num: 999, //合并到当前state对象
};
}
复制代码
render
最重要的生命周期
,没有之一。用来生成虚拟节点(vDom)
树。该方法只要遇到需要重新渲染都会运行。同样的,在render
中也严禁使用setState
,因为会导致无限递归
重新渲染导致爆栈
。
render() {
//严禁使用!!!
this.setState({
num: 1
})
return (
<>{this.state.num}</>
)
}
复制代码
截屏2022-07-13 下午4.03.13.png
componentDidMount
该方法只会运行一次,在首次渲染
时页面将真实DOM
挂载完毕之后运行。通常在这里做一些异步操作
,比如开启定时器、发起网络请求、获取真实DOM
等。在该方法中,可以大胆使用setState
,因为页面已经渲染完成。执行完该钩子函数
后,组件正式进入到活跃
状态。
componentDidMount(){
// 初始化或异步代码...
this.setState({});
setInterval(()=>{});
document.querySelectorAll("div");
}
复制代码
shouldComponentUpdate
在原理图更新阶段
中可以看到,执行完static getDerivedStateFromProps
后,会执行该钩子函数
。该方法通常用来做性能优化
。它的返回值(boolean)
决定了是否要进行渲染更新
。该方法有两个参数nextProps
和nextState
表示此次更新(下一次)的属性
和状态
。通常我们会将当前值与此次要更新的值做比较来决定是否要进行重新渲染。
在React
中,官方给我们实现好了一个基础版的优化组件PureComponent
,就是一个HOC
高阶组件,内部实现就是帮我们用shouldComponentUpdate
做了浅比较优化。如果安装了React
代码提示的插件,我们可以直接使用rpc
+ tab键
来生成模版。注意:继承了PureComponent
后不需要再使用shouldComponentUpdate
进行优化。
/**
* 决定是否要进行重新渲染
* @param {*} nextProps 此次更新的属性
* @param {*} nextState 此次更新的状态
* @returns {boolean}
*/
shouldComponentUpdate(nextProps, nextState){
// 伪代码,如果当前的值和下一次的值相等,那么就没有更新渲染的必要了
if(this.props === nextProps && this.state === nextState){
return false;
}
return true;
}
复制代码
如果shouldComponentUpdate
返回是true
,那么就会运行render
重新生成虚拟DOM树
来进行对比更新,该方法运行在render
后,表示真实DOM
已经构建完成,但还没有渲染
到页面中。可以理解为更新前的快照
,通常用来做一些附加的DOM操作。
比如我突然想针对具有某个class
的真实元素做一些事情。那么就可以在此方法中获取元素并修改。该函数有两个参数prevProps
和prevState
表示此次更新前的属性
和状态
,该函数的返回值(snapshot)
会作为componentDidUpdate
的第三个参数。
/**
* 获取更新前的快照,通常用来做一些附加的DOM操作
* @param {*} prevProps 更新前的属性
* @param {*} prevState 更新前的状态
*/
getSnapshotBeforeUpdate(prevProps, prevState){
// 获取真实DOM在渲染到页面前做一些附加操作...
document.querySelectorAll("div").forEach(it=>it.innerHTML = "123");
return "componentDidUpdate的第三个参数";
}
复制代码
该方法是更新阶段
最后运行的钩子函数
,跟getSnapshotBeforeUpdate
不同的是,它的运行时间点是在真实DOM
挂载到页面后。通常也会使用该方法来操作一些真实DOM
。它有三个参数分别是prevProps
、prevState
、snapshot
,跟Snapshot钩子函数
一样,表示更新前的属性
、状态
、Snapshot
钩子函数的返回值。
/**
* 通常用来获取真实DOM做一些操作
* @param {*} prevProps 更新前的属性
* @param {*} prevState 更新前的状态
* @param {*} snapshot getSnapshotBeforeUpdate的返回值
*/
componentDidUpdate(prevProps, prevState, snapshot){
document.querySelectorAll("div").forEach(it=>it.innerHTML = snapshot);
}
复制代码
componentWillUnmount
如开头提到的,该钩子函数
属于卸载阶段中唯一的方法。如果组件在渲染
的过程中被卸载了,React
会报出Warning:Can't perform a React state update on an unmounted component
的警告,所以通常在组件被卸载时做清除副作用的操作
。
componentWillUnmount(){
// 组件被卸载前清理副作用...
clearInterval(timer1);
clearTimeout(timer2);
this.setState = () => {};
}
复制代码
到这里,React生命周期
中每一个钩子函数
的作用以及运行时间点就已经全部了解了,斯国一!等在下文中提到的时候也有一个大致的印象。大家可以先喝口水休息一下~
先来认识下第一个概念,就是React element
,what?当我伞兵?我还不知道什是element
?别激动,这里的元素不是指真实DOM
中的元素,而是通过React.createElement
创建的类似
真实DOM的元素。比如我们在开发中通过语法糖jsx
写出来的html
结构都是React element
,为了跟真实DOM
区分开来,本文就统称为React初始元素
。
为什么要有一个初始元素
的概念?我们都知道通过jsx
编写的html
不可能直接渲染
到页面上,肯定是经历了一系列的复杂
的处理最后生成真实DOM
挂载到页面上。那么到底是怎么样的一个过程?在我们认识一些概念之后才能更深入的理解整个过程。先看看平时写的代码哪些是初始元素
。
import React, { PureComponent } from 'react'
//创建的是React初始元素
const A = React.createElement("div");
//创建的是React初始元素
const B = <div>123</div>
export default class App extends PureComponent {
render() {
return (
//创建的是React初始元素
<div>
{A}
{B}
</div>
)
}
}
复制代码
前面提到React
在渲染过程中要做很多事情,所以不可能直接通过初始元素
直接渲染。还需要一个东西就是虚拟节点
。在本文中不涉及React Fiber
的概念,将vDom
树和Fiber
树统称为虚拟节点
。有了初始元素
后,React
就会根据初始元素
和其他可以生成虚拟节点的东西
生成虚拟节点
。请记住:React
一定是通过虚拟节点
来进行渲染的。 接下来就是重点,除了初始元素
能生成虚拟节点
以外,还有哪些可能生成虚拟节点
?总共有多少种节点
类型?
此DOM非彼DOM,这里的DOM指的是
虚拟DOM节点
。当初始元素的type
属性为字符串
的时候React
就会创建虚拟DOM节点
。例如我们前面使用jsx
直接书写的const B = <div></div>
。它的属性就是"div"
,可以打印出来看一下。
微信图片_20220714212514.png
当
初始元素
的type
属性为函数
或是类
的时候,React
就会创建虚拟组件节点
。
65463463.png
789789.png
顾名思义,直接书写
字符串
或者数字
,React
会创建为文本节点
。比如我们可以直接用ReactDOM.render
方法直接渲染字符串
或数字
。
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
//root.render('一头猪'); //创建文本节点
root.render(123465); //创建文本节点
复制代码
我们平时写
React
代码的时候经常会写三目表达式{this.state.xxx ? <App /> : false}
用来进行条件渲染,只知道为false
就不会渲染,那么到底是怎么一回事?其实遇到字面量null
、false
、true
、undefined
在React
中均会被创建为一个空节点
。在渲染过程中,如果遇到空节点
,那么它将什么都不会做。
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
//root.render(false); //创建空节点
//root.render(true); //创建空节点
//root.render(null); //创建空节点
root.render(undefined); //创建空节点
复制代码
什么?
数组
还能渲染?当然不是直接渲染
数组本身啦。当React
遇到数组
时,会创建数组节点
。但是不会直接进行渲染
,而是将数组里的每一项拿出来,根据不同的节点类型
去做相应的事情。**所以数组
里的每一项只能是这里提到的五个节点类型
**。不信?那放个对象试试。
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
function FuncComp(){
return (
<div>组件节点-Function</div>
)
}
class ClassComp extends React.Component{
render(){
return (
<div>组件节点-Class</div>
)
}
}
root.render([
<div>DOM节点</div>, //创建虚拟DOM节点
<ClassComp />, //创建组件节点
<FuncComp />, //创建组件节点
false, //创建空节点
"文本节点", //创建文本节点
123456, //创建文本节点
[1,2,3], //创建数组节点
// {name: 1} //对象不能生成节点,所以会报错
]);
复制代码
微信图片_20220714225738.png
通过
document.createElement
创建的元素就是真实DOM
。了解完初始元素
、虚拟节点
以及真实DOM
这几个重要的概念后,就可以进入到原理
的学习了。再次强调:React
的工作是通过初始元素或可以生成虚拟节点的东西
生成虚拟节点
然后针对不同的节点类型
去做不同的事情最终生成真实DOM
挂载到页面上!所以为什么对象不能直接被渲染
,因为它生成不了虚拟节点
。(实际上是ReactDOM
库进行渲染,为了减少混淆本文中就直接说React
)
渲染原理.gif
如上图所示,React
首先根据初始元素
先生成虚拟节点
,然后做了一系列操作后最终渲染成真实的UI
。生成虚拟节点
的过程上面已经讲过了,所以这里说的是根据不同的虚拟节点
它到底做了些什么处理。
对于初始元素
的type
属性为字符串时,React会通过document.createElement
创建真实DOM
。因为初始元素
的type
为字符串,所以直接会根据type
属性创建不同的真实DOM
。创建完真实DOM
后会立即设置该真实DOM
的所有属性
,比如我们直接在jsx
中可以直接书写的className
、style
等等都会作用到真实DOM
上。
//jsx语法:React初始元素
const B = <div className="wrapper" style={{ color: "red" }}>
<p className="text">123</p>
</div>
复制代码
1657855131681.jpg
222222222222.jpg
当然我们的html结构
肯定不止一层,所以在设置完属性后React
会根据children
属性进行递归遍历
。根据不同的节点类型
去做不同的事情,同样的,如果children
是初始元素
,创建真实DOM
、设置属性、然后检查是否有子元素。重复此步骤,一直到最后一个元素为止。遇到其他节点类型
会做以下事情。⬇️
前面提到的,如果初始元素
的type
属性是一个class类
或者function函数
时,那么会创建一个组件节点
。所以针对类
或函数
组件,它的处理是不同的。
对于函数组件
会直接调用函数,将函数的返回值
进行递归处理(看看是什么节点类型
,然后去做对应的事情,所以一定要返回能生成虚拟节点
的东西),最终生成一颗vDOM
树。
对于类组件
而言会相对麻烦一些。但前面有了生命周期
的铺垫,结合图中挂载阶段
来看这里理解起来就很方便了。
实例
(调用constructor
)。生命周期
方法static getDerivedStateFromProps
。生命周期
方法render
,根据返回值
递归处理。跟函数组件处理返回值
一样,最终生成一颗vDom
树。生命周期
方法componentDidMount
加入到执行队列
中等待真实DOM挂载到页面后执行(注意:前面说了render
是一个递归处理,所以如果一个组件存在父子
关系的时候,那么肯定要等子组件
渲染完父组件
才能走出render
,所以子组件
的componentDidMount
一定是比父组件先入队列
的,肯定先运行!)。针对文本节点
,会直接通过document.createTextNode
创建真实
的文本节点。
如果生成的是空节点
,那么它将什么都不会做
!对,就是那么简单,啥都不做。
就像前面提到的一样,React
不会直接渲染数组,而是将里面的每一项
拿出来遍历,根据不同的节点类型
去做不同的事,直到递归
处理完数组里的每一项。(这里留个问题,为什么在数组
里我们要写key
?)
当处理完了所有的节点
后,我们的vDom
树和真实DOM
也创建好了,React
会将vDom
树保存起来,方便后续使用。然后将创建好的真实DOM
都挂载到页面上。至此,首次渲染
的阶段就全部结束了。有点懵?没事,正常,我们举个例子。
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
/**
* 组件节点-类组件
*/
class ClassSon extends React.Component {
constructor(props){
super(props);
console.log("444 ClassSon constructor");
}
static getDerivedStateFromProps(props, state){
console.log("555 ClassSon getDerivedStateFromProps");
return {};
}
componentDidMount(){
console.log("666 ClassSon componentDidMount");
}
render() {
return (
<div className="func-wrapper">
<span>
textNode22
{undefined}
</span>
{[false, "textNode33", <div>textNode44</div>]}
</div>
)
}
}
/**
* 组件节点-类组件
*/
class ClassComp extends React.Component {
constructor(props){
super(props);
console.log("111 ClassComp constructor");
}
static getDerivedStateFromProps(props, state){
console.log("222 ClassComp getDerivedStateFromProps");
return {};
}
componentDidMount(){
console.log("333 ClassComp componentDidMount");
}
render() {
return (
<div className="class-wrapper">
<ClassSon />
<p>textNode11</p>
{123456789}
</div>
)
}
}
root.render(<ClassComp />);
复制代码
从代码结构来看,渲染的是ClassComp
类组件,类组件内包含了一个函数组件
以及一些其他可以生成虚拟节点
的东西,同样的,函数组件
内也是一些可以生成虚拟节点
的结构。因为用图表示比较复杂,时间可能会有点久(gif很大已压缩...,显示有点小的话麻烦右键新标签打开
看好了)
123123.gif
从图中可以看到,在ClassComp
首次挂载运行render
的过程中,发现了ClassSon
组件,然后又开始了一个新的类组件
节点的渲染过程。要等到ClassSon
和其他兄弟节点渲染完后ClassComp
的render
才算完成。所以ClassSon
的componentDidMount
一定是先进队列的。所以控制台执行顺序一定是111
、222
、444
、555
、666
、333
。到这里,首次挂载
的所有过程就结束了。再喝口水休息一下~
挂载完成后组件进入活跃
状态,等待数据的更新进行重新渲染。那么到底有几种场景会触发更新?整个过程又是怎么样的,有哪些需要注意的地方?
setState
)最常见的,我们经常用setState
来重新设置组件的状态
进行重新渲染(本文不涉及Hooks
概念,不讲useState
)。使用setState
只会更新调用此方法的类。不会涉及到兄弟节点以及父级节点。影响范围仅仅是自己的子节点
。结合文章最前面的生命周期
图看,步骤如下:
生命周期
静态方法static getDerivedStateFromProps
。根据返回值合并当前组件的状态。生命周期
方法shouldComponentUpdate
。如果该方法返回的false
。直接终止更新流程!生命周期
方法render
,得到一个新的vDom
树,进入新旧两棵树的对比更新
。生命周期
方法getSnapshotBeforeUpdate
加入执行队列,等待将来执行。生命周期
方法componentDidUpdate
加入执行队列,等待将来执行。vDom
树。vDom
树更新真实DOM
.生命周期
方法componentDidMount
。生命周期
方法getSnapshotBeforeUpdate
。生命周期
方法componentDidUpdate
。生命周期
方法componentWillUnMount
。ReactDOM.createRoot().render
)在ReactDOM
的新版本中,已经不是直接使用ReactDOM.render
进行更新了,而是通过createRoot(要控制的DOM区域)
的返回值来调用render
,无论我们在嵌套多少的组件里去调用控制区域.render
,都会直接触发根节点
的对比更新
。一般不会这么操作。如果触发了根节点的更新,那么后续步骤是上面组件更新
的6-11
步。
知道了两个更新的场景以及会运行哪些生命周期
方法后,我们来看一下具体的过程到底是怎么样的。所谓对比更新
就是将新vDom
树跟之前首次渲染过程中保存的老vDom
树对比发现差异然后去做一系列操作的过程。那么问题来了,如果我们在一个类组件
中重新渲染了,React
怎么知道在产生的新树中它的层级呢?难道是给vDom
树全部挂上一个不同的标识来遍历寻找更新的哪个组件吗?当然不是,我们都知道React
的diff
算法将之前的复杂度O(n^3)
降为了O(n)
。它做了以下几个假设:
key
进行唯一标识。节点类型
不相同,那么它认为就是一个新的结构,比如之前是初始元素div
现在变成了初始元素span
那么它会认为整个结构全部变了,无论嵌套了多深也会全部丢弃
重新创建。如果前面copy了文中的代码例子就会发现在使用数组节点
的时候,如果里面有初始元素
,并且没有给初始元素
添加key
那么它会警告Warning: Each child in a list should have a unique "key" prop.
。那么key
值到底是干嘛用的呢?其实key
的作用非常简单,仅仅是为了通过旧节点
,寻找对应的新节点
进行对比提高节点
的复用率。我们来举个例子,假如现在有五个兄弟节点
更新后变成了四个节点
。
未添加key
渲染原理.gif
添加了key
渲染原理.gif
看完两张图会发现如果有key
的话在其他节点
未变动的情况下复用了之前的所有节点
。所以请尽量保持同一层级内key
的唯一性
和稳定性
。这就是为什么不要用Math.random
作为key
的原因,跟没写一样。
经过假设和一系列的操作找到了需要对比的目标,如果发现节点类型
一致,那么它会根据不同的节点类型做不同的事情。
1. 初始元素-DOM节点
如果是DOM节点
,React
会直接重用之前的真实DOM
。将这次变化的属性
记录下来,等待将来完成更新。然后遍历其子节点
进行递归对比更新
。
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = {
flag: true
}
render() {
console.log("render了");
return (
<div className={this.state.flag ? "wrapper" : "flagFlase"}>
<button onClick={()=>{
this.setState({
flag: !this.state.flag
});
console.log("属性名变了吗现在?", document.querySelector(".wrapper").className);
}}>更新</button>
</div>
)
}
}
复制代码
截屏2022-07-17 上午1.00.26.png
2. 初始元素-组件节点
如果是函数组件
,React
仅仅是重新调用函数
拿到新的vDom
树,然后递归进行对比更新
。
针对类组件
,React
也会重用之前的实例对象
。后续步骤如下:
1 . 运行生命周期
静态方法static getDerivedStateFromProps
。将返回值合并当前状态。
2 . 运行生命周期
方法shouldComponentUpdate
,如果该方法返回false
,终止当前流程。
3 . 运行生命周期
方法render
,得到新的vDom
树,进行新旧两棵树的递归对比更新
。
4 . 将生命周期
方法getSnapshotBeforeUpdate
加入到队列等待执行。
5 . 将生命周期
方法componentDidUpdate
加入到队列等待执行。
import React, {Component} from 'react'
export default class App extends Component {
static getDerivedStateFromProps(props, state){
console.log("111 getDerivedStateFromProps");
return {};
}
shouldComponentUpdate(){
console.log("222 shouldComponentUpdate");
return true;
}
getSnapshotBeforeUpdate(){
console.log("444 getSnapshotBeforeUpdate");
return null;
}
componentDidUpdate(){
console.log("555 getSnapshotBeforeUpdate")
}
render() {
console.log("333 render");
return (
<div className={"wrapper"}>
<button onClick={()=>{
this.setState({});
}}>更新</button>
</div>
)
}
}
复制代码
截屏2022-07-17 上午1.27.37.png
3. 文本节点
对于文本节点,同样的React
也会重用之前的真实文本节点
。将新的文本记录下来,等待将来统一更新(设置nodeValue
)。
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = {
text: "文本节点"
}
render() {
return (
<div className="wrapper">
{this.state.text}
<button onClick={()=>{
this.setState({
text: "新文本节点"
})
}}>更新</button>
</div>
)
}
}
复制代码
截屏2022-07-17 上午1.40.31.png
4. 空节点
如果节点的类型都是空节点
,那么React
啥都不会做。
5. 数组节点
首次挂载提到的,数组节点
不会直接渲染。在更新阶段也一样,遍历每一项,进行对比更新
,然后去做不同的事。
如果找到了对比目标,但是发现节点类型
不一致了,就如前面所说,React
会认为你连类型都变了,那么你的子节点
肯定也都不一样了,就算一万个
子节点,并且他们都是没有变化的,只有最外层的父节点
的节点类型
变了,照样会全部进行卸载
重新创建,与其去一个个递归查看子节点
,不如直接全部卸载
重新新建。
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = {
flag: true,
}
render() {
console.log("重新渲染render");
if (this.state.flag) {
return <span className="wrapper">
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</span>
}
return (
<div className="wrapper">
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</div>
)
}
}
复制代码
截屏2022-07-17 下午7.42.18.png
渲染原理.gif
如果未找到对比的目标,跟节点类型
不一致的做法类似,那么对于多出的节点进行挂载流程
,对于旧节点进行卸载直接弃用。如果其包含子节点进行递归卸载
。对于初始类组件节点
会多一个步骤,那就是运行生命周期
方法componentWillUnmount
。注意:尽量保持结构的稳定性,如果未添加key
的情况下,兄弟节点更新位置前后错位一个那么后续全部的比较都会错位
导致找不到对比目标从而进行卸载
新建流程,对性能大打折扣。
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = {
flag: true,
}
render() {
console.log("重新渲染render");
if (this.state.flag) {
return <div className="wrapper">
<span>123</span>
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</div>
}
return (
<div className="wrapper">
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</div>
)
}
}
复制代码
截屏2022-07-17 下午8.03.52.png
渲染原理.gif
从图中可以看到,哪怕经过条件渲染前后button
理论上没有任何变化的情况下,照样没有重用之前的真实DOM
,如果在button
之后还有一万个
兄弟节点,那么也全部都找不到对比目标从而进行卸载
重新创建流程。所以在进行条件渲染
显示隐藏时,官方推荐以下做法:
1 . 控制style:visibility
来控制显示隐藏。
2 . 在隐藏时给一个空节点
来保证对比前后能找到同一位置。不影响后续兄弟节点
的比较。
this.state.flag ? <div></div> : false
复制代码
1. 是否重用了真实DOM
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = {
flag: true,
}
render() {
console.log("重新render!");
if(this.state.flag){
return <div className="flag-true">
<button onClick={()=>{
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</div>
}
return (
<div className="flag-false">
<button onClick={()=>{
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</div>
)
}
}
复制代码
截屏2022-07-17 下午5.30.08.png
尽管从代码结构看起来像是返回了两个不同的DOM
,但其实在更新的过程中,React
发现他们的节点类型
一致,所以会重用之前的真实DOM
。所以请注意:尽量保持节点的类型
一致,如果更新前后节点类型
不一致的话无论有多少子组件将全部卸载
重新创建。
渲染原理.gif
2. 一个神奇的效果
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = { flag: false }
render() {
return (
<>
{
this.state.flag ?
<div>
<input type="password" />
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>显示/隐藏</button>
</div>
:
<div>
<input type="password" />
<input type="text" />
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>显示/隐藏</button>
</div>
}
</>
)
}
}
复制代码
渲染原理.gif
从图中可以看到,我们输入了密码后,重新渲染
生成了新的DOM,但是里面的密码还存在。这就很好的证明了React
是如何重用真实DOM
的。
一道面试题
import React, { PureComponent } from 'react'
class ClassCompA extends PureComponent {
componentDidMount() {
console.log("111 ClassCompA componentDidMount");
}
componentWillUnmount() {
console.log("222 ClassCompA componentWillUnmount");
}
render() {
return (<div className="ClassCompA"></div>)
}
}
class ClassCompB extends PureComponent {
componentDidMount() {
console.log("333 ClassCompB componentDidMount");
}
render() {
return (<div className="ClassCompB">
<ClassCompC />
</div>)
}
}
class ClassCompC extends PureComponent {
componentDidMount() {
console.log("444 ClassCompC componentDidMount");
}
render() {
return (<div className="ClassCompC"></div>)
}
}
export default class App extends PureComponent {
state = {
flag: true,
}
componentDidMount(){
console.log("666 App componentDidMount");
}
componentDidUpdate() {
console.log("555 App componentDidUpdate");
}
render() {
return (
<div className="wrapper">
{this.state.flag ? <ClassCompA/> : <ClassCompB/>}
<button onClick={() => {
this.setState({
flag: !this.state.flag
})
}}>更新</button>
</div>
)
}
}
复制代码
问:首次渲染和按下button控制台输出的顺序是什么?
看的仔细的同学,相信根本就难不倒你,我们一起来捋一捋。
App
,所以开始App
的挂载流程,运行render
的过程中发现条件渲染
先渲染ClassCompA
。ClassCompA
的挂载流程,没啥好渲染的就一个div,执行完render
后将componentDidMount
加入到队列中等待执行。此时队列里是[111]
。App
再针对初始元素button
做处理后,render
执行结束,将自己的componentDidMount
加入到队列中等待执行,此时队列里是[111、666]
。React
根据虚拟节点
生成真实DOM
后,保存vDom
树,开始运行队列。此时控制台打印111
、666
。button
后,调用setState
进行重新渲染,此时App
还会运行两个生命周期方法 getDerivedStateFromProps
和shouldComponentUpdate
,然后运行render
,生成新的vDom
树。对比更新
,虽然都是组件节点
,但生成出的实例不同,认为是不相同的节点类型
。开始卸载旧节点ClassCompA
,并将ComponentWillUnMount
加入到执行队列,等待执行。此时队列[222]
。ClassCompB
实例,调用render
生成虚拟节点
。发现存在组件节点ClassCompC
。再次进入到新节点挂载流程,创建实例。ClassComC
运行完render
生成vDom
树,将自己的componentDidMount
加入到队列,等待将来执行。此时队列[222、444]
。ClassComC
后,ClassComB
的render
才算结束,此时将自己的componentDidMount
加入到队列,等待执行,此时队列[222、444、333]
。App
的render
才算结束,将自己的componentDidUpdate
加入到队列,等待执行。此时队列[222、444、333、555]
。虚拟节点
生成的真实DOM
挂载到页面上后,开始执行队列。控制台输出222
、444
、333
、555
。对于生命周期
我们只需关注比较重要的几个生命周期的运行点即可,比如render
的作用、使用componentDidMount
在挂载完真实DOM
后做一些副作用操作、以及性能优化点shouldComponentUpdate
、还有卸载时利用componentWillUnmount
清除副作用。
对于首次挂载
阶段,我们需要了解React
的渲染流程是:通过我们书写的初始元素
和一些其他可以生成虚拟节点的东西
来生成虚拟节点
。然后针对不同的节点类型去做不同的事情,最终将真实DOM
挂载到页面上。然后执行渲染期间加入到队列的一些生命周期
。然后组件进入到活跃状态。
对于更新卸载
阶段,需要注意的是有几个更新的场景
。以及key
的作用到底是什么。有或没有会产生多大的影响。还有一些小细节,比如条件渲染
时,不要去破坏结构。尽量使用空节点
来保持前后结构顺序的统一。重点是新旧两棵树的对比更新流程
。找到目标,节点类型一致时针对不同的节点类型
会做哪些事,类型不一致时会去卸载
整个旧节点。无论有多少子节点,都会全部递归
进行卸载。
到这里,文章所有的部分就全部结束了,本文没有涉及到一行源码,全部都是总结出能在不看源码的情况下能大致了解整个渲染流程
。为了减少混淆,也没有涉及到Hooks
以及Fiber
的概念,有兴趣的同学可以留言,可以考虑下次出一篇。最后,再喝一口水休息一下。对本文内容有异议或交流欢迎评论~
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/i_xkJaT1C-MDt6XKNwWW7A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。