深入理解 React 高階組件

在目前的前端社區(qū),『推崇組合湿蛔,不推薦繼承(prefer composition than inheritance)』已經(jīng)成為了比較好的實踐怎燥,mixin 也因為自身的一些問題而漸漸不被推薦。高階組件(Higher order components)作為 mixin 之外的一種組件抽象與處理形式呼股,有哪些不同和好處呢?繼續(xù)閱讀來了解一下吧画恰!

本文翻譯自 franleplant 的博客 React Higher Order Components in depth (閱讀原文及文中部分鏈接請自備梯子)


摘要

這篇文章的目標(biāo)群體是有一定基礎(chǔ)的并想要了解高階組件的用戶彭谁。如果你是 React 新手,那么可能你應(yīng)該先閱讀 React 的文檔允扇。

高階組件是一個非常棒的形式缠局,它已經(jīng)在多個 React 庫中被證明了它的價值。這篇文章中考润,我們將回顧什么是高階組件狭园,如何使用高階組件,它的限制以及如何編寫高階組件糊治。

附錄中我們回顧了與高階組件相關(guān)的話題(非核心)唱矛,但是是我認(rèn)為應(yīng)當(dāng)涉及到的知識。

這篇文章意在寫的詳盡井辜,如果你發(fā)現(xiàn)了任何遺漏绎谦,請你報告它,我會做出相應(yīng)的改變粥脚。

這篇文章假設(shè)讀者擁有 ES6 的相關(guān)知識窃肠。

讓我們開始吧!

什么是高階組件刷允?

一個高階組件只是一個包裝了另外一個 React 組件的 React 組件冤留。

這種形式通常實現(xiàn)為一個函數(shù),本質(zhì)上是一個類工廠(class factory)树灶,它下方的函數(shù)標(biāo)簽偽代碼啟發(fā)自 Haskell

hocFactory:: W: React.Component => E: React.Component

這里 W(WrappedComponent) 指被包裝的 React.Component纤怒,E(Enhanced Component) 指返回的新的高階 React 組件。

定義中的『包裝』一詞故意被定義的比較模糊破托,因為它可以指兩件事情:

  1. 屬性代理(Props Proxy):高階組件操控傳遞給 WrappedComponent 的 props肪跋,
  2. 反向繼承(Inheritance Inversion):高階組件繼承(extends)WrappedComponent。

我們將討論這兩種形式的更多細(xì)節(jié)土砂。

我可以使用高階組件做什么呢州既?

概括的講谜洽,高階組件允許你做:

  • 代碼復(fù)用,邏輯抽象吴叶,抽離底層準(zhǔn)備(bootstrap)代碼
  • 渲染劫持
  • State 抽象和更改
  • Props 更改

在探討這些東西的細(xì)節(jié)之前阐虚,我們先學(xué)習(xí)如何實現(xiàn)一個高階組件,因為實現(xiàn)方式『允許/限制』你可以通過高階組件做哪些事情蚌卤。


高階組件工廠的實現(xiàn)

在這節(jié)中我們將學(xué)習(xí)兩種主流的在 React 中實現(xiàn)高階組件的方法:屬性代理(Props Proxy)和 反向繼承(Inheritance Inversion)实束。兩種方法囊括了幾種包裝 WrappedComponent 的方法。

Props Proxy (PP)

屬性代理的實現(xiàn)方法如下:

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

可以看到逊彭,這里高階組件的 render 方法返回了一個 type 為 WrappedComponent 的 React Element(也就是被包裝的那個組件)咸灿,我們把高階組件收到的 props 傳遞給它,因此得名 Props Proxy侮叮。

注意:

<WrappedComponent {...this.props}/>
// is equivalent to
React.createElement(WrappedComponent, this.props, null)

它們都創(chuàng)建了一個 React Element避矢,描述了 React 在『reconciliation』(可以理解為解析)階段的渲染內(nèi)容,如果你想了解更多關(guān)于 React Element 的內(nèi)容囊榜,請看 Dan Abramov 的這篇博客 和官方文檔上關(guān)于 reconciliation process 的部分审胸。

Props Proxy 可以做什么?
  • 更改 props
  • 通過 refs 獲取組件實例
  • 抽象 state
  • 把 WrappedComponent 與其它 elements 包裝在一起
更改 props

你可以『讀取卸勺,添加砂沛,修改,刪除』將要傳遞給 WrappedComponent 的 props曙求。

在修改或刪除重要 props 的時候要小心碍庵,你可能應(yīng)該給高階組件的 props 指定命名空間(namespace),以防破壞從外傳遞給 WrappedComponent 的 props圆到。

