前言
相信很多人跟我之前一样,看到源码
两个字觉得触不可及,觉得离自己还很遥远,是需要非常多年的工作经验的大佬才能触及到的领域。就在去年我改变了这个想法,当时被react
的几个生命周期执行顺序弄的睡不着觉,为什么有些时候生命周期
的执行事与愿违?又为什么数组
中必须要加上key
属性?为啥在render
中不能写setState
等等问题…在一系列的问题中,我终于还是打开了那份久违的源码,并且Ctrl + F
慢慢探索了起来。
直到今天,趁着二季度业务结束忙里偷闲总结出这份不看源码也能让你看懂的渲染原理
。因为有些地方需要承上启下,所以文本分为两大部分讲解,一部分是首次挂载渲染
原理,另一部分是更新和卸载
原理,很多地方非常抽象,希望大家仔细阅读,不然容易脱节。废话不多话,开车!!
正文
在开始之前,需要一些前置知识才能帮助我们更好的理解整个渲染过程。首先就是生命周期(16版本之后)
,为什么要讲一下生命周期?跟渲染原理有关系吗?当然有,如果你不理解渲染原理的话,更新一个嵌套很深的组件你甚至连父与子
生命周期执行的先后顺序都不知道。本文直接对照16
版本之后的新生命周期
进行讲解,就不讲解老版本了。
初探-生命周期
顾名思义,跟人生一样,生命周期
就是一个组件从诞生
到销毁
的过程。React
在组件的生命周期
中注册了一系列的钩子函数
,支持开发者在其中注入代码,并在适当的时机运行。这里指的生命周期
仅针对于类组件
中的钩子函数
。因为生命周期
不是本文的重点,所以Hooks
中的新增的钩子函数
在本文中均不涉及,可以以后出个Hooks
原理篇。
从图中可以看到,我把生命周期
分为了挂载阶段
、更新阶段
、卸载阶段
三个阶段。同时,在挂载阶段
和更新阶段
都会运行getDerivedStateFromProps
和render
,卸载阶段很好理解,只有一个componentWillUnMount
,在卸载组件之前做一些事情,通常用来清除定时器等副作用操作
。那么挂载阶段
和更新阶段
中的生命周期我们来逐一看下每个运行点及作用。
1. constructor
在同一个类组件对象只会运行一次。所以经常来做一些初始化
的操作。同一个组件对象被多次创建,它们的construcotr
互不干扰。
注意:在construcotr
中要尽量避免(最好禁止)使用setState
。 我们都知道使用setState
会造成页面的重新渲染,但是在初始化
阶段,页面都还没有将真实DOM
挂载到页面上,那么重新渲染的又有什么意义呢。除异步
的情况,比如setInterval
中使用setState
是没问题的,因为在执行的时候页面早已渲染完成
。但也最好不要,容易一些引起奇怪的问题。
constructor(props) {super(props);this.state = {num: 1};//不可以,直接Warningthis.setState({num: this.state.num + 1});//可以使用,但不建议setInterval(()=>{this.setState({num: this.state.num + 1});}, 1000);}
2. 静态属性 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对象};}
3. render
最重要的生命周期
,没有之一。用来生成虚拟节点(vDom)
树。该方法只要遇到需要重新渲染都会运行。同样的,在render
中也严禁使用setState
,因为会导致无限递归
重新渲染导致爆栈
。
render() {//严禁使用!!!this.setState({num: 1})return (<>{this.state.num}</>)}
4. componentDidMount
该方法只会运行一次,在首次渲染
时页面将真实DOM
挂载完毕之后运行。通常在这里做一些异步操作
,比如开启定时器、发起网络请求、获取真实DOM
等。在该方法中,可以大胆使用setState
,因为页面已经渲染完成。执行完该钩子函数
后,组件正式进入到活跃
状态。
componentDidMount(){// 初始化或异步代码...this.setState({});setInterval(()=>{});document.querySelectorAll("div");}
5. 性能优化 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;}
6. 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的第三个参数";}
7. componentDidUpdate
该方法是更新阶段
最后运行的钩子函数
,跟getSnapshotBeforeUpdate
不同的是,它的运行时间点是在真实DOM
挂载到页面后。通常也会使用该方