丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
Vue和React是目前前端最火的两个框架。不管是面试还是工作可以说是前端开发者们都必须掌握的。
今天我们通过对比的方式来学习Vue和React的生命周期这一部分。
本文首先讲述Vue2、Vue3、老版React、新版React的生命周期,然后分析了老版本三个生命周期方法的问题,以及在新版本的替代方案。最后对比总结了Vue和React在生命周期这部分的相同点和不同点。
希望通过这种对比方式的学习能让我们学习的时候印象更深刻,希望能够帮助到大家。
vue2生命周期函数有
引用vue官网的图,各个生命周期函数运行如下
下面我们来重点分析下各个函数。
beforeCreate在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
此函数获取不到DOM元素。
created在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
此函数能获取数据侦听、计算属性、方法、事件/侦听器的回调函数,但是依然获取不到DOM元素。
beforeMount在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
该钩子在服务器端渲染期间不被调用。
mounted在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
此函数能获取数据侦听、计算属性、方法、事件/侦听器的回调函数,还能获取到DOM元素。
beforeUpdate在数据发生改变后,DOM被更新之前被调用。这里适合在现有DOM将要被更新之前访问它,比如移除手动添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行
updated在数据更改导致的虚拟DOM重新渲染和更新完毕之后被调用。
注意,updated不会保证所有的子组件也都被重新渲染完毕。如果你希望等待整个视图都渲染完毕,可以在updated内部使用vm.$nextTick:
updated(){this.$nextTick(function(){//仅在整个视图都被重新渲染完毕之后才会运行的代码})}该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行
beforeDestroy在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
destroyed在卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
该钩子在服务器端渲染期间不被调用
错误传播规则
activated在被keep-alive缓存的组件激活时调用。
deactivated在被keep-alive缓存的组件失活时调用。
下面我们分不同情况来进行详细分析
beforeCreate->created->beforeMount->mounted
beforeUpdate->updated
beforeDestroy->destroyed
父组件beforeCreate->父组件created->父组件beforeMount->子组件beforeCreate->子组件created->子组件beforeMount->子组件mounted->父组件mounted
父组件beforeUpdate->父组件updated
父组件beforeUpdate->子组件beforeUpdate->子组件updated->父组件updated
子组件beforeUpdate->子组件updated
父组件beforeDestroy->子组件beforeDestroy->子组件destroyed->父组件destroyed
引用vue3官网的图,各个生命周期函数运行如下
vue3没有删除vue2选项式写法的生命周期函数,这些都还全部保留并支持。
vue3新增了renderTracked、renderTriggered两个生命周期方法。
vue3中销毁生命周期方法名也发生了变化,由beforeDestroy、destroyed变为beforeUnmount、unmounted,这样是为了更好的与beforeMount、mounted相对应。
vue3写在setup函数中生命周期方法名发生了改变,就是前面多加了on。并且在setup函数中不支持beforeCreate、created。
如果beforeCreate、created以及setup都存在的话,生命周期函数的运行顺序是setup->beforeCreate->created
总结
前面的方法在vue3中除了更改了名称,功能都是没有改变的。所以我们重点说下新增的renderTracked、renderTriggered两个方法。
简单理解就是,首次渲染时,模板里面进行了哪些操作,以及该操作的目标对象和键。
如果有多个属性,这个方法会被触发多次。
我们来看例子
因为模板里面用到了name和user.age所以该方法会被触发两次输出{key:'value',target:RefImpl,type:'get'}和{key:'age',target:{age:27},type:'get'}。因为name是ref定义的,所以key始终是value,并且只是读操作,所以type为get。user是reactive定义的,并且我们只使用了age属性所以key是age并且只是读操作,所以type为get。
简单理解就是,页面更新渲染时,模板里面进行了哪些操作,以及该操作的目标对象和键。
如果有多个属性被修改,这个方法会被触发多次。
setup->created->onBeforeMount->onRenderTracked->onMounted
onRenderTriggered->onBeforeUpdate->onUpdated
onBeforeDestroy->onDestroyed
父组件setup->父组件onBeforeMount->父组件onRenderTracked->子组件setup->子组件onBeforeMount->子组件onRenderTracked->子组件onMounted->父组件onMounted
父组件onRenderTriggered->父组件onBeforeUpdate->父组件onUpdated
父组件onRenderTriggered->父组件onBeforeUpdate->子组件onBeforeUpdate->子组件onUpdated->子组件onUpdated
子组件onRenderTriggered->子组件onBeforeUpdate->子组件onUpdated
父组件onBeforeDestroy->子组件onBeforeDestroy->子组件onDestroyed->父组件onDestroyed
老版本react生命周期函数有
老版本react各个生命周期函数运行如下
constructor初始化阶段运行,只运行一次,用于初始数据。比如state。
constructor(){super()this.state={title:'生命周期函数'}}componentWillMountcomponentWillMount初始化阶段运行。在这里获取不到DOM元素。
componentWillMount初始化阶段运行。在这里可以获取到DOM元素。
异步请求推荐写在该函数中,比如请求后台获取数据。
shouldComponentUpdate(nextProps,nextState)初始化阶段不运行,在组件更新时运行。
如果定义了该方法必须显示返回true或者false,true表示需要更新,false表示不需要更新,常用来做性能优化。如果没定义该方法默认返回true。
接收两个参数,分别是最新的props和最新的state,我们可以利用这点和当前的state或props作比较来决定渲染或者不渲染来进行性能优化。
shouldComponentUpdate(nextProps,nextState){//this.props和this.state还是之前的if(this.props.title!==nextProps.title){returntrue;}if(this.state.name!==nextState.name){returntrue;}returnfalse;}这里多一嘴,我们知道继承PureComponent其实就是帮我们自动实现了shouldComponentUpdate的比较,所以当我们数据没有变化的时候组件不会再次加载。但是需要注意的是PureComponent中shouldComponentUpdate对props做得只是浅层比较,不是深层比较,如果props是一个深层对象,就容易产生问题。
注意当调用forceUpdate是不会进入该生命周期函数的,会直接更新并渲染。
componentWillUpdate(nextProps,nextState)初始化阶段不运行,在组件将要更新时运行。
接收两个参数,分别是最新的props和最新的state,我们可以利用这点和当前的state或props作比较来做些特殊逻辑处理。
componentDidUpdate(prevProps,prevState)初始化阶段不运行,在组件更新完时运行。
接收两个参数,分别是之前的prevProps和之前的prevState。也就是说还可以获取上一次的props和state,可以做一些特殊处理。
componentWillReceiveProps(nextProps)初始化阶段不运行,在组件更新之前运行。
接收一个参数nextProps,表示最新的props。
组件自身state的变更引发的组件更新并不会触发该方法。
在父组件传递props给该组件,并且props发生改变时才会运行。
但是注意,如果父组件导致组件重新渲染,即使props没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。
在这里你可以使用this.props和nextProps作比较来处理一些特殊逻辑。
render渲染方法。在这里主要是书写页面元素和样式。
componentWillUnmount在组件卸载时运行,可以在这里清除一些副作用,比如监听函数。定时器等等。
componentDidCatch(error,errorInfo)在子组件发生错误是被调用。
接收error和errorInfo两个参数,error表示抛出的错误。errorInfo带有componentStackkey的对象,其中包含有关组件引发错误的栈信息。
在这里可以用来把错误上传到服务器做错误日志。
constructor->componentWillMount->render->componentDidMount
shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
componentWillUnmount
父组件constructor->父组件componentWillMount->父组件render->子组件constructor->子组件componentWillMount->子组件render->子组件componentDidMount->父组件componentDidMount
这里不管父组件是否传递props给子组件生命周期函数都如上运行。
父组件shouldComponentUpdate->父组件componentWillUpdate->父组件render->子组件componentWillReceiveProps->子组件shouldComponentUpdate->子组件componentWillUpdate->子组件render->子组件componentDidUpdate->父组件componentDidUpdate
这里不管父组件是否传递props给子组件生命周期函数都如上运行。也就是说父组件更新子组件的componentWillReceiveProps必会被触发。
子组件shouldComponentUpdate->子组件componentWillUpdate->子组件render->子组件componentDidUpdate
父组件componentWillUnmount->子组件componentWillUnmount
新版本主要是React16+,因为在新版本做了很多调整。新版本react生命周期函数有
componentWillMount,componentWillReceiveProps,componentWillUpdate这三个生命周期因为经常会被误解和滥用,所以被称为不安全(不是指安全性,而是表示使用这些生命周期的代码,有可能在未来的React版本中存在缺陷,可能会影响未来的异步渲染)的生命周期。
React16.3版本:为不安全的生命周期引入别名UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps和UNSAFE_componentWillUpdate。(旧的生命周期名称和新的别名都可以在此版本中使用)
React16.3之后的版本:为componentWillMount,componentWillReceiveProps和componentWillUpdate启用弃用警告。(旧的生命周期名称和新的别名都可以在此版本中使用,但旧名称会记录DEV模式警告)
React17.0版本:推出新的渲染方式——异步渲染(AsyncRendering),提出一种可被打断的生命周期,而可以被打断的阶段正是实际dom挂载之前的虚拟dom构建阶段,也就是要被去掉的三个生命周期componentWillMount,componentWillReceiveProps和componentWillUpdate。(从这个版本开始,只有新的“UNSAFE_”生命周期名称将起作用)
总体来说新版本的react生命周期函数就是去除了三个不安全函数(异步渲染可能会有bug)
新增了三个生命周期函数
新版本react各个生命周期函数运行如下
下面我们重点分析这三个新方法
staticgetDerivedStateFromProps(props,state)在组件初始化和组件更新时都会被调用。
接收state和props两个参数,在这里可以通过返回一个对象来更新组件自身的state,或者返回null来表示接收到的props没有变化,不需要更新state。
请注意该方法是一个静态方法,所以该生命周期钩子内部没有this,所以无法通过使用this获取组件实例的属性/方法。
该生命周期钩子的作用:将父组件传递过来的props映射到子组件的state上面,这样组件内部就不用再通过this.props.xxx获取属性值了,统一通过this.state.xxx获取。映射就相当于拷贝了一份父组件传过来的props,作为子组件自己的状态。注意:子组件通过setState更新自身状态时,不会改变父组件的props。
使用场景是若state的值在任何情况下都取决于props的时候使用该方法比较合适。相当于把传进来的props转成了组件的state。
//老版本通过该方法里更新statecomponentWillReceiveProps(nextProps){if(nextProps.translateX!==this.props.translateX){this.setState({translateX:nextProps.translateX,});}}//代替componentWillReceiveProps//通过返回对象来更新statestaticgetDerivedStateFromProps(nextProps,prevState){if(nextProps.translateX!==prevState.translateX){return{translateX:nextProps.translateX,};}returnnull;}getSnapshotBeforeUpdate(prevProps,prevState)getSnapshotBeforeUpdate(prevProps,prevState)在组件更新时被调用。被调用于render之后、更新DOM和refs之前。
此生命周期钩子必须有返回值,返回值将作为componentDidUpdate第三个参数。必须和componentDidUpdate一起使用,否则会报错。
在这里this.props和this.state是最新的,可以和该函数的参数prevProps,prevState作比较,进行逻辑处理。
该生命周期钩子的作用:它能让你在组件更新DOM和refs之前,从DOM中捕获一些信息(例如滚动位置)。
getSnapshotBeforeUpdate(prevProps,prevState){//这里的state和props已经是最新的了//console.log(this.props,this.state);return456;}componentDidUpdate(prevProps,prevState,snapshot){//第三个参数snapshot是getSnapshotBeforeUpdate返回值console.log(snapshot);//456}staticgetDerivedStateFromError(error)staticgetDerivedStateFromError(error)会在后代组件抛出错误后被调用。它将抛出的错误作为参数,并返回一个值以更新state。
可能有人会问,这个跟componentDidCatch方法有什么区别呢?
staticgetDerivedStateFromError(error)在渲染DOM之前调用,当我们遇到子组件出错的时候可以渲染备用UI,常用作错误边界。而componentDidCatch是在DOM渲染完后才会调用,可以用来输出错误信息或上传一些错误报告。
比如我们可以定义一个错误边界组件,在子组件出错的时候显示错误提示,不至于页面不渲染。
classErrorBoundaryextendsReact.Component{constructor(props){super(props);this.state={hasError:false};}staticgetDerivedStateFromError(error){//更新state使下一次渲染可以显降级UIreturn{hasError:true};}render(){if(this.state.hasError){//你可以渲染任何自定义的降级UIreturn
Somethingwentwrong.
;}returnthis.props.children;}}简单来说,如果异常发生在第一阶段(render阶段),React就会调用getDerivedStateFromError,如果异常发生在第二阶段(commit阶段),React会调用componentDidCatch。constructor->getDerivedStateFromProps->render->componentDidMount
getDerivedStateFromProps->shouldComponentUpdate->render->getSnapshotBeforeUpdate->componentDidUpdate
父组件constructor->父组件getDerivedStateFromProps->父组件render->子组件constructor->子组件getDerivedStateFromProps->子组件render->子组件componentDidMount->父组件componentDidMount
父组件getDerivedStateFromProps->父组件shouldComponentUpdate->父组件render->子组件getDerivedStateFromProps->子组件shouldComponentUpdate->子组件render->子组件getSnapshotBeforeUpdate->父组件getSnapshotBeforeUpdate->子组件componentDidUpdate->父组件componentDidUpdate
这里不管父组件是否传递props给子组件生命周期函数都如上运行。也就是说父组件更新子组件的getDerivedStateFromProps必会被触发。
子组件getDerivedStateFromProps->子组件shouldComponentUpdate->子组件render->子组件getSnapshotBeforeUpdate->子组件componentDidUpdate
上面已经介绍了,新版本react删除了componentWillMount、componentWillReceiveProps、componentWillUpdate三个方法。下面我们来说说这三个为什么会被删除,以及在新版生命周期函数中用什么来替代。
根据ReactFiber的设计,一个组件的渲染被分为两个阶段:第一个阶段(也叫做render阶段)是可以被React打断的,一旦被打断,这阶段所做的所有事情都被废弃,当React处理完紧急的事情回来,依然会重新渲染这个组件,这时候第一阶段的工作会重做一遍;第二个阶段叫做commit阶段,一旦开始就不能中断,也就是说第二个阶段的工作会稳稳当当地做到这个组件的渲染结束。
两个阶段的分界点,就是render函数。render函数之前的所有生命周期函数(包括render)都属于第一阶段,之后的都属于第二阶段。
开启异步渲染,虽然我们获得了更好的感知性能,但是考虑到第一阶段的的生命周期函数可能会被重复调用,不得不对历史代码做一些调整。
所以删除这三个生命周期方法的主要原因还是因为可能会重复调用,带来一些意想不到的后果。
但事实上在componentWillMount执行后,第一次渲染就已经开始了,所以如果在componentWillMount执行时还没有获取到异步数据的话,页面首次渲染时也仍然会处于没有异步数据的状态。换句话说,组件在首次渲染时总是会处于没有异步数据的状态,所以不论在哪里发送数据请求,都无法直接解决这一问题。
前面说了componentWillMount就可能被中途打断,中断之后渲染又要重做一遍,想一想,在componentWillMount中做AJAX调用,代码里看到只有调用一次,但是实际上可能调用N多次,这明显不合适。相反,若把AJAX放在componentDidMount,因为componentDidMount在第二阶段,所以绝对不会多次重复调用,这才是AJAX合适的位置。
将现有componentWillMount中的代码迁移至componentDidMount即可。
类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的Tab显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个Tab。
一个简单的例子如下:
//beforecomponentWillReceiveProps(nextProps){if(nextProps.translateX!==this.props.translateX){this.setState({translateX:nextProps.translateX,});}}//afterstaticgetDerivedStateFromProps(nextProps,prevState){if(nextProps.translateX!==prevState.translateX){return{translateX:nextProps.translateX,};}returnnull;}乍看下来这二者好像并没有什么本质上的区别,但这却是笔者认为非常能够体现React团队对于软件工程深刻理解的一个改动,即React团队试图通过框架级别的API来约束或者说帮助开发者写出可维护性更佳的JavaScript代码。为了解释这点,我们再来看一段代码:
将现有componentWillReceiveProps中的代码根据更新state或回调,分别在getDerivedStateFromProps及componentDidUpdate中进行相应的重写即可。
与componentWillReceiveProps类似,许多开发者也会在componentWillUpdate中根据props的变化去触发一些回调。但不论是componentWillReceiveProps还是componentWillUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。
与componentDidMount类似,componentDidUpdate也不存在这样的问题,一次更新中componentDidUpdate只会被调用一次,所以将原先写在componentWillUpdate中的回调迁移至componentDidUpdate就可以解决这个问题。
componentWillUpdate是在组件更新前被调用,读取当前某个DOM元素的状态,并在componentDidUpdate中进行相应的处理。但在React开启异步渲染模式后,render阶段和commit阶段之间并不是无缝衔接的,也就是说在render阶段读取到的DOM元素状态并不总是和commit阶段相同,这就导致在componentDidUpdate中使用componentWillUpdate中读取到的DOM元素状态是不准确的。
与componentWillUpdate不同,getSnapshotBeforeUpdate会在最终的render之后被调用(但是此时还没有真正更新真实DOM),也就是说在getSnapshotBeforeUpdate中读取到的DOM元素状态与之前的肯定是一样的。
getSnapshotBeforeUpdate返回一个值。这个值会被最为第三个参数被传入到componentDidUpdate中,然后我们就可以在componentDidUpdate中去更新组件的状态,而不是在getSnapshotBeforeUpdate中直接更新组件状态。
官方提供的一个例子如下:
classScrollingListextendsReact.Component{listRef=null;getSnapshotBeforeUpdate(prevProps,prevState){//Areweaddingnewitemstothelist//Capturethescrollpositionsowecanadjustscrolllater.//if(prevProps.list.length
如果触发某些回调函数时需要用到DOM元素的状态,则将对比或计算的过程迁移至getSnapshotBeforeUpdate,把计算值返回出来。然后在componentDidUpdate通过第三个参数获取到值,然后统一触发回调或更新状态。
Vue和React的生命周期函数我们都介绍完了,下面我们来总结下它们的相同点和不同点。