前言
相信很多人跟我之前一样,看到源码
两个字觉得触不可及,觉得离自己还很遥远,是需要非常多年的工作经验的大佬才能触及到的领域。就在去年我改变了这个想法,当时被react
的几个生命周期执行顺序弄的睡不着觉,为什么有些时候生命周期的执行事与愿违?又为什么数组中必须要加上key
属性?为啥在render
中不能写setState
等等问题…在一系列的问题中,我终于还是打开了那份久违的源码,并且Ctrl + F
慢慢探索了起来。
直到今天,趁着二季度业务结束忙里偷闲总结出这份不看源码也能让你看懂的渲染原理。因为有些地方需要承上启下,所以文本分为两大部分讲解,一部分是首次挂载渲染原理,另一部分是更新和卸载原理,很多地方非常抽象,希望大家仔细阅读,不然容易脱节。废话不多话,开车!!
正文
在开始之前,需要一些前置知识才能帮助我们更好的理解整个渲染过程。首先就是生命周期(16版本之后
),为什么要讲一下生命周期?跟渲染原理有关系吗?当然有,如果你不理解渲染原理的话,更新一个嵌套很深的组件你甚至连父与子生命周期执行的先后顺序都不知道。本文直接对照16版本之后的新生命周期进行讲解,就不讲解老版本了。
初探-生命周期
顾名思义,跟人生一样,生命周期就是一个组件从诞生到销毁的过程。React
在组件的生命周期中注册了一系列的钩子函数,支持开发者在其中注入代码,并在适当的时机运行。这里指的生命周期仅针对于类组件中的钩子函数。因为生命周期不是本文的重点,所以Hooks
中的新增的钩子函数在本文中均不涉及,可以以后出个Hooks
原理篇。
从图中可以看到,我把生命周期分为了挂载阶段、更新阶段、卸载阶段三个阶段。同时,在挂载阶段和更新阶段都会运行getDerivedStateFromProps
和render
,卸载阶段很好理解,只有一个componentWillUnMount
,在卸载组件之前做一些事情,通常用来清除定时器等副作用操作。那么挂载阶段和更新阶段中的生命周期我们来逐一看下每个运行点及作用。
constructor
在同一个类组件对象只会运行一次。所以经常来做一些初始化的操作。同一个组件对象被多次创建,它们的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);
}
静态属性 static getDerivedStateFromProps
该方法是一个静态属性,在16版本之前不存在,在新版生命周期中主要用来取代componentWillMount
和componentWillReceiveProps
,因为这两个老生命周期方法在一些开发者不规范的使用下极容易产生一些反模式的bug
。因为是静态方法,所以你在其中根本拿不到this
,更不可能调用setState
。
该方法在挂载阶段和更新阶段都会运行。它有两个参数props
和state
当前的属性值和状态。它的返回值会合并掉当前的状态(state
)。 如果返回了非Object的值,那么它啥都不会做,如果返回的是Object,那么它将会跟当前的状态合并,可以理解为Object.assign。通常情况下,几乎不怎么使用该方法。
/**
* 静态方法,首次挂载和更新渲染都会运行该方法
* @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}</>
)
}
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;
}
getSnapshotBeforeUpdate
如果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的第三个参数";
}
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(初始元素)
先来认识下第一个概念,就是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 vDom(虚拟节点)
前面提到React
在渲染过程中要做很多事情,所以不可能直接通过初始元素直接渲染。还需要一个东西就是虚拟节点。在本文中不涉及React Fiber
的概念,将vDom
树和Fiber
树统称为虚拟节点。有了初始元素后,React
就会根据初始元素和其他可以生成虚拟节点的东西生成虚拟节点。请记住:React一定是通过虚拟节点来进行渲染的。 接下来就是重点,除了初始元素能生成虚拟节点以外,还有哪些可能生成虚拟节点?总共有多少种节点类型?
DOM节点(ReactDomComponent)
此
DOM
非彼DOM
,这里的DOM
指的是虚拟DOM
节点。当初始元素的type
属性为字符串的时候React
就会创建虚拟DOM
节点。例如我们前面使用jsx
直接书写的const B = <div></div>
。它的属性就是"div"
,可以打印出来看一下。
组件节点(ReactComposite)
当初始元素的
type
属性为函数或是类的时候,Reac
t就会创建虚拟组件节点。
文本节点(ReactTextNode)
顾名思义,直接书写字符串或者数字,
React
会创建为文本节点。比如我们可以直接用ReactDOM.render
方法直接渲染字符串或数字。
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
//root.render('一头猪'); //创建文本节点
root.render(123465); //创建文本节点
空节点(ReactEmpty)
我们平时写
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); //创建空节点
数组节点(ReactArrayNode)
什么?数组还能渲染?当然不是直接渲染数组本身啦。当
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} //对象不能生成节点,所以会报错
]);
真实DOM(UI)
通过
document.createElement
创建的元素就是真实DOM
。了解完初始元素、虚拟节点以及真实DOM
这几个重要的概念后,就可以进入到原理的学习了。 再次强调:React
的工作是通过初始元素或可以生成虚拟节点的东西生成虚拟节点然后针对不同的节点类型去做不同的事情最终生成真实DOM
挂载到页面上!所以为什么对象不能直接被渲染,因为它生成不了虚拟节点。(实际上是ReactDOM
库进行渲染,为了减少混淆本文中就直接说React
)
首次渲染阶段
React
首先根据初始元素先生成虚拟节点,然后做了一系列操作后最终渲染成真实的UI
。生成虚拟节点的过程上面已经讲过了,所以这里说的是根据不同的虚拟节点它到底做了些什么处理。
初始元素-DOM节点
对于初始元素的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>
当然我们的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
都挂载到页面上。至此,首次渲染的阶段就全部结束了。
更新和卸载
挂载完成后组件进入活跃状态,等待数据的更新进行重新渲染。那么到底有几种场景会触发更新?整个过程又是怎么样的,有哪些需要注意的地方?
更新的场景
组件更新(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
步。
对比更新过程(diff)
知道了两个更新的场景以及会运行哪些生命周期方法后,我们来看一下具体的过程到底是怎么样的。所谓对比更新就是将新vDom
树跟之前首次渲染过程中保存的老vDom
树对比发现差异然后去做一系列操作的过程。那么问题来了,如果我们在一个类组件中重新渲染了,React
怎么知道在产生的新树中它的层级呢?难道是给vDom
树全部挂上一个不同的标识来遍历寻找更新的哪个组件吗?当然不是,我们都知道React
的diff
算法将之前的复杂度O(n^3)
降为了O(n)
。它做了以下几个假设:
- 假设此次更新的节点层级不会发生移动(直接找到旧树中的位置进行对比)。
- 兄弟节点之间通过
key
进行唯一标识。 - 如果新旧的节点类型不相同,那么它认为就是一个新的结构,比如之前是初始元素
div
现在变成了初始元素span
那么它会认为整个结构全部变了,无论嵌套了多深也会全部丢弃重新创建。
key的作用
如果前面copy
了文中的代码例子就会发现在使用数组节点的时候,如果里面有初始元素,并且没有给初始元素添加key
那么它会警告Warning: Each child in a list should have a unique "key" prop
.。那么key
值到底是干嘛用的呢?其实key
的作用非常简单,仅仅是为了通过旧节点,寻找对应的新节点进行对比提高节点的复用率。我们来举个例子,假如现在有五个兄弟节点更新后变成了四个节点。
tips:作者原文这里放了两张图,通过对比可以很清晰的对比出添加key
后节点复用的过程,不知道为啥gif
图片复制后是png
的静态图,故建议去这个链接看原文。
看完两张图会发现如果有key
的话在其他节点未变动的情况下复用了之前的所有节点。所以请尽量保持同一层级内key
的唯一性和稳定性。这就是为什么不要用Math.random
作为key
的原因,跟没写一样。
找到对比目标-节点类型一致
经过假设和一系列的操作找到了需要对比的目标,如果发现节点类型一致,那么它会根据不同的节点类型做不同的事情
- 初始元素-
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>
)
}
}
- 初始元素-组件节点
- 函数组件
如果是函数组件,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>
)
}
}
- 文本节点
对于文本节点,同样的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>
)
}
}
- 空节点
如果节点的类型都是空节点,那么React啥都不会做。 - 数组节点
首次挂载提到的,数组节点不会直接渲染。在更新阶段也一样,遍历每一项,进行对比更新,然后去做不同的事。
找到对比目标-节点类型不一致
如果找到了对比目标,但是发现节点类型不一致了,就如前面所说,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>
)
}
}
未找到对比目标
如果未找到对比的目标,跟节点类型不一致的做法类似,那么对于多出的节点进行挂载流程,对于旧节点进行卸载直接弃用。如果其包含子节点进行递归卸载。对于初始类组件节点会多一个步骤,那就是运行生命周期方法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>
)
}
}
从图中可以看到,哪怕经过条件渲染前后button
理论上没有任何变化的情况下,照样没有重用之前的真实DOM
,如果在button
之后还有一万个兄弟节点,那么也全部都找不到对比目标从而进行卸载重新创建流程。所以在进行条件渲染显示隐藏时,官方推荐以下做法:
- 控制
style:visibility
来控制显示隐藏。 - 在隐藏时给一个空节点来保证对比前后能找到同一位置。不影响后续兄弟节点的比较。
总结
对于生命周期我们只需关注比较重要的几个生命周期的运行点即可,比如render
的作用、使用componentDidMount
在挂载完真实DOM后做一些副作用操作、以及性能优化点shouldComponentUpdate
、还有卸载时利用componentWillUnmount
清除副作用。
对于首次挂载阶段,我们需要了解React
的渲染流程是:通过我们书写的初始元素和一些其他可以生成虚拟节点的东西来生成虚拟节点。然后针对不同的节点类型去做不同的事情,最终将真实DOM
挂载到页面上。然后执行渲染期间加入到队列的一些生命周期。然后组件进入到活跃状态。
对于更新卸载阶段,需要注意的是有几个更新的场景。以及key
的作用到底是什么。有或没有会产生多大的影响。还有一些小细节,比如条件渲染时,不要去破坏结构。尽量使用空节点来保持前后结构顺序的统一。重点是新旧两棵树的对比更新流程。找到目标,节点类型一致时针对不同的节点类型会做哪些事,类型不一致时会去卸载整个旧节点。无论有多少子节点,都会全部递归进行卸载。
到这里,文章所有的部分就全部结束了,本文没有涉及到一行源码,全部都是总结出能在不看源码的情况下能大致了解整个渲染流程