React 16.6新API

一.概览

新增了几个方便的特性:

  • React.memo:函数式组件也有“shouldComponentUpdate”生命周期了

  • React.lazy:配合Suspense特性轻松优雅地完成代码拆分(Code-Splitting)

  • static contextType:class组件可以更容易地访问单一Context

  • static getDerivedStateFromError():SSR友好的“componentDidCatch”

其中最重要的是Suspense特性,在之前的React Async Rendering中提到过:

另外,将来会提供一个suspense(挂起)API,允许挂起视图渲染,等待异步操作完成,让loading场景更容易控制,具体见Sneak Peek: Beyond React 16演讲视频里的第2个Demo

而现在(v16.6.0,发布于2018/10/23),就是大约8个月之后的“将来”

二.React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* only rerenders if props change */
});

还有个可选的compare参数:

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);

类似于PureComponent的高阶组件,包一层memo,就能让普通函数式组件拥有PureComponent的性能优势:

React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.

内部实现

实现上非常简单:

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}

无非就是外挂式shouldComponentUpdate生命周期,对比class组件:

// 普通class组件
class MyClassComponent {
  // 没有默认的shouldComponentUpdate,可以手动实现
  shouldComponentUpdate(oldProps: Props, newProps: Props): boolean {
    return true;
  }
}

// 继承自PureComponent的组件相当于
class MyPureComponent {
  // 拥有默认shouldComponentUpdate,即shallowEqual
  shouldComponentUpdate(oldProps: Props, newProps: Props): boolean {
    return shallowEqual(oldProps, newProps);
  }
}

// 函数式组件
function render() {
  // 函数式组件,不支持shouldComponentUpdate
}

// Memo组件相当于
const MyMemoComponent = {
  type: function render() {
    // 函数式组件,不支持shouldComponentUpdate
  }
  // 拥有默认的(挂在外面的)shouldComponentUpdate,即shallowEqual
  compare: shallowEqual
};

如此这般,就给函数式组件粘了个shouldComponentUpdate上去,接下来的事情猜也能猜到了:

// ref: react-16.6.3/react/packages/react-reconciler/src/ReactFiberBeginWork.js
function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  updateExpirationTime,
  renderExpirationTime: ExpirationTime,
): null | Fiber {
    // Default to shallow comparison
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  }
}

所以,从实现上来看,React.memo()这个API与memo关系倒不大,实际意义是:函数式组件也有“shouldComponentUpdate”生命周期了

注意,compare默认是shallowEqual,所以React.memo第二个参数compare实际含义是shouldNotComponentUpdate,而不是我们所熟知的相反的那个。API设计上确实有些迷惑,非要引入一个相反的东西:

Unlike the shouldComponentUpdate() method on class components, this is the inverse from shouldComponentUpdate.

P.S.RFC定稿过程中第二个参数确实备受争议(equal, arePropsEqual, arePropsDifferent, renderOnDifference, isEqual, shouldUpdate...等10000个以内),具体见React.memo()

手动实现个memo?

话说回来,这样一个高阶组件其实不难实现:

function memo(render, shouldNotComponentUpdate = shallowEqual) {
  let oldProps, rendered;
  return function(newProps) {
    if (!shouldNotComponentUpdate(oldProps, newProps)) {
      rendered = render(newProps);
      oldProps = newProps;
    }

    return rendered;
  }
}

手动实现的这个盗版与官方版本功能上等价(甚至性能也不相上下),所以又一个锦上添花的东西

三.React.lazy: Code-Splitting with Suspense

相当漂亮的特性,篇幅限制,具体见React Suspense

四.static contextType

v16.3推出了新Context API

const ThemeContext = React.createContext('light');

class ThemeProvider extends React.Component {
  state = {theme: 'light'};

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}

