React 中的高階組件及其應用場景

轉自React 中的高階組件及其應用場景

關鍵詞:高階函數、高階組件、屬性代理、反向繼承、裝飾器模式、受控組件react

本文目錄:git

  • 什麼是高階組件
  • React 中的高階組件
    • 屬性代理(Props Proxy)
    • 反向繼承(Inheritance Inversion)
  • 高階組件存在的問題
  • 高階組件的約定
  • 高階組件的應用場景
  • 裝飾者模式?高階組件?AOP?
  • 總結

什麼是高階組件

在解釋什麼是高階組件以前,能夠先了解一下什麼是 高階函數,由於它們的概念很是類似,下面是 高階函數 的定義:github

若是一個函數 接受一個或多個函數做爲參數或者返回一個函數 就可稱之爲 高階函數編程

下面就是一個簡單的高階函數:redux

function withGreeting(greeting = () => {}) {
    return greeting;
}
複製代碼複製代碼

高階組件 的定義和 高階函數 很是類似:數組

若是一個函數 接受一個或多個組件做爲參數而且返回一個組件 就可稱之爲 高階組件app

下面就是一個簡單的高階組件:less

function HigherOrderComponent(WrappedComponent) {
    return <WrappedComponent />; } 複製代碼複製代碼

因此你可能會發現,當高階組件中返回的組件是 無狀態組件(Stateless Component) 時,該高階組件其實就是一個 高階函數,由於 無狀態組件 自己就是一個純函數。async

無狀態組件也稱函數式組件。函數

React 中的高階組件

React 中的高階組件主要有兩種形式:屬性代理反向繼承

屬性代理(Props Proxy)

最簡單的屬性代理實現:

// 無狀態
function HigherOrderComponent(WrappedComponent) {
    return props => <WrappedComponent {...props} />;
}
// or
// 有狀態
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />; } }; } 複製代碼複製代碼

能夠發現,屬性代理其實就是 一個函數接受一個 WrappedComponent 組件做爲參數傳入,並返回一個繼承了 React.Component 組件的類,且在該類的 render() 方法中返回被傳入的 WrappedComponent 組件

那咱們能夠利用屬性代理類型的高階組件作一些什麼呢?

由於屬性代理類型的高階組件返回的是一個標準的 React.Component 組件,因此在 React 標準組件中能夠作什麼,那在屬性代理類型的高階組件中就也能夠作什麼,好比:

  • 操做 props
  • 抽離 state
  • 經過 ref 訪問到組件實例
  • 用其餘元素包裹傳入的組件 WrappedComponent

操做 props

WrappedComponent 添加新的屬性:

function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            const newProps = {
                name: '大板慄',
                age: 18,
            };
            return <WrappedComponent {...this.props} {...newProps} />; } }; } 複製代碼複製代碼

抽離 state

利用 props 和回調函數把 state 抽離出來:

function withOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                name: '',
            };
        }
        onChange = () => {
            this.setState({
                name: '大板慄',
            });
        }
        render() {
            const newProps = {
                name: {
                    value: this.state.name,
                    onChange: this.onChange,
                },
            };
            return <WrappedComponent {...this.props} {...newProps} />; } }; } 複製代碼複製代碼

如何使用:

const NameInput = props => (<input name="name" {...props.name} />); export default withOnChange(NameInput); 複製代碼複製代碼

這樣就將 input 轉化成受控組件了。

經過 ref 訪問到組件實例

有時會有須要訪問 DOM element (使用第三方 DOM 操做庫)的時候就會用到組件的 ref 屬性。它只能聲明在 Class 類型的組件上,而沒法聲明在函數(無狀態)類型的組件上。

ref 的值能夠是字符串(不推薦使用)也能夠是一個回調函數,若是是回調函數的話,它的執行時機是:

  • 組件被掛載後(componentDidMount),回調函數當即執行,回調函數的參數爲該組件的實例。
  • 組件被卸載(componentDidUnmount)或者原有的 ref 屬性自己發生變化的時候,此時回調函數也會當即執行,且回調函數的參數爲 null

如何在 高階組件 中獲取到 WrappedComponent 組件的實例呢?答案就是能夠經過 WrappedComponent 組件的 ref 屬性,該屬性會在組件 componentDidMount 的時候執行 ref 的回調函數並傳入該組件的實例:

