简介
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
总结
vue2 | vue3选项式 | vue3(setup) |
---|---|---|
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(16.3以前)
老版本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
表示不需要更新,常用来做性能优化。如果没定义该方法默认返回true
。
接收两个参数,分别是最新的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;
}
这里多一嘴,我们知道继承PureComponent
其实就是帮我们自动实现了shouldComponentUpdate
的比较,所以当我们数据没有变化的时候组件不会再次加载。但是需要注意的是PureComponent
中 shouldComponentUpdate
对 props
做得只是浅层比较,不是深层比较,如果 props
是一个深层对象,就容易产生问题。
注意当调用forceUpdate
是不会进入该生命周期函数的,会直接更新并渲染。
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
生命周期函数就是去除了三个不安全函数(异步渲染可能会有bug)
- 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
。
使用场景是若state
的值在任何情况下都取决于props
的时候使用该方法比较合适。相当于把传进来的props
转成了组件的state
。
// 老版本通过该方法里更新state
componentWillReceiveProps(nextProps) {
if (nextProps.translateX !== this.props.translateX) {
this.setState({
translateX: nextProps.translateX,
});
}
}
// 代替componentWillReceiveProps
// 通过返回对象来更新state
static 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;
}
}
简单来说,如果异常发生在第一阶段(render阶段),React就会调用getDerivedStateFromError
,如果异常发生在第二阶段(commit阶段),React会调用componentDidCatch
。
新版本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
三个方法。下面我们来说说这三个为什么会被删除,以及在新版生命周期函数中用什么来替代。
新版本React
引入了异步渲染,有了异步渲染之后,React
组件的渲染过程是分时间片的,不是一口气从头到尾把子组件全部渲染完,而是每个时间片渲染一点,然后每个时间片的间隔都可去看看有没有更紧急的任务(比如用户按键),如果有,就去处理紧急任务,如果没有那就继续照常渲染。
根据 React Fiber
的设计,一个组件的渲染被分为两个阶段:第一个阶段(也叫做 render
阶段)是可以被 React
打断的,一旦被打断,这阶段所做的所有事情都被废弃,当 React
处理完紧急的事情回来,依然会重新渲染这个组件,这时候第一阶段的工作会重做一遍;第二个阶段叫做 commit
阶段,一旦开始就不能中断,也就是说第二个阶段的工作会稳稳当当地做到这个组件的渲染结束。
两个阶段的分界点,就是 render
函数。render
函数之前的所有生命周期函数(包括 render)都属于第一阶段,之后的都属于第二阶段。
开启异步渲染,虽然我们获得了更好的感知性能,但是考虑到第一阶段的的生命周期函数可能会被重复调用,不得不对历史代码做一些调整。
所以删除这三个生命周期方法的主要原因还是因为可能会重复调用,带来一些意想不到的后果。
componentWillMount
首屏无数据导致白屏
在 React
应用中,许多开发者为了避免第一次渲染时页面因为没有获取到异步数据导致的白屏,而将数据请求部分的代码放在了 componentWillMount
中,希望可以避免白屏并提早异步请求的发送时间。
但事实上在 componentWillMount
执行后,第一次渲染就已经开始了,所以如果在 componentWillMount
执行时还没有获取到异步数据的话,页面首次渲染时也仍然会处于没有异步数据的状态。换句话说,组件在首次渲染时总是会处于没有异步数据的状态,所以不论在哪里发送数据请求,都无法直接解决这一问题。
前面说了componentWillMount
就可能被中途打断,中断之后渲染又要重做一遍,想一想,在 componentWillMount
中做 AJAX
调用,代码里看到只有调用一次,但是实际上可能调用 N 多次,这明显不合适。相反,若把 AJAX
放在 componentDidMount
,因为 componentDidMount
在第二阶段,所以绝对不会多次重复调用,这才是 AJAX
合适的位置。
而关于提早发送数据请求,官方也鼓励将数据请求部分的代码放在组件的 constructor
中,而不是 componentWillMount
。
新版本替代方案
将现有 componentWillMount
中的代码迁移至 componentDidMount
即可。
componentWillReceiveProps
更新由 props 决定的 state 及处理特定情况下的回调
在老版本的 React
中,如果组件自身的某个 state
跟其 props
密切相关的话,需要在 componentWillReceiveProps
中判断前后两个 props
是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。
类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。
一个简单的例子如下:
// before
componentWillReceiveProps(nextProps) {
if (nextProps.translateX !== this.props.translateX) {
this.setState({
translateX: nextProps.translateX,
});
}
}
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.translateX !== prevState.translateX) {
return {
translateX: nextProps.translateX,
};
}
return null;
}
乍看下来这二者好像并没有什么本质上的区别,但这却是笔者认为非常能够体现 React 团队对于软件工程深刻理解的一个改动,即 React 团队试图通过框架级别的 API 来约束或者说帮助开发者写出可维护性更佳的 JavaScript 代码。为了解释这点,我们再来看一段代码:
// before
componentWillReceiveProps(nextProps) {
if (nextProps.isLogin !== this.props.isLogin) {
this.setState({
isLogin: nextProps.isLogin,
});
}
if (nextProps.isLogin) {
this.handleClose();
}
}
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.isLogin !== prevState.isLogin) {
return {
isLogin: nextProps.isLogin,
};
}
return null;
}
componentDidUpdate(prevProps, prevState) {
if (!prevState.isLogin && this.props.isLogin) {
this.handleClose();
}
}
通常来讲,在 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
中直接更新组件状态。
官方提供的一个例子如下:
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
//
if (prevProps.list.length < this.props.list.length) {
return (
this.listRef.scrollHeight - this.listRef.scrollTop
);
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.setListRef}>
{/* ...contents... */}
</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
新版本替代方案
将现有的 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
中没有错误边界这个说法,不需要定义错误边界组件自定义处理渲染错误。
系列文章
Vue和React对比学习之生命周期函数(Vue2、Vue3、老版React、新版React)
Vue和React对比学习之组件传值(Vue2 12种、Vue3 9种、React 7种)
Vue和React对比学习之路由(Vue-Router、React-Router)
Vue和React对比学习之状态管理 (Vuex和Redux)
Vue和React对比学习之条件判断、循环、计算属性、属性监听
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!