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
function updateClassComponent() {
let shouldUpdate;
const instance = workInProgress.stateNode; // stateNode 是 fiber 指向类组件实例的引用
if (instance === null) { // 实例不存在,即该类组件没有被挂载过,那走初始化流程
// 组件实例在这个方法中被创建
contructorClassInstance(workInProgress, Component, nextProps);
// 初始化挂载组件流程
mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);
shouldUpdate = true; // 初始化阶段,肯定要更新
} else {
// 组件实例已经存在,那就是更新阶段
shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderExpirationTime); // 更新组件流程
}
if (shouldUpdate) {
nextChildren = instance.render(); // 执行 render 函数,获取组件的子节点
reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime); // 调和子节点
}
}
几个重要的概念:
instance
类组件对应的实例workInProgress
树,当前正在调和的 fiber
树 ,一次更新中,React 会自上而下深度遍历子代 fiber
,如果遍历到一个 fiber
,会把当前 fiber
指向 workInProgress
current
树,在初始化更新中,current = null
,在第一次 fiber
调和之后,会将 workInProgress
树赋值给 current
树 workInProgress
和 current
来确保一次更新中,快速构建,并且状态不丢失Component
就是项目中的 class
组件nextProps
作为组件在一次更新中新的 props
renderExpirationTime
作为下一次渲染的过期时间在组件实例上可以通过 _reactInternals
属性来访问组件对应的 fiber
对象。在 fiber
对象上,可以通过 stateNode
来访问当前 fiber
对应的组件实例:
React 的大部分生命周期的执行,都在 mountClassInstance
和 updateClassInstance
这两个方法中执行。
constructor
执行 mount
阶段,首先执行的 constructClassInstance
函数,用来实例化 React 组件,组件中 constructor
就是在这里执行的mountClassInstance
组件初始化getDerivedStateFromProps
执行 getDerivedStateFromProps
是第二个执行的生命周期,值得注意的是它是从 ctor
类上直接绑定的静态方法,传入 props
,state
state
合并,作为新的 state
,传递给组件实例使用componentWillMount
执行 getDerivedStateFromProps
和 getSnapshotBeforeUpdate
就不会执行生命周期 componentWillMount
render
函数执行 mountClassInstancec
函数完成,但是 updateClassComponent
函数, 在执行完 mountClassInstancec
后,执行了 render
渲染函数,形成了 children
, 接下来 React 调用 reconcileChildren
方法深度调和 children
componentDidMount
执行 fiber
节点,就会到 commit
阶段,在组件初始化 commit
阶段,会调用 componentDidMount
生命周期执行顺序:constructor
=> getDerivedStateFromProps / componentWillMount
=> render
=> componentDidMount
componentWillReceiveProps
getDerivedStateFromProps
生命周期是否存在,如果不存在就执行 componentWillReceiveProps
生命周期newProps
和 nextContext
getDerivedStateFromProps
getDerivedStateFromProps
, 返回的值用于合并 state
,生成新的 state
shouldComponentUpdate
shouldComponentUpdate
,传入新的 props
,新的 state
,和新的 context
,返回值决定是否继续执行 render
函数,调和子节点getDerivedStateFromProps
的返回值可以作为新的 state
,传递给 shouldComponentUpdate
componentWillUpdate
componentWillUpdate
,updateClassInstance
方法到此执行完毕了render
函数执行 getSnapshotBeforeUpdate
getSnapshotBeforeUpdate
的执行也是在 commit
阶段 commit
阶段细分为 before Mutation
( DOM 修改前),Mutation
( DOM 修改),Layout
( DOM 修改后) 三个阶段getSnapshotBeforeUpdate
发生在 before Mutation
阶段,生命周期的返回值,将作为第三个参数 __reactInternalSnapshotBeforeUpdate
传递给 componentDidUpdate
componentDidUpdate
componentDidUpdate
,此时 DOM 已经修改完成,可以操作修改之后的 DOM,到此为止更新阶段的生命周期执行完毕更新阶段对应生命周期执行顺序:componentWillReceiveProps
(props
改变) / getDerivedStateFromProps
=> shouldComponentUpdate
=> componentWillUpdate
=> render
=> getSnapshotBeforeUpdate
=> componentDidUpdate
componentWillUnmount
Deletion
标签 ,然后在 commit
阶段就会调用 componentWillUnmount
生命周期,接下来统一卸载组件以及 DOM 元素三个阶段生命周期 + 无状态组件总览图:
constructor
constructor
在类组件创建实例时调用,而且初始化的时候执行一次,所以可以在 constructor
做一些初始化的工作constructor
作用 state
,比如可以用来截取路由中的参数,赋值给 state
this
, 节流,防抖等getDerivedStateFromProps
nextProps
: 父组件新传递的 props
prevState
: 传入 getDerivedStateFromProps
待合并的 state
getDerivedStateFromProps
方法作为类的静态属性方法执行,内部是访问不到 this
的,它更趋向于纯函数 componentWillMount
和 componentWillReceiveProps
props
数据, 可以对 props
进行格式化,过滤等操作,返回值将作为新的 state
合并到 state
中,供给视图渲染层消费getDerivedStateFromProps
,不管是 props
改变,还是 setState
,或是 forceUpdate
getDerivedStateFromProps
作用 componentWillMount
和 componentWillReceiveProps
props
映射到 state
state
合并完,可以作为 shouldComponentUpdate
第二个参数 newState
,可以判断是否渲染组件UNSAFE_componentWillMount、UNSAFE_componentWillReceiveProps、UNSAFE_componentWillUpdate
在 React V16.3 componentWillMount
,componentWillReceiveProps
, componentWillUpdate
三个生命周期加上了不安全的标识符 UNSAFE。
这三个生命周期,都是在 render
之前执行的,React 对于执行 render
函数有着像 shouldUpdate
等条件制约,但是对于执行在 render
之前生命周期没有限制,存在一定隐匿风险,如果 updateClassInstance
执行多次,React 开发者滥用这几个生命周期,可能导致生命周期内的上下文多次被执行
componentWillMount
和 UNSAFE_componentWillMount
UNSAFE_componentWillMount
的作用还是做一些初始化操作,但是不建议在这个生命周期写componentWillReceiveProps
和 UNSAFE_componentWillReceiveProps
UNSAFE_componentWillReceiveProps
函数的执行是在更新组件阶段props
修改,但是只要父组件触发 render
函数,调用 React.createElement
方法,那么 props
就会被重新创建,生命周期 componentWillReceiveProps
就会执行了 props
没变,该生命周期也会执行componentWillReceiveProps
作用 render
props
改变,组件可以根据 props
改变,来决定是否更新 state
,因为可以访问到 this
, 所以可以在异步成功回调(接口请求数据)改变 state
(不过不建议这么使用)componentWillUpdate
和 UNSAFE_componentWillUpdate
UNSAFE_componentWillUpdate
可以意味着在更新之前,此时的 DOM 还没有更新getSnapshotBeforeUpdate
来代替 UNSAFE_componentWillUpdate
render
render
函数,就是 jsx 的各个元素被 React.createElement
创建成 React element 对象的形式render
的过程,就是创建 React.element 元素的过程render
里面做一些, createElement
创建元素 , cloneElement
克隆元素 ,React.children 遍历 children
的操作getSnapshotBeforeUpdate
prevProps
:更新前的 props
preState
:更新前的 state
commit
阶段的 before Mutation
( DOM 修改前),此时 DOM 还没有更新,但是在接下来的 Mutation
阶段会被替换成真实 DOM ,此时是获取 DOM 信息的最佳时期getSnapshotBeforeUpdate
将返回一个值作为一个 snapShot
(快照),传递给 componentDidUpdate
作为第三个参数 componentDidUpdate
也会给予警告snapShot
不限于 DOM 的信息,也可以是根据 DOM 计算出来产物getSnapshotBeforeUpdate
这个生命周期意义就是配合 componentDidUpdate
一起使用,计算形成一个 snapShot
传递给 componentDidUpdate
,保存一次更新前的信息componentDidUpdate
prevProps
: 更新之前的 props
prevState
: 更新之前的 state
snapshot
: getSnapshotBeforeUpdate
返回的快照,可以是更新前的 DOM 信息componentDidUpdate
生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态 setState
,一定要加以限制,否则会引起无限循环getSnapshotBeforeUpdate
保存的快照信息componentDidMount
componentDidMount
生命周期执行时机和 componentDidUpdate
一样,一个是在初始化,一个是组件更新shouldComponentUpdate
newProps
:新的 props
newState
:新的 state
nextContext
:新的 context
shouldComponentUpdate
返回值决定是否重新渲染的类组件newState
,如果有 getDerivedStateFromProps
生命周期 ,它的返回值将合并到 newState
,供 shouldComponentUpdate
使用componentWillUnmount
React hooks 也提供了 api ,用于弥补函数组件没有生命周期的缺陷。其原理主要是运用了 hooks 里面的 useEffect
和 useLayoutEffect
。
useEffect
useEffect(() => {
return destroy
}, dep)
callback
, 返回的 destory
, destory
作为下一次 callback
执行之前调用,用于清除上一次 callback
产生的副作用callback
返回的 destory
,和执行新的 effect
第一个参数 callback
effect
回调函数不会阻塞浏览器绘制视图 useEffect
执行, React 处理逻辑是采用异步调用 ,对于每一个 effect
的 callback
, React 会向 setTimeout
回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行useLayoutEffect
useEffect 和 useLayoutEffect 的区别
useLayoutEffect
是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect
,那 useEffect
执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。useLayoutEffect
callback
中代码执行会阻塞浏览器绘制useLayoutEffect
,其他情况就用 useEffect
`React.useEffect` 回调函数 和 `componentDidMount` / `componentDidUpdate` 执行时机有什么区别 ?
useEffect
对 React 执行栈来看是异步执行的,而 componentDidMount
/ componentDidUpdate
是同步执行的,useEffect
代码不会阻塞浏览器绘制。在时机上 ,componentDidMount
/ componentDidUpdate
和 useLayoutEffect
更类似。
useInsertionEffect
是在 React v18 新添加的 hooks ,它的用法和 useEffect
和 useLayoutEffect
一样。
useInsertionEffect
的执行时机要比 useLayoutEffect
提前,useLayoutEffect
执行的时候 DOM 已经更新了,但是在 useInsertionEffect
的执行的时候,DOM 还没有更新。
本质上 useInsertionEffect
主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。这个 hooks 主要是应用于这个场景,在其他场景下 React 不期望用这个 hooks 。
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
的执行在 DOM 更新前,所以此时使用 CSS-in-JS 避免了浏览器出现再次重回和重排的可能,解决了性能上的问题。
React.useEffect(() => {
/** 请求数据,事件监听,操作 DOM */
}, []) // 第二个参数传入空数组,表示只执行一次
dep = []
,这样当前 effect 没有任何依赖项,也就只有初始化执行一次
React.useEffect(() => {
/** 请求数据,事件监听,操作 DOM ,增加定时器,延时器 */
return function componentWillUnmount() {
/** 解除事件监听,清除定时器,延时器 */
}
}, [])
在 componentDidMount
的前提下,useEffect
第一个函数的返回函数,可以作为 componentWillUnmount
使用。
说 useEffect
代替 componentWillReceiveProps
着实有点牵强:
useEffect
会初始化执行一次,但是 componentWillReceiveProps
只有组件更新 props
变化的时候才会执行React.useEffect(() => {
console.log('props changed: componentWillReceiveProps');
}, [props])
此时依赖项就是 props
,props
变化,执行此时的 useEffect
钩子。
React.useEffect(() => {
console.log('props changed: componentWillReceiveProps');
}, [props.a])
useEffect
还可以针对 props
的某一个属性进行追踪。此时的依赖项为 props
的追踪属性。上面的例子中,props.a
变化,执行此时的 useEffect
钩子。
useEffect
和 componentDidUpdate
在执行时期虽然有点差别,useEffect
是异步执行, componentDidUpdate
是同步执行 ,但都是在 commit 阶段 。useEffect
会默认执行一次,而 componentDidUpdate
只有在组件更新完成后执行。
React.useEffect(() => {
console.log('componentDidUpdate');
})
注意此时 useEffect
没有第二个参数。没有第二个参数,那么每一次执行函数组件,都会执行该 effect
。
function FunctionLifecycle(props) {
const [num, setNum] = React.useState(0);
React.useEffect(() => {
/** 请求数据,事件监听,操作 DOM,增加定时器,延时器 */
console.log('组件挂载完成 componentDidMount');
return function componentWillUnmount() {
/** 解除事件监听,清除定时器,延时器 */
console.log('组件销毁 componentWillUnmount');
}
}, []); // 第二个参数传入空数组,表示只执行一次
React.useEffect(() => {
console.log('props 变化: componentWillReceiveProps');
}, [props]);
React.useEffect(() => {
console.log('组件更新完成 componentDidUpdate');
}); // 没有第二个参数,每次执行函数组件都会执行该 effect
return (
<div>
<p>props: {props.number}</p>
<p>state: {num}</p>
<button onClick={() => setNum(state => state + 1)}>num + 1</button>
</div>
);
}
export default () => {
const [number, setNumber] = React.useState(0);
const [isRender, setIsRender] = React.useState(true);
return <div>
{isRender && <FunctionLifecycle number={number} />}
<button onClick={() => setNumber(state => state + 1)}>number + 1</button><br/>
<button onClick={() => setIsRender(state => !state)}>isRender</button>
</div>;
}