react16的更新关注点
06 April 2019

春节期间系统的翻了一下2年没系统翻的react官方文档。记录了一些react的更新点,其实每一个改变点结合源码看都有其内部实现改动带来的原因,从外部需求看都是为了解决一些痛点。这些更新还是很给力的。

生命周期

按功能分类

装载

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

更新

  • static getDerivedStateFromProps():类似componentsWillReceiveProps
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

卸载

  • componentWillUnmount()

错误处理

  • static getDerivedStateFromError()
  • componentDidCatch()

按调用阶段分类

reconcile阶段的生命周期:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render

commit阶段的生命周期:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate

需要注意的是,由于异步渲染,在“渲染”时期(如render)和“提交”时期(如getSnapshotBeforeUpdate和componentDidUpdate)间可能会存在延迟。

变更背后

这里面其实有很多有趣的关联关系,随便挑两个说说:

  • 废除了componentsWillReceiveProps: 一开始大家非常不习惯,至少我react15的代码里大片组件都在使用这个生命周期。但是这个东西其实挺烦人,给个生命周期钩子本来是让你感知到props的变化,然而使用场景都是在setState,根据props内容去setState,甚至setState多次,根据不同条件不同的set,更甚的还有异步的setState。既然大家是要根据props来setState,不如提供个方法getDerivedStateFromProps,入参含有nextProps,让你返回新的state。这样做的好处是你是声明式一次性返回该定型的state,如何更新何时更新交给了react内部来协调,来达到更好的优化,而不是让你自己各种setState把更新调用时机交给你了,搞出很多不推荐用法和性能问题。

  • componentWillMountcomponentWillUpdate被废除了:这个是有一定原因的,fiber的协调算法让这2个声明周期不得不废除,原本的这2个调用时期因为新的协调算法会根据浏览器空闲状态去决定是否在当前时机继续diff及进行后面的commit,所以如果浏览器繁忙,协调过程会暂停,一些情况下会重新来,所以componentWillMountcomponentWillUpdate如果依旧保留,他们后续不一定会立即发生mountupdate,这2个生命周期也可能在一次组件挂载和更新过程中被调用多次,自然失去了意义。

新的生命周期文档: 组件生命周期

fiber

fiber前面写过专门的,就不重复了。这个更新点当之无愧是react16之所以是16的关键。他的设计让react16很多部分的代码简直是重构

hooks

hooks让你与React状态以及函数式组件的生命周期特性hooks的函数。hooks是为了让你抛弃class使用React的

hooks在1个多月前的react 16.8.0正式发布,我春节看它还是提案阶段,节后上班就看到它被正式发布了,这年头前端开发要跟上使用库的更新节奏得大步快跑啊。。。

这玩意在社区也引起了广泛的关注和讨论。

初次看下来我最大的感受是,react开发团队真的是在追求函数式和纯度的路上越走越远,坚持初心。之前推荐开发多用函数式组件,但是涉及到组件内部state和生命周期就没法用函数式组件。class组件含有内部state时其实是不纯的,相同props可能会有不同的渲染结果。

现在hooks终于能实现一切皆函数了,通过state Hook useState让state得到支持,通过Effect Hook useEffect让生命周期这样的需求得以支持,其实生命周期也就是根据变更在特定的阶段执行的有副作用变更的函数,譬如getDerivedStateFromProps

demo:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

hooks的意义很明显:组件state和逻辑分离复用,在不同的组件之间复用有状态的行为的方式

官方文档

我这个文档demo党,没有在项目里实用hooks,对它的理解还是不深切。可以看看创造较复杂场景使用hooks解决,并对比出优缺点给出体验报告的长文:探React Hooks

render props

render props是简单情境下替换高阶组件的方案

demo:


class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

render props: https://react.docschina.org/docs/render-props.html 。

context的变更

context在以前一直被官方标注着将废弃,这玩意确实有点别扭,你可跨组件绕过props插入数据了,很容易造成组件维护和复用问题。然而广大开发者总是遇到一个数据要传给多个组件,可能是很叶子节点的组件要用根节点的一个数据,一层层传props真的太头疼了。场景需求,官方终于把context摆上了正统地位,给了更声明化的语句让使用者了解到context的存在和内容,来解决维护问题。

demo:

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

这一波新增了相关api:

  • React.createContext
  • Context.Provider
  • Class.contextType
  • Context.Consumer

我理解通过React.createContext声明context和默认值,再通过Context.Provider在需要的地方注入,大大提高了对context的感知,这玩意不在是上游注入的黑盒了。

详情戳官网context文档

对render阶段生命周期多次调用无负作用的检测

commit阶段通常非常快,但是render阶段可能会很慢,因此,react16的异步模式将render步骤变成碎片化处理,通过暂停和恢复重启来避免阻塞浏览器。这就意味着react在commit之前可能触发render阶段的生命周期多次,因为出现错误或者插入了一个更高优先级的任务,一些render阶段的声明周期调用后甚至不会有对应的commit阶段发生。

因为这个情况的存在,所以要求render阶段的生命周期不能带有副作用,即多次调用和一次调用结果一样没有啥区别,不会带来改变。

如果开发者没有遵循这个规范该怎么保证正确性呢?react给了一个不是非常有力但是有大于无的检测方案:在开发模式下,内部会重复调用以下容易产生副作用的方法,来对比结果如果不一致抛出异常:

  • Class的constructor方法
  • render
  • setState updater functions (the first argument)
  • getDerivedStateFromProps

详情看官网detecting-unexpected-side-effects章节

传递refer

在高复用基础ui组件库下,会有将ref传给子元素的需求,以前的react版本传递变得很痛苦,react16针对此场景给出了新api来支持。

demo:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

详情查看官网forwarding-refs

关于复合的建议

用于同一路径或数据但渲染不同的children

也可用于简单情境下替换高阶组件的方案,避免多层级透传props也可以使用这个方式。

复合vs继承

这其实不能算react16的更新啦…

react.PureComponent、react.memo

通过对比props和state来决定是否走render逻辑的方法:

  • shouldComponentUpdate
  • react.PureComponent
  • 函数式组件:react.memo