很高興這一期的話題是由 epitath 的做者 grsabreu 提供的。javascript
前端發展了 20 多年,隨着發展中國家愈來愈多的互聯網從業者涌入,如今前端知識玲琅知足,概念、庫也愈來愈多。雖然內容愈來愈多,但做爲個體的你的時間並無增多,如何持續學習新知識,學什麼將會是個大問題。前端
前端精讀經過吸引優質的用戶,提供最前沿的話題或者設計理念,雖然每週一篇文章不足以歸納這一週的全部焦點,但能夠保證你閱讀的這十幾分鐘沒有在浪費時間,每一篇精讀都是通過精心篩選的,咱們既討論你們關注的焦點,也能找到倉庫角落被遺忘的珍珠。java
在介紹 Epitath 以前,先介紹一下 renderProps。react
renderProps 是 jsx 的一種實踐方式,renderProps 組件並不渲染 dom,但提供了持久化數據與回調函數幫助減小對當前組件 state 的依賴。git
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 雖然好用,但當咱們想組合使用時,可能會遇到層層嵌套的問題:函數
<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 提供了一種新方式解決這個嵌套的問題:
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
實現了僞同步代碼。
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 是一個 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 函數就基本明白其實現思路了:
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 每次拿到的組件都是全新的,雖然功能相同。
epitath 巧妙的利用了 immutagen 的不可變 generator
的特性來生成組件,而且在遞歸 .next
時,將順序代碼解析爲嵌套代碼,有效解決了 renderProps 嵌套問題。
喜歡 epitath 的同窗趕快入手吧!同時咱們也看到 generator
手動的步驟控制帶來的威力,這是 async/await
徹底沒法作到的。
是否能夠利用 immutagen 解決 React Context 與組件相互嵌套問題呢?還有哪些其餘前端功能能夠利用 immutagen 簡化的呢?歡迎加入討論。
討論地址是: 精讀《Epitath - renderProps 新用法》 · Issue #106 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。