React v16.3 小记

Jun 16 2018

新的生命周期

生命周期变化

React v16.3 最大的变动莫过于生命周期。

废弃的生命周期:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

新增的生命周期:

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

变更的生命周期:

  • componentDidUpdate 增加第三个参数 snapshot

为什么要改

旧的生命周期十分完整,基本可以捕捉到组件更新的每一个 state 和 props 的变化,没有什么逻辑上的毛病。

根据官方的说法,React v17 计划推出异步渲染,渲染过程可能被打断,而可以被打断的阶段正是实际 DOM 挂载之前的虚拟 DOM 构建阶段,被去掉的三个生命周期便在其中。

渲染过程一旦被打断,下次恢复的时候又会再跑一次之前的生命周期,componentWillMountcomponentWillReceivePropscomponentWillUpdate 都不能保证只在挂载 / 接收 props / state 变化的时候执行一次了,所以这三个方法被标记为不安全。

react异步渲染

在 React v16 之前,组件的渲染都是同步进行的,也就是说从 constructor 开始到 componentDidUpdate 结束,React 的运行都是没有中断的,生命周期开始之后就会运行到其结束为止。

这样带来的一个缺点就是,如果组件嵌套很深,渲染时间增长了之后,一些重要的高优先级的操作就会被阻塞,例如用户的输入、滚动事件等,这样就会造成体验上的不友好。

在之后即将到来的异步渲染机制中,会允许首先解决高优先级的任务,同时会暂停当前的渲染任务,当高优先级的任务结束之后,再返回继续运行当前任务,这样会大大的提高 React 应用的流畅度,给用户带来更好的体验。

迁移路径

  1. 16.3:为不安全生命周期引入别名 UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate。(旧的生命周期名称和其”UNSAFE_”别名都可以在此版本中使用。)
  2. 16.x:为 componentWillMountcomponentWillReceivePropscomponentWillUpdate 启用弃用警告。(旧的生命周期名称和”UNSAFE_”别名都可以在此版本中使用,但旧名称会记录DEV模式警告。)
  3. 17.0:删除 componentWillMountcomponentWillReceivePropscomponentWillUpdate。(从现在开始,只有新的”UNSAFE_”生命周期名称将起作用。)

新的生命周期

static getDerivedStateFromProps

触发时间:在组件实例化之后 ,以及每次接收新的 props 之后。注意前面的 static,这意味着在这个函数中我们不能使用 this,更不能调用 this.setState。而该方法的返回值将用于更新 state,如果不需要更新 state,就返回 null。配合 componentDidUpdate,基本可以覆盖 componentWillReceiveProps 的所有用法。

static getDerivedStateFromProps(nextProps, prevState) {
  if (nextProps.text === prevState.text) return null;
  return { text: nextProps.text }; // 相当于 this.setState({ text: nextProps.text });
}

一些注意事项:

  • getDerivedStateFromProps 在第一次挂载和重绘的时候都会被调用。因此基本不用在 constructor 里根据传入的 props 来 setState 设置初始的 state。
  • 如果定义了 getDerivedStateFromProps 后,又定义了 componentWillReceiveProps。那么,只有前者会被调用,并且你会收到一个警告。
  • 有时候会在 setState 里传入回调来保证某些代码在 state 更新之后才被调用的。现在可以把这些代码移到 componentDidUpdate 里。
getSnapshotBeforeUpdate

触发时间: 组件更新之前会被调用,在 render 函数之后,在实际DOM渲染之前。它返回一个值,作为 componentDidUpdate 的第三个参数。配合 componentDidUpdate,基本可以覆盖 componentWillUpdate 的所有用法。

