面试题:Hooks 与 React 生命周期的关系

React 生命周期很多人都了解,但通常我们所了解的都是 单个组件 的生命周期,但针对 Hooks 组件、多个关联组件(父子组件和兄弟组件) 的生命周期又是怎么样的喃?你有思考和了解过吗,接下来我们将完整的了解 React 生命周期。

关于 组件 ,我们这里指的是 React.Component 以及 React.PureComponent ,但是否包括 Hooks 组件喃?

一、Hooks 组件

函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。

但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useState、 useEffect() 和useLayoutEffect() 。

即:Hooks 组件(使用了Hooks的函数组件)有生命周期,而函数组件(未使用Hooks的函数组件)是没有生命周期的

下面,是具体的 class 与 Hooks 的生命周期对应关系

constructor:函数组件不需要构造函数,我们可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,也可以传一个函数给 useState。

const[num, UpdateNum] = useState(0)

getDerivedStateFromProps:一般情况下,我们不需要使用它,我们可以在渲染过程中更新 state,以达到实现 getDerivedStateFromProps 的目的。

functionScrollView({row}){

let[isScrollingDown, setIsScrollingDown] = useState(false);

let[prevRow, setPrevRow] = useState(null);

if(row !== prevRow) {

// Row 自上次渲染以来发生过改变。更新 isScrollingDown。

setIsScrollingDown(prevRow !==null&& row > prevRow);

setPrevRow(row);

}

return`Scrolling down: ${isScrollingDown}`;

}

React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。

shouldComponentUpdate:可以用 React.memo 包裹一个组件来对它的 props 进行浅比较

constButton = React.memo((props) =>{

// 具体的组件

});

注意:React.memo 等效于 PureComponent,它只浅比较 props。这里也可以使用useMemo 优化每一个节点。

render:这是函数组件体本身。

componentDidMount, componentDidUpdate:useLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用useLayoutEffect。useEffect 可以表达所有这些的组合。

// componentDidMount

useEffect(()=>{

// 需要在 componentDidMount 执行的内容

}, [])

useEffect(()=>{

// 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容

document.title = `You clicked ${count} times`;

return()=>{

// 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)

// 以及 componentWillUnmount 执行的内容       

}// 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关

}, [count]);// 仅在 count 更改时更新

请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便

componentWillUnmount:相当于 useEffect 里面返回的 cleanup 函数

// componentDidMount/componentWillUnmount

useEffect(()=>{

// 需要在 componentDidMount 执行的内容

returnfunctioncleanup(){

// 需要在 componentWillUnmount 执行的内容      

}

}, [])

componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

为方便记忆,大致汇总成表格如下。

class 组件Hooks 组件

constructoruseState

getDerivedStateFromPropsuseState 里面 update 函数

shouldComponentUpdateuseMemo

render函数本身

componentDidMountuseEffect

componentDidUpdateuseEffect

componentWillUnmountuseEffect  里面返回的函数

componentDidCatch无

getDerivedStateFromError无

二、单个组件的生命周期

1. 生命周期

V16.3 之前

我们可以将生命周期分为三个阶段:

挂载阶段

组件更新阶段

卸载阶段

分开来讲:

挂载阶段

constructor:避免将 props 的值复制给 state

componentWillMount

render:react 最重要的步骤,创建虚拟 dom,进行 diff 算法,更新 dom 树都在此进行

componentDidMount

组件更新阶段

componentWillReceiveProps

shouldComponentUpdate

componentWillUpdate

render

componentDidUpdate

卸载阶段

componentWillUnMount

这种生命周期会存在一个问题,那就是当更新复杂组件的最上层组件时,调用栈会很长,如果在进行复杂的操作时,就可能长时间阻塞主线程,带来不好的用户体验,Fiber 就是为了解决该问题而生。

V16.3 之后

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。

对于异步渲染,分为两阶段:

reconciliation:

componentWillMount

componentWillReceiveProps

shouldConmponentUpdate

