Vue和React对比学习之生命周期函数(Vue2、Vue3、老版React、新版React)

简介: Vue2、Vue3、老版React、新版React生命周期函数

简介

VueReact是目前前端最火的两个框架。不管是面试还是工作可以说是前端开发者们都必须掌握的。

今天我们通过对比的方式来学习VueReact的生命周期这一部分。

本文首先讲述Vue2Vue3、老版React、新版React的生命周期,然后分析了老版本三个生命周期方法的问题,以及在新版本的替代方案。最后对比总结了VueReact在生命周期这部分的相同点和不同点。

希望通过这种对比方式的学习能让我们学习的时候印象更深刻,希望能够帮助到大家。

image.png

Vue2

vue2生命周期函数有

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted
  5. beforeUpdate
  6. updated
  7. beforeDestroy
  8. destroyed
  9. errorCaptured
  10. activated
  11. deactivated

引用vue官网的图,各个生命周期函数运行如下

image.png

下面我们来重点分析下各个函数。

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官网的图,各个生命周期函数运行如下

image.png

Vue3生命周期函数分析

vue3没有删除vue2选项式写法的生命周期函数,这些都还全部保留并支持。

vue3新增了renderTrackedrenderTriggered两个生命周期方法。

vue3中销毁生命周期方法名也发生了变化,由beforeDestroydestroyed变为beforeUnmountunmounted,这样是为了更好的与beforeMountmounted 相对应。

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中除了更改了名称,功能都是没有改变的。所以我们重点说下新增的 renderTrackedrenderTriggered两个方法。

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方法。

因为模板里面用到了nameuser.age所以该方法会被触发两次输出{key: 'value', target: RefImpl, type: 'get'}{key: 'age', target: {age: 27}, type: 'get'}。因为nameref定义的,所以key始终是value,并且只是读操作,所以typegetuserreactive定义的,并且我们只使用了age属性所以keyage并且只是读操作,所以typeget

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'},因为是修改所以typeset

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生命周期函数有

  1. constructor
  2. componentWillMount
  3. componentDidMount
  4. shouldComponentUpdate
  5. componentWillUpdate
  6. componentDidUpdate
  7. componentWillReceiveProps
  8. render
  9. componentWillUnmount
  10. componentDidCatch

老版本react各个生命周期函数运行如下

image.png

老版本React生命周期函数分析

constructor

constructor初始化阶段运行,只运行一次,用于初始数据。比如state

constructor() {
  super()
  this.state = {title: '生命周期函数'}
}

componentWillMount

componentWillMount 初始化阶段运行。在这里获取不到DOM元素。

componentDidMount

componentWillMount 初始化阶段运行。在这里可以获取到DOM元素。

异步请求推荐写在该函数中,比如请求后台获取数据。

shouldComponentUpdate(nextProps, nextState)

shouldComponentUpdate(nextProps, nextState)初始化阶段不运行,在组件更新时运行。

如果定义了该方法必须显示返回true或者falsetrue表示需要更新,false表示不需要更新,常用来做性能优化。如果没定义该方法默认返回true

接收两个参数,分别是最新的props和最新的state,我们可以利用这点和当前的stateprops作比较来决定渲染或者不渲染来进行性能优化。

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是不会进入该生命周期函数的,会直接更新并渲染。

image.png

componentWillUpdate(nextProps, nextState)

componentWillUpdate(nextProps, nextState)初始化阶段不运行,在组件将要更新时运行。

接收两个参数,分别是最新的props和最新的state,我们可以利用这点和当前的stateprops作比较来做些特殊逻辑处理。

componentDidUpdate(prevProps, prevState)

componentDidUpdate(prevProps, prevState)初始化阶段不运行,在组件更新完时运行。

接收两个参数,分别是之前的prevProps和之前的prevState。也就是说还可以获取上一次的propsstate,可以做一些特殊处理。

componentWillReceiveProps(nextProps)

componentWillReceiveProps(nextProps)初始化阶段不运行,在组件更新之前运行。

接收一个参数nextProps,表示最新的props

组件自身state的变更引发的组件更新并不会触发该方法。

在父组件传递props给该组件,并且props发生改变时才会运行。

但是注意,如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

在这里你可以使用this.propsnextProps作比较来处理一些特殊逻辑。

render

render 渲染方法。在这里主要是书写页面元素和样式。

componentWillUnmount

componentWillUnmount在组件卸载时运行,可以在这里清除一些副作用,比如监听函数。定时器等等。

componentDidCatch(error, errorInfo)

componentDidCatch(error, errorInfo)子组件发生错误是被调用。

接收errorerrorInfo两个参数,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生命周期函数有

  1. constructor
  2. componentDidMount
  3. static getDerivedStateFromProps
  4. shouldComponentUpdate
  5. getSnapshotBeforeUpdate
  6. componentDidUpdate
  7. render
  8. componentWillUnmount
  9. componentDidCatch
  10. getDerivedStateFromError

版本说明

componentWillMountcomponentWillReceivePropscomponentWillUpdate 这三个生命周期因为经常会被误解和滥用,所以被称为 不安全(不是指安全性,而是表示使用这些生命周期的代码,有可能在未来的 React 版本中存在缺陷,可能会影响未来的异步渲染) 的生命周期。

