React16变更汇总

自2017年9月React正式发布了16以来,已经有一年多的时间。相信各项目也都已经完成了升级工作,相信这个过程应该是轻松愉快的。当然,前提是你的项目之前升级过15.6或者之后的任意版本,去除掉了各种警告。那么你有关注过16为我们带来了什么吗?它有哪些让人欣喜的变化?这篇文章主要摘取了一些影响我们未来开发或者令人欣喜的新特性新做以总结。

  • Fiber
  • 生命周期函数
  • Context
  • Refs
  • Fragment
  • React Strict Mode

Fiber

介绍Fiber的文章很多,这里只用5张图片简单回忆一下,以方便我们理解生命周期函数变更的原因。

  • Stack reconciler——React在进行组件渲染时,从setState开始直到渲染完成的整个过程都是同步的,整个过程可以类比为函数的递归调用,从最外层父节点开始遍历找到所有的变化节点,然后对所有diff node遍历完成之后才能计算出真是的dom信息,传递给renderer,开始渲染。所以,当React需要渲染一个庞大的vDom时,这个过程就会很长,而在这期间,主线程是一直被占用的,就会出现卡顿,影响用户交互的现象。

    React16变更汇总_第1张图片
  • Fiber reconciler

    可以简单粗暴的理解为异步,渲染工作会被拆分成若干个fiber对象,fiber对象中不仅存储着对应元素的基本信息,还要保存一些用于调度的信息。每个fiber执行完后,都会查看是否还继续拥有主线程时间片,如果有,继续下一个fiber,如果没有,则先处理其它优先级更高的事物。

    React16变更汇总_第2张图片

    在这个异步的过程中,生命周期函数分别在两个不同的阶段执行——render和commit

    React16变更汇总_第3张图片

    当重新获得主线程时间片时,之前被中断的任务并不会继续之前的工作,而是重新开始,也就是说,render阶段的生命周期函数可能会被多次执行。所以,对于某些只期望执行一次,或者需要在两个生命周期函数的操作中执行对称操作的情况而言,要考虑这种case,确保不会让整个App crash掉。

    React16变更汇总_第4张图片

    新的生命周期图示

    React16变更汇总_第5张图片

生命周期函数

对于大多数开发人员来说,生命周期函数的变更应该是React16带来的最大影响(虽然它在React17中才会真正发生)。由于采用了新的Fiber架构,导致部分原有函数存在着风险,同时也引入了适配Fiber的新的函数。

新增

  • static getDerivedStateFromProps(nextProps, prevState)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • componentDidCatch(error, info)
static getDerivedStateFromProps(nextProps, prevState)

该函数根据传入的新的props来更新state。它在render阶段执行,所以每次更新都会触发,为了保证它的纯粹性,React将其设置为了静态方法——无法访问实例,无法通过ref访问DOM对象。

值得注意的是,开发者仍然可能会通过props的操作带来副作用,此时应该把props的操作移到componentDidUpdate 中,以减少触发次数。

但在使用时要非常小心,因为它不像 componentWillReceiveProps 一样,只在父组件重新渲染时才触发,本身调用 setState 也会触发。官方提供了 3 条 checklist, 这里搬运一下:

  1. 如果改变 props 的同时,有副作用的产生(如异步请求数据,动画效果),这时应该使用 componentDidUpdate
  2. 如果想要根据 props 计算属性,应该考虑将结果 memoization 化
  3. 如果想要根据 props 变化来重置某些状态,应该考虑使用受控组件
    配合 componentDidUpdate 周期函数,getDerivedStateFromProps 是为了替代 componentWillReceiveProps 而出现的。它将原本 componentWillReceiveProps 功能进行划分 —— 更新 state 和 操作/调用props,很大程度避免了职责不清而导致过多的渲染,从而影响应该性能。

推荐大家阅读React官网上的一篇文章:You Probably Don’t Need Derived State

  componentWillReceiveProps(nextProps) {
    const { drawerIsOpening } = this.props;
    if (
      drawerIsOpening != nextProps.drawerIsOpening &&
      false == nextProps.drawerIsOpening
    ) {
      this.drawer && this.drawer._close();
      this.props.getSkuSetInfoList(); 
    }

    if (this.state.nameIsDuplicate != nextProps.checkSkuUnitNameDuplicate) {
      this.setState({
        nameIsDuplicate: nextProps.checkSkuUnitNameDuplicate,
      });
    }
  }

这段代码做了两件事

  • 如果抽屉状态从打开变成了关闭,则要发送一个请求来更新商品集合列表
  • 如果props传入的checkSkuUnitNameDuplicate与当前组建中的相应state不同,则更新state

在新的架构下应该被拆分为

  static getDerivedStateFromProps(nextProps, prevState){
    if (prevState.nameIsDuplicate != nextProps.checkSkuUnitNameDuplicate) {
      return {
        nameIsDuplicate: nextProps.checkSkuUnitNameDuplicate,
      };
    }
    return null;
  }
  
  componentDidUpdate(prevProps) {
    const { drawerIsOpening } = this.props;
    if (
      drawerIsOpening != prevProps.drawerIsOpening &&
      false == drawerIsOpening
    ) {
      this.drawer && this.drawer._close();
      this.props.getSkuSetInfoList(); //创建完成后投放单元查询商品集合
    }
  }