componentWillUpdate

commit

componentDidMount

componentDidUpdate

其中,reconciliation 阶段是可以被打断的,所以 reconcilation 阶段执行的函数就会出现多次调用的情况,显然,这是不合理的。

所以 V16.3 引入了新的 API 来解决这个问题:

static getDerivedStateFromProps:该函数在挂载阶段和组件更新阶段都会执行,即每次获取新的props 或 state 之后都会被执行在挂载阶段用来代替componentWillMount;在组件更新阶段配合 componentDidUpdate,可以覆盖componentWillReceiveProps 的所有用法。

同时它是一个静态函数,所以函数体内不能访问 this,会根据 nextProps 和prevState 计算出预期的状态改变,返回结果会被送给 setState返回 null 则说明不需要更新 state,并且这个返回是必须的

getSnapshotBeforeUpdate: 该函数会在 render 之后, DOM 更新前被调用,用于读取最新的 DOM 数据。

返回一个值,作为 componentDidUpdate 的第三个参数;配合 componentDidUpdate, 可以覆盖componentWillUpdate 的所有用法。

注意:V16.3 中只用在组件挂载或组件 props 更新过程才会调用,即如果是因为自身 setState 引发或者forceUpdate 引发,而不是由父组件引发的话,那么static getDerivedStateFromProps也不会被调用,在 V16.4 中更正为都调用。

即更新后的生命周期为:

挂载阶段

constructor

static getDerivedStateFromProps

render

componentDidMount

更新阶段

static getDerivedStateFromProps

shouldComponentUpdate

render

getSnapshotBeforeUpdate

componentDidUpdate

卸载阶段

componentWillUnmount

2. 生命周期,误区

误解一:getDerivedStateFromProps 和 componentWillReceiveProps 只会在 props 改变 时才会调用

实际上,只要父级重新渲染,getDerivedStateFromProps 和 componentWillReceiveProps都会重新调用,不管 props 有没有变化。所以,在这两个方法内直接将 props 赋值到 state 是不安全的。

// 子组件

classPhoneInputextendsComponent{

state = {phone:this.props.phone };

handleChange =e=>{

this.setState({phone: e.target.value });

};

render() {

const{ phone } =this.state;

return;

}

componentWillReceiveProps(nextProps) {

// 不要这样做。

// 这会覆盖掉之前所有的组件内 state 更新!

this.setState({ phone: nextProps.phone });

}

}

// 父组件

class App extends Component {

constructor() {

super();

this.state = {

count: 0

};

}

componentDidMount() {

// 使用了 setInterval,

// 每秒钟都会更新一下 state.count

// 这将导致 App 每秒钟重新渲染一次

this.interval = setInterval(

() =>

this.setState(prevState => ({

count: prevState.count + 1

})),

1000

);

}

componentWillUnmount() {

clearInterval(this.interval);

}

render() {

return (

<>

Start editing to see some magic happen :)

This component will re-render every second. Each time it renders, the

text you type will be reset. This illustrates a derived state

anti-pattern.

);

}

}

实例可点击这里查看

当然,我们可以在 父组件App 中 shouldComponentUpdate 比较 props 的 email 是不是修改再决定要不要重新渲染,但是如果子组件接受多个 props(较为复杂),就很难处理,而且shouldComponentUpdate 主要是用来性能提升的,不推荐开发者操作shouldComponetUpdate(可以使用 React.PureComponet)。

我们也可以使用 在 props 变化后修改 state

classPhoneInputextendsComponent{

state = {

phone:this.props.phone

};

componentWillReceiveProps(nextProps) {

// 只要 props.phone 改变,就改变 state

if(nextProps.phone !==this.props.phone) {

this.setState({

phone: nextProps.phone

});

}

}

// ...

}

但这种也会导致一个问题,当 props 较为复杂时,props 与 state 的关系不好控制,可能导致问题

解决方案一:完全可控的组件

functionPhoneInput(props){

return;

}