class ThemedButton extends React.Component {
  render() {
    return (
      // 这一部分看起来很麻烦,读个context而已
      <ThemeContext.Consumer>
        {theme => <Button theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
}

为了让class组件访问Context数据方便一些,新增了static contextType特性:

class ThemedButton extends React.Component {
  static contextType = ThemeContext;

  render() {
    let theme = this.context;

    return (
      // 喧嚣停止了
      <Button theme={theme} />
    );
  }
}

其中contextType注意,之前那个旧的多个s,叫contextTypes)只支持React.createContext()返回类型,翻新了旧Context APIthis.context(变成单一值了,之前是对象)

用法上不那么变态了,但只支持访问单一Context值。要访问一堆Context值的话,只能用上面看起来很麻烦的那种方式

// A component may consume multiple contexts
function Content() {
  return (
    // 。。。。
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

五.static getDerivedStateFromError()

static getDerivedStateFromError(error)

又一个错误处理API:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

用法与v16.0的componentDidCatch(error, info)非常相像:

class ErrorBoundary extends React.Component {
  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
}

二者都会在子树渲染出错后触发,但触发时机上存在微妙的差异

  • static getDerivedStateFromError:在render阶段触发,不允许含有副作用(否则多次执行会出问题)

  • componentDidCatch:在commit阶段触发,因此允许含有副作用(如logErrorToMyService

前者的触发时机足够早,所以能够多做一些补救措施,比如避免null ref引发连锁错误

另一个区别是Did系列生命周期(如componentDidCatch)不支持SSR,而getDerivedStateFromError从设计上就考虑到了SSR(目前v16.6.3还不支持,但说了会支持)

目前这两个API在功能上是有重叠的,都可以在子树出错之后通过改变state来做UI降级,但后续会细分各自的职责

  • static getDerivedStateFromError:专做UI降级

  • componentDidCatch:专做错误上报

六.过时API

又两个API要被打入冷宫:

  • ReactDOM.findDOMNode():性能原因以及设计上的问题,建议换用ref forwarding

  • 旧Context API:性能及实现方面的原因,建议换用新Context API

P.S.暂时还能用,但将来版本会去掉,可以借助StrictMode完成迁移

七.总结

函数式组件也迎来了“shouldComponentUpdate”,还有漂亮的Code-Splitting支持,以及缓解Context Consumer繁琐之痛的补丁API,和职责清晰的UI层兜底方案

13种React组件

v16.6新增了几类组件(REACT_MEMO_TYPEREACT_LAZY_TYPEREACT_SUSPENSE_TYPE),细数一下,竟然有这么多了:

  • REACT_ELEMENT_TYPE:普通React组件类型,如<MyComponent />

  • REACT_PORTAL_TYPEProtals组件,ReactDOM.createPortal()

  • REACT_FRAGMENT_TYPEFragment虚拟组件,<></><React.Fragment></React.Fragment>[,]

  • REACT_STRICT_MODE_TYPE:带过时API检查的严格模式组件,<React.StrictMode>

  • REACT_PROFILER_TYPE:用来开启组件范围性能分析,见Profiler RFC,目前还是实验性API,<React.unstable_Profiler>稳定之后会变成<React.Profiler>

  • REACT_PROVIDER_TYPE:Context数据的生产者Context.Provider<React.createContext(defaultValue).Provider>

  • REACT_CONTEXT_TYPE:Context数据的消费者Context.Consumer<React.createContext(defaultValue).Consumer>

  • REACT_ASYNC_MODE_TYPE:开启异步特性的异步模式组件,过时了,换用REACT_CONCURRENT_MODE_TYPE

  • REACT_CONCURRENT_MODE_TYPE:用来开启异步特性,暂时还没放出来,处于Demo阶段<React.unstable_ConcurrentMode>稳定之后会变成<React.ConcurrentMode>

  • REACT_FORWARD_REF_TYPE:向下传递Ref的组件React.forwardRef()

  • REACT_SUSPENSE_TYPE:组件范围延迟渲染<Suspense fallback={<MyLoadingComponent>}>

  • REACT_MEMO_TYPE类似于PureComponent的高阶组件React.memo()

  • REACT_LAZY_TYPE动态引入的组件React.lazy()

曾几何时,v15-只有1种REACT_ELEMENT_TYPE……

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code