相信不少看過一些框架或者是類庫的人都有印象,一個函數叫什么creator或者是什么什么createToFuntion,總是接收一個函數,來返回另一個函數。這是一個高階函數,它可以接收函數可以當參數,也可以當返回值,這就是函數式編程。像柯里化、裝飾器模式、高階組件,都是相通的,一個道理。
本文重點是React高階組件,要理解高階組件,不得不說函數式編程。
1. 函數式編程
函數式編程是一種編程模式,在這種編程模式種最常用函數和表達式,函數式編程把函數作為一等公民,強調從函數的角度考慮問題,函數式編程傾向用一系列嵌套的函數來解決問題。
簡單寫個例子
function OCaml () {
console.log('I\'m FP language OCaml')
}
function clojure() {
console.log('I\'m FP language clojure')
}
現在想在每條console語句前后各加一條console語句,如果在每個函數都加上console語句,會產生不必要的耦合,所以高階函數就派上了用場。
function FuncWrapper(func) {
return function () {
console.log('before')
func()
console.log('after')
}
}
var OCaml = FuncWrapper(OCaml)
var clojure = FuncWrapper(clojure)
我們寫了一個函數FuncWrapper,該函數接一個函數作為參數,將參數函數裝飾了一層,返回出去,減少了代碼耦合。在設計模式中稱這種模式為裝飾器或裝飾者模式。
當然函數式編程的好處不止這一條,有些人吹捧OCaml,clojure, scala等FP語言特性比如:純函數無副作用、不變的數據、流計算模式、尾遞歸、柯里化等等。
在React中,高階組件HOC就相當於這么一個FuncWrapper,傳入一個組件,返回被包裝或者被處理的另一個組件。
2. 高階組件
上邊已經簡單說過了什么是高階組件,其實本質上是一個類工廠。先舉個例在再說
第一個組件
import React from 'react'
export default class OCaml extends React.Component {
constructor (props) {
super(props)
this.changeHandle = this.changeHandle.bind(this)
}
changeHandle (value) {
console.log(value)
}
render () {
return (
<div>
<h2>I'm OCaml</h2>
<input type="text" onchange={value => this.changeHandle(value)}/>
</div>
)
}
}
第二個組件
import React from 'react'
export default class Clojure extends React.Component {
constructor (props) {
super(props)
this.changeHandle = this.changeHandle.bind(this)
}
changeHandle (value) {
console.log(value)
}
render () {
return (
<div>
<h2>I'm Clojure</h2>
<input type="text" onchange={value => this.changeHandle(value)}/>
</div>
)
}
}
有兩個不相同的組件,但是有部分功能重合,就是那個changeHandle函數,理解了高階函數,再解決這類問題就不難了吧?不還是一樣嗎?
import React from 'react'
export default function CompWrapper (Component) {
return class WarpComponent extends React.Component {
constructor (props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange (value) {
console.log(value)
}
render () {
return <Component handleChange={this.handleChange} {...this.props}></Component>
}
}
}
OCaml = CompWrapper(OCaml)
Clojure = CompWrapper(Clojure)
這是一個最簡單的高階組件。注意,再高階組件的返回包裝好的組件的時候,我們將高階組件的props展開並傳入包裝好的組件中,這是確保給高階組件的props也能給到被包裝的組件上。
高階組件的通途很多,可以用來,代碼復用,邏輯抽象,抽離底層代碼,渲染劫持,更改state、更改props等等。
我們主要說一下兩種功能的React高階組件:屬性代理、反向繼承。
3. 屬性代理(props proxy)
很好說了,上面已經提到過了,再來一遍,高階組件將它收到的props傳遞給被包裝的組件,所叫屬性代理
export default function CompWrapper (Component) {
return class WarpComponent extends React.Component {
render () {
return <Component {...this.props}></Component>
}
}
}
屬性代理主要用來處理以下問題
- 更改props
- 抽取state
- 通過refs獲取組件實例
- 將組件與其他原生DOM包裝到一起
更改props
例子是增加props的,其他的類似,都是在在高階組件內部加以處理。
import React from 'react'
export default function CompWrapper (Component) {
return class WarpComponent extends React.Component {
say () {
console.log('我是被高階組件包裝過的組件!')
}
newProps = {
isLogin: true,
msgList: [1,2,3,4,5]
}
render () {
return <Component say={this.say} {...this.props} {...this.newProps}></Component>
}
}
}
包裝好的組件可以用this.props.say調用say方法,可以用this.props.isLogin判斷登陸狀態等等。
抽像state
我們可以通過props和回調函數來抽象state
import React from 'react'
export function CompWrapper (Component) {
return class WarpComponent extends React.Component {
constructor (props) {
super(props)
this.state = {
inputValue: '暫時還沒喲'
}
this.changeHandle = this.changeHandle.bind(this)
}
changeHandle (event) {
this.setState({
inputValue: event.target.value
})
}
render () {
return <Component {...this.props} inputValue={this.state.inputValue} changeHandle={this.changeHandle}></Component>
}
}
}
這個高階組件將一切數據都綁定到了自己的身上,只需要出觸發被包裝組件的特定事件,就將改變自己的state,再將自己的state通過props傳遞給被包裝組件。
@CompWrapper
export class InputComp extends React.Component {
render () {
return (
<div>
<h2>{this.props.inputValue}</h2>
<input type="text" onChange={this.props.changeHandle}/>
</div>
)
}
}
這里的input就成了完全受控的組件。注意:在定義組件的語句上邊寫上@CompWrapper是和InputComp = CompWrapper(InputComp)
作用是一樣的。@操作符是ES7的decorator,也就是裝飾器。
通過 refs 獲取組件實例
從你的 render 方法中返回你的 UI 結構后,你會發現你想要“伸手”調用從 render 返回的組件實例的方法,我們可以通過ref獲取組件的實例,但是想讓ref生效,必須先經過一次正常的渲染來使ref得到計算,怎么先讓組件經過一次正常的渲染呢?高階組件又來了,明白了嗎?高階組件的render返回了被包裝的組件,然后我們就可以通過ref獲取這個組件的實例了。
import React from 'react'
export function CompWrapper (Component) {
return class WarpComponent extends React.Component {
proc(wrappedComponentInstance) {
wrappedComponentInstance.say()
}
render () {
const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
return <Component {...props}></Component>
}
}
}
@CompWrapper
export class InputComp extends React.Component {
say () {
console.log('I\'m InputComp')
}
render () {
return (
<button onClick={()=>{console.log(this.props)}}>點擊</button>
)
}
}
當被包裝的組件被渲染后,就可以執行自己實例的方法了,因為計算ref這件事已經由高階組件做完了。
將組件與其他原生DOM包裝到一起
這個很好理解,如果想把布局什么的和組件結合到一起,使用高階組件是一個辦法。
import React from 'react'
export function CompWrapper (Component) {
return class WarpComponent extends React.Component {
render () {
return (
<div style={{marginTop: 100}}>
<Component {...this.props}/>
</div>
)
}
}
}
4. 反向繼承
為什么叫反向繼承,是高階組件繼承被包裝組件,按照我們想的被包裝組件繼承高階組件。
import React from 'react'
export function CompWrapper (Component) {
return class WarpComponent extends Component {
render () {
return super.render()
}
}
}
反向代理主要用來做渲染劫持
渲染劫持
所謂的渲染劫持,就是最后組件所渲染出來的東西或者我們叫React Element完全由高階組件來決定,通過所以我們可以對任意一個React Element的props進行操作;我們也可以操作React Element的Child。
import React from 'react'
export function CompWrapper (Component) {
return class WarpComponent extends Component {
render () {
const reactElm = super.render()
let newProps = {}
if (reactElm.type === 'input') {
newProps = {value: '這是一個input'}
}
const props = Object.assign({}, reactElm.props, newProps)
const newReactElm = React.cloneElement(reactElm, props, reactElm.props.child)
return newReactElm
}
}
}
這個例子,判斷組件的頂層元素是否為一個input,如果是的話,通過cloneElement這個方法來克隆出一個一樣的組件,並將新的props傳入,這樣input就有值了。
用過React-Redux的人可能會有印象,使用connect可以將react和redux關聯起來,這里的connect就是一個高階組件,想到這,就很容易想出connect高階組件是怎么實現了,我會在寫一篇隨筆,自己實現一個redux、connect、midlleware還有thunk。