例子:添加新 props怎抛。這個應(yīng)用目前登陸的一個用戶可以在 WrappedComponent 通過 this.props.user 獲取

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}
通過 refs 獲取組件實例

你可以通過 ref 獲取關(guān)鍵詞 this(WrappedComponent 的實例),但是想要它生效芽淡,必須先經(jīng)歷一次正常的渲染過程來讓 ref 得到計算马绝,這意味著你需要在高階組件的 render 方法中返回 WrappedComponent,讓 React 進(jìn)行 reconciliation 過程挣菲,這之后你就通過 ref 獲取到這個 WrappedComponent 的實例了富稻。

例子:下方例子中,我們實現(xiàn)了通過 ref 獲取 WrappedComponent 實例并調(diào)用實例方法白胀。

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}/>
    }
  }
}

當(dāng) WrappedComponent 被渲染后椭赋,ref 上的回調(diào)函數(shù) proc 將會執(zhí)行,此時就有了這個 WrappedComponent 的實例的引用或杠。這個可以用來『讀取哪怔,添加』實例的 props 或用來執(zhí)行實例方法。

抽象 state

你可以通過向 WrappedComponent 傳遞 props 和 callbacks(回調(diào)函數(shù))來抽象 state,這和 React 中另外一個組件構(gòu)成思想 Presentational and Container Components 很相似认境。

例子:在下面這個抽象 state 的例子中胚委,我們幼稚地(原話是naively :D)抽象出了 name input 的 value 和 onChange。我說這是幼稚的是因為這樣寫并不常見叉信,但是你會理解到點亩冬。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }
      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

然后這樣使用它:

@ppHOC
class Example extends React.Component {
  render() {
    return <input name="name" {...this.props.name}/>
  }
}

這里的 input 自動成為一個受控的 input

點擊此鏈接查看一個更常見的雙向綁定的高階組件例子

把 WrappedComponent 與其它 elements 包裝在一起

出于操作樣式硼身、布局或其它目的硅急,你可以將 WrappedComponent 與其它組件包裝在一起。一些基本的用法也可以使用正常的父組件來實現(xiàn)(附錄 B)佳遂,但是就像之前所描述的营袜,使用高階組件你可以獲得更多的靈活性。

例子:包裝來操作樣式

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return (
        <div style={{display: 'block'}}>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
}

Inheritance Inversion(II)

反向繼承(II)可以像這樣簡單地實現(xiàn):

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

如你所見讶迁,返回的高階組件類(Enhancer)繼承了 WrappedComponent连茧。這被叫做反向繼承是因為 WrappedComponent 被動地被 Enhancer 繼承,而不是 WrappedComponent 去繼承 Enhancer巍糯。通過這種方式他們之間的關(guān)系倒轉(zhuǎn)了。

反向繼承允許高階組件通過 this 關(guān)鍵詞獲取 WrappedComponent客扎,意味著它可以獲取到 state祟峦,props,組件生命周期(component lifecycle)鉤子徙鱼,以及渲染方法(render)宅楞。

我不會詳細(xì)介紹你可以使用組件生命周期方法做什么,因為這是 React 的內(nèi)容袱吆,而不是高階組件的厌衙。但是請注意,你可以通過高階組件來給 WrappedComponent 創(chuàng)建新的生命周期掛鉤方法绞绒,別忘了調(diào)用 super.[lifecycleHook] 防止破壞 WrappedComponent婶希。

Reconciliation 過程

介紹之前先來總結(jié)一些理論。

React Element 在 React 執(zhí)行它的 reconciliation 的過程時描述什么將被渲染蓬衡。

React Element 可以是兩個種類其中的一種:String 或 Function喻杈。String 類型的 React Element 代表原聲 DOM 節(jié)點,F(xiàn)unction 類型的 React Element 代表通過 React.Component 創(chuàng)建的組件狰晚。想要了解更多關(guān)于 Elements 和 Components 的知識請閱讀此推文筒饰。

Function 類型的 React Element 將在 reconciliation 階段被解析成 DOM 類型的 React Element (最終結(jié)果一定都是 DOM 元素)。

這點非常重要壁晒,這意味著『反向繼承的高階組件不保證一定解析整個子元素樹』瓷们。這對渲染劫持非常重要。

可以用反向繼承高階組件做什么?

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

它被叫做渲染劫持是因為高階組件控制了 WrappedComponent 生成的渲染結(jié)果谬晕,并且可以做各種操作式镐。

通過渲染劫持你可以:

  • 『讀取、添加固蚤、修改娘汞、刪除』任何一個將被渲染的 React Element 的 props
  • 在渲染方法中讀取或更改 React Elements tree,也就是 WrappedComponent 的 children
  • 根據(jù)條件不同夕玩,選擇性的渲染子樹
  • 給子樹里的元素變更樣式

