1. 程式人生 > >React 16.6新API

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

相當漂亮 的特性,篇幅限制(此處刪掉了276行),暫不展開

四.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 API 的 this.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_TYPE 、 REACT_LAZY_TYPE 、 REACT_SUSPENSE_TYPE),細數一下,竟然有這麼多了:

  • REACT_ELEMENT_TYPE :普通React元件型別,如 <MyComponent />

  • REACT_PORTAL_TYPE : Protals 元件, ReactDOM.createPortal()

  • REACT_FRAGMENT_TYPE : Fragment 虛擬元件, <></> 或 <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 ……

參考資料

原文https://www.tuicool.com/articles/miqAjyF