精讀《Epitath 原始碼 - renderProps 新用法》
1 引言
很高興這一期的話題是由Astrocoders/epitath" rel="nofollow,noindex" target="_blank">epitath 的作者grsabreu 提供的。
前端發展了 20 多年,隨著發展中國家越來越多的網際網路從業者湧入,現在前端知識玲琅滿足,概念、庫也越來越多。雖然內容越來越多,但作為個體的你的時間並沒有增多,如何持續學習新知識,學什麼將會是個大問題。
前端精讀通過吸引優質的使用者,提供最前沿的話題或者設計理念,雖然每週一篇文章不足以概括這一週的所有焦點,但可以保證你閱讀的這十幾分鐘沒有在浪費時間,每一篇精讀都是經過精心篩選的,我們既討論大家關注的焦點,也能找到倉庫角落被遺忘的珍珠。
2 概述
在介紹 Epitath 之前,先介紹一下 renderProps。
renderProps 是 jsx 的一種實踐方式,renderProps 元件並不渲染 dom,但提供了持久化資料與回撥函式幫助減少對當前元件 state 的依賴。
RenderProps 的概念
react-powerplug 就是一個 renderProps 工具庫,我們看看可以做些什麼:
<Toggle initial={true}> {({ on, toggle }) => <Checkbox checked={on} onChange={toggle} />} </Toggle>
Toggle
就是一個 renderProps 元件,它可以幫助控制受控元件。比如僅僅利用Toggle
,我們可以大大簡化Modal
元件的使用方式:
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
簡化為:
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、一堆回撥函式,而且程式碼更簡潔,更語義化。
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 是一個 immutablegenerator
輔助庫,每次呼叫.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
如果你想參與討論,請點選這裡 ,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。