本文正在参加「金石计划」
flag:每月至少产出三篇高质量文章~
最近前 leader
找到我,让我帮他面试一个前端开发的岗位(react技术栈,3年+),在整理面试题的时候,想到几年前跳槽的时候面阿里高德时被问到的一个 “刁钻” 面试题:注意! 是 【componentWillMount】
,不是 componentDidMount
、componentDidUpdate
、componentWillUnmount
。大家可以先不看下文,自己考虑一下,会怎么回答这个问题。
这个之所以让我印象这么深刻,是因为当时我被这个问题问懵了。常规的 React
面试题可能会问你怎么实现 componentDidMount
、componentDidUpdate
、componentWillUnmount
之类的,而这个问题却并不按常理出牌,让你实现一个在之前 class component
时代都不怎么常用的生命周期 —— componentWillMount
。
个人觉得这个问题虽然角度 “刁钻”,但是不失为一道挺有水平的面试题,是因为:
- 需要面试者对
React
类组件的生命周期和v16.8
以后的函数组件中官方提供的React Hooks
执行时机有足够深入的了解才能比较好地回答这个问题;- 如果是在
React Hooks
时代才入坑 React 的新晋选手恐怕会难以理解这个问题的本质;- 即便是经验丰富的
React
选手,由于疏于对componentWillMount
的使用(这个钩子确实用得也少),也可能翻车,需要有比较丰富、扎实的 React 基础知识;
那接下来,我们就尝试怎么比较好地回答这个问题吧~ 阅读此文,你将对新旧版本的 React 的生命周期以及 React Hooks
的执行时机有更深刻的理解。
一、Class Component 时代的生命周期
为了回答好上述面试题,我们首先得对 componentWillMount
这个生命周期及其执行时机有足够的了解。所以,先来回顾一下 class component
的生命周期。
我想很多人应该对下面两张图挺熟悉的吧,来自 wojtekmaj 的项目:react-lifecycle-methods-diagram。
- react v16.3
- react v16.4注意:
componentWillReceiveProps()
在v16.4
已经被标记为不建议使用,官方建议使用新的getDerivedStateFromProps()
方法代替。
- 还有一个细微的更新是:
setState
和forceUpdate
的调用也会触发getDerivedStateFromProps
。可能会引发一些让人疑惑的bug,比如这个:getDerivedStatefromProps in react 16.4 results in no state changes
React class 组件在其生命周期中经历三个阶段:挂载、更新和卸载。
- 挂载阶段是在创建新组件并将其插入 DOM 时,或者换句话说,在组件生命周期开始时。这只会发生一次,通常称为“初始渲染”。
- 更新阶段是组件更新或重新渲染的时候。当道具更新或状态更新时,会触发此反应。这个阶段可以发生多次,这就是 React 的意义所在。
- 组件生命周期的最后一个阶段是卸载阶段,当组件从 DOM 中移除时。
以下是每个生命周期函数的详细描述和执行时机:
1、挂载阶段
这个阶段发生在组件被创建并插入到 DOM 中的时候。按照上图,这个阶段会执行这几个钩子函数:constructor
、static getDerivedStateFromProps
、componentWillMount/UNSAVE_componentWillMount
,render
和componentDidMount
。
constructor()
构造函数,在组件创建时调用,用于初始化状态和绑定方法。
需要注意:如果你使用了
constructor
函数,你需要首先调用super(props)
才能使用this关键字。
static getDerivedStateFromProps()
需要注意的是:
props
和state
是完全不同的概念,一个成熟的 React 开发者最基本的是要知道组件的数据从哪里来,要往哪里去。
顾名思义,getDerivedStateFromProps
的字面意思就是:从 props
获取 衍生state
。但在许多情况下,你的组件的 state
实际上是其 props
的衍生品。这个方法允许你用 任何props值
来修改 state 值
。
这个方法在组件挂载前调用,并且在组件每次更新时也会被调用。它的作用是根据 props
的改变来更新 state
,返回一个 新的state
。如果不需要更新 state
,返回 null
即可。
componentWillMount/UNSAVE_componentWillMount
React v16.3
版本中将componentWillMount
,componentWillReceiveProps
以及componentWillUpdate
加上了UNSAFE_
前缀,这些钩子将在React 17.0
废除,如果你确实选择继续使用它,你应该使用UNSAFE_componentWillMount()
。
这个生命周期函数在 render
之前调用,在此生命周期中使用 setState
不会触发额外渲染,因为你不可能在创建的时候把数据渲染出来。只能在 componentDidMount
中使用 setState
把数据塞回去,通过更新界面来展示数据。所以一般建议把网络请求的逻辑放在 componentDidMount
,而不是 componentWillMount
中。
render()
render()
方法是唯一必须的钩子函数,它在 getDerivedStateFromProps
方法之后被调用,用于渲染组件的UI。
注意:不要在
render()
方法中改变state
,否则会陷入死循环,导致程序崩溃。
componentDidMount()
componentDidMount
是在挂载阶段调用的最后一个生命周期方法,组件被挂载后调用,这个方法可以用于发起网络请求或者设置定时器等异步操作。它可能在组件被渲染或挂载到DOM之后被调用。
这个方法中,你可以添加副作用,如发送网络请求或更新组件的状态,
componentDidMount
中还可以订阅Redux store
。你也可以立即调用this.setState
方法;但这将导致重新渲染,因为它启动了更新阶段,因为状态已经改变。所以,你需要小心使用
componentDidMount
,因为它可能导致不必要的重新渲染。
2、更新阶段
当组件的 props 或 state
改变时,组件会被重新渲染,此时就会进入到更新阶段。这个阶段会执行这几个钩子函数:static getDerivedFromProps
、shouldComponentUpdate
、render
、getSnapshotBeforeUpdate
和 componentDidUpdate
。
static getDerivedStateFromProps()
在更新阶段,第一个调用的生命周期方法是 getDerivedStateFromProps
。在组件更新前被调用,和挂载阶段的作用相同,但是尽量不要在这个方法中执行副作用操作,因为这个方法会在每次更新时都被调用。
例如,一个组件的状态可能取决于其
props
的值。通过getDerivedStateFromProps
,在组件被重新渲染之前,它的state
可以反映这些变化,并且可以显示在新更新的组件中。
shouldComponentUpdate()
shouldComponentUpdate
是专门用于性能优化的, 通常来说,只有 props
或 state
变化时才需要再重新渲染。这个方法接受两个参数:nextProps 和 nextState,可以用于控制组件是否需要重新渲染,如果返回 false,组件将不会重新渲染,默认返回true。
注意,当调用
forceUpdate()
时,shouldComponentUpdate
方法被忽略。
render()
render()
方法会根据 最新的props和state
来重新渲染组件的UI,在挂载阶段已经说明,这里就不赘述了。
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate
方法让你可以访问组件更新前的 props
和 state
。这使你能够处理或检查 props
和 state
的先前值。这是一个很少使用的方法。
例如,这个方法的一个很好的使用场景是处理
聊天APP
中的滚动位置。当用户在查看旧的信息时有一条新的信息进来,它不应该把旧的信息推到视野之外。
getSnapshotBeforeUpdate
在渲染方法之后,组件 DidUpdate
之前被调用。如果 getSnapshotBeforeUpdate
方法返回任何东西,它将被传递给 componentDidUpdate
方法作为参数:
componentDidUpdate()
componentDidUpdate
方法是在更新阶段调用的最后一个生命周期方法。组件更新后被调用,可以用于处理 DOM的更新
或者 发起网络请求
等异步操作。
这个方法最多可以接受三个参数:prevProps
、prevState
和 snapshot
(如果你调用了 getSnapshotBeforeUpdate
方法)。
下面是一个使用 componentDidUpdate
方法来实现自动保存功能的例子:
3、卸载阶段
当组件从DOM中移除时,就会进入到卸载阶段。卸载阶段只涉及一个生命周期方法:componentWillUnmount
。
componentWillUnmount()
组件被卸载时调用,可以用于清除定时器、取消网络请求等操作。一旦这个方法执行完毕,该组件将被销毁。
下面是一个使用 componentWillUnmount
的例子:
二、React Hooks 时代的生命周期
1、你以为的生命周期
React 16.8
之前的版本有两种组件:基于类的有状态组件和无状态的函数组件。随着 React 16.8
的发布,引入了 Hooks
,使我们也能够在函数组件中操作状态。
大多数同学在学习
React Hooks
的时候应该都是如下图这样理解函数组件和类组价生命周期的对应关系,大多数文章也仅限于此了,不太会再深究。
具体的例子:
componentDidMount
搞清楚 React Hooks 的“生命周期” 和 各个 React Hooks 的执行时机之后,我们至少可以尝试使用 useRef、useState、useMemo 来模拟 componentWillMount 这个生命周期函数。三、实现 componentWillMount在类组件中,componentWillMount 被认为是 legacy 的(“遗留的”),就是要被干掉的。因为它可能会运行不止一次,而且有一个替代方法 —— constructor。从 React 16.9.0 开始,componentWillMount 被废弃, 适用 UNSAFE_componentWillMount 代替。
componentDidUpdate
componentWillUnmount合在一起
2、模拟 Class Component 生命周期由于官方也没有对函数组件的生命周期做描述,这里我们就自己造点术语,以方便我们对齐类组件的生命周期。
顺便强调一下,函数组件实际上不存在所谓生命周期方法,因为在函数组件中没有这样的东西。另外,接下来的执行流程是基于 “非 StrictMode” 下的。挂载initializerendereffect
更新initializerenderremoveEffecteffect
卸载removeEffectinitialize: 函数组件中没有构造函数。initialize 执行的就是初始化工作。render: 在浏览器中渲染 DOM 或者更新已经在 DOM 中渲染的数据。effect:执行一个副作用。它被定位为 componentDidMount 和 componentDidUpdate 的组合,但严格来说它不是。removeEffect:副作用被清理掉。定位像 componentWillUnmount,但严格来说不是。3、实际的 “生命周期”实际的”生命周期“之所以打引号,是因为,严格来说,React 的生命周期在类组件和函数组件中是不同的概念。官方在新的文档中也并没有函数组件生命周期的描述(我理解是官方想把大家的开发思维方式从原来的类组件切换到函数组件,嘴上说函数组件不是用来替代类组件的,但是身体却很诚实~ 哈哈哈)。把类组件的生命周期的概念强行应用到函数组件上,有点强迫症?但我觉得这是更好地理解 React 的很有效的方式。下面两幅图是国外大神基于 Dan Abramov's tweet 的灵感画出来的 React Hook LifeCycle:hook-flow作者:_你当像鸟飞往你的山链接:https://juejin.cn/post/7218942994467389498来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
react-hooks-lifecycle
react-hooks-lifecycle
基于上面两图的启示,绘制下图中这样一个比较容易理解的流程:
挂载正如上图中看到的,挂载阶段按照下面的顺序执行:首先 react 运行 (惰性初始化程序)第一次渲染React 更新 DOM运行 LayoutEffects浏览器绘制屏幕运行 Effects这里发生了什么? 首先是惰性初始化器,然后 React 进行第一次渲染并更新 DOM,然后 React 运行 LayoutEffects。下一个活动是浏览器屏幕绘制,最后 React 运行 Effects。更新在每次更新时,React都会从由 state 或 props 变化引起的重新渲染开始。现在就没有惰性的初始化调用了。renderReact 更新 DOM清除 LayoutEffects运行 LayoutEffects浏览器绘制屏幕清理 Effects运行 Effects注意,在渲染之后,React 清理了 LayoutEffects,使其紧接着运行。浏览器然后绘制屏幕,之后React清理 Effects 并紧接着运行它。挂载和更新之间的主要区别是:惰性初始化仅在挂载时挂载阶段不存在清理工作卸载在卸载期间,React 清理所有效果:清理 LayoutEffects清理 Effects验证为了证明上面的理论,我们可以看一个代码片段示例。在下面的代码中,我创建了父子组件。父组件有惰性初始化渲染开始日志渲染结束日志useEffects 日志useEffects 清理日志子组件有渲染开始日志渲染结束日志useEffects
你可以看到浏览器日志,是符合我们上面的流程的:
4、React Hooks 的执行时机接下来,我们来看这样一个例子:
jfieh
初次渲染的结果是:
1、基于 useState 的实现使用:
2、基于 useMemo 实现
还可以使用 useMemo
来实现:
是因为
useMemo
不需要实际返回一个值,你也不需要实际使用它,但是因为它根据依赖关系缓存了一个值,而这个依赖关系只运行一次(在依赖为"[]"的情况下),而且当组件挂载时,它在其他东西之前运行一次。但不建议这么做:这可能会使用当前实现,React文档特别说明不要这样操作。你应该将
useMemo
作为性能优化的工具,而不是作为语义保证。
3、基于 useRef 实现
useRef
是在函数组件初始渲染之前就会执行,而且它的值改变不会触发重渲染。
甚至,我们还可以用来防止 useEffect
在挂载的时候执行:
4、基于 useLayoutEffect 实现
useLayoutEffect
在第二个依赖值为空的情况下可以实现跟 componentWillMount
相似的作用。useLayoutEffect
会在第一次页面挂载之前运行第一个函数里的回调。虽然实际上有两个更新,但在绘制到屏幕之前它们是同步的。