function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        executeInstanceMethod = (wrappedComponentInstance) => {
            wrappedComponentInstance.someMethod();
        }
        render() {
            return <WrappedComponent {...this.props} ref={this.executeInstanceMethod} />; } }; } 複製代碼複製代碼

注意:不能在無狀態組件(函數類型組件)上使用 ref 屬性,由於無狀態組件沒有實例。

用其餘元素包裹傳入的組件 WrappedComponent

WrappedComponent 組件包一層背景色爲 #fafafadiv 元素:

function withBackgroundColor(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ backgroundColor: '#fafafa' }}> <WrappedComponent {...this.props} {...newProps} /> </div> ); } }; } 複製代碼複製代碼

反向繼承(Inheritance Inversion)

最簡單的反向繼承實現:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}
複製代碼複製代碼

反向繼承其實就是 一個函數接受一個 WrappedComponent 組件做爲參數傳入,並返回一個繼承了該傳入 WrappedComponent 組件的類,且在該類的 render() 方法中返回 super.render() 方法

會發現其屬性代理和反向繼承的實現有些相似的地方,都是返回一個繼承了某個父類的子類,只不過屬性代理中繼承的是 React.Component,反向繼承中繼承的是傳入的組件 WrappedComponent

反向繼承能夠用來作什麼:

  • 操做 state
  • 渲染劫持(Render Highjacking)

操做 state

高階組件中能夠讀取、編輯和刪除 WrappedComponent 組件實例中的 state。甚至能夠增長更多的 state 項,可是 很是不建議這麼作 由於這可能會致使 state 難以維護及管理。

function withLogging(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return (
                <div> <h2>Debugger Component Logging...</h2> <p>state:</p> <pre>{JSON.stringify(this.state, null, 4)}</pre> <p>props:</p> <pre>{JSON.stringify(this.props, null, 4)}</pre> {super.render()} </div>
            );
        }
    };
}
複製代碼複製代碼

在這個例子中利用高階函數中能夠讀取 stateprops 的特性,對 WrappedComponent 組件作了額外元素的嵌套,把 WrappedComponent 組件的 stateprops 都打印了出來,

渲染劫持

之因此稱之爲 渲染劫持 是由於高階組件控制着 WrappedComponent 組件的渲染輸出,經過渲染劫持咱們能夠:

  • 有條件地展現元素樹(element tree
  • 操做由 render() 輸出的 React 元素樹
  • 在任何由 render() 輸出的 React 元素中操做 props
  • 用其餘元素包裹傳入的組件 WrappedComponent (同 屬性代理
條件渲染

經過 props.isLoading 這個條件來判斷渲染哪一個組件。

function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />; } else { return super.render(); } } }; } 複製代碼複製代碼
修改由 render() 輸出的 React 元素樹

修改元素樹:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            const tree = super.render();
            const newProps = {};
            if (tree && tree.type === 'input') {
                newProps.value = 'something here';
            }
            const props = {
                ...tree.props,
                ...newProps,
            };
            const newTree = React.cloneElement(tree, props, tree.props.children);
            return newTree;
        }
    };
}
複製代碼複製代碼

高階組件存在的問題

  • 靜態方法丟失
  • refs 屬性不能透傳
  • 反向繼承不能保證完整的子組件樹被解析

靜態方法丟失

由於原始組件被包裹於一個容器組件內,也就意味着新組件會沒有原始組件的任何靜態方法:

// 定義靜態方法
WrappedComponent.staticMethod = function() {}
// 使用高階組件
const EnhancedComponent = HigherOrderComponent(WrappedComponent);
// 加強型組件沒有靜態方法
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼複製代碼

因此必須將靜態方法作拷貝:

function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    // 必須得知道要拷貝的方法
    Enhance.staticMethod = WrappedComponent.staticMethod;
    return Enhance;
}
複製代碼複製代碼

可是這麼作的一個缺點就是必須知道要拷貝的方法是什麼,不過 React 社區實現了一個庫 hoist-non-react-statics 來自動處理,它會 自動拷貝全部非 React 的靜態方法

import hoistNonReactStatic from 'hoist-non-react-statics'; 複製代碼function HigherOrderComponent(WrappedComponent) { class Enhance extends React.Component {} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance; } 複製代碼複製代碼

refs 屬性不能透傳

通常來講高階組件能夠傳遞全部的 props 給包裹的組件 WrappedComponent,可是有一種屬性不能傳遞,它就是 ref。與其餘屬性不一樣的地方在於 React 對其進行了特殊的處理。

