高階組件(Higher-Order Components)

有時候人們很喜歡造一些名字很嚇人的名詞,讓人一聽這個名詞就以爲本身不可能學會,從而讓人望而卻步。可是其實這些名詞背後所表明的東西其實很簡單。html

我不能說高階組件就是這麼一個東西。可是它是一個概念上很簡單,但卻很是經常使用、實用的東西,被大量 React.js 相關的第三方庫頻繁地使用。在前端的業務開發當中,你不掌握高階組件其實也能夠完成項目的開發,可是若是你可以靈活地使用高階組件,可讓你代碼更加優雅,複用性、靈活性更強。它是一個加分項,並且加的分還很多。前端

本章節可能有部份內容理解起來會有難度,若是你以爲沒法徹底理解本節內容。能夠先簡單理解高階組件的概念和做用便可,其餘內容選擇性地跳過。react

瞭解高階組件對咱們理解各類 React.js 第三方庫的原理頗有幫助。git

什麼是高階組件

高階組件就是一個函數,傳給它一個組件,它返回一個新的組件。github

const NewComponent = higherOrderComponent(OldComponent)

重要的事情再重複一次,高階組件是一個函數(而不是組件),它接受一個組件做爲參數,返回一個新的組件。這個新的組件會使用你傳給它的組件做爲子組件,咱們看看一個很簡單的高階組件:ajax

import React, { Component } from 'react'

export default (WrappedComponent) => {
  class NewComponent extends Component {
    // 能夠作不少自定義邏輯
    render () {
      return <WrappedComponent />
    }
  }
  return NewComponent
}

如今看來好像什麼用都沒有,它就是簡單的構建了一個新的組件類 NewComponent,而後把傳進入去的 WrappedComponent 渲染出來。可是咱們能夠給 NewCompoent 作一些數據啓動工做:設計模式

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      let data = localStorage.getItem(name)
      this.setState({ data })
    }

    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

如今 NewComponent 會根據第二個參數 name 在掛載階段從 LocalStorage 加載數據,而且 setState 到本身的 state.data 中,而渲染的時候將 state.data 經過 props.data 傳給 WrappedComponent服務器

這個高階組件有什麼用呢?假設上面的代碼是在 src/wrapWithLoadData.js 文件中的,咱們能夠在別的地方這麼用它:app

import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

假如 InputWithUserName 的功能需求是掛載的時候從 LocalStorage 裏面加載 username 字段做爲 <input /> 的 value 值,如今有了 wrapWithLoadData,咱們能夠很容易地作到這件事情。函數

只須要定義一個很是簡單的 InputWithUserName,它會把 props.data 做爲 <input /> 的 value 值。然把這個組件和 'username' 傳給 wrapWithLoadDatawrapWithLoadData 會返回一個新的組件,咱們用這個新的組件覆蓋原來的 InputWithUserName,而後再導出去模塊。

別人用這個組件的時候實際是用了被加工過的組件:

import InputWithUserName from './InputWithUserName'

class Index extends Component {
  render () {
    return (
      <div>
        用戶名:<InputWithUserName />
      </div>
    )
  }
}

根據 wrapWithLoadData 的代碼咱們能夠知道,這個新的組件掛載的時候會先去 LocalStorage 加載數據,渲染的時候再經過 props.data 傳給真正的 InputWithUserName

若是如今咱們須要另一個文本輸入框組件,它也須要 LocalStorage 加載 'content' 字段的數據。咱們只須要定義一個新的 TextareaWithContent

import wrapWithLoadData from './wrapWithLoadData'

class TextareaWithContent extends Component {
  render () {
    return <textarea value={this.props.data} />
  }
}

TextareaWithContent = wrapWithLoadData(TextareaWithContent, 'content')
export default TextareaWithContent

寫起來很是輕鬆,咱們根本不須要重複寫從 LocalStorage 加載數據字段的邏輯,直接用 wrapWithLoadData 包裝一下就能夠了。

咱們來回顧一下到底發生了什麼事情,對於 InputWithUserName 和 TextareaWithContent 這兩個組件來講,它們的需求有着這麼一個相同的邏輯:「掛載階段從 LocalStorage 中加載特定字段數據」。

若是按照以前的作法,咱們須要給它們兩個都加上 componentWillMount 生命週期,而後在裏面調用 LocalStorage。要是有第三個組件也有這樣的加載邏輯,我又得寫一遍這樣的邏輯。但有了 wrapWithLoadData 高階組件,咱們把這樣的邏輯用一個組件包裹了起來,而且經過給高階組件傳入 name 來達到不一樣字段的數據加載。充分複用了邏輯代碼。

到這裏,高階組件的做用其實不言而喻,其實就是爲了組件之間的代碼複用。組件可能有着某些相同的邏輯,把這些邏輯抽離出來,放到高階組件中進行復用。高階組件內部的包裝組件和被包裝組件之間經過 props 傳遞數據。

高階組件的靈活性

代碼複用的方法、形式有不少種,你能夠用類繼承來作到代碼複用,也能夠分離模塊的方式。可是高階組件這種方式頗有意思,也很靈活。學過設計模式的同窗其實應該能反應過來,它其實就是設計模式裏面的裝飾者模式。它經過組合的方式達到很高的靈活程度。

