React16 新特性

twobin彬彬發表於2018-12-10

1 引言

於 2017.09.26 Facebook 釋出 React v16.0 版本,時至今日已更新到 React v16.6,且引入了大量的令人振奮的新特性,本文章將帶領大家根據 React 更新的時間脈絡瞭解 React16 的新特性。

2 概述

按照 React16 的更新時間,從 React v16.0 ~ React v16.6 進行概述。

React v16.0

  • render 支援返回陣列和字串、Error Boundaries、createPortal、支援自定義 DOM 屬性、減少檔案體積、fiber;

React v16.1

  • react-call-return;

React v16.2

  • Fragment;

React v16.3

  • createContext、createRef、forwardRef、生命週期函式的更新、Strict Mode;

React v16.4

  • Pointer Events、update getDerivedStateFromProps;

React v16.5

  • Profiler;

React v16.6

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

React v16.7(~Q1 2019)

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

如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。