天天看點

精讀 React 高階元件1 引言2 内容概要3 精讀4 總結

1 引言

高階元件( higher-order component ,HOC )是 React 中複用元件邏輯的一種進階技巧。它本身并不是 React 的 API,而是一種 React 元件的設計理念,衆多的 React 庫已經證明了它的價值,例如耳熟能詳的 react-redux。

高階元件的概念其實并不難,我們能通過類比高階函數迅速掌握。高階函數是把函數作為參數傳入到函數中并傳回一個新的函數。這裡我們把函數替換為元件,就是高階元件了。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

當然了解高階元件的概念隻是萬裡長征第一步,精讀文章在闡述其概念與實作外,也強調了其重要性與局限性,以及與其他方案的比較,讓我們一起來領略吧。

2 内容概要

高階元件常見有兩種實作方式,一種是 Props Proxy,它能夠對 WrappedComponent 的 props 進行操作,提取 WrappedComponent state 以及使用其他元素來包裹 WrappedComponent。Props Proxy 作為一層代理,具有隔離的作用,是以傳入 WrappedComponent 的 ref 将無法通路到其本身,需要在 Props Proxy 内完成中轉,具體可參考以下代碼,react-redux 也是這樣實作的。

此外各個 Props Proxy 的預設名稱是相同的,需要根據 WrappedComponent 來進行不同命名。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    // 實作 HOC 不同的命名
    static displayName = `HOC(${WrappedComponent.displayName})`;

    getWrappedInstance() {
      return this.wrappedInstance;
    }

    // 實作 ref 的通路
    setWrappedInstance(ref) {
      this.wrappedInstance = ref;
    }

    render() {
      return <WrappedComponent {
        ...this.props,
        ref: this.setWrappedInstance.bind(this),
      } />
    }
  }
}

@ppHOC
class Example extends React.Component {
  static displayName = 'Example';
  handleClick() { ... }
  ...
}

class App extends React.Component {
  handleClick() {
    this.refs.example.getWrappedInstance().handleClick();
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick.bind(this)}>按鈕</button>
        <Example ref="example" />
      </div>  
    );
  }
}           

複制

另一種是 Inheritance Inversion,HOC 類繼承了 WrappedComponent,意味着可以通路到 WrappedComponent 的 state、props、生命周期和 render 等方法。如果在 HOC 中定義了與 WrappedComponent 同名方法,将會發生覆寫,就必須手動通過 super 進行調用了。通過完全操作 WrappedComponent 的 render 方法傳回的元素樹,可以真正實作渲染劫持。這種方案依然是繼承的思想,對于 WrappedComponent 也有較強的侵入性,是以并不常見。

function ppHOC(WrappedComponent) {
  return class ExampleEnhance extends WrappedComponent {
    ...
    componentDidMount() {
      super.componentDidMount();
    }
    componentWillUnmount() {
      super.componentWillUnmount();
    }
    render() {
      ...
      return super.render();
    }
  }
}           

複制

3 精讀

本次提出獨到觀點的同學有: @monkingxue @alcat2008 @淡蒼 @camsong,精讀由此歸納。

HOC 的适用範圍

對比 HOC 範式

compose(render)(state)

與父元件(Parent Component)的範式

render(render(state))

,如果完全利用 HOC 來實作 React 的 implement,将操作與 view 分離,也未嘗不可,但卻不優雅。HOC 本質上是統一功能抽象,強調邏輯與 UI 分離。但在實際開發中,前端無法逃離 DOM ,而邏輯與 DOM 的相關性主要呈現 3 種關聯形式:

  • 與 DOM 相關,建議使用父元件,類似于原生 HTML 編寫
  • 與 DOM 不相關,如校驗、權限、請求發送、資料轉換這類,通過資料變化間接控制 DOM,可以使用 HOC 抽象
  • 交叉的部分,DOM 相關,但可以做到完全内聚,即這些 DOM 不會和外部有關聯,均可

DOM 的渲染适合使用父元件,這是 React JSX 原生支援的方式,清晰易懂。最好是能封裝成木偶元件(Dumb Component)。HOC 适合做 DOM 不相關又是多個元件共性的操作。如 Form 中,validator 校驗操作就是純資料操作的,放到了 HOC 中。但 validator 資訊沒有放到 HOC 中。但如果能把 Error 資訊展示這些邏輯能夠完全隔離,也可以放到 HOC 中(可結合下一小節 Form 具體實踐詳細了解)。資料請求是另一類 DOM 不相關的場景,react-refetch 的實作就是使用了 HOC,做到了高效和優雅:

connect(props => ({
  usersFetch: `/users?status=${props.status}&page=${props.page}`,
  userStatsFetch: { url: `/users/stats`, force: true }
}))(UsersList)           

複制

HOC 的具體實踐

HOC 在真實場景下的運作非常多,之前筆者在 基于Decorator的元件擴充實踐 一文中也提過使用高階元件将更細粒度的元件組合成 Selector 與 Search。結合精讀文章,這次讓我們通過 Form 元件的抽象來表現 HOC 具有的良好擴充機制。

Form 中會包含各種不同的元件,常見的有 Input、Selector、Checkbox 等等,也會有根據業務需求加入的自定義元件。Form 靈活多變,從功能上看,表單校驗可能為單元件值校驗,也可能為全表單值校驗,可能為正常檢驗,比如:非空、輸入限制,也可能需要與服務端配合,甚至需要根據業務特點進行定制。從 UI 上看,檢驗結果顯示的位置,可能在元件下方,也可能是在元件右側。

直接裸寫 Form,無疑是機械而又重複的。将 Form 中元件的 value 經過 validator,把 value,validator 産生的 error 資訊儲存到 state 或 redux store 中,然後在 view 層完成顯示。這條路大家都是相同的,可以進行複用,隻是我們面對的是不同的元件,不同的 validator,不同的 view 而已。對于 Form 而言,既要滿足通用,又要滿足部分個性化的需求,以往單純的配置化隻會讓使用愈加繁瑣,我們所需要抽象的是 Form 功能而非 UI,是以通過 HOC 針對 Form 的功能進行提取就成為了必然。至于 HOC 在 Form 上的具體實作,首先将表單中的元件(Input、Selector...)與相應 validator 與元件值回調函數名(trigger)傳入 Decorator,将 validator 與 trigger 相綁定。Decorator 完成了各種不同元件與 Form 内置 Store 間 value 的傳遞、校驗功能的抽象,即精讀文章中提到 Props Proxy 方式的其中兩種作用:提取state 與 操作props

精讀 React 高階元件1 引言2 内容概要3 精讀4 總結
function formFactoryFactory({
  validator,
  trigger = 'onChange',
  ...
}) {
  return FormFactory(WrappedComponent) {
    return class Decorator extends React.Component {
      getBind(trigger, validator) {
        ...
      }
      render() {
        const newProps = {
          ...this.props,
          [trigger]: this.getBind(trigger, validator),
          ...
        }
        return <WrappedComponent {...newProps} />
      }
    }
  }
}

// 調用
formFactoryFactory({
  validator: (value) => {
    return value !== '';
  }
})(<Input placeholder="請輸入..." />)           

複制

當然為了考慮個性化需求,Form Store 也向外暴露很多 API,可以直接擷取和修改 value、error 的值。現在我們需要對一個表單的所有值送出到後端進行校驗,根據後端傳回,分别列出各項的校驗錯誤資訊,就需要借助相應項的 setError 去完成了。

這裡主要參考了 rc-form 的實作方式,有興趣的讀者可以閱讀其源碼。

import { createForm } from 'rc-form';

class Form extends React.Component {
  submit = () => {
    this.props.form.validateFields((error, value) => {
      console.log(error, value);
    });
  }

  render() {
    const { getFieldError, getFieldDecorator } = this.props.form;
    const errors = getFieldError('required');
    return (
      <div>
        {getFieldDecorator('required', {
          rules: [{ required: true }],
        })(<Input />)}
        {errors ? errors.join(',') : null}
        <button onClick={this.submit}>submit</button>
      </div>
    );
  }
}

export createForm()(Form);           

複制

4 總結

React 始終強調組合優于繼承的理念,期望通過複用小元件來建構大元件使得開發變得簡單而又高效,與傳統面向對象思想是截然不同的。高階函數(HOC)的出現替代了原有 Mixin 侵入式的方案,對比隐式的 Mixin 或是繼承,HOC 能夠在 Devtools 中顯示出來,滿足抽象之餘,也友善了開發與測試。當然,不可過度抽象是我們始終要秉持的原則。希望讀者通過本次閱讀與讨論,能結合自己具體的業務開發場景,獲得一些啟發。