假設如今咱們需求變化了,如今要的是經過 Ajax 加載數據而不是從 LocalStorage 加載數據。咱們只須要新建一個 wrapWithAjaxData 高階組件:

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      ajax.get('/data/' + name, (data) => {
        this.setState({ data })
      })
    }

    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

其實就是改了一下 wrapWithLoadData 的 componentWillMount 中的邏輯,改爲了從服務器加載數據。如今只須要把 InputWithUserName 稍微改一下:

import wrapWithAjaxData from './wrapWithAjaxData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithAjaxData(InputWithUserName, 'username')
export default InputWithUserName

只要改一下包裝的高階組件就能夠達到須要的效果。並且咱們並無改動 InputWithUserName 組件內部的任何邏輯,也沒有改動 Index 的任何邏輯,只是改動了中間的高階組件函數。

(如下內容爲選讀內容,有興趣的同窗能夠繼續往下讀,不然也能夠直接跳到文末的總結部分。)

多層高階組件(選讀)

假如如今需求有變化了:咱們須要先從 LocalStorage 中加載數據,再用這個數據去服務器取數據。咱們改一下(或者新建一個)wrapWithAjaxData 高階組件,修改其中的 componentWillMount

...
    componentWillMount () {
      ajax.get('/data/' + this.props.data, (data) => {
        this.setState({ data })
      })
    }
...

它會用傳進來的 props.data 去服務器取數據。這時候修改 InputWithUserName

import wrapWithLoadData from './wrapWithLoadData'
import wrapWithAjaxData from './wrapWithAjaxData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithAjaxData(InputWithUserName)
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

你們能夠看到,咱們給 InputWithUserName 應用了兩種高階組件:先用 wrapWithAjaxData 包裹 InputWithUserName,再用 wrapWithLoadData 包含上次包裹的結果。它們的關係就以下圖的三個圓圈:

實際上最終獲得的組件會先去 LocalStorage 取數據,而後經過 props.data 傳給下一層組件,下一層用這個 props.data 經過 Ajax 去服務端取數據,而後再經過 props.data 把數據傳給下一層,也就是 InputWithUserName。你們能夠體會一下下圖尖頭表明的組件之間的數據流向:

用高階組件改造評論功能(選讀)

你們對這種在掛載階段從 LocalStorage 加載數據的模式都很熟悉,在上一階段的實戰中,CommentInput 和 CommentApp 都用了這種方式加載、保存數據。實際上咱們能夠構建一個高階組件把它們的相同的邏輯抽離出來,構建一個高階組件 wrapWithLoadData

export default (WrappedComponent, name) => {
  class LocalStorageActions extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      let data = localStorage.getItem(name)
      try {
        // 嘗試把它解析成 JSON 對象
        this.setState({ data: JSON.parse(data) })
      } catch (e) {
        // 若是出錯了就當普通字符串讀取
        this.setState({ data })
      }
    }

    saveData (data) {
      try {
        // 嘗試把它解析成 JSON 字符串
        localStorage.setItem(name, JSON.stringify(data))
      } catch (e) {
        // 若是出錯了就當普通字符串保存
        localStorage.setItem(name, `${data}`)
      }
    }

    render () {
      return (
        <WrappedComponent
          data={this.state.data}
          saveData={this.saveData.bind(this)}
          // 這裏的意思是把其餘的參數原封不動地傳遞給被包裝的組件
          {...this.props} />
      )
    }
  }
  return LocalStorageActions
}

CommentApp 能夠這樣使用:

class CommentApp extends Component {
  static propTypes = {
    data: PropTypes.any,
    saveData: PropTypes.func.isRequired
  }

  constructor (props) {
    super(props)
    this.state = { comments: props.data }
  }

  handleSubmitComment (comment) {
    if (!comment) return
    if (!comment.username) return alert('請輸入用戶名')
    if (!comment.content) return alert('請輸入評論內容')
    const comments = this.state.comments
    comments.push(comment)
    this.setState({ comments })
    this.props.saveData(comments)
  }

  handleDeleteComment (index) {
    const comments = this.state.comments
    comments.splice(index, 1)
    this.setState({ comments })
    this.props.saveData(comments)
  }

  render() {
    return (
      <div className='wrapper'>
        <CommentInput onSubmit={this.handleSubmitComment.bind(this)} />
        <CommentList
          comments={this.state.comments}
          onDeleteComment={this.handleDeleteComment.bind(this)} />
      </div>
    )
  }
}

CommentApp = wrapWithLoadData(CommentApp, 'comments')
export default CommentApp

一樣地能夠在 CommentInput 中使用 wrapWithLoadData,這裏就不貼代碼了。有興趣的同窗能夠查看高階組件重構的 CommentApp 版本

總結

高階組件就是一個函數,傳給它一個組件,它返回一個新的組件。新的組件使用傳入的組件做爲子組件。

高階組件的做用是用於代碼複用,能夠把組件之間可複用的代碼、邏輯抽離到高階組件當中。新的組件和傳入的組件經過 props 傳遞信息。

高階組件有助於提升咱們代碼的靈活性,邏輯的複用性。靈活和熟練地掌握高階組件的用法須要經驗的積累還有長時間的思考和練習,若是你以爲本章節的內容沒法徹底消化和掌握也沒有關係,能夠先簡單瞭解高階組件的定義、形式和做用便可。

相關文章
相關標籤/搜索