React16 新特性

1 引言

于 2017.09.26 Facebook 发布 React v16.0 版本,时至今日已更新到 React v16.6,且引入了大量的使人振奋的新特性,本文章将带领你们根据 React 更新的时间脉络了解 React16 的新特性。前端

2 概述

按照 React16 的更新时间,从 React v16.0 ~ React v16.6 进行概述。node

React v16.0react

  • render 支持返回数组和字符串、Error Boundaries、createPortal、支持自定义 DOM 属性、减小文件体积、fiber;

React v16.1git

  • react-call-return;

React v16.2github

  • Fragment;

React v16.3算法

  • createContext、createRef、forwardRef、生命周期函数的更新、Strict Mode;

React v16.4npm

  • Pointer Events、update getDerivedStateFromProps;

React v16.5api

  • Profiler;

React v16.6数组

  • memo、lazy、Suspense、static contextType、static getDerivedStateFromError();

React v16.7(~Q1 2019)promise

  • Hooks;

React v16.8(~Q2 2019)

  • Concurrent Rendering;

React v16.9(~mid 2019)

  • Suspense for Data Fetching;

下面将按照上述的 React16 更新路径对每一个新特性进行详细或简短的解析。

3 精读

React v16.0

render 支持返回数组和字符串

// 不须要再将元素做为子元素装载到根元素下面
render() {
  return [
    <li/>1</li>,
    <li/>2</li>,
    <li/>3</li>,
  ];
}
复制代码

Error Boundaries

React15 在渲染过程当中遇到运行时的错误,会致使整个 React 组件的崩溃,并且错误信息不明确可读性差。React16 支持了更优雅的错误处理策略,若是一个错误是在组件的渲染或者生命周期方法中被抛出,整个组件结构就会从根节点中卸载,而不影响其余组件的渲染,能够利用 error boundaries 进行错误的优化处理。

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  componentDidCatch(error, info) {
    this.setState({ hasError: true });

    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>数据错误</h1>;
    }
    
    return this.props.children;
  }
}
复制代码

createPortal

createPortal 的出现为 弹窗、对话框 等脱离文档流的组件开发提供了便利,替换了以前不稳定的 API unstable_renderSubtreeIntoContainer,在代码使用上能够作兼容,如:

const isReact16 = ReactDOM.createPortal !== undefined;

const getCreatePortal = () =>
  isReact16
    ? ReactDOM.createPortal
    : ReactDOM.unstable_renderSubtreeIntoContainer;
复制代码

使用 createPortal 能够快速建立 Dialog 组件,且不须要牵扯到 componentDidMount、componentDidUpdate 等生命周期函数。

而且经过 createPortal 渲染的 DOM,事件能够从 portal 的入口端冒泡上来,若是入口端存在 onDialogClick 等事件,createPortal 中的 DOM 也可以被调用到。

import React from 'react';
import { createPortal } from 'react-dom';

class Dialog extends React.Component {
  constructor() {
    super(props);

    this.node = document.createElement('div');
    document.body.appendChild(this.node);
  }

  render() {
    return createPortal(
      <div> {this.props.children} </div>,
      this.node
    );
  }
}
复制代码

支持自定义 DOM 属性

之前的 React 版本 DOM 不识别除了 HTML 和 SVG 支持的之外属性,在 React16 版本中将会把所有的属性传递给 DOM 元素。这个新特性可让咱们摆脱可用的 React DOM 属性白名单。笔者以前写过一个方法,用于过滤非 DOM 属性 filter-react-dom-props,16 以后便可再也不须要这样的方法。

减小文件体积

React16 使用 Rollup 针对不一样的目标格式进行代码打包,因为打包工具的改变使得库文件大小获得缩减。

  • React 库大小从 20.7kb(压缩后 6.9kb)下降到 5.3kb(压缩后 2.2kb)
  • ReactDOM 库大小从 141kb(压缩后 42.9kb)下降到 103.7kb(压缩后 32.6kb)
  • React + ReactDOM 库大小从 161.7kb(压缩后 49.8kb)下降到 109kb(压缩后 43.8kb)

Fiber

Fiber 是对 React 核心算法的一次从新实现,将本来的同步更新过程碎片化,避免主线程的长时间阻塞,使应用的渲染更加流畅。

在 React16 以前,更新组件时会调用各个组件的生命周期函数,计算和比对 Virtual DOM,更新 DOM 树等,这整个过程是同步进行的,中途没法中断。当组件比较庞大,更新操做耗时较长时,就会致使浏览器惟一的主线程都是执行组件更新操做,而没法响应用户的输入或动画的渲染,很影响用户体验。

