精讀《Epitath 源碼 - renderProps 新用法》

1 引言

很高興這一期的話題是由 epitath 的做者 grsabreu 提供的。javascript

前端發展了 20 多年,隨着發展中國家愈來愈多的互聯網從業者涌入,如今前端知識玲琅知足,概念、庫也愈來愈多。雖然內容愈來愈多,但做爲個體的你的時間並無增多,如何持續學習新知識,學什麼將會是個大問題。前端

前端精讀經過吸引優質的用戶,提供最前沿的話題或者設計理念,雖然每週一篇文章不足以歸納這一週的全部焦點,但能夠保證你閱讀的這十幾分鐘沒有在浪費時間,每一篇精讀都是通過精心篩選的,咱們既討論你們關注的焦點,也能找到倉庫角落被遺忘的珍珠。java

2 概述

在介紹 Epitath 以前,先介紹一下 renderProps。react

renderProps 是 jsx 的一種實踐方式,renderProps 組件並不渲染 dom,但提供了持久化數據與回調函數幫助減小對當前組件 state 的依賴。git

RenderProps 的概念

react-powerplug 就是一個 renderProps 工具庫,咱們看看能夠作些什麼:github

<Toggle initial={true}>
  {({ on, toggle }) => <Checkbox checked={on} onChange={toggle} />}
</Toggle>

Toggle 就是一個 renderProps 組件,它能夠幫助控制受控組件。好比僅僅利用 Toggle,咱們能夠大大簡化 Modal 組件的使用方式:dom

class App extends React.Component {
  state = { visible: false };

  showModal = () => {
    this.setState({
      visible: true
    });
  };

  handleOk = e => {
    this.setState({
      visible: false
    });
  };

  handleCancel = e => {
    this.setState({
      visible: false
    });
  };

  render() {
    return (
      <div>
        <Button type="primary" onClick={this.showModal}>
          Open Modal
        </Button>
        <Modal
          title="Basic Modal"
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
        >
          <p>Some contents...</p>
          <p>Some contents...</p>
          <p>Some contents...</p>
        </Modal>
      </div>
    );
  }
}

ReactDOM.render(<App />, mountNode);

這是 Modal 標準代碼,咱們能夠使用 Toggle 簡化爲:async

class App extends React.Component {
  render() {
    return (
      <Toggle initial={false}>
        {({ on, toggle }) => (
          <Button type="primary" onClick={toggle}>
            Open Modal
          </Button>
          <Modal
            title="Basic Modal"
            visible={on}
            onOk={toggle}
            onCancel={toggle}
          >
            <p>Some contents...</p>
            <p>Some contents...</p>
            <p>Some contents...</p>
          </Modal>
        )}
      </Toggle>
    );
  }
}

ReactDOM.render(<App />, mountNode);

省掉了 state、一堆回調函數,並且代碼更簡潔,更語義化。ide

renderProps 內部管理的狀態不方便從外部獲取,所以只適合保存業務無關的數據,好比 Modal 顯隱。

RenderProps 嵌套問題的解法

renderProps 雖然好用,但當咱們想組合使用時,可能會遇到層層嵌套的問題:函數

<Counter initial={5}>
  {counter => {
    <Toggle initial={false}>
      {toggle => {
        <MyComponent counter={counter.count} toggle={toggle.on} />;
      }}
    </Toggle>;
  }}
</Counter>

所以 react-powerplugin 提供了 compose 函數,幫助聚合 renderProps 組件:

import { compose } from 'react-powerplug'

const ToggleCounter = compose(
  <Counter initial={5} />,
  <Toggle initial={false} />
)

<ToggleCounter>
  {(toggle, counter) => (
    <ProductCard {...} />
  )}
</ToggleCounter>

使用 Epitath 解決嵌套問題

Epitath 提供了一種新方式解決這個嵌套的問題:

const App = epitath(function*() {
  const { count } = yield <Counter />
  const { on } = yield <Toggle />

  return (
    <MyComponent counter={count} toggle={on} />
  )
})