*渲染 指的是 WrappedComponent.render 方法

無法更改或創(chuàng)建 props 給 WrappedComponent 實例你弦,因為 React 不允許變更一個組件收到的 props,但是你可以在 render 方法里更改子元素/子組件們的 props燎孟。

就像之前所說的禽作,反向繼承的高階組件不能保證一定渲染整個子元素樹,這同時也給渲染劫持增添了一些限制揩页。通過反向繼承旷偿,你只能劫持 WrappedComponent 渲染的元素,這意味著如果 WrappedComponent 的子元素里有 Function 類型的 React Element爆侣,你不能劫持這個元素里面的子元素樹的渲染萍程。

例子1:條件性渲染。如果 this.props.loggedIn 是 true兔仰,這個高階組件會原封不動地渲染 WrappedComponent茫负,如果不是 true 則不渲染(假設(shè)此組件會收到 loggedIn 的 prop)

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render()
      } else {
        return null
      }
    }
  }
}

例子2:通過 render 來變成 React Elements tree 的結(jié)果

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
    }
  }
}

在這個例子中,如果 WrappedComponent 的頂層元素是一個 input乎赴,則改變它的值為 “may the force be with you”忍法。

這里你可以做任何操作,比如你可以遍歷整個 element tree 然后變更某些元素的 props榕吼。這恰好就是 Radium 的工作方式饿序。

注意:你不能通過 Props Proxy 來做渲染劫持

即使你可以通過 WrappedComponent.prototype.render 獲取它的 render 方法,你需要自己手動模擬整個實例以及生命周期方法羹蚣,而不是依靠 React原探,這是不值當(dāng)?shù)模瑧?yīng)該使用反向繼承來做到渲染劫持度宦。要記住 React 在內(nèi)部處理組件的實例踢匣,而你只通過 thisrefs 來處理實例。

操作 state

高階組件可以 『讀取戈抄、修改离唬、刪除』WrappedComponent 實例的 state,如果需要也可以添加新的 state划鸽。需要記住的是输莺,你在弄亂 WrappedComponent 的 state戚哎,可能會導(dǎo)致破壞一些東西。通常不建議使用高階組件來讀取或添加 state嫂用,添加 state 需要使用命名空間來防止與 WrappedComponent 的 state 沖突型凳。

例子:通過顯示 WrappedComponent 的 props 和 state 來 debug

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>
      )
    }
  }
}

命名

當(dāng)通過高階組件來包裝一個組件時,你會丟失原先 WrappedComponent 的名字嘱函,可能會給開發(fā)和 debug 造成影響甘畅。

