当前位置: 首页 > Web前端 > JavaScript

深入理解React:理解React生命周期

时间:2023-03-27 14:11:40 JavaScript

前言如果说JSX是学习React框架必须了解的概念,那么“生命周期”就是第二个需要学习的内容。虽然最新的React版本推荐使用函数组件结合钩子来组织应用,淡化了函数组件中生命周期的引入,但是学习和理解类组件的生命周期可以帮助我们追根溯源。建立更全面的React框架视图,同时从底层理解React的两个重要阶段:render和commit,帮助我们写出正确高效的代码。本次分享将从以下几个问题开始:什么是生命周期?类组件的生命周期功能有哪些,在什么场景下使用?生命周期函数进化的原因是什么?Hooks和生命周期函数的对应关系是什么?React中的生命周期是什么?用于描述组件挂载(创建)、更新(存在)、卸载(销毁)三个阶段。基本上,我们每个新同学都会被要求通读React官网的资料,以此建立一个React框架的粗略全息图。在这个过程中,你应该能注意到React一直在反复提到两个关键词:virtualDOM和components。在前面理解JSX的过程中,我们知道虚拟DOM的本质是通过编译JSX得到的JavaScript对象形式的DOM结构描述。在组件初始化阶段,会通过生命周期方法render生成虚拟DOM节点,然后调用ReactDOM.render方法完成从虚拟DOM节点到真实DOM节点的转换。在组件更新阶段,会再次调用render方法生成新的虚拟DOM节点,然后使用Diffing算法比较两个虚拟DOM节点的差异,找出变化的部分,实现最小DOM更新。所以也可以说虚拟DOM是React核心算法Diffing的基石。组件化是一种优秀的软件设计思想,在React框架中得到了很好的体现。React项目中的基本单元基本上都是组件,而应用程序是由各种组件组合而成的。每个组件既是封闭的又是开放的。封闭性体现在组件有自己的一套渲染逻辑(state),在没有数据流交互的情况下,组件之间不会相互干扰。开放性体现在组件之间的通信,基于单向数据流(props)的原则,数据通信会影响渲染结果。通过数据的桥梁,组件之间相互开放,相互影响。封闭和开放的特性使得React组件具有高度的可维护性和可重用性。虚拟DOM节点的生成依赖于render方法,组件化中的渲染工作流程(从组件数据变化到组件实际更新的过程)也离不开render方法。可见render方法的重要性。综上所述,生命周期是虚拟DOM实现的基石,也是React组件化软件思想的直接体现。生命周期功能支持具有封闭和开放特性的组件。组件生命周期概览很多文章和教程介绍了类组件的生命函数,在最新的React中都有讲解。这样做的好处是初学者可以快速掌握最新的API,然后马上投入开发。但同时也会造成初学者停留在“能用”的阶段,离“会用”还有一段距离。在本文中,我们从Reactv15开始,然后进入v16.3和v16.4来比较几个版本之间的差异。这个方法不是让大家多背几个API,而是找出不同点,提出Question,让我们在下一节搞清楚为什么要完善生命周期功能。与直接学习API相比,这种方法无非是多花几分钟思考,好处是帮助我们从“能用”走向“会用”。Reactv15中的生命周期函数主要如下图所示:()componentWillUnmount()//挂载:constructor->componentWillMount->render->componentDidMount//update(由父组件触发):componentWillReceiveProps->shouldComponentUpdate(true)->componentWillUpdate->render->componentDidUpdate//update(在组件内部触发):shouldComponentUpdate(true)->componentWillUpdate->render->componentDidUpdate//Unmount:componentWillUnmountReact早期也可以使用React.createClass()方法创建组件。在这种情况下,有两个生命周期函数getDefaultProps和getInitState。ES6流行之后,这种创建组件的方式就不推荐了,这里就不做过多的补充了。关于生命周期函数,这里有几点需要特别注意:shouldComponentUpdate方法可以指定一个布尔类型的返回值。如果该方法的返回值为false,则可以跳过更新,不会触发后续的生命周期方法componentWillReceiveProps。传递的props发生变化,但是只要父组件重新渲染(re-rendered),那么子组件的componentWillReceiveProps就会被执行。componentDidUpdate生命周期函数的两个参数与其他生命周期函数不同,传入的不是nextProps和nextState,而是prevProps和prevState。当前的道具和状态需要从这个对象中获取。componentWillUnmount将在组件被销毁时执行。一般情况下,组件会在两种情况下被销毁:一种??是从父组件中移除,另一种是为组件设置了key值,父组件在render过程中发现key与之前的不一致,那么这个组件也会被销毁,然后重新初始化,并重置键值。这里就不对每个生命周期函数进行解释了,更多的细节可以点击下方阅读原文,自己查看React官网了解,或者通过这个例子来加强理解:React15LifecycleDemo。PS:如果在打开示例时无法正常预览示例,报错:TargetcontainerisnotaDOMelement,不要慌,这是codesandbox的问题,这时候你可以打开任何源代码文件左边,然后保存,预览区就可以恢复正常了。从Reactv16开始,对生命周期功能做了一些改动,分为两个版本:v16.3及之前版本,v16.4及之后版本React生命周期查看在线地址:https://projects.wojtekmaj.pl...constructor(props)staticgetDerivedStateFromProps(props,state)shouldComponentUpdate(nextProps,nextState)getSnapshotBeforeUpdate(prevProps,prevState)componentDidMount()componentDidUpdate(prevProps,prevState,snapshot)render()componentWillUnmount()//构造函数->getDerivedStateFromProps(null)->render->componentDidMount//Update(由父组件触发):getDerivedStateFromProps(null)->shouldComponentUpdate(true)->getSnapshotBeforeUpdate->render->componentDidUpdate//Update(在组件内部触发)):getDerivedStateFromProps(null)->shouldComponentUpdate(true)->getSnapshotBeforeUpdate->render->componentDidUpdate//Uninstall:componentWillUnmountReactv16相比v15有很多变化(chan16.3-4之间ges小),主要有以下几个方面:componentWillMount和componentWillUpdate和componentWillReceiveProps添加了getDerivedStateFromProps和getSnapshotBeforeUpdate添加了getDerivedStateFromError和componentDidCatch错误处理函数虽然这两个新方法在触发顺序上与废弃方法大致相同,但不能简单地认为是新方法替代旧方法。getDerivedStateFromProps方法的目的不是替换componentWillMount,而是替换componentWillReceiveProps。这个方法是一个静态方法(static)。静态方法不依赖于组件实例,所以方法内部无法读取this对象,应该返回一个新对象,或者null值。它存在的目的是并且只有一个:使用props来推导/更新状态,所有不以此为目的的使用在原则上都是错误的。getDerivedStateFromProps不仅在update阶段会被调用,在mount阶段也会被调用,因为推导state的需求不仅在update的时候有,在初始化state的时候也有。通过该方法派生状态不会导致render函数重复执行。从这个角度来看,这个方法的出现并不是简单的替换逻辑,而是承载着简化代码的期待。getSnapshotBeforeUpdate方法提供了读取当前DOM的一些信息的机会,并将返回值赋值给componentDidUpdate方法的snapshot参数,主要用于处理UI显示,比如某些区域的滚动位置信息。你可以通过这个例子来加强你对新的生命周期功能的理解:React16LifecycleDemo。生命周期函数进化的原因我们大致知道了componentWillMount这个方法被废弃的原因,因为这个方法真的没什么用。但是为什么使用getDerivedStateFromProps而不是componentWillReceiveProps,除了简化了获取状态的代码之外,还有其他原因吗?原来componentWillReceiveProps方法只是在update阶段才会被调用,在这个函数中调用setState方法更新state会导致额外的re-renders,处理不当可能会导致大量无用的re-renders。与componentWillReceiveProps相比,getDerivedStateFromProps不做加法,而是做减法。React正在推广仅使用getDerivedStateFromProps完成props到state映射的最佳实践,确保生命周期函数的行为更加可控和可预测。它帮助开发者避免不合理的编程方式,同时也在为新的Fiber架构铺路。getSnapshotBeforeUpdate结合componentDidUpdate方法可以覆盖componentWillUpdate的所有使用场景,那么放弃componentWillUpdate的原因是不是要换个方法呢?其实根本原因在于componentWillUpdate方法是Fiber架构实现的绊脚石,不得不舍弃。Fiber是Reactv16中对React核心算法的重写。简单理解就是Fiber会将原来的同步渲染过程变成增量渲染模式。在Reactv16之前,每次触发组件更新时,都会构建一个新的虚拟DOM树,通过与上一个虚拟DOM树的Diff比较,实现对真实DOM的定向更新。整个过程是递归的(想想React应用的组织形式),同步渲染的递归调用栈很深(代码写得不好很容易造成栈溢出),而且只有底层调用returns,整个渲染过程会逐层返回。这个漫长的更新过程是不间断的。一旦同步渲染开始,主线程(JavaScript解析和执行)就会被占用,直到递归完全完成。在此期间,浏览器除了渲染之外没有办法处理任何事情(例如响应用户事件)。这个问题对于大型React应用程序来说是不可接受的。Reactv16中的Fiber架构就是为了解决这个问题而提出的:Fiber会将一个大的更新任务分解成许多小的任务。每个小任务执行完后,渲染进程会返回(释放)主线程,看看是否有其他优先级更高的任务(用户事件响应等)需要处理,如果有,则执行高-优先任务。如果没有,请继续执行其余的小任务。这样就避免了主线程被长期独占,从而避免了应用卡顿的问题。这种可以中断的渲染过程称为异步渲染。Fiber带来了两个重要的特性:任务拆卸和渲染过程可以被中断。关于可中断性,并不是说任何一个环节都可以被中断再重新执行,可中断性的时机也不同。按照是否可以中断的标准,Reactv16的生命周期分为render和commit两个阶段(commit又细分为pre-commit和commit)。renderphase:pureandhasnosideeffects,canbesuspended,terminatedorrestartedbyReactpre-commitphase:canreadDOMcommitphase:canconsumeDOM,runsideeffects,scheduleupdatesInterrupted,commitphase始终同步执行。之所以确定这样的标准,也是深思熟虑的。render阶段的所有操作一般都是不可见的,所以重复中断和重新执行用户是感觉不到的,commit阶段才是真正的操作。如果DOM操作反复中断并重新执行,会导致UI界面多次改变渲染,这是必须要避免的问题。了解完Fiber架构的执行机制,我们再回过头来看看被废弃的生命周期函数:而如果开发者在这些函数中运行副作用(或操作DOM),那么副作用函数可能会被执行多次,从而导致意想不到的严重错误。最后梳理一下React生命周期函数演进背后的逻辑:为Fiber架构的实现扫清障碍,引入增量渲染机制解决同步渲染带来的应用卡顿风险,防止开发者滥用生命周期函数丢弃和改进API。强制最佳实践(每个值都有且只有一个明确来源)可以参考生命周期函数滥用:**你可能不需要使用派生状态*https://zh-hans.reactjs.org/b...*Hooks和生命周期函数生命周期函数只存在于类组件中。对于Hooks之前的函数组件,没有组件生命周期的概念(函数组件除了render没有其他过程),但是Hooks之后,问题就变得有些复杂了。Hooks使函数组件具有使用和管理状态的能力,从而演化出函数组件生命周期的概念(除了render之外还增加了其他进程)。主要涉及几个Hook:useState、useMemo、useEffect。更全面的Hooks介绍可以复制查看:https://zh-hans.reactjs.org/d...生命周期方法与Hooks的对应关系:https://zh-hans.reactjs.org/d。..总的来说,大部分生命周期都可以用Hook来模拟,而一些难以模拟的往往是React不推荐的反模式。至于为什么要设计Hook,为什么要赋予函数组件使用和管理状态的能力,React官网也对Hook进行了深入细致的介绍。总结起来有以下几点:分离和复用组件的状态逻辑(Mixin、高阶组件、渲染回调模式等)复杂组件变得难以理解(越来越多的状态和副作用,生命周期函数滥用)无法理解thispointing(bindsyntax)inclasscomponents难以进一步优化(组件预编译,压缩不好,hot-reload不稳定)综上所述,文章前面提出的几个问题都得到了深入的解答,并且在寻求答案的过程中,我也对React框架有了更立体的认识。希望这篇文章能帮助你从“能用”到“能用”迈出几步。也欢迎大家在评论区写下自己的想法或者问题,方便我们进一步交流讨论。参考State&Lifecycle:https://zh-hans.reactjs.org/d...Components&Props:https://zh-hans.reactjs.org/d...React.Component:https://zh-hans.reactjs.org/d...Hook介绍:https://zh-hans.reactjs.org/d...Hook概述:https://zh-hans.reactjs.org/d...HookAPI索引:https://zh-hans.reactjs.org/d...渲染和提交(测试版):https://beta.reactjs.org/lear...