若是你向一個由高階組件建立的組件的元素添加 ref 引用,那麼 ref 指向的是最外層容器組件實例的,而不是被包裹的 WrappedComponent 組件。

那若是有必定要傳遞 ref 的需求呢,別急,React 爲咱們提供了一個名爲 React.forwardRef 的 API 來解決這一問題(在 React 16.3 版本中被添加):

function withLogging(WrappedComponent) { class Enhance extends WrappedComponent { componentWillReceiveProps() { console.log('Current props', this.props); console.log('Next props', nextProps); } render() { const {forwardedRef, ...rest} = this.props; // 把 forwardedRef 賦值給 ref return <WrappedComponent {...rest} ref={forwardedRef} />; } };
// React.forwardRef 方法會傳入 props 和 ref 兩個參數給其回調函數
// 因此這邊的 ref 是由 React.forwardRef 提供的
function forwardRef(props, ref) {
    return &lt;Enhance {...props} forwardRef={ref} /&gt;
}

return React.forwardRef(forwardRef);
複製代碼
複製代碼// React.forwardRef 方法會傳入 props 和 ref 兩個參數給其回調函數 // 因此這邊的 ref 是由 React.forwardRef 提供的 function forwardRef(props, ref) { return &lt;Enhance {...props} forwardRef={ref} /&gt; } return React.forwardRef(forwardRef); 複製代碼} const EnhancedComponent = withLogging(SomeComponent); 複製代碼複製代碼

反向繼承不能保證完整的子組件樹被解析

React 組件有兩種形式,分別是 class 類型和 function 類型(無狀態組件)。

咱們知道反向繼承的渲染劫持能夠控制 WrappedComponent 的渲染過程,也就是說這個過程當中咱們能夠對 elements treestatepropsrender() 的結果作各類操做。

可是若是渲染 elements tree 中包含了 function 類型的組件的話,這時候就不能操做組件的子組件了。

高階組件的約定

高階組件帶給咱們極大方便的同時,咱們也要遵循一些 約定

  • props 保持一致
  • 你不能在函數式(無狀態)組件上使用 ref 屬性,由於它沒有實例
  • 不要以任何方式改變原始組件 WrappedComponent
  • 透傳不相關 props 屬性給被包裹的組件 WrappedComponent
  • 不要再 render() 方法中使用高階組件
  • 使用 compose 組合高階組件
  • 包裝顯示名字以便於調試

props 保持一致

高階組件在爲子組件添加特性的同時,要儘可能保持原有組件的 props 不受影響,也就是說傳入的組件和返回的組件在 props 上儘可能保持一致。

不要改變原始組件 WrappedComponent

不要在高階組件內以任何方式修改一個組件的原型,思考一下下面的代碼:

function withLogging(WrappedComponent) {
    WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
        console.log('Current props', this.props);
        console.log('Next props', nextProps);
    }
    return WrappedComponent;
}
const EnhancedComponent = withLogging(SomeComponent);
複製代碼複製代碼

會發如今高階組件的內部對 WrappedComponent 進行了修改,一旦對原組件進行了修改,那麼就失去了組件複用的意義,因此請經過 純函數(相同的輸入總有相同的輸出) 返回新的組件:

function withLogging(WrappedComponent) {
    return class extends React.Component {
        componentWillReceiveProps() {
            console.log('Current props', this.props);
            console.log('Next props', nextProps);
        }
        render() {
            // 透傳參數,不要修改它
            return <WrappedComponent {...this.props} />; } }; } 複製代碼複製代碼

這樣優化以後的 withLogging 是一個 純函數,並不會修改 WrappedComponent 組件,因此不須要擔憂有什麼反作用,進而達到組件複用的目的。

透傳不相關 props 屬性給被包裹的組件 WrappedComponent

function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent name="name" {...this.props} />; } }; } 複製代碼複製代碼

不要在 render() 方法中使用高階組件

class SomeComponent extends React.Component {
    render() {
        // 調用高階函數的時候每次都會返回一個新的組件
        const EnchancedComponent = enhance(WrappedComponent);
        // 每次 render 的時候,都會使子對象樹徹底被卸載和從新
        // 從新加載一個組件會引發原有組件的狀態和它的全部子組件丟失
        return <EnchancedComponent />; } } 複製代碼複製代碼

使用 compose 組合高階組件

// 不要這麼使用
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// 可使用一個 compose 函數組合這些高階組件
// lodash, redux, ramda 等第三方庫都提供了相似 `compose` 功能的函數
const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);
複製代碼複製代碼

