嘿,各位代码界的弄潮儿们,今天老司机要跟大家聊聊React和Vue这两个前端框架里的组件生命周期。这玩意儿,就像人的一生,从出生到死亡,每个阶段都有不同的事情要做。掌握了它,你才能更好地操控你的组件,让它们活得更精彩!
咱们先从React开始,再聊Vue,最后来个对比总结,保证让你听得明明白白。
React 组件生命周期:组件的一生
React的组件生命周期,可以简单理解为组件从被创建到被卸载的过程。在这个过程中,React提供了一些钩子函数,让你可以在特定的时间点执行一些操作。
React的生命周期主要分为三个阶段:
-
挂载(Mounting):组件出生
-
constructor()
:组件的构造函数。这是组件创建的第一个环节,也是唯一一个可以初始化this.state
的地方。如果你不初始化 state,或者不绑定方法到实例,你就不需要实现它。class MyComponent extends React.Component { constructor(props) { super(props); // 必须调用 super(props) this.state = { count: 0 }; // 初始化 state this.handleClick = this.handleClick.bind(this); // 绑定 this } handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.handleClick}>Increment</button> </div> ); } }
注意: 在
constructor
中,你不能直接使用setState()
,因为它还没有被初始化。 -
static getDerivedStateFromProps(props, state)
:这个静态方法在每次渲染之前被调用,包括组件第一次挂载的时候。它应该返回一个对象来更新 state,如果不需要更新任何东西,就返回null
。class MyComponent extends React.Component { constructor(props) { super(props); this.state = { message: props.initialMessage, derivedMessage: '' }; } static getDerivedStateFromProps(props, state) { if (props.initialMessage !== state.message) { return { message: props.initialMessage, derivedMessage: 'Message updated from props!' }; } return null; // No state update } render() { return ( <div> <p>Message: {this.state.message}</p> <p>Derived Message: {this.state.derivedMessage}</p> </div> ); } }
场景: 当你需要根据 props 的变化来更新 state 时,可以用它。但要小心使用,过度使用可能会导致性能问题。
-
render()
:这个方法是必须的。它负责渲染组件的 UI。它应该是一个纯函数,也就是说,它不应该修改 state 或 props,每次调用都应该返回相同的结果。class MyComponent extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> </div> ); } }
重点:
render()
方法只负责渲染 UI,不要在里面做任何副作用操作(例如,修改 DOM、发起 AJAX 请求等)。 -
componentDidMount()
:组件挂载后立即调用。在这个方法里,你可以执行副作用操作,例如发起 AJAX 请求、设置定时器、直接操作 DOM 等。class MyComponent extends React.Component { componentDidMount() { // 发起 AJAX 请求 fetch('/api/data') .then(response => response.json()) .then(data => this.setState({ data: data })); // 设置定时器 this.timerID = setInterval( () => this.tick(), 1000 ); } componentWillUnmount() { // 清除定时器 clearInterval(this.timerID); } render() { return ( <div> {/* Render data */} </div> ); } }
最佳实践: 数据请求、事件监听、定时器等操作,都应该放在
componentDidMount()
里。
-
-
更新(Updating):组件成长
-
static getDerivedStateFromProps(props, state)
:和挂载阶段一样,在每次渲染之前都会调用。 -
shouldComponentUpdate(nextProps, nextState)
:在渲染之前被调用,让你决定是否需要重新渲染组件。如果返回false
,React 会跳过渲染。class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { // 只有当 count 属性发生变化时,才重新渲染 return nextProps.count !== this.props.count; } render() { console.log('Rendering...'); return ( <div> <p>Count: {this.props.count}</p> </div> ); } }
性能优化:
shouldComponentUpdate()
是一个性能优化的利器,但要谨慎使用。如果判断逻辑过于复杂,反而会影响性能。通常,可以使用React.PureComponent
来避免手动实现shouldComponentUpdate()
。 -
render()
:和挂载阶段一样,负责渲染 UI。 -
getSnapshotBeforeUpdate(prevProps, prevState)
:在 DOM 更新之前被调用。它可以让你在 DOM 发生变化之前,读取一些信息(例如,滚动位置)。返回值将作为参数传递给componentDidUpdate()
。class MyComponent extends React.Component { constructor(props) { super(props); this.listRef = React.createRef(); } getSnapshotBeforeUpdate(prevProps, prevState) { // 获取滚动位置 if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; } componentDidUpdate(prevProps, prevState, snapshot) { // 恢复滚动位置 if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } } render() { return ( <ul ref={this.listRef}> {this.props.list.map(item => ( <li key={item}>{item}</li> ))} </ul> ); } }
场景:
getSnapshotBeforeUpdate()
常用于处理滚动位置、焦点等需要在 DOM 更新前保存的状态。 -
componentDidUpdate(prevProps, prevState, snapshot)
:在 DOM 更新后立即调用。你可以在这里执行副作用操作,例如发起 AJAX 请求、更新 DOM 等。class MyComponent extends React.Component { componentDidUpdate(prevProps) { // 当 props.userID 发生变化时,发起 AJAX 请求 if (this.props.userID !== prevProps.userID) { this.fetchData(this.props.userID); } } render() { return ( <div> {/* Render data */} </div> ); } }
重要提示: 在
componentDidUpdate()
中,你需要比较prevProps
和this.props
,或者prevState
和this.state
,以避免无限循环。
-
-
卸载(Unmounting):组件谢幕
-
componentWillUnmount()
:在组件卸载之前调用。你可以在这里执行清理操作,例如清除定时器、取消订阅、取消 AJAX 请求等。class MyComponent extends React.Component { componentWillUnmount() { // 清除定时器 clearInterval(this.timerID); // 取消 AJAX 请求 this.abortController.abort(); } render() { return ( <div> {/* Render data */} </div> ); } }
必做事项:
componentWillUnmount()
是一个非常重要的生命周期方法,务必在这里清理掉所有副作用操作,以避免内存泄漏。
-
React 16+ 新增的生命周期方法
React 16 引入了两个新的生命周期方法:getDerivedStateFromProps
和 getSnapshotBeforeUpdate
。这两个方法旨在解决一些在旧的生命周期方法中难以处理的问题。
错误处理
static getDerivedStateFromError(error)
: 此生命周期会在后代组件抛出错误后被调用。 它将接收到的错误作为参数,并返回一个值以更新 state。componentDidCatch(error, info)
: 此生命周期在后代组件抛出错误后被调用。 它接收两个参数:error
– 抛出的错误。info
– 一个包含有关哪个组件引发了错误的componentStack
键的对象。
React 生命周期总结
生命周期方法 | 阶段 | 作用 |
---|---|---|
constructor() |
挂载 | 初始化 state,绑定方法。 |
static getDerivedStateFromProps() |
挂载/更新 | 根据 props 更新 state。 |
render() |
挂载/更新 | 渲染 UI。 |
componentDidMount() |
挂载 | 执行副作用操作,例如发起 AJAX 请求、设置定时器、直接操作 DOM 等。 |
shouldComponentUpdate() |
更新 | 决定是否需要重新渲染组件。 |
getSnapshotBeforeUpdate() |
更新 | 在 DOM 更新之前读取信息(例如,滚动位置)。 |
componentDidUpdate() |
更新 | 在 DOM 更新后执行副作用操作。 |
componentWillUnmount() |
卸载 | 清理操作,例如清除定时器、取消订阅、取消 AJAX 请求等。 |
static getDerivedStateFromError() |
错误处理 | 此生命周期会在后代组件抛出错误后被调用。 它将接收到的错误作为参数,并返回一个值以更新 state。 |
componentDidCatch() |
错误处理 | 此生命周期在后代组件抛出错误后被调用。 它接收两个参数: 抛出的错误和包含有关哪个组件引发了错误的 componentStack 键的对象。 |
Vue 组件生命周期:Vue 的一生
Vue 的组件生命周期和 React 类似,也是组件从创建到销毁的过程。Vue 也提供了一些钩子函数,让你可以在特定的时间点执行一些操作。
Vue 的生命周期主要分为四个阶段:
-
创建(Creation):组件诞生
-
beforeCreate()
:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。export default { beforeCreate() { console.log('beforeCreate'); // 此时 data 和 methods 都还没有初始化 }, data() { return { message: 'Hello, Vue!' }; }, methods: { greet() { console.log(this.message); } } };
用处: 很少使用,因为此时 data 和 methods 都还没有初始化。
-
created()
:在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性的计算,方法的绑定,事件/侦听器的回调。然而,挂载阶段还没开始,$el 属性目前尚不可用。export default { created() { console.log('created'); // 此时 data 和 methods 已经初始化,可以访问 console.log(this.message); } };
常用场景:
created()
是一个常用的生命周期钩子,你可以在这里发起 AJAX 请求、初始化数据等。
-
-
挂载(Mounting):组件上岗
-
beforeMount()
:在挂载开始之前被调用:相关的 render 函数首次被调用。export default { beforeMount() { console.log('beforeMount'); // 此时 render 函数已经被调用,但还没有挂载到 DOM console.log(this.$el); // undefined } };
注意:
$el
属性此时还不可用。 -
mounted()
:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。export default { mounted() { console.log('mounted'); // 此时组件已经挂载到 DOM,可以访问 $el console.log(this.$el); // <div>...</div> } };
最佳实践:
mounted()
是一个非常重要的生命周期钩子,你可以在这里执行副作用操作,例如操作 DOM、初始化第三方库等。
-
-
更新(Updating):组件成长
-
beforeUpdate()
:数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。export default { beforeUpdate() { console.log('beforeUpdate'); // 此时 DOM 还没有更新 } };
场景: 在 DOM 更新之前,你需要做一些准备工作,例如保存滚动位置。
-
updated()
:由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致无限循环更新。export default { updated() { console.log('updated'); // 此时 DOM 已经更新 } };
重要提示: 避免在
updated()
中修改 state,除非你有明确的理由,并且知道自己在做什么。
-
-
销毁(Destruction):组件谢幕
-
beforeDestroy()
:实例销毁之前调用。在这一步,实例仍然完全可用。export default { beforeDestroy() { console.log('beforeDestroy'); // 此时组件仍然可用 } };
用处:
beforeDestroy()
可以用来做一些准备工作,例如取消订阅、清除定时器等。 -
destroyed()
:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。export default { destroyed() { console.log('destroyed'); // 此时组件已经被销毁 } };
必做事项:
destroyed()
是一个非常重要的生命周期钩子,务必在这里清理掉所有副作用操作,以避免内存泄漏。
-
Vue 生命周期总结
生命周期方法 | 阶段 | 作用 |
---|---|---|
beforeCreate() |
创建 | 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。 |
created() |
创建 | 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性的计算,方法的绑定,事件/侦听器的回调。然而,挂载阶段还没开始,$el 属性目前尚不可用。 |
beforeMount() |
挂载 | 在挂载开始之前被调用:相关的 render 函数首次被调用。 |
mounted() |
挂载 | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。 |
beforeUpdate() |
更新 | 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。 |
updated() |
更新 | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致无限循环更新。 |
beforeDestroy() |
销毁 | 实例销毁之前调用。在这一步,实例仍然完全可用。 |
destroyed() |
销毁 | Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 |
React vs Vue:生命周期大 PK
特性 | React | Vue |
---|---|---|
生命周期阶段 | 挂载(Mounting)、更新(Updating)、卸载(Unmounting) | 创建(Creation)、挂载(Mounting)、更新(Updating)、销毁(Destruction) |
钩子函数数量 | 较多,分为新旧两种,需要区分使用。 | 相对较少,更容易理解和记忆。 |
命名风格 | 驼峰命名法(camelCase),例如 componentDidMount 。 |
短横线命名法(kebab-case),例如 beforeCreate 。 |
数据请求位置 | 通常在 componentDidMount 中发起数据请求。 |
通常在 created 或 mounted 中发起数据请求。 |
DOM 操作位置 | 通常在 componentDidMount 和 componentDidUpdate 中操作 DOM。 |
通常在 mounted 和 updated 中操作 DOM。 |
清理操作位置 | 通常在 componentWillUnmount 中执行清理操作。 |
通常在 beforeDestroy 中执行清理操作。 |
性能优化 | 可以使用 shouldComponentUpdate 或 React.PureComponent 来避免不必要的渲染。 |
可以使用 v-once 指令来缓存静态内容,或者使用 computed 属性来缓存计算结果。 |
错误处理 | 使用 static getDerivedStateFromError(error) 和 componentDidCatch(error, info) 处理错误。 |
Vue 提供了 errorCaptured 钩子函数来捕获子组件的错误。 |
总结:生命周期的意义
组件生命周期是 React 和 Vue 中非常重要的概念。它让你可以在组件的不同阶段执行一些操作,例如初始化数据、发起 AJAX 请求、操作 DOM、清理副作用等。掌握了组件生命周期,你才能更好地操控你的组件,让它们活得更精彩!
记住,每个生命周期钩子都有自己的作用和适用场景。选择合适的钩子函数,可以让你写出更高效、更健壮的代码。
好了,今天的讲座就到这里。希望大家有所收获!下次有机会再跟大家聊聊其他的技术话题。 祝大家代码写得飞起,bug 越来越少!