生命周期的简单说法
react在render阶段会深度遍历react fiber 树,目的就是发现不同(diff),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行render函数,在一次render过程中结束后,就回到commit阶段,commit阶段会创建修改真实的DOM节点。
生命周期执行过程
1. 初始化阶段
- constructor 执行
在mount阶段,首先执行的是constructClassInstance函数,用来实例化React组件,在组件章节已经介绍了这个函数,组件中constructor就是在这里执行的。
在组件实例化之后,它会调用mountClassInstance组件初始化
- getDerivedStateFormProps执行
在初始化阶段,getDerivedStateFromProps是第二个执行的生命周期,值得注意的是它是从ctor类上直接绑定的静态方法,传入props,State。返回值将之前的state合并,作为新的state,传递给组件实例使用.
- componentWillMount 执行
- render 函数执行
- componentDidMount执行
从这一步之前react其实一直在render阶段进行执行。一旦react,调和完所有的fiber节点,就会到commit阶段,在组件初始化commit阶段,会调用componentDidMount生命周期。
react 开始的执行顺序为:constructor -> getDerivedStateFromProps / componentWillMount -> rednder -> componentDidMount
2. 更新阶段
- 执行生命周期componentWillReceiveProps
首先判断getDerivedStateFromProps 生命周期是否存在,如果不存在就执行componentWillReceiveProps生命周期,传入该生命周期有两个参数,分别为newProps和nextContext。
- 执行 getDerivedStateFromProps
上述下来执行它,返回的值用于合并state,生成新的state。
- 执行shouldComponentUpdate
下来执行它,传入新的props,新的state,和新的context,返回值决定是否继续执行render函数,tiao'he 子节点。
- 执行componentWillUpdate
updateClassInstance方法到此结束。
- 执行render
执行render,得到最新的react element 元素,然后继续调和子节点。
- 执行getSnapshotBeforeUpdate
getSnapshotBeforeUpdate的执行也是在 commit 阶段,commit 阶段细分为before Mutation( DOM 修改前),Mutation( DOM 修改),Layout( DOM 修改后) 三个阶段,getSnapshotBeforeUpdate 发生在before Mutation阶段,生命周期的返回值,将作为第三个参数 __reactInternalSnapshotBeforeUpdate 传递给 componentDidUpdate 。
- 执行componentDidUpdate
这里dom已经修改完成,可以操作修改之后的dom。
各个生命周期的能力
- constructor
constructor 在类组件创建实例时调用,而且初始化的时候执行一次,所以可以在 constructor 做一些初始化的工作。
- componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot){
const style = getComputedStyle(this.node)
const newPosition = { /* 获取元素最新位置信息 */
cx:style.cx,
cy:style.cy
}
}
三个参数:
- 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() /* 数据请求 */
}
3. 销毁阶段
执行生命周期 componentWillUnmount
在一次调和更新中,如果发现元素被移除,就会调用 componentWillUnmount 生命周期,接下来统一卸载组件以及 DOM 元素。
函数组件生命周期替代方案
React hooks 也提供了 api ,用于弥补函数组件没有生命周期的缺陷。其原理主要是运用了 hooks 里面的 useEffect 和 useLayoutEffect。
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 ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。useLayoutEffectcallback中代码执行会阻塞浏览器绘制- 修改 DOM ,改变布局就用
useLayoutEffect,其他情况就用useEffect
React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ?
useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect 代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。
useInsertionEffect
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 避免了浏览器出现再次重回和重排的可能,解决了性能上的问题。
componentDidMount 替代方案
React.useEffect(() => {
/** 请求数据,事件监听,操作 DOM */
}, []) // 第二个参数传入空数组,表示只执行一次
dep = [] ,这样当前 effect 没有任何依赖项,也就只有初始化执行一次
componentWillUnmount 替代方案
React.useEffect(() => {
/** 请求数据,事件监听,操作 DOM ,增加定时器,延时器 */
return function componentWillUnmount() {
/** 解除事件监听,清除定时器,延时器 */
}
}, [])
在 componentDidMount 的前提下,useEffect 第一个函数的返回函数,可以作为 componentWillUnmount 使用。
componentWillReceiveProps 代替方案
说 useEffect 代替 componentWillReceiveProps 着实有点牵强:
- 首先因为二者的执行阶段根本不同,一个是在 render 阶段,一个是在 commit 阶段
- 其次
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 钩子。
componentDidUpdate 替代方案
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>;
}