如下,用于在重绘期间手动保存滚动位置:

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
        // 获取先前的滚动位置待后续使用
    if (prevProps.list.length < this.props.list.length) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // snapshot 为 getSnapshotBeforeUpdate 的返回值
    if (snapshot !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      `<div>`
        {/* ...contents... */}
      `</div>`
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

生命周期完整视图

React lifecycle

新的 Context API

我们都知道在 React 中父子组件可以通过 props 自顶向下的传递数据。但是当组件深度嵌套时,从顶层组件向最内层组件传递数据就不那么方便了。手动在每一层组件上逐级传递 props 不仅书写起来很繁琐同时还会为夹在中间的组件引入不必要的 props。

旧版的缺陷

Context API 能帮助我们缩短父组件到子组件的数据传递路径。

首先在父组件上声明要传递给子组件的 context

const PropTypes = require('prop-types');

class MessageList extends React.Component {
   constructor(props) {
    super(props);
    this.state = { color:'yellow' };
  }

  updateContext() {
    this.setState({ color: 'pink' });
  }

  getChildContext() {
    return { color: this.state.color };
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

MessageList.childContextTypes = {
  color: PropTypes.string
};

然后从子孙组件访问 context

const PropTypes = require('prop-types');

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text}
          <Button>Delete</Button>
      </div>
    );
  }
}

class Button extends React.Component {
  render() {
    return (
      <button style={{ background: this.context.color }}>
        {this.props.children}
      </button>
    );
  }
}

Button.contextTypes = {
  color: PropTypes.string
};

通过将 childContextTypesgetChildContext 添加到 MessageList (context 提供者),React 会自动地向下传递数据。之后子树中的任何组件(这个例子中的 Button)都可以通过定义 contextTypes 去访问它。

getChildContext 函数将会在每次 state 或者 props 改变时调用,为了更新 context 中的数据,可以使用 this.setState 触发本地状态的更新。但在组件树中,如果中间某一个组件的 ShouldComponentUpdate return false 了,就会阻碍 context 的正常传值,导致子组件无法更新。

那么有没有方案可以让现有 Context API 和 shouldComponetUpdate 完美配合呢?答案是有的,这篇文章里有详细的讨论。但该方案属于较 hack 的方式,对新手不太友好。React 的官方文档里也不建议我们使用现有的 Context API。

The vast majority of applications do not need to use context.If you want your application to be stable, don’t use context. It is an experimental API and it is likely to break in future releases of React.

所以在更多时候我们会使用 Redux 或 Mobx 这样的第三方库来管理状态,它们允许你在任何地方从 store 获取数据,你需要做的只是使用 包装一下,然后就可以神奇地在被 connected 的组件中轻松地获取想要的数据了。但实际上,这些库的底层正是使用了 Context API 来实现的。

重生的 Context API

现在,React v16.3 带来了船新版本的 Context API,它提供了一个更简洁的管理全局状态的方式。

将上面的例子用新版 API 重写:

传入默认值,用 React.createContext 创建一个 context 实例,它含有 ProviderConsumer 两个组件。

import React from 'react';
import ReactDOM from 'react-dom';

const ThemeContext = React.createContext({
  color: 'yellow'
});

const { Provider, Consumer } = ThemeContext;

Provider 组件用于将 context 数据传给该组件树下的所有组件 value 属性是 context 的内容。这是不是很像 react-redux 里的 Provider 组件?

class MessageList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {color:'yellow'};
  }

  updateContext() {
    this.setState({color: 'pink'});
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return (
        <Provider value={{color: this.state.color}}>
          <div>{children}</div>
        </Provider>
    );
  }
}

而要使用 context 的数据时,我们需要使用 Consumer 组件。

class Button extends React.Component {
  render() {
    return (
      <Consumer>
        {context => (
          <button style={{background: context.color}}>
            {this.props.children}
          </button>
            )}
      </Consumer>
    );
  }
}

下面结合代码来说一说新版 Context API 的几个特点:

  • ProviderConsumer 必须来自同一次 React.createContext 调用。也就是说 AContext.Provider 和 BContext.Consumer 是无法搭配使用的。
  • Provider 组件的 value props 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新渲染,且不受 shouldComponentUpdete 方法的影响。
  • Consumer 外层没有对应的 Provider 时就会使用 React.createContext 方法接收的参数作为该 context 的值。
  • Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。
  • Consumer 下不能写其它内容,比如 <Consumer>color: {context => <p>{context.color}</p>}</Consumer>

