前言
"高階"二字聽起來非常唬人,因為大學高數課上的高階方程讓人抓狂,從而讓第一次接觸"高階元件"概念的人們誤以為又是什麼高深的思想和複雜的邏輯。但相信在你學習完成後和生產環境大量使用過程中,就會發現這個所謂"高階元件"真的一點也不高階,非常簡單易懂。本文通過回答三個問題帶你深入淺出React高階元件。
1.為什麼需要高階元件?
2.高階元件是什麼?
3.如何實現高階元件?
1.為什麼需要高階元件?
這個問題很簡單,為什麼我們需要react/vue/angular?使用框架最核心的原因之一就是提高開發效率,能早點下班。同理,react高階元件能夠讓我們寫出更易於維護的react程式碼,能再早點下班~
舉個例子,ES6支援使用import/export的方式拆分程式碼功能和模組,避免一份檔案裡面出現"成坨"的程式碼。同理對於複雜的react元件,如果這個元件有幾十個自定義的功能函式,自然要進行拆分,不然又成了"一坨"元件,那麼該如何優雅地拆分元件呢?react高階元件應運而生。
在使用ES5編寫react程式碼時,可以使用Mixin這一傳統模式進行拆分。新版本的react全面支援ES6並提倡使用ES6編寫jsx,同時取消了Mixin。因此高階元件越來越受到開源社群的重視,例如redux等知名第三方庫都大量使用了高階元件。
2.高階元件是什麼?
回答這個問題前,我們先看下本文的封面圖,為什麼筆者用初中生就掌握的一元一次函式來代表這篇文章呢?很顯然,高階函式就是形如y=kx+b
的東西,x
是我們想要改造的原元件,y
就是改造過後輸出的元件。那具體是怎麼改造的呢?k
和b
就是改造的方法。這就是高階元件的基本原理,是不是一點也不高階~
再舉個例子相信更能讓你明白:我們寫程式碼需要進行加法計算,於是我們把加法計算的方法單獨抽出來寫成一個加法函式,這個加法函式可以在各處呼叫使用,從而減少了工作量和程式碼量。而我們獨立出來的這個可以隨處使用的加法函式,類比地放在react裡,就是高階元件。
3.如何實現高階元件?
從上面的問題回答中,我們知道了:高階元件其實就是處理react元件的函式。那麼我們如何實現一個高階元件?有兩種方法:
1.屬性代理 2.反向繼承
第一種方法屬性代理是最常見的實現方式,將被處理元件的props
和新的props
一起傳遞給新元件,程式碼如下:
//WrappedComponent為被處理元件
function HOC(WrappedComponent){
return class HOC extends Component {
render(){
const newProps = {type:'HOC'};
return <div>
<WrappedComponent {...this.props} {...newProps}/>
</div>
}
}
}
@HOC
class OriginComponent extends Component {
render(){
return <div>這是原始元件</div>
}
}
//const newComponent = HOC(OriginComponent)
複製程式碼
屬性代理聽起來好像很麻煩,然而從程式碼中看,就是使用HOC這個函式,向被處理的元件WrappedComponent上面新增一些屬性,並返回一個包含原元件的新元件。從chrome除錯臺上我們可以看到原始元件已經被包裹起來了並具有type
屬性:
上述程式碼使用了ES7的decorator裝飾器來實現對OriginComponent
元件的裝飾和增強,或者使用註釋中的函式方法一樣可以達到相同的效果。
使用屬性代理的好處就是,可以把常用的方法獨立出來並多次複用。比如我們實現了一個加法函式,那麼我們把加法函式改造成形如上述HOC
函式的形式,之後對其他元件進行包裹,就可以在元件裡使用這個方法了。
第二種方法反向繼承就有意思了,先看程式碼:
function HOC(WrappedComponent){
return class HOC extends WrappedComponent {
//繼承了傳入的元件
test1(){
return this.test2() + 5;
}
componentDidMount(){
console.log('1');
this.setState({number:2});
}
render(){
//使用super呼叫傳入元件的render方法
return super.render();
}
}
}
@HOC
class OriginComponent extends Component {
constructor(props){
super(props);
this.state = {number:1}
}
test2(){
return 4;
}
componentDidMount(){
console.log('2');
}
render(){
return (
<div>
{this.state.number}{'and'}
{this.test1()}
這是原始元件
</div>
)
}
}
//const newComponent = HOC(OriginComponent)
複製程式碼
程式碼看完我們可能還有點懵,那我們先來剖析關鍵詞"繼承"。何謂繼承?新生成的HOC
元件通過extends
關鍵字,獲得了傳入元件OriginComponent
所有的屬性和方法,是謂"繼承"。也就是說繼承之後,HOC
元件能夠實現OriginComponent
元件的全部功能,而且,HOC
可以拿到state
和props
進行修改,從而改變元件的行為,也就是所謂的"渲染劫持"。可以說,通過反向繼承方法實現的高階元件相比於屬性代理實現的高階元件,功能更強大,個性化程度更高,適應更多的場景。
如上的程式碼,我們可以看到:
第一:this.test1()
輸出了9。為什麼?
因為在ES6中,super
作為物件呼叫父類方法時,super
繫結子類的this
。故執行super.render()
時OriginComponent
中的this
指向的是HOC
元件,所以能夠成功地執行test1
函式。
第二:控制檯輸出的是1而不是2。為什麼?
首先,decorator是在程式碼編譯階段執行,故HOC
的render
方法在OriginComponent
的render
方法之前執行。並且子元件HOC
是繼承於父元件OriginComponent
,兩者具有繼承關係HOC.__proto__ === OriginComponent
,當執行componentDidMount
方法時,子元件已存在該方法,故執行完畢後結束,不再根據__proto__
向上繼續尋找。如果我們將子元件HOC
中的componentDidMount
方法去掉,那麼控制檯將輸出2。
當我們有多個高階元件需要同時增強一個元件時該怎麼辦呢?我們可以這樣寫:
@fun1
@fun2
@fun3
class OriginComponent extends Component {
...
}
複製程式碼
也可以使用lodash
的flowRight
方法:
const enchance = lodash.flowRight(fun1,fun2,fun3);
@enchance
class OriginComponent extends Component {
...
}
複製程式碼
因為fun1
fun2
fun3
都是處理類的函式,只要實現按順序依次對類進行處理即可。
以上就是關於高階元件實現方式的全部內容。為了查缺補漏,官方文件中有兩條建議很中肯,在這裡摘抄給大家:
一句話總結,為了避免在除錯時,因為高階元件的存在而導致滿屏的HOC
(以上述程式碼為例),可以設定類的displayName
屬性修改元件的名稱。
如果你對ES6中的繼承非常瞭解的話,那理解上述文字應該非常簡單。ES6的繼承中,子類不能繼承父類的靜態方法,即使用static
關鍵字定義的方法。如果子類想使用,那麼一定要copy
之後才能使用。
總結一下,高階元件其實就是處理元件的函式,他有兩種實現方式:
一是屬性代理,類似於一元一次方程的y = x + b
,輸入x
是原元件,引數b
是你要新增或刪除的屬性/方法,y
是最終輸出的元件。
二是反向繼承,類似於一元一次方程的y = kx + b
,可以拿到state
和props
進行渲染劫持(k
),改變元件的行為。