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…)

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

相關文章
相關標籤/搜索