面向初學者的高階組件教程

作者:Brandon Newton

原文:Higher-Order Components (HOCs) for Beginners

談點:一篇面向初學者的 HOC 介紹。高階組件聽起來挺唬人的,只看名字恐怕不是那麼容易明白究竟是何物,而且通常來講高階組件並不是組件,而是接受組件作為參數,並且返回組件的函數。早期利用 ES5 的 mixin 語法來做的事,基本都可以使用高階組件代替,而且能做的還有更多。

前言

寫這篇文章的起因是其他關於高階組件(Higher-Order Components)的文章,包含官方文檔,都令初學者感到相當困惑。我知道有高階組件這樣一個東西,但不知道它到底有什麼用。所以,想通過一篇文章來對高階組件有一個更好的理解。

在此之前,我們需要先來講一下 JavaScript 中的函數。

ES6 箭頭函數簡介

接下來將提供一些箭頭函數的簡單示例,如果之前沒有使用過,可以認為它們與普通函數基本一致。下面的代碼會展示箭頭函數與普通函數的區別。

function () { return 42}// same as:() => 42// same as:() => { return 42}function person(name) { return { name: name }}// same as:(name) => { return { name: name }}

閱讀 MDN 的箭頭函數文檔 了解更多信息。

作為值的函數與部分調用

就像是數字、字元串、布爾值 一樣,函數也是值,意味著可以像傳遞其他數據一樣傳遞函數,可以將函數作為參數傳遞給另外一個函數。

const execute = (someFunction) => someFunction()execute(() => alert("Executed"))

也可以在在函數中返回一個函數:

const getOne = () => () => 1getOne()()

之所以在 getOne 後面有兩個 () ,是因為第一個返回的返回值是一個函數。如下:

const getOne = () => () => 1getOne//=> () => () => 1getOne()//=> () => 1getOne()()//=> 1

從函數返回函數可以幫助我們追蹤初始輸入函數。例如,下面的函數接受一個數字作為參數,並返回一個將該參數乘以新參數的函數:

const multiply = (x) => (y) => x * ymultiply(5)(20)

這個示例跟上述 getOne 一樣,在下面這個例子,讓 x = 5,y = 20。

const multiply = (x) => (y) => x * ymultiply//=> (x) => (y) => x * ymultiply(5)//=> (y) => 5 * ymultiply(5)(20)//=> 5 * 20

在只傳入一個參數調用 multiply 函數時,即部分調用該函數。比如,multiply(5) 講得到一個將其輸入值乘以 5 的函數,multiply(7) 將得到一個將其輸入值乘以 7 的函數。依此類推。通過部分調用可以創建一個預定義功能的新函數:

const multiply = (x) => (y) => x * yconst multiplyByFive = multiply(5)const multiplyBy100 = multiply(100)multiplyByFive(20)//=> 100multiply(5)(20)//=> 100multiplyBy100(5)//=> 500multiply(100)(5)//=> 500

一開始看起來似乎沒什麼用,但是,通過部分調用這種方式可以編寫可讀性更高,更易於理解的代碼。舉個例子,可以用一種更清晰的方式來代替 style-components 的函數插入語法。

// beforeconst Button = styled.button` background-color: ${({ theme }) => theme.bgColor} color: ${({ theme }) => theme.textColor}`<Button theme={themes.primary}>Submit</Button>// afterconst fromTheme = (prop) => ({ theme }) => theme[prop]const Button = styled.button` background-color: ${fromTheme("bgColor")} color: ${fromTheme("textColor")}`<Button theme={themes.primary}>Submit</Button>

我們創建一個接受一個字元串作為參數的函數 fromTheme("textColor"):它返回一個接受具有 theme 屬性的對象的函數:({ theme }) => theme[prop],然後再通過初始傳入的字元串 "textColor" 進行查找。我們可以做得更多,寫類似的 backgroundColor 和 textColor 這種部分調用 fromTheme 的函數:

const fromTheme = (prop) => ({ theme }) => theme[prop]const backgroundColor = fromTheme("bgColor")const textColor = fromTheme("textColor")const Button = styled.button` background-color: ${backgroundColor} color: ${textColor}`<Button theme={themes.primary}>Submit</Button>

高階函數

高階函數的定義是,接受函數作為參數的函數。如果曾經使用過類似 map 這樣的函數,可能已經很熟悉高階函數。如果不熟悉 map,它是一個數組遍歷的方法,接受一個函數作為參數應用到數組中的每個元素。例如,可以像這樣對一個數組作平方:

const square = (x) => x * x[1, 2, 3].map(square)//=> [ 1, 4, 9 ]

可以實現一個我們自己的 map 版本來說明這個概念:

