跳到主要內容
黯羽輕揚每天積累一點點

React 16.6 新 API

免費2018-11-17#JS#React代码拆分#React组件动态加载#React动态加载#React Lazy Component#React contextType vs contextTypes

函數式組件也迎來了"shouldComponentUpdate",還有漂亮的 Code-Splitting 支持

一。概覽

新增了幾個方便的特性:

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

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論