getSnapshotBeforeUpdate(prevProps, prevState)

其在组件更新之前获取一个 snapshot —— 可以将计算得的值或从 DOM 得到的信息传递到 componentDidUpdate(prevProps, prevState, snapshot) 周期函数的第三个参数,常常用于 scroll 位置的定位。摘自官方的示例:

    class ScrollingList extends React.Component {
      constructor(props) {
        super(props)
        // 取得dom 节点
        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) {
        // 根据 snapshot 计算得到偏移量,得到最终滚动位置
        if (snapshot !== null) {
          const list = this.listRef.current
          list.scrollTop = list.scrollHeight - snapshot
        }
      }
    
      render() {
        return 
{/* ...contents... */}
} }
componentDidCatch(error, info)

让开发者可以自主处理错误信息,诸如展示,上报错误到BetterJS等,用户可以创建自己的Error Boundary 来捕获错误。例:

  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    if (window.BJ_REPORT) {
      BJ_REPORT.report(error, info);
    }
  }

标记为不安全

  • componentWillMount(nextProps, nextState)
  • componentWillReceiveProps(nextProps)
  • componentWillUpdate(nextProps, nextState)

这些生命周期方法经常被误解和微妙地滥用;此外,我们预计它们的潜在误用可能会在异步呈现中出现更多问题。因此,我们将在即将发布的版本中为这些生命周期添加一个“UNSAFE_”前缀。(在这里,“不安全”并不是指安全性,而是指使用这些生命周期的代码更有可能在React的未来版本中出现bug,尤其是在启用了异步呈现之后。)

这段话是React的官方解释,结合上一节说的Fiber的运行机制不难看出,这三个全部是在render阶段执行,所以都存在着多次执行的可能。

  • componentWillMount

    1. 通常被用来获取首屏数据,或者订阅事件。开发者为了快速得到数据,将首屏请求放在这里,但实际上在执行componentWillMount时,第一次渲染已经开始,所以,把首屏数据请求放在这里与否都不能解决无异步数据的问题。废弃后,官方建议将首屏数据请求放在constructor或componentDidMount中

    2. 此外事件订阅也被常在 componentWillMount 用到,并在 componentWillUnmount 中取消掉相应的事件订阅。但事实上 React 并不能够保证在 componentWillMount 被调用后,同一组件的 componentWillUnmount 也一定会被调用。另一方面,在未来 React 开启异步渲染模式后,在componentWillMount被调用之后,组件的渲染也很有可能会被其他的事务所打断,导致 componentWillUnmount 不会被调用。而 componentDidMount 就不存在这个问题,在 componentDidMount 被调用后,componentWillUnmount 一定会随后被调用到,并根据具体代码清除掉组件中存在的事件订阅。对此的升级方案是把 componentWillMount 改为 componentDidMount 即可

  • componentWillReceiveProps、componentWillUpdate

    被废弃的原因前面已经说过,替代方案是使用 getDerivedStateFromPropscomponentDidUpdate

Context

这是一个令人欣喜的改变,对于这个Api可能大家并不陌生,通过将数据附着在context上,可以方便的在各组件之间进行传递,在 React 16.3 之前,Context API 一直被官方置为不推荐使用(don’t use context),究其原因是因为老的 Context API 作为一个实验性的产品,破坏了 React 的分形结构。同时在使用的过程中,如果在穿透组件的过程中,某个组件的 shouldComponentUpdate 返回了 false, 则 Context API 就不能穿透了。其带来的不确定性也就导致被不推荐使用。随着 React 16.3 的发布,全新 Context API 成了一等 API,可以很容易穿透组件而无副作用。

代码演示

Refs

React.createRef

新版本的React对获取元素ref进行了较大的变更,在React16中,获取ref需要使用createRef

  componentDidMount() {
    const el = this.refs.myRef
  }

  render() {
    return 
} ··· ··· // React 16+ constructor(props) { super(props) this.myRef = React.createRef() } render() { return
}

React.forwardRef

如果需要获得子组件的ref,就需要使用这个React.forwardRef,示例代码

//子组件 TextInput.jsx
const TextInput = props => {
  return ;
};
export default TextInput;
//父组件
import Input from './TextInput';

const TextInput = React.forwardRef((props, ref) => );
const inputRef = React.createRef();

class App extends Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }

  handleSubmit = () => {
    alert(inputRef.current.value);
  };
  render() {
    return (
      
); } }

Fragment

这是React提供的一个新组件,它可以在不渲染任何wrapper的情况下渲染多个组件。

render() {
  return (
    
      
      
      
    
  )
}

React Strict Mode

StrictMode是一个工具,用于强调应用程序中的潜在问题。像Fragment一样,StrictMode不会呈现任何可见的UI。它会为后代启动额外的检查和警告。

import React from 'react';

function ExampleApplication() {
  return (
    
); }

StrictMode目前可以帮助我们

  • 识别不安全的生命周期组件
  • 警告对遗留字符串ref API的使用
  • 警告对已经废弃的方法findDOMNode的使用
  • 探测某些产生副作用的方法
  • 检测是否使用了老的Context API

你可能感兴趣的:(react)