Fiber 利用分片的思想,把一个耗时长的任务分红不少小片,每个小片的运行时间很短,在每一个小片执行完以后,就把控制权交还给 React 负责任务协调的模块,若是有紧急任务就去优先处理,若是没有就继续更新,这样就给其余任务一个执行的机会,惟一的线程就不会一直被独占。

所以,在组件更新时有可能一个更新任务尚未完成,就被另外一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所作的工做则会彻底做废,而后等待机会重头再来。因此 React Fiber 把一个更新过程分为两个阶段:

  • 第一个阶段 Reconciliation Phase,Fiber 会找出须要更新的 DOM,这个阶段是能够被打断的;
  • 第二个阶段 Commit Phase,是没法别打断,完成 DOM 的更新并展现;

在使用 Fiber 后,须要要检查与第一阶段相关的生命周期函数,避免逻辑的屡次或重复调用:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

与第二阶段相关的生命周期函数:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

React v16.1

Call Return(react-call-return npm)

react-call-return 目前仍是一个独立的 npm 包,主要是针对 父组件须要根据子组件的回调信息去渲染子组件场景 提供的解决方案。

在 React16 以前,针对上述场景通常有两个解决方案:

  • 首先让子组件初始化渲染,经过回调函数把信息传给父组件,父组件完成处理后更新子组件 props,触发子组件的第二次渲染才能够解决,子组件须要通过两次渲染周期,可能会形成渲染的抖动或闪烁等问题;

  • 首先在父组件经过 children 得到子组件并读取其信息,利用 React.cloneElement 克隆产生新元素,并将新的属性传递进去,父组件 render 返回的是克隆产生的子元素。虽然这种方法只须要使用一个生命周期,可是父组件的代码编写会比较麻烦;

React16 支持的 react-call-return,提供了两个函数 unstable_createCall 和 unstable_createReturn,其中 unstable_createCall 是 父组件使用,unstable_createReturn 是 子组件使用,父组件发出 Call,子组件响应这个 Call,即 Return。

  • 在父组件 render 函数中返回对 unstable_createCall 的调用,第一个参数是 props.children,第二个参数是一个回调函数,用于接受子组件响应 Call 所返回的信息,第三个参数是 props;

  • 在子组件 render 函数返回对 unstable_createReturn 的调用,参数是一个对象,这个对象会在unstable_createCall 第二个回调函数参数中访问到;

  • 当父组件下的全部子组件都完成渲染周期后,因为子组件返回的是对 unstable_createReturn 的调用因此并无渲染元素,unstable_createCall 的第二个回调函数参数会被调用,这个回调函数返回的是真正渲染子组件的元素;

针对普通场景来讲,react-call-return 有点过分设计的感受,可是若是针对一些特定场景的话,它的做用仍是很是明显,好比,在渲染瀑布流布局时,利用 react-call-return 能够先缓存子组件的 ReactElement,等必要的信息足够以后父组件再触发 render,完成渲染。

import React from 'react';
import { unstable_createReturn, unstable_createCall } from 'react-call-return';

const Child = (props) => {
  return unstable_createReturn({
    size: props.children.length,
    renderItem: (partSize, totalSize) => {
      return <div>{ props.children } { partSize } / { totalSize }</div>;
    }
  });
};

const Parent = (props) => {
  return (
    <div> { unstable_createCall( props.children, (props, returnValues) => { const totalSize = returnValues.map(v => v.size).reduce((a, b) => a + b, 0); return returnValues.map(({ size, renderItem }) => { return renderItem(size, totalSize); }); }, props ) } </div>
  );
};
复制代码

React v16.2

Fragment

Fragment 组件其做用是能够将一些子元素添加到 DOM tree 上且不须要为这些元素提供额外的父节点,至关于 render 返回数组元素。

render() {
  return (
    <Fragment> Some text. <h2>A heading</h2> More text. <h2>Another heading</h2> Even more text. </Fragment>
  );
}
复制代码

React v16.3

createContext

全新的 Context API 能够很容易穿透组件而无反作用,其包含三部分:React.createContext,Provider,Consumer。

  • React.createContext 是一个函数,它接收初始值并返回带有 Provider 和 Consumer 组件的对象;
  • Provider 组件是数据的发布方,通常在组件树的上层并接收一个数据的初始值;
  • Consumer 组件是数据的订阅方,它的 props.children 是一个函数,接收被发布的数据,而且返回 React Element;
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 (
      <ThemeContext.Consumer>
        {theme => <Button theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
}
复制代码

createRef / forwardRef

React16 规范了 Ref 的获取方式,经过 React.createRef 取得 Ref 对象。

// before React 16
···

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

  render() {
    return <div ref="myRef" />
  }

···

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

  render() {
    return <div ref={this.myRef} />
  }
···
复制代码

React.forwardRef 是 Ref 的转发, 它可以让父组件访问到子组件的 Ref,从而操做子组件的 DOM。 React.forwardRef 接收一个函数,函数参数有 props 和 ref。

const TextInput = React.forwardRef((props, ref) => (
  <input type="text" placeholder="Hello forwardRef" ref={ref} />
))

const inputRef = React.createRef()

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

  handleSubmit = event => {
    event.preventDefault()
    
    alert('input value is:' + inputRef.current.value)
  }
  
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <TextInput ref={inputRef} />
        <button type="submit">Submit</button>
      </form>
    )
  }
}
复制代码

