有時候人們很喜歡造一些名字很嚇人的名詞,讓人一聽這個名詞就以爲本身不可能學會,從而讓人望而卻步。可是其實這些名詞背後所表明的東西其實很簡單。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'
傳給 wrapWithLoadData
,wrapWithLoadData
會返回一個新的組件,咱們用這個新的組件覆蓋原來的 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
傳遞信息。
高階組件有助於提升咱們代碼的靈活性,邏輯的複用性。靈活和熟練地掌握高階組件的用法須要經驗的積累還有長時間的思考和練習,若是你以爲本章節的內容沒法徹底消化和掌握也沒有關係,能夠先簡單瞭解高階組件的定義、形式和做用便可。