最近在學習React的封裝,雖然平常的開發中也有用到HOC或者Render Props,但從繼承到組合,靜態構建到動態渲染,都是似懂非懂,索性花時間系統性的整理,若有錯誤,請輕噴~~html
如下是React官方的一個例子,我會採用不一樣的封裝方法來嘗試代碼複用,例子地址。react
組件在 React 是主要的代碼複用單元,但如何共享狀態或一個組件的行爲封裝到其餘須要相同狀態的組件中並非很明瞭
。
例如,下面的組件在 web 應用追蹤鼠標位置:web
class MouseTracker extends React.Component { constructor(props) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.state = { x: 0, y: 0 }; } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>Move the mouse around!</h1> <p>The current mouse position is ({this.state.x}, {this.state.y})</p> </div> ); } }
隨着鼠標在屏幕上移動,在一個 編程
的組件上顯示它的 (x, y) 座標。數組
如今的問題是:咱們如何在另外一個組件中重用行爲?換句話說,若另外一組件須要知道鼠標位置,咱們可否封裝這一行爲以讓可以容易在組件間共享?app
因爲組件是 React 中最基礎的代碼重用單元
,如今嘗試重構一部分代碼可以在
// The <Mouse> component encapsulates the behavior we need... class Mouse extends React.Component { constructor(props) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.state = { x: 0, y: 0 }; } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {/* ...but how do we render something other than a <p>? */} <p>The current mouse position is ({this.state.x}, {this.state.y})</p> </div> ); } } class MouseTracker extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> <Mouse /> </div> ); } }
如今
例如,假設咱們如今有一個在屏幕上跟隨鼠標渲染一張貓的圖片的
首先,你可能會像這樣,嘗試在
class Cat extends React.Component { render() { const mouse = this.props.mouse return ( <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} /> ); } } class MouseWithCat extends React.Component { constructor(props) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.state = { x: 0, y: 0 }; } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {/* We could just swap out the <p> for a <Cat> here ... but then we would need to create a separate <MouseWithSomethingElse> component every time we need to use it, so <MouseWithCat> isn't really reusable yet. */} <Cat mouse={this.state} /> </div> ); } } class MouseTracker extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> <MouseWithCat /> </div> ); } }
這一方法對咱們的具體用例來講可以生效,但咱們卻無法實現真正的將行爲封裝成可重用的方式的目標。如今,每次咱們在不一樣的用例中想要使用鼠標的位置,咱們就不得不建立一個新的針對那一用例渲染不一樣內容的組件 (如另外一個關鍵的 <MouseWithCat>)
。
React Mixin將通用共享的方法包裝成Mixins方法,而後注入各個組件實現,事實上已是不被官方推薦使用
了,但仍然能夠學習一下,瞭解其爲何被遺棄,先從API看起。
React Mixin只能經過React.createClass()使用, 以下:
var mixinDefaultProps = {} var ExampleComponent = React.createClass({ mixins: [mixinDefaultProps], render: function(){} });
// 封裝的Mixin const mouseMixin = { getInitialState() { return { x: 0, y: 0 } }, handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }) } } const Mouse = createReactClass({ mixins: [mouseMixin], render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <p>The current mouse position is ({this.state.x}, {this.state.y})</p> </div> ) } }) const Cat = createReactClass({ mixins: [mouseMixin], render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <img src="/cat.jpg" style={{ position: 'absolute', left: this.state.x, top: this.state.y }} alt="" /> </div> ) } })
然而,爲何Mixin會被不推薦使用?概括起來就是如下三點
1. Mixin引入了隱式依賴關係 如:
你可能會寫一個有狀態的組件,而後你的同事可能會添加一個讀取這個狀態的mixin。在幾個月內,您可能須要將該狀態移至父組件,以便與兄弟組件共享。你會記得更新mixin來讀取道具嗎?若是如今其餘組件也使用這個mixin呢?
2. Mixin致使名稱衝突 如:
你在該Mixin定義了getSomeName, 另一個Mixin又定義了一樣的名稱getSomeName, 形成了衝突。
3. Mixin致使複雜的滾雪球
隨着時間和業務的增加, 你對Mixin的修改愈來愈多, 到最後會變成一個難以維護的Mixin。
4. 擁抱ES6,ES6的class不支持Mixin
高階組件(HOC)是react中的高級技術,用來重用組件邏輯。但高階組件自己並非React API。它只是一種模式,這種模式是由react自身的組合性質必然產生的,是React社區發展中產生的一種模式
。
高階組件的名稱是從高階函數來的, 若是瞭解過函數式編程, 就會知道高階函數就是一個入參是函數,返回也是函數的函數,那麼高階組件顧名思義,就是一個入參是組件,返回也是組件的函數
,如:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高階組件在社區中, 有兩種使用方式, 分別是:
其中 W (WrappedComponent) 指被包裹的 React.Component,E (EnhancedComponent) 指返回類型爲 React.Component 的新的 HOC。
Props Proxy
: HOC 對傳給 WrappedComponent W 的 porps 進行操做。Inheritance Inversion
: HOC 繼承 WrappedComponent W。依然是使用以前的例子, 先從比較普通使用的Props Proxy
看起:
class Mouse extends React.Component { render() { const { x, y } = this.props.mouse return ( <p>The current mouse position is ({x}, {y})</p> ) } } class Cat extends React.Component { render() { const { x, y } = this.props.mouse return ( <img src="/cat.jpg" style={{ position: 'absolute', left: x, top: y }} alt="" /> ) } } const MouseHoc = (MouseComponent) => { return class extends React.Component { constructor(props) { super(props) this.handleMouseMove = this.handleMouseMove.bind(this) this.state = { x: 0, y: 0 } } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <MouseComponent mouse={this.state} /> </div> ) } } } const EnhanceMouse = MouseHoc(Mouse) const EnhanceCat = MouseHoc(Cat)
那麼在Hoc的Props Proxy模式下, 咱們能夠作什麼?
操做Props
如上面的MouseHoc, 假設在平常開發中,咱們須要傳入一個props給Mouse或者Cat,那麼咱們能夠在HOC裏面對props進行增刪查改
等操做,以下:
const MouseHoc = (MouseComponent, props) => { props.text = props.text + '---I can operate props' return class extends React.Component { ...... render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <MouseComponent {...props} mouse={this.state} /> </div> ) } } } MouseHoc(Mouse, { text: 'some thing...' })
經過 Refs 訪問組件實例
function refsHOC(WrappedComponent) { return class RefsHOC extends React.Component { proc(wrappedComponentInstance) { wrappedComponentInstance.method() } render() { const props = Object.assign({}, this.props, {ref: this.proc.bind(this)}) return <WrappedComponent {...props}/> } } }
提取state
就是咱們的例子。
<MouseComponent mouse={this.state} />
包裹 WrappedComponent
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <MouseComponent mouse={this.state} /> </div>
另一種HOC模式則是Inheritance Inversion
,不過該模式比較少見,一個最簡單的例子以下:
function iiHOC(WrappedComponent) { return class Enhancer extends WrappedComponent { render() { return super.render() } } }
你能夠看到,返回的 HOC 類(Enhancer)繼承了 WrappedComponent。之因此被稱爲 Inheritance Inversion 是由於 WrappedComponent 被 Enhancer 繼承了,而不是 WrappedComponent 繼承了 Enhancer。在這種方式中,它們的關係看上去被反轉(inverse)了。Inheritance Inversion 容許
HOC 經過 this 訪問到 WrappedComponent,意味着它能夠訪問到 state、props、組件生命週期方法和 render 方法
。
那麼在咱們的例子中它是這樣的:
class Mouse extends React.Component { render(props) { const { x, y } = props.mouse return ( <p>The current mouse position is ({x}, {y})</p> ) } } class Cat extends React.Component { render(props) { const { x, y } = props.mouse return ( <img src="/cat.jpg" style={{ position: 'absolute', left: x, top: y }} alt="" /> ) } } const MouseHoc = (MouseComponent) => { return class extends MouseComponent { constructor(props) { super(props) this.handleMouseMove = this.handleMouseMove.bind(this) this.state = { x: 0, y: 0 } } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }) } render() { const props = { mouse: this.state } return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {super.render(props)} </div> ) } } } const EnhanceMouse = MouseHoc(Mouse) const EnhanceCat = MouseHoc(Cat)
一樣, 在II模式下,咱們能作些什麼呢?
渲染劫持
由於render()返回的就是JSX編譯後的對象,以下:
能夠經過手動修改這個tree,來達到一些需求效果,不過這一般不會用到:
function iiHOC(WrappedComponent) { return class Enhancer extends WrappedComponent { render() { const elementsTree = super.render() let newProps = {}; if (elementsTree && elementsTree.type === 'input') { newProps = {value: 'may the force be with you'} } const props = Object.assign({}, elementsTree.props, newProps) const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children) return newElementsTree } } }
操做 state
HOC 能夠讀取、編輯和刪除 WrappedComponent 實例的 state,若是你須要,你也能夠給它添加更多的 state。記住,這會搞亂 WrappedComponent 的 state,致使你可能會破壞某些東西。要限制 HOC 讀取或添加 state,添加 state 時應該放在單獨的命名空間裏,而不是和 WrappedComponent 的 state 混在一塊兒。
export function IIHOCDEBUGGER(WrappedComponent) { return class II extends WrappedComponent { render() { return ( <div> <h2>HOC Debugger Component</h2> <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre> <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre> {super.render()} </div> ) } } }
可能有人看到這裏會有疑惑,爲何有Class而不去使用繼承返回來使用HOC
, 這裏推薦知乎的一個比較好的答案
OOP和FP並不矛盾,因此混着用沒毛病,不少基於FP思想的庫也須要OOP來搭建。
爲何React推崇HOC和組合的方式,個人理解是React但願組件是按照最小可用的思想來進行封裝的,理想的說,就是一個組件只作一件的事情,且把它作好,DRY。在OOP原則,這叫單一職責原則。若是要對組件加強,首先應該先思路這個加強的組件須要用到哪些功能,這些功能由哪些組件提供,而後把這些組件組合起來.D中A相關的功能交由D內部的A來負責,D中B相關的功能交由D內部的B來負責,D僅僅負責維護A,B,C的關係,另外也能夠額外提供增長項,實現組件的加強。
繼承沒有什麼很差,注意,React只是推薦,但沒限制。其實用繼承來擴展組件也沒問題,並且也存在這樣的場景。好比:有一個按鈕組件,僅僅是對Button進行一個包裝,咱們且叫它Button,但是,按照產品需求,不少地方的按鈕都是帶着一個icon的,咱們須要提供一個IconButton。這是時候,就能夠經過繼承來擴展,同時組合另一個獨立的組件,咱們且叫它Icon,顯示icon的功能交給Icon組件來作,原來按鈕的功能繼續延續着。對於這種同類型組件的擴展,我認爲用繼承的方式是不要緊的,靈活性,複用性還在。
可是,用繼承的方式擴展前,要先思考,新組件是否與被繼承的組件是否是同一類型的,同一類職責的。若是是,能夠繼承,若是不是,那麼就用組合。怎麼定義同一類呢,回到上面的Button的例子,所謂同一類,就是說,我直接用IconButton直接替換掉Button,不去改動其餘代碼,頁面依然能夠正常渲染,功能能夠正常使用,就能夠認爲是同一類的,在OOP中,這叫作里氏替換原則。繼承會帶來什麼問題,以個人實踐經驗,過渡使用繼承,雖然給編碼帶來便利,但容易致使代碼失控,組件膨脹,下降組件的複用性。好比:有一個列表組件,叫它ListView吧,能夠上下滾動顯示一個item集,忽然有一天需求變了,PM說,我要這個ListView能像iOS那樣有個回彈效果。好,用繼承對這個ListView進行擴展,加入了回彈效果,任務closed。次日PM找上門來了,但願全部上下滾動的地方均可以支持回彈效果,這時候就懵逼啦,怎麼辦?把ListView中回彈效果的代碼copy一遍?這就和DRY原則相悖了不是,並且有可能受到其餘地方代碼的影響,處理回彈效果略有不一樣,要是有一天PM但願對這個回彈效果作升級,那就有得改啦。應對這種場景,最好的辦法是啥?用組合,封裝一個帶回彈效果的Scroller,ListView當作是Scroller和item容器組件的組合,其餘地方須要要用到滾動的,直接套一個Scroller,之後無論回彈效果怎麼變,我只要維護這個Scroller就行了。固然,最理想的,把回彈效果也作成一個組件SpringBackEffect,從Scroller分離出來,這樣,須要用回彈效果的地方就加上SpringBackEffect組件就行了,這就是爲何組合優先於繼承的緣由。
頁面簡單的時候,組合也好,繼承也罷,可維護就好,可以快速的響應需求迭代就好,用什麼方式實現到無所謂。但若是是一個大項目,頁面用到不少組件,或者是團隊多人共同維護的話,就要考慮協做中可能存在的矛盾,而後經過必定約束來閉坑。組合的方式是能夠保證組件具備充分的複用性,靈活度,遵照DRY原則的其中一種實踐。
Mixin就像他的名字,他混入了組件中
,咱們很難去對一個混入了多個Mixin的組件進行管理,比如一個盒子,咱們在盒子裏面塞入了各類東西(功能),最後確定是難以理清其中的脈絡。
HOC則像是一個裝飾器
,他是在盒子的外面一層一層的裝飾,當咱們想要抽取某一層或者增長某一層都很是容易。
貫穿傳遞不相關props屬性給被包裹的組件
高階組件應該貫穿傳遞與它專門關注無關的props屬性。
render() { // 過濾掉專用於這個階組件的props屬性, // 不該該被貫穿傳遞 const { extraProp, ...passThroughProps } = this.props; // 向被包裹的組件注入props屬性,這些通常都是狀態值或 // 實例方法 const injectedProp = someStateOrInstanceMethod; // 向被包裹的組件傳遞props屬性 return ( <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); }
最大化的組合性
// 不要這樣作…… const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // ……你可使用一個函數組合工具 // compose(f, g, h) 和 (...args) => f(g(h(...args)))是同樣的 const enhance = compose( // 這些都是單獨一個參數的高階組件 withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)
包裝顯示名字以便於調試
最經常使用的技術是包裹顯示名字給被包裹的組件。因此,若是你的高階組件名字是 withSubscription,且被包裹的組件的顯示名字是 CommentList,那麼就是用 WithSubscription(CommentList)這樣的顯示名字
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription; } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }
Render Props從名知義,也是一種剝離重複使用的邏輯代碼,提高組件複用性的解決方案。在被複用的組件中,
經過一個名爲「render」(屬性名也能夠不是render,只要值是一個函數便可)的屬性,該屬性是一個函數,這個函數接受一個對象並返回一個子組件,會將這個函數參數中的對象做爲props傳入給新生成的組件。
能夠看下最初的例子在render props中的應用:
class Cat extends React.Component { render() { const mouse = this.props.mouse; return ( <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} /> ); } } class Mouse extends React.Component { constructor(props) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.state = { x: 0, y: 0 }; } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */} {this.props.render(this.state)} </div> ); } } class MouseTracker extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> <Mouse render={mouse => ( <Cat mouse={mouse} /> )}/> </div> ); } }
這裏簡單的說下動態構建,由於React官方推崇動態組合
,然而HOC其實是一個靜態構建,好比,在某個需求下,咱們須要根據Mouse中某個字段來決定渲染Cat組件或者Dog組件,使用HOC會是以下:
const MouseHoc = (Component) => { return Class extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> { isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} /> } </div> ); } } }
能夠看到,咱們不得不提早靜態構建好Cat和Dog組件
假如咱們用Render props:
class MouseTracker extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> <Mouse render={(mouse, isCat) => ( isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} /> )}/> </div> ); } }
很明顯,在動態構建的時候,咱們具備更多的靈活性,咱們能夠更好的利用生命週期
,相比較HOC,就不得不引入Cat和Dog組件,污染了MouseHoc。
沒法使用SCU作優化, 具體參考官方文檔。
拋開被遺棄的Mixin和還沒有穩定的Hooks,目前社區的代碼複用方案主要仍是HOC和Render Props,我的感受,若是是多層組合或者須要動態渲染那就選擇Render Props,而若是是諸如在每一個View都要執行的簡單操做,如埋點、title設置等或者是對性能要求比較高如大量表單能夠採用HOC。
Function as Child Components Not HOCs
React高階組件和render props的適用場景有區別嗎,仍是更多的是我的偏好?
深刻理解 React 高階組件
高階組件-React
精讀《我再也不使用高階組件》
爲何 React 推崇 HOC 和組合的方式,而不是繼承的方式來擴展組件?
React 中的 Render Props
使用 Render props 吧!
渲染屬性(Render Props)