const map = (fn, array) => { const mappedArray = [] for (let i = 0; i < array.length; i++) { mappedArray.push( // apply fn with the current element of the array fn(array[i]) ) } return mappedArray}

然後再使用我們的 map 版本來對一個數組作平方:

const square = (x) => x * xconsole.log(map(square, [1, 2, 3, 4, 5]))//=> [ 1, 4, 9, 16, 25 ]

譯者註:我們也可以將 map 方法從對象中解耦出來:

const map = (fn, array) => Array.prototype.map.call(array, fn)

這樣也可以像上述例子一樣調用。 或者更函數式的做法,再來點柯里化:

const map = array => fn => Array.prototype.map.call(array, fn)

或者是返回一個 <li>的 React 元素數組:

const HeroList = ({ heroes }) => ( <ul> {map((hero) => ( <li key={hero}>{hero}</li> ), heroes)} </ul>)<HeroList heroes=[ "Wonder Woman", "Black Widow", "Spider Man", "Storm", "Deadpool"]/>/*=> ( <ul> <li>Wonder Woman</li> <li>Black Widow</li> <li>Spider Man</li> <li>Storm</li> <li>Deadpool</li> </ul>)*/

高階組件

我們知道,高階函數是接受函數作為參數的函數。在 React 中,任何返回 JSX 的函數都被稱為無狀態函數組件,簡稱為函數組件。基本的函數組件如下所示:

const Title = (props) => <h1>{props.children}</h1><Title>Higher-Order Components(HOCs) for React Newbies</Title>//=> <h1>Higher-Order Components(HOCs) for React Newbies</h1>

高階組件則是接受組件作為參數並返回組件的函數。如何使用傳入組件完全取決於你,甚至可以完全忽視它:

// Technically an HOCconst ignore = (anything) => (props) => <h1>:)</h1>const IgnoreHeroList = ignore(HeroList)<IgnoreHeroList />//=> <h1>:)</h1>

可以編寫一個將輸入轉換成大寫的 HOC:

const yell = (PassedComponent) => ({ children, ...props }) => <PassedComponent {...props}> {children.toUpperCase()}! </PassedComponent>const Title = (props) => <h1>{props.children}</h1>const AngryTitle = yell(Title)<AngryTitle>Whatever</AngryTitle>//=> <h1>WHATEVER!</h1>

你也可以返回一個有狀態組件,因為 JavaScript 中的類不過是函數的語法糖。這樣就可以使用到 React 生命周期的方法,比如 componentDidMount。這是 HOCs 真正有用的地方。我們現在可以做一些稍微有趣點的事,比如將 HTTP 請求的結果傳遞給函數組件。

const withGists = (PassedComponent) => class WithGists extends React.Component { state = { gists: [] } componentDidMount() { fetch("https://api.github.com/gists/public") .then((r) => r.json()) .then((gists) => this.setState({ gists: gists })) } render() { return ( <PassedComponent {...this.props} gists={this.state.gists} /> ) } }const Gists = ({ gists }) => ( <pre>{JSON.stringify(gists, null, 2)}</pre>)const GistsList = withGists(Gists)<GistsList />//=> Before api request finishes:// <Gists gists={[]} />// //=> After api request finishes:// <Gists gists={[// { /* … */ },// { /* … */ },// { /* … */ }// ]} />

withGists 會傳遞 gist api 調用的結果,並且你可以在任何組件上使用。點擊這裡 可以看到一個更加完整的例子。

結論:高階組件是 ??????

react-redux 也是使用 HOC, connect 將應用 store 的值傳遞到「已連接」 的組件。它還會執行一些錯誤檢查和組件生命周期優化,如果手動完成將導致編寫大量重複代碼。

如果你發現自己在不同地方編寫了大量的代碼,那麼也可以將代碼重構成可重用的 HOC。

HOCs 非常具有表現力,可以使用它們創造很多很酷的東西。

儘可能地保持你的 HOC 簡單,不要編寫需要閱讀長篇大論才能理解的代碼

附加練習

下面有一些練習,來鞏固對 HOC 的理解:

  • 寫一個反轉其輸入的 HOC
  • 編寫一個HOC,將 API 中的數據提供給組件
  • 寫一個HOC來實現 shouldComponentUpdate,以避免更新。
  • 編寫一個 HOC,使用 React.Children.toArray 對傳入組件子元素進行排序。

推薦閱讀:

基於Webpack 2的React組件懶載入
非計算機專業,半路出家,以前做 UI ,現在想改前端,學習 JS 過程中遇到瓶頸了,如何突破?
11行代碼帶你搞懂 Generator 函數
PHP 是做前台還是後台?前端和前台的區別?

TAG:前端开发 | React |