常見的解決方法是在原先的 WrappedComponent 的名字前面添加一個前綴。下面這個方法是從 React-Redux 中拿來的往弓。

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//or
class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`
  ...
}

方法 getDisplayName 被如下定義:

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || 
         WrappedComponent.name || 
         ‘Component’
}

實際上你不用自己寫這個方法疏唾,因為 recompose 庫已經(jīng)提供了。


案例學(xué)習(xí)

React-Redux

React-Redux 是 Redux 官方的對于 React 的綁定函似。 其中一個方法 connect 處理了所有關(guān)于監(jiān)聽 store 的 bootstrap 代碼 以及清理工作槐脏,這是通過 Props Proxy 來實現(xiàn)的。

如果你曾經(jīng)使用過 Flux 你會知道 React 組件需要和一個或多個 store 連接撇寞,并且添加/刪除對 store 的監(jiān)聽顿天,從中選擇需要的那部分 state。而 React-Redux 幫你把它們實現(xiàn)了蔑担,自己就不用再去寫這些了牌废。

Radium

Radium 是一個增強了行內(nèi)(inline)css 能力的庫,它允許了在 inline css 使用 CSS 偽選擇器钟沛。點擊此鏈接了解關(guān)于使用 inline css 的好處畔规,這是 Vjeux 做的一個演講分享,又叫做 CSS in JS恨统。

那么,Radium 是怎么允許 inline css 來實現(xiàn) CSS 偽選擇器的呢(比如 hover)三妈?它實現(xiàn)了一個反向繼承來使用渲染劫持畜埋,添加適當(dāng)?shù)氖录O(jiān)聽來模擬 CSS 偽選擇器。這要求 Radium 讀取整個 WrappedComponent 將要渲染的元素樹畴蒲,每當(dāng)找個某個元素帶有 style prop悠鞍,它就添加對應(yīng)的時間監(jiān)聽 props。簡單地說模燥,Radium 修改了原先元素樹的 props(實際上會更復(fù)雜咖祭,但這么說你可以理解到要點所在)。

Radium 只暴露了一個非常簡單的 API 給開發(fā)者蔫骂。這非常驚艷么翰,因為開發(fā)者幾乎不會注意到它的存在和它是怎么發(fā)揮作用的,而實現(xiàn)了想要的功能辽旋。這揭露了高階組件的能力浩嫌。


附錄 A:高階組件和參數(shù)

以下內(nèi)容不是必須閱讀的檐迟,你可以略過。

有時码耐,在高階組件中使用參數(shù)是很有用的追迟。這個在以上所有例子中都不是很明顯,但是對于中等的 JavaScript 開發(fā)者是比較自然的事情骚腥。讓我們迅速的介紹一下敦间。

例子:一個簡單的 Props Proxy 高階組件搭配參數(shù)。重點是這個 HOCFactoryFactory 方法束铭。

function HOCFactoryFactory(...params) {
  // do something with params
  return function HOCFactory(WrappedComponent) {
    return class HOC extends React.Component {
      render() {
        return <WrappedComponent {...this.props}/>
      }
    }
  }
}

你可以這樣使用它:

HOCFactoryFactory(params)(WrappedComponent)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}

附錄 B:和父組件的不同之處

以下內(nèi)容不是必須閱讀的廓块,你可以略過。

父組件就是單純的 React 組件包含了一些子組件(children)纯露。React 提供了獲取和操作一個組件的 children 的 APIs剿骨。

例子:父組件獲取它的 children

class Parent extends React.Component {
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}

render((
  <Parent>
    {children}
  </Parent>
), mountNode)

現(xiàn)在來總結(jié)一下父組件能做和不能做的事情(與高階組件對比):

  • 渲染劫持
  • 操作內(nèi)部 props
  • 抽象 state。但是有缺點埠褪,不能再父組件外獲取到它的 state浓利,除非明確地實現(xiàn)了鉤子。
  • 與新的 React Element 包裝钞速。這似乎是唯一一點贷掖,使用父組件要比高階組件強,但高階組件也同樣可以實現(xiàn)渴语。
  • Children 的操控苹威。如果 children 不是單一 root,則需要多添加一層來包括所有 children驾凶,可能會使你的 markup 變得有點笨重牙甫。使用高階組件可以保證單一 root。
  • 父組件可以在元素樹立隨意使用调违,它們不像高階組件一樣限制于一個組件窟哺。

通常來講,能使用父組件達(dá)到的效果技肩,盡量不要用高階組件且轨,因為高階組件是一種更 hack 的方法,但同時也有更高的靈活性虚婿。

總結(jié)

希望你在讀完這篇文章后旋奢,能對 React 高階組件多一絲了解。它們在多個庫中被證明非常有效然痊。

React 帶來了很多創(chuàng)新至朗,人們維護著像 Radium,React-Redux玷过,React-Router 之類的項目爽丹,都是很好的證明筑煮。

如果你想聯(lián)系我,請在 Twitter 關(guān)注我并 @franleplant粤蝎。

這個倉庫里有我為了寫這篇文章而寫的一些代碼真仲,有興趣可以看看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末初澎,一起剝皮案震驚了整個濱河市秸应,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碑宴,老刑警劉巖软啼,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異延柠,居然都是意外死亡祸挪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門贞间,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贿条,“玉大人,你說我怎么就攤上這事增热≌裕” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵峻仇,是天一觀的道長公黑。 經(jīng)常有香客問我,道長摄咆,這世上最難降的妖魔是什么凡蚜? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮吭从,結(jié)果婚禮上番刊,老公的妹妹穿的比我還像新娘。我一直安慰自己影锈,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布蝉绷。 她就那樣靜靜地躺著鸭廷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熔吗。 梳的紋絲不亂的頭發(fā)上辆床,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音桅狠,去河邊找鬼讼载。 笑死轿秧,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的咨堤。 我是一名探鬼主播菇篡,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼一喘!你這毒婦竟也來了驱还?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤凸克,失蹤者是張志新(化名)和其女友劉穎议蟆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萎战,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡咐容,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚂维。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戳粒。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鸟雏,靈堂內(nèi)的尸體忽然破棺而出享郊,到底是詐尸還是另有隱情,我是刑警寧澤孝鹊,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布炊琉,位于F島的核電站,受9級特大地震影響又活,放射性物質(zhì)發(fā)生泄漏苔咪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一柳骄、第九天 我趴在偏房一處隱蔽的房頂上張望团赏。 院中可真熱鬧,春花似錦耐薯、人聲如沸舔清。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽体谒。三九已至,卻和暖如春臼婆,著一層夾襖步出監(jiān)牢的瞬間抒痒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工颁褂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留故响,地道東北人傀广。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像彩届,于是被迫代替她去往敵國和親伪冰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容