React/Vue 中的组件生命周期有哪些?在不同阶段可以进行哪些操作?

嘿,各位代码界的弄潮儿们,今天老司机要跟大家聊聊React和Vue这两个前端框架里的组件生命周期。这玩意儿,就像人的一生,从出生到死亡,每个阶段都有不同的事情要做。掌握了它,你才能更好地操控你的组件,让它们活得更精彩!

咱们先从React开始,再聊Vue,最后来个对比总结,保证让你听得明明白白。

React 组件生命周期:组件的一生

React的组件生命周期,可以简单理解为组件从被创建到被卸载的过程。在这个过程中,React提供了一些钩子函数,让你可以在特定的时间点执行一些操作。

React的生命周期主要分为三个阶段:

  1. 挂载(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() 里。

  2. 更新(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() 中,你需要比较 prevPropsthis.props,或者 prevStatethis.state,以避免无限循环。

  3. 卸载(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 引入了两个新的生命周期方法:getDerivedStateFromPropsgetSnapshotBeforeUpdate。这两个方法旨在解决一些在旧的生命周期方法中难以处理的问题。

错误处理

  • 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 的生命周期主要分为四个阶段:

  1. 创建(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 请求、初始化数据等。

  2. 挂载(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、初始化第三方库等。

  3. 更新(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,除非你有明确的理由,并且知道自己在做什么。

  4. 销毁(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 中发起数据请求。 通常在 createdmounted 中发起数据请求。
DOM 操作位置 通常在 componentDidMountcomponentDidUpdate 中操作 DOM。 通常在 mountedupdated 中操作 DOM。
清理操作位置 通常在 componentWillUnmount 中执行清理操作。 通常在 beforeDestroy 中执行清理操作。
性能优化 可以使用 shouldComponentUpdateReact.PureComponent 来避免不必要的渲染。 可以使用 v-once 指令来缓存静态内容,或者使用 computed 属性来缓存计算结果。
错误处理 使用 static getDerivedStateFromError(error)componentDidCatch(error, info) 处理错误。 Vue 提供了 errorCaptured 钩子函数来捕获子组件的错误。

总结:生命周期的意义

组件生命周期是 React 和 Vue 中非常重要的概念。它让你可以在组件的不同阶段执行一些操作,例如初始化数据、发起 AJAX 请求、操作 DOM、清理副作用等。掌握了组件生命周期,你才能更好地操控你的组件,让它们活得更精彩!

记住,每个生命周期钩子都有自己的作用和适用场景。选择合适的钩子函数,可以让你写出更高效、更健壮的代码。

好了,今天的讲座就到这里。希望大家有所收获!下次有机会再跟大家聊聊其他的技术话题。 祝大家代码写得飞起,bug 越来越少!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注