总之,新的 Context API 非常高效并且支持静态类型检查和深度更新,看起来更加的“用户友好”了。当你不想使用 redux、mobx 的时候,可以选择它来实现更简单的跨级状态管理。

新的 Ref API

createRef Api

有时候会不可避免的需要使用组件中的一些 DOM节 点,比如管理聚焦,选择或者动画。
React 提供了 refs 作为一种方式去解决这个问题,有两种形式去管理 refs:字符串 API 和回调函数 API。
字符串的方式虽然简洁,但考虑到未来会支持的异步模式,官方通常是建议使用回调函数。

而在 React v16.3 中提供了一个新的 API 去管理 refs,它兼顾了字符串和回调函数的优点。

class MyComponent extends React.Component{
        inputRef = React.createRef()

    render(){
        return <input type="text" ref={this.inputRef} />
    }

    componentDidMount(){
        this.inputRef.current.focus()
    }
}

注意:在v16.3版本前,我们可以直接通过变量访问 DOM 节点的方法,在v16.3后,我们则需要通过 this.inputRef.current,即真实的DOM是通过 current 属性来引用的。

forwardRef Api

高阶组件 (or HOCs) 是在组件间复用的代码的常用方法。基于之前 Context API 的例子,创建了高阶组件 withTheme,它以 props 的方式将 ThemeContext 的数据注入到子组件。

function withTheme(Component) {
  return function ThemedComponent(props) {
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    )
  }
}

我们可以使用上面的 withTheme 将组件直接连接到 ThemeContext,而不直接在组件中引入 Consumer

class Button extends React.Component {
  buttonRef = React.createRef()

  focus = () => {
    this.buttonRef.current.focus()
  }

  render() {
    const { color, onClick } = this.props
    return (
      <button
        style={{background: color}}
        ref={this.buttonRef}>
          onClick={onClick}
      >
        {this.props.children}
      </button>
    );
  }
}

const ThemedButton = withTheme(Button);

class Message extends React.Component {
  themedButtonRef = React.createRef()

    handleClick = () => {
    this.themedButtonRef.focus() // Don't work
  }

  render() {
    return (
      <div>
        {this.props.text}
        <ThemedButton
          onClick={this.handleClick}
              ref={this.themedButtonRef}
        >
           Delete
        </ThemedButton>
      </div>
    )
  }
}

高阶组件通常将会把 props 传递给包裹着的组件。不幸的是,refs 不会被传递。这意味着,如果使用 ThemedButton,我们不能将 themedButtonRef 和 Button 进行连接,因此也无法调用它的 focus()。

新的 forwardRef API 则解决了这个问题,它提供了一个方法来拦截 ref,并将它作为了普通的 props 进行传递,以便调用子组件的 ref。

function withTheme(Component) {
  function ThemedComponent({forwardedRef, ...rest}) {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <Component
            {...rest}
            ref={forwardedRef}
            theme={theme}
          />
        )}
      </ThemeContext.Consumer>
    )
  }

  return React.forwardRef((props, ref) => (
    <ThemedComponent {...props} forwardedRef={ref} />
  ));
}

StrictMode 和 AsyncMode

v16.3 的严格模式能为其子组件激活额外的检查和警告,暴露潜在问题,来确保你的代码是按照最佳实践开发的。

import React, { StrictMode } from 'react';

class App extends React.Component {
  render() {
    return (
      <StrictMode>
        <Header />
        <Main />
        <Footer />
      </StrictMode>
    );
  }
}

warning

在 v16.3 版本中,StrictMode 帮助:

  1. 识别不安全的生命周期。
  2. 字符串 refs API 警告。
  3. 检测意想不到的副作用。
  4. 旧版的 Context API 警告。

尽管在严格模式下不可能捕获所有的问题,但在它在大多数情况下还是很有用的。

异步模式组件在 React.unsafe_AsyncMode 下,用于未来将支持的异步渲染。
使用 AsncyMode 同时也会打开 StrictMode,触发警告。

参考资料