React高階組件與mixin使用

轉:http://www.lxweimin.com/p/9ab8a2e0403a
在多個不同的組件中需要用到相同的功能,其解決辦法有兩種:mixin和高階組件。

1、mixin
mixin一直被廣泛用于各種面向對象語言中,其作用是為單繼承語言創造一種類似多重繼承的效果。

廣義的mixin方法,就是用賦值的方式將mixin對象中的方法都掛載到原對象上,來實現對象的混入,類似ES6中的Object.assign()的作用。原理如下:

const mixin = function(obj, mixins){
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);

for(let prop in mixins){ // 遍歷mixins的屬性
    if(mixins.hasOwnPrototype(prop)){ // 判斷是否為mixin的自身屬性
        newObj.prototype[prop] = mixins[prop]; // 賦值
    }
}

return newObj;

}
實質上就是把任意多個源對象擁有的自身可枚舉屬性復制給目標對象,然后返回目標對象。那么React中的mixin是這樣的么?

在React中使用mixin
React在使用createClass構建組件時提供了mixin屬性,比如官方封裝的PureRenderMixin:

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
mixins: [PureRenderMixin],

render(){
    return <div>foo</div>;
}

});
可以看出,mixins是一個數組,封裝了我們需要的模塊。不同mixin的方法或許會有重合,如何處理視重合部分是普通方法還是生命周期方法而定。

在不同mixin里實現兩個名字一樣的普通方法:并不會覆蓋,且控制臺會報錯。
重合的是生命周期方法:將各個mixin的生命周期方法疊加在一起順序執行。
可以看到,使用createClass實現的mixin為組件做了兩件事:

工具方法:mixin的基本功能。用來定義共享的工具類方法,以便在各個組件中使用。
生命周期繼承,props與state合并:mixin可以合并生命周期方法。如果有多個mixin定義了componentDidMount(),React會自動將它們合并處理。同樣,mixin也可以作用在getInitialState的結果上,作state的合并,而props的合并也是這樣的。
然而,使用ES6 classes構建組件時,并不支持mixin。這就不得不說到decorator語法糖。

ES6 classes 與 decorator
decorator是運用在運行時的方法,用以對組件進行“修飾”。現在,使用decorator來實現mixin:

function handleClass(target, mixins){
if(mixins.length){
for(let i=0, l=mixins.length; i<l; i++){
// 獲取mixins的attribute對象
const decs = getOwnPropertyDescriptors(mixins[i]);
}

    // 定義mixins的attribute對象
    for(const key in decs){
        if(!(key in target.prototype)){
            defineProperty(target.prototype, key, decs[key]);
        }
    }
}

}

function mixin(...mixins){
if(typeof mixins[0] === 'function'){
return handleClass(mixins[0], []);
}else{
return target=>{
return handleClass(target,mixins);
}
}
}
不難看出,這個mixin與本文開頭createClass的mixin的實現是不一樣的:createClass的mixin是直接給對象的prototype屬性賦值,而這里是使用getOwnPropertyDescriptors和defineProperty進行定義。賦值與定義的區別在于賦值會覆蓋已有的定義,而后者不會。兩者在本質上都與官方的mixin方法存在區別,除了定義方法級別不能覆蓋之外,還得加上對生命周期方法的繼承以及對state的合并。

當然,decorator除作用在類上,還可以作用在方法上,但不在此處討論。

minxin的缺陷
破壞了原有組件的封裝:可能會帶來新的state和props,意味著會有些“不可見”的狀態需維護。
命名沖突:不同mixin中的命名不可知,故非常容易發生沖突,需要花一定成本解決。
增加了復雜性,難以維護。
2、高階組件
由于mixin存在上述缺陷,故React剝離了mixin。改用高階組件來取代它。
高階組件其實是一個函數,接收一個組件作為參數,返回一個新的組件作為返回值,類似于高階函數。高階組件和decorator是同一模式,因此,因此高階組件可以作為decorator來使用。高階組件基本形式:

const EnhancedComponent = higherOrderComponent(WrappedComponent);
decorator形式:

@higherOrderComponent
WrappedComponent
高階組件有以下好處:

適用范圍廣,它不需要es6或者其它需要編譯的特性,有函數的地方,就有HOC。
Debug友好,它能夠被React組件樹顯示,所以可以很清楚地知道有多少層,每層做了什么。
高階組件實現的方法有兩種:

屬性代理:通過被包裹組件的props來進行相關操作。主要進行組件的復用。
反向繼承:繼承被包裹的組件。主要進行渲染的劫持。
1、屬性代理
屬性代理主要是四個作用:操作props、通過refs訪問組件實例、抽象state、使用其他元素包裹WrappedComponent。

(1)操作props
包括對props的讀取、增加、刪除、修改。刪除和修改要注意不能影響原組件。
示例:增加一個props