完全由 props 控制,不派生 state

解决方案二:有 key 的非可控组件

classPhoneInputextendsComponent{

state = {phone:this.props.defaultPhone };

handleChange =event=>{

this.setState({phone: event.target.value });

};

render() {

return;

}

}

defaultPhone={this.props.user.phone}

key={this.props.user.id}

/>

当 key 变化时, React 会创建一个新的而不是更新一个既有的组件

误解二:将 props 的值直接复制给 state

应避免将 props 的值复制给 state

constructor(props) {

super(props);

// 千万不要这样做

// 直接用 props,保证单一数据源

this.state = {phone: props.phone };

}

三、多个组件的执行顺序

1. 父子组件

挂载阶段

分 两个 阶段:

第  阶段,由父组件开始执行到自身的 render,解析其下有哪些子组件需要渲染,并对其中 同步的子组件 进行创建,按 递归顺序 挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并 commit 到 DOM。

第  阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件各自的 componentDidMount,最后触发父组件的。

注意:如果父组件中包含异步子组件,则会在父组件挂载完成后被创建。

所以执行顺序是:

父组件 getDerivedStateFromProps —> 同步子组件 getDerivedStateFromProps —> 同步子组件 componentDidMount —> 父组件 componentDidMount —> 异步子组件 getDerivedStateFromProps —> 异步子组件 componentDidMount

更新阶段

React 的设计遵循单向数据流模型 ,也就是说,数据均是由父组件流向子组件。

第  阶段,由父组件开始,执行

更新到自身的 render,解析其下有哪些子组件需要渲染,并对 子组件 进行创建,按 递归顺序 挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并与已有的 Virtual DOM 树 比较,计算出 Virtual DOM 真正变化的部分 ,并只针对该部分进行的原生DOM操作。

static getDerivedStateFromProps

shouldComponentUpdate

第  阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件以下函数,最后触发父组件的。

React 会按照上面的顺序依次执行这些函数,每个函数都是各个子组件的先执行,然后才是父组件的执行。

所以执行顺序是:

父组件 getDerivedStateFromProps —> 父组件 shouldComponentUpdate —> 子组件 getDerivedStateFromProps —> 子组件 shouldComponentUpdate —> 子组件 getSnapshotBeforeUpdate —>  父组件 getSnapshotBeforeUpdate —> 子组件 componentDidUpdate —> 父组件 componentDidUpdate

getSnapshotBeforeUpdate()

componentDidUpdate()

卸载阶段

componentWillUnmount(),顺序为 父组件的先执行,子组件按照在 JSX 中定义的顺序依次执行各自的方法

注意 :如果卸载旧组件的同时伴随有新组件的创建,新组件会先被创建并执行完render,然后卸载不需要的旧组件,最后新组件执行挂载完成的回调。

2. 兄弟组件

挂载阶段

若是同步路由,它们的创建顺序和其在共同父组件中定义的先后顺序是 一致 的。

若是异步路由,它们的创建顺序和 js 加载完成的顺序一致。

更新阶段、卸载阶段

兄弟节点之间的通信主要是经过父组件(Redux 和 Context 也是通过改变父组件传递下来的 props 实现的),满足React 的设计遵循单向数据流模型, 因此任何两个组件之间的通信,本质上都可以归结为父子组件更新的情况 。

所以,兄弟组件更新、卸载阶段,请参考 父子组件

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,428评论 1 33
  • 组件的生命周期 React中组件也有生命周期,也就是说也有很多钩子函数供我们使用, 组件的生命周期,我们会分为四个...
    解勾股阅读 737评论 0 0
  • 起步 安装官方脚手架: npm install -g create-react-app 创建项目: create-...
    Twoold阅读 1,203评论 0 0
  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 1,005评论 0 1
  • 在React Native中使用组件来封装界面模块时,整个界面就是一个大的组件,开发过程就是不断优化和拆分界面组件...
    朱_源浩阅读 8,942评论 6 38