生命周期函数的更新

React16 采用了新的内核架构 Fiber,Fiber 将组件更新分为两个阶段:Render Parse 和 Commit Parse,所以 React 也引入了 getDerivedStateFromProps 、 getSnapshotBeforeUpdate 及 componentDidCatch 等三个全新的生命周期函数。同时也将 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 标记为不安全的方法。

static getDerivedStateFromProps(nextProps, prevState)

getDerivedStateFromProps(nextProps, prevState) 其做用是根据传递的 props 来更新 state。它的一大特色是无反作用,因为处在 Render Phase 阶段,因此在每次的更新都会触发该函数, 在 API 设计上采用了静态方法,使其没法访问实例、没法经过 ref 访问到 DOM 对象等,保证了该函数的纯粹高效。

为了配合将来的 React 异步渲染机制,React v16.4 对 getDerivedStateFromProps 作了一些改变, 使其不只在 props 更新时会被调用,setState 时也会被触发。

  • 若是改变 props 的同时,有反作用的产生,这时应该使用 componentDidUpdate;
  • 若是想要根据 props 计算属性,应该考虑将结果 memoization 化;
  • 若是想要根据 props 变化来重置某些状态,应该考虑使用受控组件;
static getDerivedStateFromProps(props, state) {
  if (props.value !== state.controlledValue) {
    return {
      controlledValue: props.value,
    };
  }
  
  return null;
}
复制代码

getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate(prevProps, prevState) 会在组件更新以前获取一个 snapshot,并能够将计算得的值或从 DOM 获得的信息传递到 componentDidUpdate(prevProps, prevState, snapshot) 函数的第三个参数,经常用于 scroll 位置定位等场景。

componentDidCatch(error, info)

componentDidCatch 函数让开发者能够自主处理错误信息,诸如错误展现,上报错误等,用户能够建立本身的 Error Boundary 来捕获错误。

componentWillMount(nextProps, nextState)

componentWillMount 被标记为不安全,由于在 componentWillMount 中获取异步数据或进行事件订阅等操做会产生一些问题,好比没法保证在 componentWillUnmount 中取消掉相应的事件订阅,或者致使屡次重复获取异步数据等问题。

componentWillReceiveProps(nextProps) / componentWillUpdate(nextProps, nextState)

componentWillReceiveProps / componentWillUpdate 被标记为不安全,主要是由于操做 props 引发的 re-render 问题,而且对 DOM 的更新操做也可能致使从新渲染。

Strict Mode

StrictMode 能够在开发阶段开启严格模式,发现应用存在的潜在问题,提高应用的健壮性,其主要能检测下列问题:

  • 识别被标志位不安全的生命周期函数
  • 对弃用的 API 进行警告
  • 探测某些产生反作用的方法
  • 检测是否使用 findDOMNode
  • 检测是否采用了老的 Context API
class App extends React.Component {
  render() {
    return (
      <div> <React.StrictMode> <ComponentA /> </React.StrictMode> </div> ) } } 复制代码

React v16.4

Pointer Events

指针事件是为指针设备触发的 DOM 事件。它们旨在建立单个 DOM 事件模型来处理指向输入设备,例如鼠标,笔 / 触控笔或触摸(例如一个或多个手指)。指针是一个与硬件无关的设备,能够定位一组特定的屏幕坐标。拥有指针的单个事件模型能够简化建立 Web 站点和应用程序,并提供良好的用户体验,不管用户的硬件如何。可是,对于须要特定于设备的处理的场景,指针事件定义了一个 pointerType 属性,用于检查产生事件的设备类型。

React 新增 onPointerDown / onPointerMove / onPointerUp / onPointerCancel / onGotPointerCapture / onLostPointerCapture / onPointerEnter / onPointerLeave / onPointerOver / onPointerOut 等指针事件。

这些事件只能在支持 指针事件 规范的浏览器中工做。若是应用程序依赖于指针事件,建议使用第三方指针事件 polyfill。

React v16.5

Profiler

React 16.5 添加了对新的 profiler DevTools 插件的支持。这个插件使用 React 的 Profiler 实验性 API 去收集全部 component 的渲染时间,目的是为了找出 React App 的性能瓶颈,它将会和 React 即将发布的 时间片 特性彻底兼容。

