最近在一些項目中遇到高階組件的身影,不是很瞭解,於是深入鑽研了一番,以下權當是學習記錄了~
Mixin
在談及高階組件之前,我們先來講講它的前身 mixin ~
mixin 的作用是:如果多個組件中包含相同的方法(包括普通函數和組件生命週期函數),就可以把這一類函數提取到 mixin 中,然後在需要公共方法的組件中使用 mixin, 就可以避免每個組件都去聲明一次,從而達到複用。
React 在早期是使用 createClass 來創建一個 Component 的,而且 createClass 支持 mixin 屬性,最常見的就是 react-addons-pure-render-mixin 庫提供的 PureRenderMixin 方法,用來減少組件使用中一些不必要的渲染,使用方式如下:
import PureRenderMixin from 'react-addons-pure-render-mixin'; React.createClass({ mixins: [PureRenderMixin], render: function() { return <div>{this.props.name}</div>; } });
和需要在每一個組件中都重複實現一遍 PureRenderMixin 中淺比較的邏輯相比,上面 mixin 中的使用顯得更加簡便和明瞭,同時減少了代碼的冗餘和重複。
minin 既可以定義多個組件中共享的工具方法,同時還可以定義一些組件的生命週期函數(例如上例的 shouldComponentUpdate), 以及初始的 props 和 states。
如下所示:
var propsMixin1 = { getDefaultProps: () => { return { name: "Amy" }; } }; var propsMixin2 = { getDefaultProps: () => { return { title: "mixin" }; } }; var MixinExample = createReactClass({ mixins: [propsMixin1, propsMixin2], render: function() { return ( <div> <p>{this.props.name}</p> <p>{this.props.title}</p> </div> ); } });
但是在使用 mixin 的時候,會有如下的幾點需要注意:
-
不同 mixin 中有相同的函數
-
-
組件中使用多個 mixin, 同時不同 mixin 中定義了相同的工具函數,此時會報錯(而不是前者覆蓋後者)
-
-
-
組件中使用多個 mixin, 同時 mixin 中定義了相同的組件生命週期函數,不會報錯,此時會按傳給 createClass 的 mixin 數組順序依次調用,全部調用結束後再調用組件內部的相同的生命週期
-
-
不同 mixin 中設置 props 或者 states
-
-
組件中含有多個 mixin,不同的 mixin 中默認 props 或初始 state 中存在相同的 key 值時,React 會拋出異常
-
-
-
組件中含有多個 mixin, 不同的 mixin 中默認 props 或初始 state 中存在不同的 key 值時,則默認 props 和初始 state 都會被合併。
-
附上具體示例代碼地址
雖然 mixin 在一定程度上解決了 React 實踐中的一些痛點,但是 React 從 v0.13.0 開始,ES6 class 組件寫法中不支持 mixins, 但是還是可以使用 createClass 來使用 mixin。之後,React 社區提出了一種新的方式來取代 mixin,那就是高階組件 Higher-Order Components。
高階組件
高階組件 (Higher-Order Components) 是接受一個組件作爲參數,然後經過一些處理,返回一個相對增強的組件的函數。它是 React 中的一種模式,而不是 API 的一部分。React 官方給出一個公式描述如下:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
一個最簡單的 HOC 例子如下:
function HOC(WrappedComponent) { return class PP extends React.Component { render() { return <WrappedComponent {...this.props}/> } } } class Example extends React.PureComponent { render() { return ( <div> <p>{this.props.age}</p> </div> ); } } const HocComponent = HOC(Example); ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
高階組件的適用場景
它的使用場景有如下幾點:
-
需要抽離可複用的代碼邏輯
-
渲染劫持
-
更改 state
-
組裝修改 props
高階組件的實現方式
高階組件有兩種實現方式: 屬性代理 (Props Proxy) 和反向繼承 (Inheritance Inversion)
屬性代理
屬性代理是指所有的數據都是從最外層的 HOC 中傳給被包裹的組件,它有權限對傳入的數據進行修改,對於被包裹組件來說,HOC 對傳給自己的屬性 (Props) 起到了一層代理作用。
屬性代理可以實現如下一些功能:
-
更改 props
class Example extends React.PureComponent { constructor(props) { super(props); } render() { const { name, age, github } = this.props; return ( <div> <p>{name}</p> <p>{age}</p> <p>{github}</p> </div> ); } } function HOC(WrappedComponent) { class EnhancedComponent extends React.PureComponent { render() { const props = Object.assign({}, this.props, { name: "SunShinewyf", github: "http://github.com/SunShinewyf" }); return <WrappedComponent {...props} />; } } return EnhancedComponent; } const HocComponent = HOC(Example); ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
如上面的例子中,HOC 對最外層傳入的 props 進行了二次組裝,擴展了 props 的數據能力。
通過 refs 獲取被包裹的組件實例
class Example extends React.PureComponent { constructor(props) { super(props); this.consoleFun.bind(this); } consoleFun() { console.log("hello world"); } render() { const { age } = this.props; return ( <div> <p>{age}</p> </div> ); } } function HOC(WrappedComponent) { class EnhancedComponent extends React.PureComponent { initFunc(instance) { instance.consoleFun(); } render() { const props = Object.assign({}, this.props, { ref: this.initFunc.bind(this) }); return <WrappedComponent {...props} />; } } return EnhancedComponent; } const HocComponent = HOC(Example); ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
如果想要在 HOC 中執行被包裹組件的一些方法,就可以在 props 上組裝一下 ref 這個屬性,就可以獲取到被包裹組件的實例,從而獲取到實例的 props 以及它的方法。
組裝被包裹組件(WrappedComponent)
function HOC(WrappedComponent) { return class PP extends React.Component { render() { <div> //添加一些樣式 return <WrappedComponent {...this.props}/> </div> } } }
這個比較簡單,不詳述~
反向繼承 (Inheritance Inversion)
反向繼承是指 HOC 繼承被包裹組件,這樣被包裹的組件 (WrappedComponent) 就是 HOC 的父組件了,子組件就可以直接操作父組件的所有公開的方法和字段。
反向繼承可以實現如下功能:
對 WrappedComponent 的所有生命週期函數進行重寫,或者修改其 props 或者 state
class Example extends React.PureComponent { constructor(props) { super(props); } componentDidMount() { console.log("wrappedComponent did mount"); } render() { const { age } = this.props; return ( <div> <p>{age}</p> </div> ); } } function HOC(WrapperComponent) { return class Inheritance extends WrapperComponent { componentDidMount() { console.log("HOC did mount"); super.componentDidMount(); } render() { return super.render(); } }; } const HocComponent = HOC(Example); ReactDom.render(<HocComponent age={24} />, document.getElementById("root")); // HOC did mount // wrappedComponent did mount
由上面可以看到,HOC 中定義的生命週期方法可以訪問到 WrappedComponent 中的生命週期方法。兩者的執行順序由代碼的執行順序決定。
劫持渲染
class Example extends React.PureComponent { constructor(props) { super(props); } render() { const { age } = this.props; return <input />; } } function HOC(WrapperComponent) { return class Inheritance extends WrapperComponent { render() { const elementsTree = super.render(); let newProps = {}; if (elementsTree && elementsTree.type === "input") { newProps = { defaultValue: "the initialValue of input" }; } const props = Object.assign({}, elementsTree.props, newProps); const newElementsTree = React.cloneElement( elementsTree, props, elementsTree.props.children ); return newElementsTree; } }; } const HocComponent = HOC(Example); ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
運行如上代碼,就可以得到一個默認值爲 the initialValue of input 的 input 標籤。因爲 HOC 在 render 之前獲取了 WrappedComponent 的 Dom 結構,從而可以自定義一些自己的東西,然後再執行本身的渲染操作。
HOC 的功能雖然很強大,但是在使用過程中還是需要注意,React 官方給出了一些注意事項,在此不贅述~
附上具體示例代碼地址
mixin VS HOC
mixin 和 HOC 都能解決代碼複用的問題,但是 mixin 存在如下缺點:
-
降低代碼的可讀性:組件的優勢在於將邏輯與是界面直接結合在一起,mixin 本質上會分散邏輯,理解起來難度大
-
mixin 會導致命名衝突:多個 mixin 和組件本身,方法名稱會有命名衝突風險,如果遇到了,不得不重命名某些方法
除了上面的顯著缺點外,還有一些其他的,詳見 Mixins Considered Harmful
而且 HOC 更接近於函數式變成的思想,在使用上也更加靈活,包括的功能點也更多。一張圖可以很形象地表達出兩者的區別:
由圖可以看出 mixin 是一種打補丁的做法,而 HOC 則是組件的嵌套,書寫起來也更加優雅~