由於按照 約定 實現的高階組件其實就是一個純函數,若是多個函數的參數同樣(在這裏 withRouter 函數和 connect(commentSelector) 所返回的函數所需的參數都是 WrappedComponent),因此就能夠經過 compose 方法來組合這些函數。

使用 compose 組合高階組件使用,能夠顯著提升代碼的可讀性和邏輯的清晰度。

包裝顯示名字以便於調試

高階組件建立的容器組件在 React Developer Tools 中的表現和其它的普通組件是同樣的。爲了便於調試,能夠選擇一個顯示名字,傳達它是一個高階組件的結果。

const getDisplayName = WrappedComponent => WrappedComponent.displayName || WrappedComponent.name || 'Component';
function HigherOrderComponent(WrappedComponent) {
    class HigherOrderComponent extends React.Component {/* ... */}
    HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(WrappedComponent)})`;
    return HigherOrderComponent;
}
複製代碼複製代碼

實際上 recompose 庫實現了相似的功能,懶的話能夠不用本身寫:

import getDisplayName from 'recompose/getDisplayName';
HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(BaseComponent)})`;
// Or, even better:
import wrapDisplayName from 'recompose/wrapDisplayName';
HigherOrderComponent.displayName = wrapDisplayName(BaseComponent, 'HigherOrderComponent');
複製代碼複製代碼

高階組件的應用場景

不談場景的技術就是在耍流氓,因此接下來講一下如何在業務場景中使用高階組件。

權限控制

利用高階組件的 條件渲染 特性能夠對頁面進行權限控制,權限控制通常分爲兩個維度:頁面級別頁面元素級別,這裏以頁面級別來舉一個栗子:

// HOC.js
function withAdminAuth(WrappedComponent) {
    return class extends React.Component {
        state = {
            isAdmin: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                isAdmin: currentRole === 'Admin',
            });
        }
        render() {
            if (this.state.isAdmin) {
                return <WrappedComponent {...this.props} />; } else { return (<div>您沒有權限查看該頁面,請聯繫管理員!</div>); } } }; } 複製代碼複製代碼

而後是兩個頁面:

// pages/page-a.js class PageA extends React.Component { constructor(props) { super(props); // something here... } componentWillMount() { // fetching data } render() { // render page with data } } export default withAdminAuth(PageA); 複製代碼// pages/page-b.js class PageB extends React.Component { constructor(props) { super(props); // something here... } componentWillMount() { // fetching data } render() { // render page with data } } export default withAdminAuth(PageB); 複製代碼複製代碼

使用高階組件對代碼進行復用以後,能夠很是方便的進行拓展,好比產品經理說,PageC 頁面也要有 Admin 權限才能進入,咱們只須要在 pages/page-c.js 中把返回的 PageC 嵌套一層 withAdminAuth 高階組件就行,就像這樣 withAdminAuth(PageC)。是否是很是完美!很是高效!!可是。。次日產品經理又說,PageC 頁面只要 VIP 權限就能夠訪問了。你三下五除二實現了一個高階組件 withVIPAuth。第三天。。。

其實你還能夠更高效的,就是在高階組件之上再抽象一層,無需實現各類 withXXXAuth 高階組件,由於這些高階組件自己代碼就是高度類似的,因此咱們要作的就是實現一個 返回高階組件的函數,把 變的部分(Admin、VIP) 抽離出來,保留 不變的部分,具體實現以下:

// HOC.js
const withAuth = role => WrappedComponent => {
    return class extends React.Component {
        state = {
            permission: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                permission: currentRole === role,
            });
        }
        render() {
            if (this.state.permission) {
                return <WrappedComponent {...this.props} />; } else { return (<div>您沒有權限查看該頁面,請聯繫管理員!</div>); } } }; } 複製代碼複製代碼

能夠發現通過對高階組件再進行了一層抽象後,前面的 withAdminAuth 能夠寫成 withAuth('Admin') 了,若是此時須要 VIP 權限的話,只需在 withAuth 函數中傳入 'VIP' 就能夠了。

有沒有發現和 react-reduxconnect 方法的使用方式很是像?沒錯,connect 其實也是一個 返回高階組件的函數

組件渲染性能追蹤

藉助父組件子組件生命週期規則捕獲子組件的生命週期,能夠方便的對某個組件的渲染時間進行記錄:

class Home extends React.Component { render() { return (<h1>Hello World.</h1>); } } function withTiming(WrappedComponent) { return class extends WrappedComponent { constructor(props) { super(props); this.start = 0; this.end = 0; } componentWillMount() { super.componentWillMount && super.componentWillMount(); this.start = Date.now(); } componentDidMount() { super.componentDidMount && super.componentDidMount(); this.end = Date.now(); console.log(`${WrappedComponent.name} 組件渲染時間爲 ${this.end - this.start} ms`); } render() { return super.render(); } }; } 複製代碼export default withTiming(Home); 複製代碼複製代碼

withTiming

withTiming 是利用 反向繼承 實現的一個高階組件,功能是計算被包裹組件(這裏是 Home 組件)的渲染時間。

頁面複用

假設咱們有兩個頁面 pageApageB 分別渲染兩個分類的電影列表,普通寫法多是這樣:

// pages/page-a.js class PageA extends React.Component { state = { movies: [], } // ... async componentWillMount() { const movies = await fetchMoviesByType('science-fiction'); this.setState({ movies, }); } render() { return <MovieList movies={this.state.movies} /> } } export default PageA; 複製代碼// pages/page-b.js class PageB extends React.Component { state = { movies: [], } // ... async componentWillMount() { const movies = await fetchMoviesByType('action'); this.setState({ movies, }); } render() { return <MovieList movies={this.state.movies} /> } } export default PageB; 複製代碼複製代碼

頁面少的時候可能沒什麼問題,可是假如隨着業務的進展,須要上線的愈來愈多類型的電影,就會寫不少的重複代碼,因此咱們須要重構一下:

const withFetching = fetching => WrappedComponent => {
    return class extends React.Component {
        state = {
            data: [],
        }
        async componentWillMount() {
            const data = await fetching();
            this.setState({
                data,
            });
        }
        render() {
            return <WrappedComponent data={this.state.data} {...this.props} />; } } } // pages/page-a.js export default withFetching(fetching('science-fiction'))(MovieList); // pages/page-b.js export default withFetching(fetching('action'))(MovieList); // pages/page-other.js export default withFetching(fetching('some-other-type'))(MovieList); 複製代碼複製代碼

會發現 withFetching 其實和前面的 withAuth 函數相似,把 變的部分(fetching(type)) 抽離到外部傳入,從而實現頁面的複用。

裝飾者模式?高階組件?AOP?

可能你已經發現了,高階組件其實就是裝飾器模式在 React 中的實現:經過給函數傳入一個組件(函數或類)後在函數內部對該組件(函數或類)進行功能的加強(不修改傳入參數的前提下),最後返回這個組件(函數或類),即容許向一個現有的組件添加新的功能,同時又不去修改該組件,屬於 包裝模式(Wrapper Pattern) 的一種。

什麼是裝飾者模式:在不改變對象自身的前提下在程序運行期間動態的給對象添加一些額外的屬性或行爲

相比於使用繼承,裝飾者模式是一種更輕便靈活的作法。

使用裝飾者模式實現 AOP

面向切面編程(AOP)和麪向對象編程(OOP)同樣,只是一種編程範式,並無規定說要用什麼方式去實現 AOP。

// 在須要執行的函數以前執行某個新添加的功能函數
Function.prototype.before = function(before = () => {}) {
    return () => {
        before.apply(this, arguments);
        return this.apply(this, arguments);
    };
}
// 在須要執行的函數以後執行某個新添加的功能函數
Function.prototype.after = function(after = () => {}) {
    return () => {
        const result = after.apply(this, arguments);
        this.apply(this, arguments);
        return result;
    };
}
複製代碼複製代碼

能夠發現其實 beforeafter 就是一個 高階函數,和高階組件很是相似。

面向切面編程(AOP)主要應用在 與核心業務無關但又在多個模塊使用的功能好比權限控制、日誌記錄、數據校驗、異常處理、統計上報等等領域

類比一下 AOP 你應該就知道高階組件一般是處理哪一類型的問題了吧。

總結

React 中的 高階組件 實際上是一個很是簡單的概念,但又很是實用。在實際的業務場景中合理的使用高階組件,能夠提升代碼的複用性和靈活性

最後的最後,再對高階組件進行一個小小的總結:

  • 高階組件 不是組件 一個把某個組件轉換成另外一個組件的 函數
  • 高階組件的主要做用是 代碼複用
  • 高階組件是 裝飾器模式在 React 中的實現
相關文章
相關標籤/搜索