React v16.6

memo

React.memo() 只能做用在简单的函数组件上,本质是一个高阶函数,能够自动帮助组件执行shouldComponentUpdate(),但只是执行浅比较,其意义和价值有限。

const MemoizedComponent = React.memo(props => {
  /* 只在 props 更改的时候才会从新渲染 */
});
复制代码

lazy / Suspense

React.lazy() 提供了动态 import 组件的能力,实现代码分割。

Suspense 做用是在等待组件时 suspend(暂停)渲染,并显示加载标识。

目前 React v16.6 中 Suspense 只支持一个场景,即便用 React.lazy() 和 <React.Suspense> 实现的动态加载组件。

import React, {lazy, Suspense} from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense>
  );
}
复制代码

static contextType

static contextType 为 Context API 提供了更加便捷的使用体验,能够经过 this.context 来访问 Context。

const MyContext = React.createContext();

class MyClass extends React.Component {
  static contextType = MyContext;
  
  componentDidMount() {
    const value = this.context;
  }
  
  componentDidUpdate() {
    const value = this.context;
  }
  
  componentWillUnmount() {
    const value = this.context;
  }
  
  render() {
    const value = this.context;
  }
}
复制代码

getDerivedStateFromError

static getDerivedStateFromError(error) 容许开发者在 render 完成以前渲染 Fallback UI,该生命周期函数触发的条件是子组件抛出错误,getDerivedStateFromError 接收到这个错误参数后更新 state。

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }
  
  componentDidCatch(error, info) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
  
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children; 
  }
}
复制代码

React v16.7(~Q1 2019)

Hooks

Hooks 要解决的是状态逻辑复用问题,且不会产生 JSX 嵌套地狱,其特性以下:

  • 多个状态不会产生嵌套,依然是平铺写法;
  • Hooks 能够引用其余 Hooks;
  • 更容易将组件的 UI 与状态分离;

Hooks 并非经过 Proxy 或者 getters 实现,而是经过数组实现,每次 useState 都会改变下标,若是 useState 被包裹在 condition 中,那每次执行的下标就可能对不上,致使 useState 导出的 setter 更新错数据。

更多 Hooks 使用场景能够阅读下列文章:

function App() {
  const [open, setOpen] = useState(false);
  
  return (
    <>
      <Button type="primary" onClick={() => setOpen(true)}>
        Open Modal
      </Button>
      <Modal
        visible={open}
        onOk={() => setOpen(false)}
        onCancel={() => setOpen(false)}
      />
    </>
  );
}
复制代码

React v16.8(~Q2 2019)

Concurrent Rendering

Concurrent Rendering 并发渲染模式是在不阻塞主线程的状况下渲染组件树,使 React 应用响应性更流畅,它容许 React 中断耗时的渲染,去处理高优先级的事件,如用户输入等,还能在高速链接时跳过没必要要的加载状态,用以改善 Suspense 的用户体验。

目前 Concurrent Rendering 还没有正式发布,也没有详细相关文档,须要等待 React 团队的正式发布。

React v16.9(~mid 2019)

Suspense for Data Fetching

Suspense 经过 ComponentDidCatch 实现用同步的方式编写异步数据的请求,而且没有使用 yield / async / await,其流程:调用 render 函数 -> 发现有异步请求 -> 暂停渲染,等待异步请求结果 -> 渲染展现数据。

不管是什么异常,JavaScript 都能捕获,React就是利用了这个语言特性,经过 ComponentDidCatch 捕获了全部生命周期函数、render函数等,以及事件回调中的错误。若是有缓存则读取缓存数据,若是没有缓存,则会抛出一个异常 promise,利用异常作逻辑流控制是一种拥有较深的调用堆栈时的手段,它是在虚拟 DOM 渲染层作的暂停拦截,代码可在服务端复用。

import { fetchMovieDetails } from '../api';
import { createFetch } from '../future';

const movieDetailsFetch = createFetch(fetchMovieDetails);

function MovieDetails(props) {
  const movie = movieDetailsFetch.read(props.id);

  return (
    <div>
      <MoviePoster src={movie.poster} />
      <MovieMetrics {...movie} />
    </div>
  );
}
复制代码

4 总结

从 React16 的一系列更新和新特性中咱们能够窥见,React 已经不只仅只在作一个 View 的展现库,而是想要发展成为一个包含 View / 数据管理 / 数据获取 等场景的前端框架,以 React 团队的技术实力以及想法,笔者仍是很期待和看好 React 的将来,不过它渐渐地已经对开发新手们不太友好了。

5 更多讨论

讨论地址是:[精读《React16 新特性》 · Issue #115 · dt-fe/weekly]github.com/dt-fe/weekl…)

若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

相关文章
相关标签/搜索