React 16.3 版本:为不安全的生命周期引入别名 UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate。(旧的生命周期名称和新的别名都可以在此版本中使用

React 16.3 之后的版本:为 componentWillMountcomponentWillReceivePropscomponentWillUpdate 启用弃用警告。(旧的生命周期名称和新的别名都可以在此版本中使用,但旧名称会记录DEV模式警告

React 17.0 版本: 推出新的渲染方式——异步渲染( Async Rendering),提出一种可被打断的生命周期,而可以被打断的阶段正是实际 dom 挂载之前的虚拟 dom 构建阶段,也就是要被去掉的三个生命周期 componentWillMountcomponentWillReceivePropscomponentWillUpdate。(从这个版本开始,只有新的“UNSAFE_”生命周期名称将起作用

总体来说新版本的react生命周期函数就是去除了三个不安全函数(异步渲染可能会有bug)

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

新增了三个生命周期函数

  1. static getDerivedStateFromProps
  2. getSnapshotBeforeUpdate
  3. static getDerivedStateFromError

新版本react各个生命周期函数运行如下

image.png

下面我们重点分析这三个新方法

新生命周期函数分析

static getDerivedStateFromProps(props, state)

static getDerivedStateFromProps(props, state)在组件初始化和组件更新时都会被调用。

接收stateprops两个参数,在这里可以通过返回一个对象来更新组件自身的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 之后、更新 DOMrefs 之前。

此生命周期钩子必须有返回值,返回值将作为 componentDidUpdate 第三个参数。必须和 componentDidUpdate 一起使用,否则会报错。

在这里this.propsthis.state是最新的,可以和该函数的参数prevProps, prevState作比较,进行逻辑处理。

该生命周期钩子的作用: 它能让你在组件更新 DOMrefs 之前,从 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删除了componentWillMountcomponentWillReceivePropscomponentWillUpdate三个方法。下面我们来说说这三个为什么会被删除,以及在新版生命周期函数中用什么来替代。

新版本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 与触发回调重新分配到了 getDerivedStateFromPropscomponentDidUpdate 中,使得组件整体的更新逻辑更为清晰。而且在 getDerivedStateFromProps 中还禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。

新版本替代方案

将现有 componentWillReceiveProps 中的代码根据更新 state 或回调,分别在 getDerivedStateFromPropscomponentDidUpdate 中进行相应的重写即可。

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 通过第三个参数获取到值,然后统一触发回调或更新状态。

对比总结

VueReact的生命周期函数我们都介绍完了,下面我们来总结下它们的相同点和不同点。

相同点

  1. VueReact生命周期函数基本类似,组件的创建、更新、卸载都有对应的函数。都很完善,能监控到组件从创建到消亡各个阶段。
  2. 组件创建 更新 销毁都符合洋葱模型

image.png

不同点

  1. Vue多了缓存的生命周期函数 activated、deactivated和用于开发调试的renderTrackedrenderTriggered生命周期函数。
  2. Vue生命周期函数除了errorCaptured都是没有参数的。而React很多生命后期函数都有参数并且能访问到旧的或新的propsstate
  3. React父组件更新,子组件一定都会更新渲染,除非自己手动优化。而在Vue中这一部分是已经实现了的。也就是说,除非子组件依赖父组件的数据改变了,否则子组件是不会重新渲染的。但是React需要自己手动优化,比如继承PureComponent或者实现shoouldComponent方法,来手动优化。
  4. Vue中,更新操作都已经完全封装好,所以数据改变就一定会重新渲染,没办法阻止。但是在React中能通过shoouldComponent方法来决定是否需要渲染,这块是比Vue更灵活的。
  5. Vue在组件报错的时候页面还是会渲染,只是引发出错的地方可能数值不对。所以在Vue中没有错误边界这个说法,不需要定义错误边界组件自定义处理渲染错误。

系列文章

Vue和React对比学习之生命周期函数(Vue2、Vue3、老版React、新版React)

Vue和React对比学习之组件传值(Vue2 12种、Vue3 9种、React 7种)

Vue和React对比学习之Style样式

Vue和React对比学习之Ref和Slot

Vue和React对比学习之Hooks

Vue和React对比学习之路由(Vue-Router、React-Router)

Vue和React对比学习之状态管理 (Vuex和Redux)

Vue和React对比学习之条件判断、循环、计算属性、属性监听

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

相关文章
|
30天前
|
前端开发 JavaScript API
阿珊比较Vue和React:两大前端框架的较量
阿珊比较Vue和React:两大前端框架的较量
|
9天前
|
XML JavaScript 前端开发
Vue和React的区别
Vue和React的区别
|
10天前
|
JavaScript 前端开发
react的参数值和Vue的参数值有什么区别
react的参数值和Vue的参数值有什么区别
|
10天前
|
前端开发 JavaScript
vue的v-model、v-if、v-for用react语法实现
vue的v-model、v-if、v-for用react语法实现
|
11天前
|
前端开发 JavaScript API
react挂载后函数
react挂载后函数
7 1
|
11天前
|
JavaScript
vue组件中data为什么必须是一个函数?
vue组件中data为什么必须是一个函数?
12 1
|
11天前
|
JavaScript
vue中的data为什么是一个函数
vue中的data为什么是一个函数
|
18天前
|
前端开发 JavaScript
在React中,如何通过事件处理函数来管理按钮的点击行为?
【5月更文挑战第28天】在React中,如何通过事件处理函数来管理按钮的点击行为?
24 1
|
18天前
|
前端开发 JavaScript
在React中,如何通过事件处理函数来管理输入框的获取和失去焦点行为?
【5月更文挑战第28天】在React中,如何通过事件处理函数来管理输入框的获取和失去焦点行为?
24 1
|
30天前
|
前端开发 JavaScript 开发者
掌握React中的useEffect:函数组件中的魔法钩子
掌握React中的useEffect:函数组件中的魔法钩子