# React 生命周期
React 类组件为开发者提供了一些生命周期钩子函数,能让开发者在 React 执行的重要阶段,在钩子函数里做一些该做的事。自从 React Hooks 问世以来,函数组件也能优雅地使用 Hooks ,弥补函数组件没有生命周期的缺陷。
# 类组件生命周期
React 两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历 React fiber 树,目的就是发现不同( diff ),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行 render 函数。在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。
如果在一次调和的过程中,发现了一个 fiber tag = 1 类组件的情况,就会按照类组件的逻辑来处理。对于类组件的处理逻辑,首先判断类组件是否已经被创建过
// react-reconciler/src/ReactFiberBeginWork.js
/* workloop React 处理类组件的主要功能方法 */
function updateClassComponent(){
let shouldUpdate
const instance = workInProgress.stateNode // stateNode 是 fiber 指向 类组件实例的指针。
if (instance === null) { // instance 为组件实例,如果组件实例不存在,证明该类组件没有被挂载过,那么会走初始化流程
constructClassInstance(workInProgress, Component, nextProps); // 组件实例将在这个方法中被new。
mountClassInstance( workInProgress,Component, nextProps,renderExpirationTime ); //初始化挂载组件流程
shouldUpdate = true; // shouldUpdate 标识用来证明 组件是否需要更新。
}else{
shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderExpirationTime) // 更新组件流程
}
if(shouldUpdate){
nextChildren = instance.render(); /* 执行render函数 ,得到子节点 */
reconcileChildren(current,workInProgress,nextChildren,renderExpirationTime) /* 继续调和子节点 */
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
几个重要概念:
- instance 类组件对应实例。
- workInProgress 树,当前正在调和的 fiber 树 ,一次更新中,React 会自上而下深度遍历子代 fiber ,如果遍历到一个 fiber ,会把当前 fiber 指向 workInProgress。
- current 树,在初始化更新中,current = null ,在第一次 fiber 调和之后,会将 workInProgress 树赋值给 current 树。React 来用 workInProgress 和 current 来确保一次更新中,快速构建,并且状态不丢失。
- Component 就是项目中的 class 组件。
- nextProps 作为组件在一次更新中新的 props 。
- renderExpirationTime 作为下一次渲染的过期时间。
在组件实例上可以通过 _reactInternals 属性来访问组件对应的 fiber 对象。在 fiber 对象上,可以通过 stateNode 来访问当前 fiber 对应的组件实例。
# React 类组件生命周期执行过程
React 的大部分生命周期的执行,都在 mountClassInstance 和updateClassInstance 这两个方法中执行,把流程简化成 mount (初始化渲染) 和 update (更新)两个方向。从组件初始化,组件更新 , 组件销毁 ,三大阶段分析。
# 初始化阶段
# constructor 执行
在 mount 阶段,首先执行的 constructClassInstance 函数,用来实例化 React 组件,组件中 constructor 就是在这里执行的。在实例化组件之后,会调用 mountClassInstance 组件初始化。
function mountClassInstance(workInProgress,ctor,newProps,renderExpirationTime){
const instance = workInProgress.stateNode;
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') { /* ctor 就是我们写的类组件,获取类组件的静态方法 */
const partialState = getDerivedStateFromProps(nextProps, prevState); /* 这个时候执行 getDerivedStateFromProps 生命周期 ,得到将合并的state */
const memoizedState = partialState === null || partialState === undefined ? prevState : Object.assign({}, prevState, partialState); // 合并state
workInProgress.memoizedState = memoizedState;
instance.state = workInProgress.memoizedState; /* 将state 赋值给我们实例上,instance.state 就是我们在组件中 this.state获取的state*/
}
if(typeof ctor.getDerivedStateFromProps !== 'function' && typeof instance.getSnapshotBeforeUpdate !== 'function' && typeof instance.componentWillMount === 'function' ){
instance.componentWillMount(); /* 当 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 不存在的时候 ,执行 componentWillMount*/
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# getDerivedStateFromProps 执行
在初始化阶段,getDerivedStateFromProps 是第二个执行的生命周期,值得注意的是它是从 ctor 类上直接绑定的静态方法,传入 props ,state 。 返回值将和之前的 state 合并,作为新的 state ,传递给组件实例使用。
# componentWillMount 执行
如果存在 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 就不会执行生命周期componentWillMount。
# render 函数执行
到此为止 mountClassInstancec 函数完成,但是上面 updateClassComponent 函数, 在执行完 mountClassInstancec 后,执行了 render 渲染函数,形成了 children , 接下来 React 调用 reconcileChildren 方法深度调和 children 。
# componentDidMount执行
细心的同学可能发现,生命周期 componentDidMount 还没有出现,那么 componentDidMount 是如何执行的呢?上文中简单介绍了 render 和 commit 两个阶段,上述提及的几生命周期都是在 render 阶段执行的。一旦 React 调和完所有的 fiber 节点,就会到 commit 阶段,在组件初始化 commit 阶段,会调用 componentDidMount 生命周期。
// react-reconciler/src/ReactFiberCommitWork.js
function commitLifeCycles(finishedRoot,current,finishedWork){
switch (finishedWork.tag){ /* fiber tag 在第一节讲了不同fiber类型 */
case ClassComponent: { /* 如果是 类组件 类型 */
const instance = finishedWork.stateNode /* 类实例 */
if(current === null){ /* 类组件第一次调和渲染 */
instance.componentDidMount()
}else{ /* 类组件更新 */
instance.componentDidUpdate(prevProps,prevState,instance.__reactInternalSnapshotBeforeUpdate);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
从上面可以直观看到 componentDidMount 执行时机 和 componentDidUpdate 执行时机是相同的 ,只不过一个是针对初始化,一个是针对组件再更新。到此初始化阶段,生命周期执行完毕。
执行顺序:constructor -> getDerivedStateFromProps / componentWillMount -> render -> componentDidMount
# 更新阶段
类组件的更新阶段,到底会执行那些生命周期函数呢,回到了最开始 updateClassComponent 函数了,当发现 current 不为 null 的情况时,说明该类组件被挂载过,那么直接按照更新逻辑来处理。
// react-reconciler/src/ReactFiberClassComponent.js
function updateClassInstance(current,workInProgress,ctor,newProps,renderExpirationTime){
const instance = workInProgress.stateNode; // 类组件实例
const hasNewLifecycles = typeof ctor.getDerivedStateFromProps === 'function' // 判断是否具有 getDerivedStateFromProps 生命周期
if(!hasNewLifecycles && typeof instance.componentWillReceiveProps === 'function' ){
if (oldProps !== newProps || oldContext !== nextContext) { // 浅比较 props 不相等
instance.componentWillReceiveProps(newProps, nextContext); // 执行生命周期 componentWillReceiveProps
}
}
let newState = (instance.state = oldState);
if (typeof getDerivedStateFromProps === 'function') {
ctor.getDerivedStateFromProps(nextProps,prevState) /* 执行生命周期getDerivedStateFromProps ,逻辑和mounted类似 ,合并state */
newState = workInProgress.memoizedState;
}
let shouldUpdate = true
if(typeof instance.shouldComponentUpdate === 'function' ){ /* 执行生命周期 shouldComponentUpdate 返回值决定是否执行render ,调和子节点 */
shouldUpdate = instance.shouldComponentUpdate(newProps,newState,nextContext,);
}
if(shouldUpdate){
if (typeof instance.componentWillUpdate === 'function') {
instance.componentWillUpdate(); /* 执行生命周期 componentWillUpdate */
}
}
return shouldUpdate
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 执行生命周期 componentWillReceiveProps
首先判断 getDerivedStateFromProps 生命周期是否存在,如果不存在就执行componentWillReceiveProps生命周期。传入该生命周期两个参数,分别是 newProps 和 nextContext 。
# 执行生命周期 getDerivedStateFromProps
接下来执行生命周期getDerivedStateFromProps, 返回的值用于合并state,生成新的state。
# 执行生命周期 shouldComponentUpdate
接下来执行生命周期shouldComponentUpdate,传入新的 props ,新的 state ,和新的 context ,返回值决定是否继续执行 render 函数,调和子节点。这里应该注意一个问题,getDerivedStateFromProps 的返回值可以作为新的 state ,传递给 shouldComponentUpdate 。
# 执行生命周期 componentWillUpdate
接下来执行生命周期 componentWillUpdate。updateClassInstance 方法到此执行完毕了。
# 执行 render 函数
接下来会执行 render 函数,得到最新的 React element 元素。然后继续调和子节点。
# 执行 getSnapshotBeforeUpdate
function commitBeforeMutationLifeCycles(current,finishedWork){
switch (finishedWork.tag) {
case ClassComponent:{
const snapshot = instance.getSnapshotBeforeUpdate(prevProps,prevState) /* 执行生命周期 getSnapshotBeforeUpdate */
instance.__reactInternalSnapshotBeforeUpdate = snapshot; /* 返回值将作为 __reactInternalSnapshotBeforeUpdate 传递给 componentDidUpdate 生命周期 */
}
}
}
2
3
4
5
6
7
8
getSnapshotBeforeUpdate
的执行也是在 commit 阶段,commit 阶段细分为 before Mutation
( DOM 修改前),Mutation ( DOM 修改),Layout( DOM 修改后) 三个阶段,getSnapshotBeforeUpdate
发生在 before Mutation
阶段,生命周期的返回值,将作为第三个参数 __reactInternalSnapshotBeforeUpdate
传递给 componentDidUpdate
。
# 执行 componentDidUpdate
接下来执行生命周期 componentDidUpdate ,此时 DOM 已经修改完成。可以操作修改之后的 DOM 。到此为止更新阶段的生命周期执行完毕。
更新阶段对应的生命周期的执行顺序:
componentWillReceiveProps( props 改变) / getDerivedStateFromProp -> shouldComponentUpdate -> componentWillUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate
# 销毁阶段
function callComponentWillUnmountWithTimer(){
instance.componentWillUnmount();
}
2
3
# 执行生命周期 componentWillUnmount
销毁阶段就比较简单了,在一次调和更新中,如果发现元素被移除,就会打对应的 Deletion 标签 ,然后在 commit 阶段就会调用 componentWillUnmount 生命周期,接下来统一卸载组件以及 DOM 元素。
# 类组件生命周期总览
# 各阶段生命周期的功能
上面介绍了 React 各生命周期的执行时机和执行顺序。下面介绍一下各个 lifecycle 能做些什么?
# constructor
React 在不同时期抛出不同的生命周期钩子,也就意味这这些生命周期钩子的使命。上面讲过 constructor 在类组件创建实例时调用,而且初始化的时候执行一次,所以可以在 constructor 做一些初始化的工作。
constructor(props){
super(props) // 执行 super ,别忘了传递props,才能在接下来的上下文中,获取到props。
this.state={ //① 可以用来初始化state,比如可以用来获取路由中的
name:'alien'
}
this.handleClick = this.handleClick.bind(this) /* ② 绑定 this */
this.handleInputChange = debounce(this.handleInputChange , 500) /* ③ 绑定防抖函数,防抖 500 毫秒 */
const _render = this.render
this.render = function(){
return _render.bind(this) /* ④ 劫持修改类组件上的一些生命周期 */
}
}
/* 点击事件 */
handleClick(){ /* ... */ }
/* 表单输入 */
handleInputChange(){ /* ... */ }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor 作用:
- 初始化 state ,比如可以用来截取路由中的参数,赋值给 state 。
- 对类组件的事件做一些处理,比如绑定 this , 节流,防抖等。
- 对类组件进行一些必要生命周期的劫持,渲染劫持,这个功能更适合反向继承的 HOC ,在 HOC 环节,会详细讲解反向继承这种模式。React HOC
# getDerivedStateFromProps
getDerivedStateFromProps(nextProps,prevState)
两个参数:
- nextProps 父组件新传递的 props ;
- prevState 传入 getDerivedStateFromProps 待合并的 state 。
getDerivedStateFromProps 方法作为类的静态属性方法执行,内部是访问不到 this 的,它更趋向于纯函数,从源码中就能够体会到 React 对该生命周期定义为取缔 componentWillMount 和 componentWillReceiveProps 。
这个生命周期用于,在初始化和更新阶段,接受父组件的 props 数据, 可以对 props 进行格式化,过滤等操作,返回值将作为新的 state 合并到 state 中,供给视图渲染层消费。
从源码中可以看到,只要组件更新,就会执行 getDerivedStateFromProps,不管是 props 改变,还是 setState ,或是 forceUpdate 。
static getDerivedStateFromProps(newProps){
const { type } = newProps
switch(type){
case 'fruit' :
return { list:['苹果','香蕉','葡萄' ] } /* ① 接受 props 变化 , 返回值将作为新的 state ,用于 渲染 或 传递给s houldComponentUpdate */
case 'vegetables':
return { list:['菠菜','西红柿','土豆']}
}
}
render(){
return <div>{ this.state.list.map((item)=><li key={item} >{ item }</li>) }</div>
}
2
3
4
5
6
7
8
9
10
11
12
getDerivedStateFromProps 作用:
- 代替 componentWillMount 和 componentWillReceiveProps
- 组件初始化或者更新时,将 props 映射到 state。
- 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。(请不要把 getDerivedStateFromProps 和 shouldComponentUpdate 强行关联到一起,两者没有必然联系)
# componentWillMount 和 UNSAFE_componentWillMount
在 React V16.3 componentWillMount
,componentWillReceiveProps
, componentWillUpdate
三个生命周期加上了不安全的标识符 UNSAFE
,变成了如下形式,在目前最新的版本 React V17.0.2
也没有废弃这三个生命周期。可能不久之后更高级的版本会被废除吧,首先先来看一下为什么要加 UNSAFE
,首先根据源码,大家有没有发现一个问题,就是这三个生命周期,都是在 render 之前执行的,React 对于执行 render 函数有着像 shouldUpdate 等条件制约,但是对于执行在 render 之前生命周期没有限制,存在一定隐匿风险,如果 updateClassInstance
执行多次,React 开发者滥用这几个生命周期,可能导致生命周期内的上下文多次被执行。
- UNSAFE_componentWillMount
- UNSAFE_componentWillReceiveProps
- UNSAFE_componentWillUpdate
UNSAFE_componentWillMount
的作用还是做一些初始化操作,但是不建议在这个生命周期写,毕竟未来 React 可能完全取缔它。
# omponentWillReceiveProps 和 UNSAFE_componentWillReceiveProps
UNSAFE_componentWillReceiveProps
函数的执行是在更新组件阶段,该生命周期执行驱动是因为父组件更新带来的 props 修改,但是只要父组件触发 render 函数,调用 React.createElement
方法,那么 props 就会被重新创建,生命周期 componentWillReceiveProps
就会执行了。这就解释了即使 props 没变,该生命周期也会执行。
componentWillReceiveProps
可以用来干什么
UNSAFE_componentWillReceiveProps(newProps){
const { type } = newProps
console.log('父组件render执行') /* ① 监听父组件执行render */
setTimeout(()=>{ /* ② 异步控制props改变,派生出来的 state 的修改 */
switch(type){
case 'fruit' :
this.setState({list:['苹果','香蕉','葡萄' ] })
break
case 'vegetables':
this.setState({list:['苹果','香蕉','葡萄' ] })
break
}
},0)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
componentWillReceiveProps
可以用来监听父组件是否执行 render 。componentWillReceiveProps
可以用来接受 props 改变,组件可以根据 props 改变,来决定是否更新 state ,因为可以访问到 this , 所以可以在异步成功回调(接口请求数据)改变 state 。这个是getDerivedStateFromProps
不能实现的。
不建议用这种方式,props 改变,再触发 componentWillReceiveProps
异步请求数据渲染,这样首先在没做优化前提下会带来两次子组件的更新,第一次 props 改变,第二次 props 改变,异步改变state 。其次该生命周期的不安全性。再者需要在该生命周期内部,设置大量的条件判断语句,通过 this.props , nextProps 判断 props 到底改变与否。所以完全可以换一种思路,那就是状态提升,把数据层完全托管父组件,子组件没有副作用,只负责渲染父组件传递的 props 即可。
当 props 不变的前提下, PureComponent 组件能否阻止 componentWillReceiveProps
执行?
答案是否定的,componentWillReceiveProps
生命周期的执行,和纯组件没有关系,纯组件是在 componentWillReceiveProps
执行之后浅比较 props 是否发生变化。所以 PureComponent 下不会阻止该生命周期的执行。
# componentWillUpdate 和 UNSAFE_componentWillUpdate
UNSAFE_componentWillUpdate
可以意味着在更新之前,此时的 DOM 还没有更新。在这里可以做一些获取 DOM 的操作。就比如说在一次更新中,保存 DOM 之前的信息(记录上一次位置)。但是 React 已经出了新的生命周期 getSnapshotBeforeUpdate
来代替 UNSAFE_componentWillUpdate
。
UNSAFE_componentWillUpdate(){
const position = this.getPostion(this.node) /* 获取元素节点 node 位置 */
}
2
3
作用:获取组件更新之前的状态。比如 DOM 元素位置等。
# render
所谓 render 函数,就是 jsx 的各个元素被 React.createElement 创建成 React element 对象的形式。一次 render 的过程,就是创建 React.element 元素的过程。那么可以在 render 里面做一些,createElement创建元素 , cloneElement 克隆元素 ,React.children 遍历 children 的操作。
# getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps,preState){}
- prevProps更新前的props
- prevState更新前的state
获取更新前 DOM 的状态。该生命周期是在 commit 阶段的before Mutation ( DOM 修改前),此时 DOM 还没有更新,但是在接下来的 Mutation 阶段会被替换成真实 DOM 。此时是获取 DOM 信息的最佳时期,getSnapshotBeforeUpdate 将返回一个值作为一个snapShot(快照),传递给 componentDidUpdate作为第三个参数。
getSnapshotBeforeUpdate 这个生命周期意义就是配合componentDidUpdate 一起使用,计算形成一个 snapShot 传递给 componentDidUpdate 。保存一次更新前的信息。
# componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot){
const style = getComputedStyle(this.node)
const newPosition = { /* 获取元素最新位置信息 */
cx:style.cx,
cy:style.cy
}
}
2
3
4
5
6
7
- prevProps 更新之前的 props ;
- prevState 更新之前的 state ;
- snapshot 为 getSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息。
作用
- componentDidUpdate 生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态。这个函数里面如果想要使用 setState ,一定要加以限制,否则会引起无限循环。
- 接受 getSnapshotBeforeUpdate 保存的快照信息。
# componentDidMount
componentDidMount 生命周期执行时机和 componentDidUpdate 一样,一个是在初始化,一个是组件更新。此时 DOM 已经创建完,既然 DOM 已经创建挂载,就可以做一些基于 DOM 操作,DOM 事件监听器。
async componentDidMount(){
this.node.addEventListener('click',()=>{
/* 事件监听 */
})
const data = await this.getData() /* 数据请求 */
}
2
3
4
5
6
作用:
- 可以做一些关于 DOM 操作,比如基于 DOM 的事件监听器。
- 对于初始化向服务器请求数据,渲染视图,这个生命周期也是蛮合适的。
# shouldComponentUpdate
shouldComponentUpdate(newProps,newState,nextContext){}
shouldComponentUpdate 三个参数,第一个参数新的 props ,第二个参数新的 state ,第三个参数新的 context 。
这个生命周期,一般用于性能优化,shouldComponentUpdate 返回值决定是否重新渲染的类组件。需要重点关注的是第二个参数 newState ,如果有 getDerivedStateFromProps 生命周期 ,它的返回值将合并到 newState ,供 shouldComponentUpdate 使用。
shouldComponentUpdate(newProps,newState){
if(newProps.a !== this.props.a ){ /* props中a属性发生变化 渲染组件 */
return true
}else if(newState.b !== this.props.b ){ /* state 中b属性发生变化 渲染组件 */
return true
}else{ /* 否则组件不渲染 */
return false
}
}
2
3
4
5
6
7
8
9
# componentWillUnmount
componentWillUnmount 是组件销毁阶段唯一执行的生命周期,主要做一些收尾工作,比如清除一些可能造成内存泄漏的定时器,延时器,或者是一些事件监听器。
componentWillUnmount(){
clearTimeout(this.timer) /* 清除延时器 */
this.node.removeEventListener('click',this.handerClick) /* 卸载事件监听器 */
}
2
3
4
作用
清除延时器,定时器。 一些基于 DOM 的操作,比如事件监听器。
# 函数组件生命周期
React hooks也提供了 api ,用于弥补函数组件没有生命周期的缺陷。其原理主要是运用了 hooks 里面的 useEffect 和 useLayoutEffect。
# useEffect
useEffect(()=>{
return destory
},dep)
2
3
- useEffect 第一个参数 callback, 返回的 destory , destory 作为下一次callback执行之前调用,用于清除上一次 callback 产生的副作用。
- 第二个参数作为依赖项,是一个数组,可以有多个依赖项,依赖项改变,执行上一次callback 返回的 destory ,和执行新的 effect 第一个参数 callback 。
对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, React 会向 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。
# useLayoutEffect
useLayoutEffect 和 useEffect 不同的地方是采用了同步执行,那么和useEffect有什么区别呢?
首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。
useLayoutEffect callback 中代码执行会阻塞浏览器绘制。
# useEffect 与 useLayoutEffect 区别
一句话概括如何选择 useEffect 和 useLayoutEffect :修改 DOM ,改变布局就用 useLayoutEffect ,其他情况就用 useEffect 。
React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ?
useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。
# useInsertionEffect
useInsertionEffect 是在 React v18 新添加的 hooks ,它的用法和 useEffect 和 useLayoutEffect 一样。那么这个 hooks 用于什么呢?
在介绍 useInsertionEffect 用途之前,先看一下 useInsertionEffect 的执行时机。
React.useEffect(()=>{
console.log('useEffect 执行')
},[])
React.useLayoutEffect(()=>{
console.log('useLayoutEffect 执行')
},[])
React.useInsertionEffect(()=>{
console.log('useInsertionEffect 执行')
},[])
// useInsertionEffect 执行 useLayoutEffect 执行 useEffect 执行
2
3
4
5
6
7
8
9
10
11
12
13
可以看到 useInsertionEffect 的执行时机要比 useLayoutEffect 提前,useLayoutEffect 执行的时候 DOM 已经更新了,但是在 useInsertionEffect 的执行的时候,DOM 还没有更新。
本质上 useInsertionEffect 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。这个 hooks 主要是应用于这个场景,在其他场景下 React 不期望用这个 hooks
css-in-js 问题
CSS-in-JS 的注入会引发哪些问题呢? 首先看部分 CSS-in-JS 的实现原理,拿 Styled-components 为例子,通过styled-components,你可以使用ES6的标签模板字符串语法(Tagged Templates)为需要 styled 的 Component 定义一系列CSS属性,当该组件的JS代码被解析执行的时候,styled-components 会动态生成一个 CSS 选择器,并把对应的 CSS 样式通过 style 标签的形式插入到 head 标签里面。动态生成的 CSS 选择器会有一小段哈希值来保证全局唯一性来避免样式发生冲突。这种模式下本质上是动态生成 style 标签。
在 useLayoutEffect 使用 CSS-in-JS 会造成哪里问题呢?
- 首先 useLayoutEffect 执行的时机 DOM 已经更新完成,布局也已经确定了,剩下的就是交给浏览器绘制就行了。
- 如果在 useLayoutEffect 动态生成 style 标签,那么会再次影响布局,导致浏览器再次重回和重排。
这个是时候 useInsertionEffect 的作用就出现了,useInsertionEffect 的执行在 DOM 更新前,所以此时使用 CSS-in-JS 避免了浏览器出现再次重回和重排的可能,解决了性能上的问题。
在 useInsertionEffect 使用 CSS-in-JS 流程
export default function Index(){
React.useInsertionEffect(()=>{
/* 动态创建 style 标签插入到 head 中 */
const style = document.createElement('style')
style.innerHTML = `
.css-in-js{
color: red;
font-size: 20px;
}
`
document.head.appendChild(style)
},[])
return <div className="css-in-js" > hello , useInsertionEffect </div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 生命周期在函数组件中的替代方案
# componentDidMount 替代方案
React.useEffect(()=>{
/* 请求数据 , 事件监听 , 操纵dom */
},[]) /* 切记 dep = [] */
2
3
这里要记住 dep = [] ,这样当前 effect 没有任何依赖项,也就只有初始化执行一次。
# componentWillUnmount 替代方案
React.useEffect(()=>{
/* 请求数据 , 事件监听 , 操纵dom , 增加定时器,延时器 */
return function componentWillUnmount(){
/* 解除事件监听器 ,清除定时器,延时器 */
}
},[])/* 切记 dep = [] */
2
3
4
5
6
在 componentDidMount 的前提下,useEffect 第一个函数的返回函数,可以作为 componentWillUnmount 使用。
# componentWillReceiveProps 代替方案
说 useEffect 代替 componentWillReceiveProps 着实有点牵强。
- 首先因为二者的执行阶段根本不同,一个是在render阶段,一个是在commit阶段。
- 其次 useEffect 会初始化执行一次,但是 componentWillReceiveProps 只有组件更新 props 变化的时候才会执行。
React.useEffect(()=>{
console.log('props变化:componentWillReceiveProps')
},[ props ])
2
3
此时依赖项就是 props,props 变化,执行此时的 useEffect 钩子。
React.useEffect(()=>{
console.log('props中number变化:componentWillReceiveProps')
},[ props.number ]) /* 当前仅当 props中number变化,执行当前effect钩子 */
2
3
useEffect 还可以针对 props 的某一个属性进行追踪。此时的依赖项为 props 的追踪属性。如上述代码,只有 props 中 number 变化,执行 effect 。
# componentDidUpdate 替代方案
useEffect 和 componentDidUpdate 在执行时期虽然有点差别,useEffect 是异步执行,componentDidUpdate 是同步执行 ,但都是在 commit 阶段 。但是向上面所说 useEffect 会默认执行一次,而 componentDidUpdate 只有在组件更新完成后执行。
React.useEffect(()=>{
console.log('组件更新完成:componentDidUpdate ')
}) /* 没有 dep 依赖项 */
2
3
没有第二个参数,那么每一次执行函数组件,都会执行该 effect。
# 实现一个 ScrollView 组件
主要用于长列表渲染,滑动底部请求渲染列表
class ScrollView extends React.Component{
/* -----自定义事件---- */
/* 控制滚动条滚动 */
handerScroll=(e)=>{
const { scroll } = this.props
scroll && scroll(e)
this.handerScrolltolower()
}
/* 判断滚动条是否到底部 */
handerScrolltolower(){
const { scrolltolower } = this.props
const { scrollHeight , scrollTop , offsetHeight } = this.node
if(scrollHeight === scrollTop + offsetHeight){ /* 到达容器底部位置 */
scrolltolower && scrolltolower()
}
}
node = null
/* ---——---生命周期------- */
constructor(props) {
super(props)
this.state={ /* 初始化 Data */
list:[]
}
this.handerScrolltolower = debounce(this.handerScrolltolower,200) /* 防抖处理 */
}
/* 接收props, 合并到state */
static getDerivedStateFromProps(newProps){
const { data } = newProps
return {
list : data.list || [] ,
}
}
/* 性能优化,只有列表数据变化,渲染列表 */
shouldComponentUpdate(newProps,newState){
return newState.list !== this.state.list
}
/* 获取更新前容器高度 */
getSnapshotBeforeUpdate(){
return this.node.scrollHeight
}
/* 获取更新后容器高度 */
componentDidUpdate(prevProps, prevState, snapshot){
console.log('scrollView容器高度变化:' , this.node.scrollHeight - snapshot )
}
/* 绑定事件监听器 - 监听scorll事件 */
componentDidMount() {
this.node.addEventListener('scroll',this.handerScroll)
}
/* 解绑事件监听器 */
componentWillUnmount(){
this.node.removeEventListener('scroll',this.handerScroll)
}
render() {
const { list } = this.state
const { component } = this.props
return <div className="list_box" ref={(node) => this.node = node } >
<div >
{
list.map((item) => (
React.createElement(component,{ item , key: item.id }) //渲染 Item 列表内容。
))
}
</div>
</div>
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
scrollview组件各个生命周期功能:
- constructor: 做数据初始化,将滑动处理函数,做防抖处理。
- getDerivedStateFromProps: 将 props 中的 list ,合并到 state 。
- componentDidMount: 绑定监听 scroll 事件。
- shouldComponentUpdate:性能优化,只有 list 改变,渲染视图。
- render: 渲染视图,渲染 Item 。
- getSnapshotBeforeUpdate:保存组件更新前的 scrollview 容器高度。
- componentDidUpdate:根据渲染前后容器高度,计算一次高度变化量。
- componentWillUnmount:解除 scroll 事件监听器。
使用:
/* item 完全是单元项的渲染ui */
function Item({item}) {
return <div className="goods_item" >
<img src={item.giftImage} className="item_image" />
<div className="item_content" >
<div className="goods_name" >
{item.giftName}
</div>
<div className="hold_price" />
<div className="new_price" >
<div className="new_price" >
<div className="one view">
¥ {item.price}
</div>
</div>
</div>
<img className='go_share go_text' />
</div>
</div>
}
function () {
const [ data , setData ] = useState({ list:[],page:0,pageCount:1 }) /* 记录列表数据 */
/* 请求数据 */
const getData = async ()=>{
if(data.page === data.pageCount) return console.log('没有数据了~')
const res = await fetchData(data.page + 1)
if(res.code === 0) setData({
...res,
list:res.page === 1 ? res.list : data.list.concat(res.list)
})
}
/* 滚动到底部触发 */
const handerScrolltolower = () => {
console.log('scroll已经到底部')
getData()
}
/* 初始化请求数据 */
useEffect(()=>{
getData()
},[])
return <ScrollView
data={ data } /* */
component={ Item } /* Item 渲染的单元组件 */
scrolltolower={ handerScrolltolower }
scroll={()=>{}}
/>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 问题
# Q1. useEffect/useLayoutEffect/useInsertionEffect 有什么区别?
一句话概括如何选择 useEffect 和 useLayoutEffect :修改 DOM ,改变布局就用 useLayoutEffect ,其他情况就用 useEffect 。
- useInsertionEffect: useInsertionEffect 的执行时机要比 useLayoutEffect 提前,useLayoutEffect 执行的时候 DOM 已经更新了,但是在 useInsertionEffect 的执行的时候,DOM 还没有更新。本质上 useInsertionEffect 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。这个 hooks 主要是应用于这个场景,在其他场景下 React 不期望用这个 hooks。useInsertionEffect 的执行在 DOM 更新前,所以此时使用 CSS-in-JS 避免了浏览器出现再次重回和重排的可能,解决了性能上的问题。
- useLayoutEffect: useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。useLayoutEffect callback 中代码执行会阻塞浏览器绘制。
- useEffect:对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, React 会向 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。
顺序:useInsertionEffect -> DOM 更新 -> useLayoutEffect -> render -> useEffect
# Q2. React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ?
useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。
# Q3. 类组件生命周期
← React Props React Ref →