function compHOC(WrappedComponent) {
return class Comp extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
(2)通過refs訪問組件實例

可以通過ref回調函數的形式來訪問傳入組件的實例,進而調用組件相關方法或其他操作(如實例的props操作)。

//WrappedComponent初始渲染時候會調用ref回調,傳入組件實例,在proc方法中,就可以調用組件方法
function refsHOC(WrappedComponent) {
return class RefsHOC extends React.Component {
proc(wrappedComponentInstance) {
wrappedComponentInstance.method()
}

render() {
  const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
  return <WrappedComponent {...props}/>
}

}
}
(3)抽象state
通過傳入 props 和回調函數抽象state。高階組件可以通過原組件抽象為展示型組件,分離內部狀態。
示例:抽象 <Input />的 value 和 onChange 方法。

function compHOC(WrappedComponent) {
return class Comp extends React.Component {
constructor(props) {
super(props)
this.state = {
name: ''
}

  this.onNameChange = this.onNameChange.bind(this)
}

// 將對name屬性的onChange方法提取到此處=>提取到高階組件,有效的抽象了同樣的state操作
onNameChange(event) {
  this.setState({
    name: event.target.value
  })
}
render() {
  const newProps = {
    name: {
      value: this.state.name,
      onChange: this.onNameChange
    }
  }
   return <WrappedComponent {...this.props} {...newProps}/>
}

}
}

//使用方式如下
@compHOC
class Example extends React.Component {
render() {
//使用ppHOC裝飾器之后,組件的props被添加了name屬性,可以通過下面的方法,將 value 和 onChange方法 添加到input上面

return <input name="name" {...this.props.name}/>

// 變成<input name="name" value={this.state.name} onChange={this.onNameChange} />,這樣我們就得到了一個受控組件。

}
}

(4)使用其他元素包裹組件
用于加樣式、布局等。

function compHOC(WrappedComponent) {
return class Comp extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
2、反向繼承
高階組件繼承了WrappedComponent,意味著可以訪問并使用WrappedComponent的state,props,生命周期和render方法,但它不能保證完整的子組件樹被解析。如果在高階組件中定義了與WrappedComponent中同名的方法,將會發生覆蓋,就必須手動通過super進行調用。反向繼承有兩個比較大的特點:渲染劫持和控制state。

(1)渲染劫持
渲染劫持指的就是高階組件可以控制 WrappedComponent 的渲染過程,并渲染各種各樣的結果。我們可以在這個過程中在任何React元素輸出的結果中讀取、增加、修改、刪除props,或讀取或修改React元素樹,或條件顯示元素樹,又或者是用元素包裹元素樹。
大致形式如下:

function compHOC(WrappedComponent) {
return class ExampleEnhance extends WrappedComponent {
...
componentDidMount() {
super.componentDidMount();
}
componentWillUnmount() {
super.componentWillUnmount();
}
render() {
...
return super.render();
}
}
}
例如,實現一個顯示loading的請求。組件中存在網絡請求,完成請求前顯示loading,完成后再顯示具體內容。(條件渲染)
可以用高階組件實現如下:

function hoc(ComponentClass) {
return class HOC extends ComponentClass { // 繼承原組件
render() {
if (this.state.success) {
return super.render()
}
return <div>Loading...</div>
}
}
}

@hoc
export default class ComponentClass extends React.Component {
state = {
success: false,
data: null
};
async componentDidMount() {
const result = await fetch(...請求);
     this.setState({
success: true,
data: result.data
});
}
render() {
return <div>主要內容</div>
}
}
正如前面所說,反向繼承不能保證完整的子組件樹被解析,這意味著會限制渲染劫持功能。渲染劫持的經驗法則是:我們可以操控 WrappedComponent 的元素樹,并輸出正確的結果。但如果元素樹中包括了函數類型的React組件,就不能操作組件的子組件。

(2)控制state
高階組件可以讀取,編輯和刪除WrappedComponent實例的state,可以添加state。不過這個可能會破壞WrappedComponent的state,所以,要限制高階組件讀取或添加state,添加的state應該放在單獨的命名空間里,而不是和WrappedComponent的state混在一起。
例如:通過訪問WrappedComponent的props和state來做調試

export function IIHOCDEBUGGER(WrappedComponent) {
return class II extends WrappedComponent {
render() {
return (
<div>
<h2>HOC Debugger Component</h2>
<p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
<p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
{super.render()}
</div>
)
}
}
}
3、組件命名
用HOC包裹的組件會丟失原先的名字,影響開發和調試。可以通過在WrappedComponent的名字上加一些前綴來作為HOC的名字,以方便調試。
參考react-redux實現:

HOC.displayName = HOC(${getDisplayName(WrappedComponent)});
//或
class HOC extends ... {
static displayName = HOC(${getDisplayName(WrappedComponent)});
...
}

//getDisplayName
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
4、組件參數
有時候,在調用高階組件時,需要傳入一些參數。可以這樣實現:

function HocFactoryFactory(...params){
// 可以做一些改變params的事
return function HocFactory(WrappedCompinent){
return class Hoc extends Component {
render(){
return <WrappedComponent {...this.props} />
}
}
}
}
使用方式如下:

HocFactoryFactory(params)(WrappedComponent);
或者:

@HocFactoryFactory(params)
class WrappedComponent extends Component{
...
}

作者:南風知我意ZD
鏈接:http://www.lxweimin.com/p/9ab8a2e0403a
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容