<App />

renderProps 方案與 Epitath 方案,能夠類比爲 回調 方案與 async/await 方案。Epitath 和 compose 都解決了 renderProps 可能帶來的嵌套問題,而 compose 是經過將多個 renderProps merge 爲一個,而 Epitath 的方案更接近 async/await 的思路,利用 generator 實現了僞同步代碼。

3 精讀

Epitath 源碼一共 40 行,咱們分析一下其精妙的方式。

下面是 Epitath 完整的源碼:

import React from "react";
import immutagen from "immutagen";

const compose = ({ next, value }) =>
  next
    ? React.cloneElement(value, null, values => compose(next(values)))
    : value;

export default Component => {
  const original = Component.prototype.render;
  const displayName = `EpitathContainer(${Component.displayName ||
    "anonymous"})`;

  if (!original) {
    const generator = immutagen(Component);

    return Object.assign(
      function Epitath(props) {
        return compose(generator(props));
      },
      { displayName }
    );
  }

  Component.prototype.render = function render() {
    // Since we are calling a new function to be called from here instead of
    // from a component class, we need to ensure that the render method is
    // invoked against `this`. We only need to do this binding and creation of
    // this function once, so we cache it by adding it as a property to this
    // new render method which avoids keeping the generator outside of this
    // method's scope.
    if (!render.generator) {
      render.generator = immutagen(original.bind(this));
    }

    return compose(render.generator(this.props));
  };

  return class EpitathContainer extends React.Component {
    static displayName = displayName;
    render() {
      return <Component {...this.props} />;
    }
  };
};

immutagen

immutagen 是一個 immutable generator 輔助庫,每次調用 .next 都會生成一個新的引用,而不是本身發生 mutable 改變:

import immutagen from "immutagen";

const gen = immutagen(function*() {
  yield 1;
  yield 2;
  return 3;
})(); // { value: 1, next: [function] }

gen.next(); // { value: 2, next: [function] }
gen.next(); // { value: 2, next: [function] }

gen.next().next(); // { value: 3, next: undefined }

compose

看到 compose 函數就基本明白其實現思路了:

const compose = ({ next, value }) =>
  next
    ? React.cloneElement(value, null, values => compose(next(values)))
    : value;
const App = epitath(function*() {
  const { count } = yield <Counter />;
  const { on } = yield <Toggle />;
});

經過 immutagen,依次調用 next,生成新組件,且下一個組件是上一個組件的子組件,所以會產生下面的效果:

yield <A>
yield <B>
yield <C>
// 等價於
<A>
  <B>
    <C />
  </B>
</A>

到此其源碼精髓已經解析完了。

存在的問題

crimx 在討論中提到,Epitath 方案存在的最大問題是,每次 render 都會生成全新的組件,這對內存是一種挑戰。

稍微解釋一下,不管是經過 原生的 renderProps 仍是 compose,同一個組件實例只生成一次,React 內部會持久化這些組件實例。而 immutagen 在運行時每次執行渲染,都會生成不可變數據,也就是全新的引用,這會致使廢棄的引用存在大量 GC 壓力,同時 React 每次拿到的組件都是全新的,雖然功能相同。

4 總結

epitath 巧妙的利用了 immutagen 的不可變 generator 的特性來生成組件,而且在遞歸 .next 時,將順序代碼解析爲嵌套代碼,有效解決了 renderProps 嵌套問題。

喜歡 epitath 的同窗趕快入手吧!同時咱們也看到 generator 手動的步驟控制帶來的威力,這是 async/await 徹底沒法作到的。

是否能夠利用 immutagen 解決 React Context 與組件相互嵌套問題呢?還有哪些其餘前端功能能夠利用 immutagen 簡化的呢?歡迎加入討論。

5 更多討論

討論地址是: 精讀《Epitath - renderProps 新用法》 · Issue #106 · dt-fe/weekly

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

相關文章
相關標籤/搜索