简介
Vue
和React
是目前前端最火的两个框架。不管是面试还是工作可以说是前端开发者们都必须掌握的。
今天我们通过对比的方式来学习Vue
和React
的生命周期这一部分。
本文首先讲述Vue2
、Vue3
、老版React
、新版React
的生命周期,然后分析了老版本三个生命周期方法的问题,以及在新版本的替代方案。最后对比总结了Vue
和React
在生命周期这部分的相同点和不同点。
希望通过这种对比方式的学习能让我们学习的时候印象更深刻,希望能够帮助到大家。
Vue2
vue2
生命周期函数有
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed
errorCaptured
activated
deactivated
引用vue
官网的图,各个生命周期函数运行如下
下面我们来重点分析下各个函数。
Vue2生命周期函数分析
beforeCreate
beforeCreate
在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
此函数获取不到DOM
元素。
created
created
在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
此函数能获取数据侦听、计算属性、方法、事件/侦听器的回调函数,但是依然获取不到DOM
元素。
beforeMount
beforeMount
在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
该钩子在服务器端渲染期间不被调用。
mounted
mounted
在组件初始化的时候会运行,只会运行一次。可以在此函数里面调用后台接口获取数据。
此函数能获取数据侦听、计算属性、方法、事件/侦听器的回调函数,还能获取到DOM
元素。
该钩子在服务器端渲染期间不被调用。
beforeUpdate
beforeUpdate
在数据发生改变后,DOM
被更新之前被调用。这里适合在现有 DOM
将要被更新之前访问它,比如移除手动添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行
updated
updated
在数据更改导致的虚拟 DOM
重新渲染和更新完毕之后被调用。
注意,updated
不会保证所有的子组件也都被重新渲染完毕。如果你希望等待整个视图都渲染完毕,可以在 updated
内部使用 vm.$nextTick
:
updated() { this.$nextTick(function () { // 仅在整个视图都被重新渲染完毕之后才会运行的代码 })}
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行
beforeDestroy
beforeDestroy
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
该钩子在服务器端渲染期间不被调用。
destroyed
destroyed
在卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
该钩子在服务器端渲染期间不被调用
errorCaptured
errorCaptured(err: Error, instance: Component, info: string)
在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false
以阻止该错误继续向上传播。
你可以在此钩子中修改组件的状态。因此在捕获错误时,在模板或渲染函数中有一个条件判断来绕过其它内容就很重要;不然该组件可能会进入一个无限的渲染循环。
错误传播规则
默认情况下,如果全局的 config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。
如果一个组件的继承链或父级链中存在多个 errorCaptured
钩子,则它们将会被相同的错误逐个唤起。
如果此 errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 config.errorHandler
。
一个 errorCaptured
钩子能够返回 false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured
钩子和全局的 config.errorHandler
。
activated
activated
在被 keep-alive
缓存的组件激活时调用。
该钩子在服务器端渲染期间不被调用。
deactivated
deactivated
在被 keep-alive
缓存的组件失活时调用。
该钩子在服务器端渲染期间不被调用。
Vue2周期函数调用顺序
下面我们分不同情况来进行详细分析
单组件初始化
beforeCreate -> created -> beforeMount -> mounted
单组件更新
beforeUpdate -> updated
单组件卸载
beforeDestroy -> destroyed
父子组件初始化
父组件beforeCreate -> 父组件created -> 父组件beforeMount -> 子组件beforeCreate -> 子组件created -> 子组件beforeMount -> 子组件mounted -> 父组件mounted
父组件更新data,此data没传递给子组件
父组件beforeUpdate -> 父组件updated
父组件更新data,此data传递给了子组件
父组件beforeUpdate -> 子组件beforeUpdate -> 子组件updated -> 父组件updated
子组件更新data
子组件beforeUpdate -> 子组件updated
父子组件卸载
父组件beforeDestroy -> 子组件beforeDestroy -> 子组件destroyed -> 父组件destroyed
Vue3
引用vue3
官网的图,各个生命周期函数运行如下
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
总结
beforeCreate
beforeCreate
无
created
created
无
beforeMount
beforeMount
onBeforeMount
mounted
mounted
onMounted
beforeUpdate
beforeUpdate
onBeforeUpdate
updated
updated
onUpdated
beforeDestroy
beforeUnmount
onBeforeUnmount
destroyed
unmounted
onUnmounted
errorCaptured
errorCaptured
onErrorCaptured
无
renderTracked
onRenderTracked
无
renderTriggered
onRenderTriggered
activated
activated
onActivated
deactivated
deactivated
onDeactivated
前面的方法在vue3
中除了更改了名称,功能都是没有改变的。所以我们重点说下新增的 renderTracked
、renderTriggered
两个方法。
renderTracked
简单理解就是,首次渲染时,模板里面进行了哪些操作,以及该操作的目标对象和键。
如果有多个属性,这个方法会被触发多次。
我们来看例子
<template> <div> <div>{{ name }}</div> <div>user: {{ user.age }}</div> </div></template><script>import { defineComponent, onRenderTracked, onRenderTriggered, ref, reactive,} from "vue";export default defineComponent({ setup() { const name = ref("randy"); const user = reactive({ age: 27 }); onRenderTracked(({ key, target, type }) => { console.log("onRenderTracked", { key, target, type }); }); onRenderTriggered(({ key, target, type }) => { console.log("onRenderTriggered", { key, target, type }); }); return { name, user, }; },});</script>
页面首次加载只会触发onRenderTracked
方法。
因为模板里面用到了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
。
renderTriggered
简单理解就是,页面更新渲染时,模板里面进行了哪些操作,以及该操作的目标对象和键。
如果有多个属性被修改,这个方法会被触发多次。
我们来看例子
<template> <div> <div>{{ name }}</div> <button @click="changeName">changeName</button> <div>user: {{ user.age }}</div> </div></template><script>import { defineComponent, onRenderTracked, onRenderTriggered, ref, reactive,} from "vue";export default defineComponent({ setup() { const name = ref("randy"); const changeName = () => { name.value = "demi"; }; const user = reactive({ age: 27 }); onRenderTracked(({ key, target, type }) => { console.log("onRenderTracked", { key, target, type }); }); onRenderTriggered(({ key, target, type }) => { console.log("onRenderTriggered", { key, target, type }); }); return { name, changeName, user, }; },});</script>
我们点击changeName
按钮来修改name,这里只会触发onRenderTriggered
方法一次。并且输出{key: 'value', target: RefImpl, type: 'set'}
,因为是修改所以type
是set
。
Vue3周期函数调用顺序
下面我们分不同情况来进行详细分析
单组件初始化
setup -> created -> onBeforeMount -> onRenderTracked -> onMounted
单组件更新
onRenderTriggered-> onBeforeUpdate -> onUpdated
单组件卸载
onBeforeDestroy -> onDestroyed
父子组件初始化
父组件setup -> 父组件onBeforeMount -> 父组件onRenderTracked -> 子组件setup -> 子组件onBeforeMount -> 子组件onRenderTracked -> 子组件onMounted -> 父组件onMounted
父组件更新data,此data没传递给子组件
父组件onRenderTriggered-> 父组件onBeforeUpdate ->父组件 onUpdated
父组件更新data,此data传递给了子组件
父组件onRenderTriggered -> 父组件onBeforeUpdate -> 子组件onBeforeUpdate -> 子组件onUpdated -> 子组件onUpdated
子组件更新data
子组件onRenderTriggered -> 子组件onBeforeUpdate -> 子组件onUpdated
父子组件卸载
父组件onBeforeDestroy -> 子组件onBeforeDestroy -> 子组件onDestroyed -> 父组件onDestroyed
老版本React
老版本react
生命周期函数有
constructor
componentWillMount
componentDidMount
shouldComponentUpdate
componentWillUpdate
componentDidUpdate
componentWillReceiveProps
render
componentWillUnmount
componentDidCatch
老版本react
各个生命周期函数运行如下
老版本React生命周期函数分析
constructor
constructor
初始化阶段运行,只运行一次,用于初始数据。比如state
。
constructor() { super() this.state = {title: '生命周期函数'}}
componentWillMount
componentWillMount
初始化阶段运行,只运行一次。在这里获取不到DOM
元素。
componentDidMount
componentWillMount
初始化阶段运行,只运行一次。在这里可以获取到DOM
元素。
异步请求推荐写在该函数中,比如请求后台获取数据。
shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate(nextProps, nextState)
初始化阶段不运行,在组件更新时运行。
如果定义了该方法必须显示返回true
或者false
,true
表示需要更新,false
表示不需要更新,常用来做性能优化。
接收两个参数,分别是最新的props
和最新的state
,我们可以利用这点和当前的state
或props
作比较来决定渲染或者不渲染来进行性能优化。
shouldComponentUpdate(nextProps, nextState) { // this.props和this.state还是之前的 if (this.props.title !== nextProps.title) { return true; } if (this.state.name !== nextState.name) { return true; } return false;}
componentWillUpdate(nextProps, nextState)
componentWillUpdate(nextProps, nextState)
初始化阶段不运行,在组件将要更新时运行。
接收两个参数,分别是最新的props
和最新的state
,我们可以利用这点和当前的state
或props
作比较来做些特殊逻辑处理。
componentDidUpdate(prevProps, prevState)
componentDidUpdate(prevProps, prevState)
初始化阶段不运行,在组件更新完时运行。
接收两个参数,分别是之前的prevProps
和之前的prevState
。也就是说还可以获取上一次的props
和state
,可以做一些特殊处理。
componentWillReceiveProps(nextProps)
componentWillReceiveProps(nextProps)
初始化阶段不运行,在组件更新之前运行。
接收一个参数nextProps
,表示最新的props
。
组件自身state
的变更引发的组件更新并不会触发该方法。
在父组件传递props
给该组件,并且props
发生改变时才会运行。
但是注意,如果父组件导致组件重新渲染,即使 props
没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。
在这里你可以使用this.props
和nextProps
作比较来处理一些特殊逻辑。
render
render
渲染方法。在这里主要是书写页面元素和样式。
componentWillUnmount
componentWillUnmount
在组件卸载时运行,可以在这里清除一些副作用,比如监听函数。定时器等等。
componentDidCatch(error, errorInfo)
componentDidCatch(error, errorInfo)
在子组件发生错误是被调用。
接收error
和errorInfo
两个参数,error
表示抛出的错误。 errorInfo
带有 componentStack
key 的对象,其中包含有关组件引发错误的栈信息。
在这里可以用来把错误上传到服务器做错误日志。
老版本React周期函数调用顺序
下面我们分不同情况来进行详细分析
单组件初始化
constructor -> componentWillMount -> render -> componentDidMount
单组件更新state
shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
单组件卸载
componentWillUnmount
父子组件初始化
父组件constructor -> 父组件componentWillMount -> 父组件render -> 子组件constructor -> 子组件componentWillMount -> 子组件render -> 子组件componentDidMount -> 父组件componentDidMount
这里不管父组件是否传递props给子组件生命周期函数都如上运行。
父组件更新state
父组件shouldComponentUpdate -> 父组件componentWillUpdate -> 父组件render -> 子组件componentWillReceiveProps -> 子组件shouldComponentUpdate -> 子组件componentWillUpdate -> 子组件render -> 子组件componentDidUpdate -> 父组件componentDidUpdate
这里不管父组件是否传递props
给子组件生命周期函数都如上运行。也就是说父组件更新子组件的componentWillReceiveProps
必会被触发。
子组件更新state
子组件shouldComponentUpdate -> 子组件componentWillUpdate -> 子组件render -> 子组件componentDidUpdate
父子组件卸载
父组件componentWillUnmount -> 子组件componentWillUnmount
新版本React
新版本主要是React 16+
,因为在新版本做了很多调整。新版本react
生命周期函数有
constructor
componentDidMount
static getDerivedStateFromProps
shouldComponentUpdate
getSnapshotBeforeUpdate
componentDidUpdate
render
componentWillUnmount
componentDidCatch
getDerivedStateFromError
版本说明
componentWillMount
,componentWillReceiveProps
,componentWillUpdate
这三个生命周期因为经常会被误解和滥用,所以被称为 不安全(不是指安全性,而是表示使用这些生命周期的代码,有可能在未来的 React 版本中存在缺陷,可能会影响未来的异步渲染) 的生命周期。
React 16.3 版本:为不安全的生命周期引入别名 UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
和 UNSAFE_componentWillUpdate
。(旧的生命周期名称和新的别名都可以在此版本中使用)
React 16.3 之后的版本:为 componentWillMount
,componentWillReceiveProps
和 componentWillUpdate
启用弃用警告。(旧的生命周期名称和新的别名都可以在此版本中使用,但旧名称会记录DEV模式警告)
React 17.0 版本: 推出新的渲染方式——异步渲染( Async Rendering),提出一种可被打断的生命周期,而可以被打断的阶段正是实际 dom
挂载之前的虚拟 dom
构建阶段,也就是要被去掉的三个生命周期 componentWillMount
,componentWillReceiveProps
和 componentWillUpdate
。(从这个版本开始,只有新的“UNSAFE_”生命周期名称将起作用)
总体来说新版本的react
生命周期函数就是去除了三个不安全函数
componentWillMount
componentWillReceiveProps
componentWillUpdate
新增了三个生命周期函数
static getDerivedStateFromProps
getSnapshotBeforeUpdate
static getDerivedStateFromError
新版本react
各个生命周期函数运行如下
下面我们重点分析这三个新方法
新生命周期函数分析
static getDerivedStateFromProps(props, state)
static getDerivedStateFromProps(props, state)
在组件初始化和组件更新时都会被调用。
接收state
和props
两个参数,在这里可以通过返回一个对象来更新组件自身的state
,或者返回 null
来表示接收到的 props
没有变化,不需要更新 state
。
请注意该方法是一个静态方法,所以该生命周期钩子内部没有this
,所以无法通过使用 this
获取组件实例的属性/方法。
该生命周期钩子的作用: 将父组件传递过来的 props
映射 到子组件的 state
上面,这样组件内部就不用再通过 this.props.xxx
获取属性值了,统一通过 this.state.xxx
获取。映射就相当于拷贝了一份父组件传过来的 props
,作为子组件自己的状态。注意:子组件通过 setState
更新自身状态时,不会改变父组件的 props
// 老版本通过该方法里更新statecomponentWillReceiveProps(nextProps) { if (nextProps.translateX !== this.props.translateX) { this.setState({ translateX: nextProps.translateX, }); } }// 代替componentWillReceiveProps// 通过返回对象来更新statestatic getDerivedStateFromProps(nextProps, prevState) { if (nextProps.translateX !== prevState.translateX) { return { translateX: nextProps.translateX, }; } return null;}
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); return 456;}componentDidUpdate(prevProps, prevState, snapshot) { // 第三个参数 snapshot 是 getSnapshotBeforeUpdate返回值 console.log(snapshot); // 456}
static getDerivedStateFromError(error)
static getDerivedStateFromError(error)
会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
。
可能有人会问,这个跟componentDidCatch
方法有什么区别呢?
static getDerivedStateFromError(error)
在渲染DOM
之前调用,当我们遇到子组件出错的时候可以渲染备用UI
,常用作错误边界。而componentDidCatch
是在DOM
渲染完后才会调用,可以用来输出错误信息或上传一些错误报告。
比如我们可以定义一个错误边界组件,在子组件出错的时候显示错误提示,不至于页面不渲染。
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染可以显降级 UI return { hasError: true }; } render() { if (this.state.hasError) { // 你可以渲染任何自定义的降级 UI return <h1>Something went wrong.</h1>; } return this.props.children; }}
新版本React周期函数调用顺序
下面我们分不同情况来进行详细分析
单组件初始化
constructor -> getDerivedStateFromProps -> render -> componentDidMount
单组件更新state
getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate
单组件卸载
componentWillUnmount
父子组件初始化
父组件constructor -> 父组件getDerivedStateFromProps -> 父组件render -> 子组件constructor -> 子组件getDerivedStateFromProps -> 子组件render -> 子组件componentDidMount -> 父组件componentDidMount
父组件更新state
父组件getDerivedStateFromProps -> 父组件shouldComponentUpdate -> 父组件render -> 子组件getDerivedStateFromProps -> 子组件shouldComponentUpdate -> 子组件render -> 子组件getSnapshotBeforeUpdate -> 父组件getSnapshotBeforeUpdate -> 子组件componentDidUpdate -> 父组件componentDidUpdate
这里不管父组件是否传递props
给子组件生命周期函数都如上运行。也就是说父组件更新子组件的getDerivedStateFromProps
必会被触发。
子组件更新state
子组件getDerivedStateFromProps -> 子组件shouldComponentUpdate -> 子组件render -> 子组件getSnapshotBeforeUpdate -> 子组件componentDidUpdate
父子组件卸载
父组件componentWillUnmount -> 子组件componentWillUnmount
老版本React生命周期问题
上面已经介绍了,新版本react删除了componentWillMount
、componentWillReceiveProps
、componentWillUpdate
三个方法。下面我们来说说这三个为什么会被删除,以及在新版生命周期函数中用什么来替代。
componentWillMount
首屏无数据导致白屏
在 React
应用中,许多开发者为了避免第一次渲染时页面因为没有获取到异步数据导致的白屏,而将数据请求部分的代码放在了 componentWillMount
中,希望可以避免白屏并提早异步请求的发送时间。
但事实上在 componentWillMount
执行后,第一次渲染就已经开始了,所以如果在 componentWillMount
执行时还没有获取到异步数据的话,页面首次渲染时也仍然会处于没有异步数据的状态。换句话说,组件在首次渲染时总是会处于没有异步数据的状态,所以不论在哪里发送数据请求,都无法直接解决这一问题。而关于提早发送数据请求,官方也鼓励将数据请求部分的代码放在组件的 constructor
中,而不是 componentWillMount
。
事件订阅
另一个常见的用例是在 componentWillMount
中订阅事件,并在 componentWillUnmount
中取消掉相应的事件订阅。但事实上 React
并不能够保证在 componentWillMount
被调用后,同一组件的 componentWillUnmount
也一定会被调用。
一个当前版本的例子如服务端渲染时,componentWillUnmount
是不会在服务端被调用的,所以在 componentWillMount
中订阅事件就会直接导致服务端的内存泄漏。
另一方面,在未来 React
开启异步渲染模式后,在 componentWillMount
被调用之后,组件的渲染也很有可能会被其他的事务所打断,导致 componentWillUnmount
不会被调用。而 componentDidMount
就不存在这个问题,在 componentDidMount
被调用后,componentWillUnmount
一定会随后被调用到,并根据具体代码清除掉组件中存在的事件订阅。
新版本替代方案
将现有 componentWillMount
中的代码迁移至 componentDidMount
即可。
componentWillReceiveProps
更新由 props 决定的 state 及处理特定情况下的回调
在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,需要在 componentWillReceiveProps
中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。
类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。
一个简单的例子如下:
// beforecomponentWillReceiveProps(nextProps) { if (nextProps.translateX !== this.props.translateX) { this.setState({ translateX: nextProps.translateX, }); } }// afterstatic getDerivedStateFromProps(nextProps, prevState) { if (nextProps.translateX !== prevState.translateX) { return { translateX: nextProps.translateX, }; } return null;}
乍看下来这二者好像并没有什么本质上的区别,但这却是笔者认为非常能够体现 React 团队对于软件工程深刻理解的一个改动,即 React 团队试图通过框架级别的 API 来约束或者说帮助开发者写出可维护性更佳的 JavaScript 代码。为了解释这点,我们再来看一段代码:
// beforecomponentWillReceiveProps(nextProps) { if (nextProps.isLogin !== this.props.isLogin) { this.setState({ isLogin: nextProps.isLogin, }); } if (nextProps.isLogin) { this.handleClose(); }}
<template> <div> <div>{{ name }}</div> <div>user: {{ user.age }}</div> </div></template><script>import { defineComponent, onRenderTracked, onRenderTriggered, ref, reactive,} from "vue";export default defineComponent({ setup() { const name = ref("randy"); const user = reactive({ age: 27 }); onRenderTracked(({ key, target, type }) => { console.log("onRenderTracked", { key, target, type }); }); onRenderTriggered(({ key, target, type }) => { console.log("onRenderTriggered", { key, target, type }); }); return { name, user, }; },});</script>0
通常来讲,在 componentWillReceiveProps
中,我们一般会做以下两件事,一是根据 props 来更新 state,二是触发一些回调,如动画或页面跳转等。在老版本的 React 中,这两件事我们都需要在 componentWillReceiveProps
中去做。而在新版本中,官方将更新 state 与触发回调重新分配到了 getDerivedStateFromProps
与 componentDidUpdate
中,使得组件整体的更新逻辑更为清晰。而且在 getDerivedStateFromProps
中还禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps
这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。
新版本替代方案
将现有 componentWillReceiveProps
中的代码根据更新 state 或回调,分别在 getDerivedStateFromProps
及 componentDidUpdate
中进行相应的重写即可。
componentWillUpdate
处理因为 props 改变而带来的副作用
与 componentWillReceiveProps
类似,许多开发者也会在 componentWillUpdate
中根据 props 的变化去触发一些回调。但不论是 componentWillReceiveProps
还是 componentWillUpdate
,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。
与 componentDidMount
类似,componentDidUpdate
也不存在这样的问题,一次更新中 componentDidUpdate
只会被调用一次,所以将原先写在 componentWillUpdate
中的回调迁移至 componentDidUpdate
就可以解决这个问题。
在组件更新前读取 DOM 元素状态
componentWillUpdate
是在组件更新前被调用,读取当前某个 DOM 元素的状态,并在 componentDidUpdate
中进行相应的处理。但在 React 开启异步渲染模式后,render 阶段和 commit 阶段之间并不是无缝衔接的,也就是说在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在 componentDidUpdate
中使用 componentWillUpdate
中读取到的 DOM 元素状态是不准确的。
与 componentWillUpdate
不同,getSnapshotBeforeUpdate
会在最终的 render 之后被调用(但是此时还没有真正更新真实DOM),也就是说在 getSnapshotBeforeUpdate
中读取到的 DOM 元素状态与之前的肯定是一样的。
getSnapshotBeforeUpdate
返回一个值。这个值会被最为第三个参数被传入到 componentDidUpdate
中,然后我们就可以在 componentDidUpdate
中去更新组件的状态,而不是在 getSnapshotBeforeUpdate
中直接更新组件状态。
官方提供的一个例子如下:
<template> <div> <div>{{ name }}</div> <div>user: {{ user.age }}</div> </div></template><script>import { defineComponent, onRenderTracked, onRenderTriggered, ref, reactive,} from "vue";export default defineComponent({ setup() { const name = ref("randy"); const user = reactive({ age: 27 }); onRenderTracked(({ key, target, type }) => { console.log("onRenderTracked", { key, target, type }); }); onRenderTriggered(({ key, target, type }) => { console.log("onRenderTriggered", { key, target, type }); }); return { name, user, }; },});</script>1
新版本替代方案
将现有的 componentWillUpdate
中的回调函数迁移至 componentDidUpdate
。
如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate
,把计算值返回出来。然后在 componentDidUpdate
通过第三个参数获取到值,然后统一触发回调或更新状态。
对比总结
Vue
和React
的生命周期函数我们都介绍完了,下面我们来总结下它们的相同点和不同点。
相同点
Vue
、React
生命周期函数基本类似,组件的创建、更新、卸载都有对应的函数。都很完善,能监控到组件从创建到消亡各个阶段。
组件创建 更新 销毁都符合洋葱模型
不同点
Vue
多了缓存的生命周期函数 activated、deactivated
和用于开发调试的renderTracked
、renderTriggered
生命周期函数。
Vue
生命周期函数除了errorCaptured
都是没有参数的。而React
很多生命后期函数都有参数并且能访问到旧的或新的props
和state
。
React
父组件更新,子组件一定都会更新渲染,除非自己手动优化。而在Vue
中这一部分是已经实现了的。也就是说,除非子组件依赖父组件的数据改变了,否则子组件是不会重新渲染的。但是React
需要自己手动优化,比如继承PureComponent
或者实现shoouldComponent
方法,来手动优化。
在Vue
中,更新操作都已经完全封装好,所以数据改变就一定会重新渲染,没办法阻止。但是在React
中能通过shoouldComponent
方法来决定是否需要渲染,这块是比Vue
更灵活的。
Vue
在组件报错的时候页面还是会渲染,只是引发出错的地方可能数值不对。所以在Vue
中没有错误边界这个说法,不需要定义错误边界组件自定义处理渲染错误。
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
原文:https://juejin.cn/post/7101531970194112543