【React深入】從Mixin到HOC再到Hook(原創)

導讀

前端發展速度非常之快,頁面和組件變得越來越複雜,如何更好的實現 狀態邏輯複用一直都是應用程序中重要的一部分,這直接關係着應用程序的質量以及維護的難易程度。

本文介紹了 React採用的三種實現 狀態邏輯複用的技術,並分析了他們的實現原理、使用方法、實際應用以及如何選擇使用他們。

本文略長,下面是本文的思維導圖,您可以從頭開始閱讀,也可以選擇感興趣的部分閱讀:

Mixin設計模式

Mixin(混入)是一種通過擴展收集功能的方式,它本質上是將一個對象的屬性拷貝到另一個對象上面去,不過你可以拷貝 任意多個對象的 任意個方法到一個新對象上去,這是 繼承所不能實現的。它的出現主要就是爲了解決代碼複用問題。

很多開源庫提供了 Mixin的實現,如 Underscore_.extend方法、 JQueryextend方法。

使用 _.extend方法實現代碼複用:

var LogMixin = {

actionLog: function() {

console.log('action...');

},

requestLog: function() {

console.log('request...');

},

};

function User() { /*..*/ }

function Goods() { /*..*/ }

_.extend(User.prototype, LogMixin);

_.extend(Goods.prototype, LogMixin);

var user = new User();

var good = new Goods();

user.actionLog();

good.requestLog();

我們可以嘗試手動寫一個簡單的 Mixin方法:

function setMixin(target, mixin) {

if (arguments[2]) {

for (var i = 2, len = arguments.length; i < len; i++) {

target.prototype[arguments[i]] = mixin.prototype[arguments[i]];

}

}

else {

for (var methodName in mixin.prototype) {

if (!Object.hasOwnProperty(target.prototype, methodName)) {

target.prototype[methodName] = mixin.prototype[methodName];

}

}

}

}

setMixin(User,LogMixin,'actionLog');

setMixin(Goods,LogMixin,'requestLog');

您可以使用 setMixin方法將任意對象的任意方法擴展到目標對象上。

React中應用Mixin

React也提供了 Mixin的實現,如果完全不同的組件有相似的功能,我們可以引入來實現代碼複用,當然只有在使用 createClass來創建 React組件時纔可以使用,因爲在 React組件的 es6寫法中它已經被廢棄掉了。

例如下面的例子,很多組件或頁面都需要記錄用戶行爲,性能指標等。如果我們在每個組件都引入寫日誌的邏輯,會產生大量重複代碼,通過 Mixin我們可以解決這一問題:

var LogMixin = {

log: function() {

console.log('log');

},

componentDidMount: function() {

console.log('in');

},

componentWillUnmount: function() {

console.log('out');

}

};



var User = React.createClass({

mixins: [LogMixin],

render: function() {

return (<div>...</div>)

}

});



var Goods = React.createClass({

mixins: [LogMixin],

render: function() {

return (<div>...</div>)

}

});

Mixin帶來的危害

React官方文檔在Mixins Considered Harmful一文中提到了 Mixin帶來了危害:

  • Mixin 可能會相互依賴,相互耦合,不利於代碼維護

  • 不同的 Mixin中的方法可能會相互衝突

  • Mixin非常多時,組件是可以感知到的,甚至還要爲其做相關處理,這樣會給代碼造成滾雪球式的複雜性

React現在已經不再推薦使用 Mixin來解決代碼複用問題,因爲 Mixin帶來的危害比他產生的價值還要巨大,並且 React全面推薦使用高階組件來替代它。另外,高階組件還能實現更多其他更強大的功能,在學習高階組件之前,我們先來看一個設計模式。

裝飾模式

裝飾者( decorator)模式能夠在不改變對象自身的基礎上,在程序運行期間給對像動態的添加職責。與繼承相比,裝飾者是一種更輕便靈活的做法。

高階組件(HOC)

高階組件可以看作 React對裝飾模式的一種實現,高階組件就是一個函數,且該函數接受一個組件作爲參數,並返回一個新的組件。

高階組件( HOC)是 React中的高級技術,用來重用組件邏輯。但高階組件本身並不是 ReactAPI。它只是一種模式,這種模式是由 React自身的組合性質必然產生的。

function visible(WrappedComponent) {

return class extends Component {

render() {

const { visible, ...props } = this.props;

if (visible === false) return null;

return <WrappedComponent {...props} />;

}

}

}

上面的代碼就是一個 HOC的簡單應用,函數接收一個組件作爲參數,並返回一個新組件,新組建可以接收一個 visible props,根據 visible的值來判斷是否渲染Visible。

下面我們從以下幾方面來具體探索 HOC

HOC的實現方式

屬性代理

函數返回一個我們自己定義的組件,然後在 render中返回要包裹的組件,這樣我們就可以代理所有傳入的 props,並且決定如何渲染,實際上 ,這種方式生成的高階組件就是原組件的父組件,上面的函數 visible就是一個 HOC屬性代理的實現方式。

function proxyHOC(WrappedComponent) {

    return class extends Component {

        render() {

            return <WrappedComponent {...this.props} />;

        }

    }

}

對比原生組件增強的項:

  • 可操作所有傳入的 props

  • 可操作組件的生命週期

  • 可操作組件的 static方法

  • 獲取 refs

反向繼承

返回一個組件,繼承原組件,在 render中調用原組件的 render。由於繼承了原組件,能通過this訪問到原組件的 生命週期、props、state、render等,相比屬性代理它能操作更多的屬性。

function inheritHOC(WrappedComponent) {

    return class extends WrappedComponent {

        render() {

            return super.render();

        }
    }
}

對比原生組件增強的項:

  • 可操作所有傳入的 props

  • 可操作組件的生命週期

  • 可操作組件的 static方法

  • 獲取 refs

  • 可操作 state

  • 可以渲染劫持

HOC可以實現什麼功能

組合渲染

可使用任何其他組件和原組件進行組合渲染,達到樣式、佈局複用等效果。

通過屬性代理實現

function stylHOC(WrappedComponent) {

return class extends Component {

render() {

return (<div>

<div className="title">{this.props.title}</div>

<WrappedComponent {...this.props} />

</div>);

}

}

}

通過反向繼承實現

function styleHOC(WrappedComponent) {

return class extends WrappedComponent {

render() {

return <div>

<div className="title">{this.props.title}</div>

{super.render()}

</div>

}

}

}

條件渲染

根據特定的屬性決定原組件是否渲染

通過屬性代理實現

function visibleHOC(WrappedComponent) {

return class extends Component {

render() {

if (this.props.visible === false) return null;

return <WrappedComponent {...props} />;

}

}

}

通過反向繼承實現

function visibleHOC(WrappedComponent) {

return class extends WrappedComponent {

render() {

if (this.props.visible === false) {

return null

} else {

return super.render()

}

}

}

}

操作props

可以對傳入組件的 props進行增加、修改、刪除或者根據特定的 props進行特殊的操作。

通過屬性代理實現

function proxyHOC(WrappedComponent) {

return class extends Component {

render() {

const newProps = {

...this.props,

user: 'ConardLi'

}

return <WrappedComponent {...newProps} />;

}

}

}

獲取refs

高階組件中可獲取原組件的 ref,通過 ref獲取組件實力,如下面的代碼,當程序初始化完成後調用原組件的log方法。(不知道refs怎麼用,請👇Refs & DOM)

通過屬性代理實現

function refHOC(WrappedComponent) {

return class extends Component {

componentDidMount() {

this.wapperRef.log()

}

render() {

return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;

}

}

}

這裏注意:調用高階組件的時候並不能獲取到原組件的真實 ref,需要手動進行傳遞,具體請看傳遞refs

狀態管理

將原組件的狀態提取到 HOC中進行管理,如下面的代碼,我們將 Inputvalue提取到 HOC中進行管理,使它變成受控組件,同時不影響它使用 onChange方法進行一些其他操作。基於這種方式,我們可以實現一個簡單的 雙向綁定,具體請看雙向綁定。

通過屬性代理實現

function proxyHoc(WrappedComponent) {

return class extends Component {

constructor(props) {

super(props);

this.state = { value: '' };

}

onChange = (event) => {

const { onChange } = this.props;

this.setState({

value: event.target.value,

}, () => {

if(typeof onChange ==='function'){

onChange(event);

}

})

}



render() {

const newProps = {

value: this.state.value,

onChange: this.onChange,

}

return <WrappedComponent {...this.props} {...newProps} />;

}

}

}



class HOC extends Component {

render() {

return <input {...this.props}></input>

}

}



export default proxyHoc(HOC);

操作state

上面的例子通過屬性代理利用HOC的state對原組件進行了一定的增強,但並不能直接控制原組件的 state,而通過反向繼承,我們可以直接操作原組件的 state。但是並不推薦直接修改或添加原組件的 state,因爲這樣有可能和組件內部的操作構成衝突。

通過反向繼承實現

function debugHOC(WrappedComponent) {

return class extends WrappedComponent {

render() {

console.log('props', this.props);

console.log('state', this.state);

return (

<div className="debuging">

{super.render()}

</div>

)

}

}

}

上面的 HOCrender中將 propsstate打印出來,可以用作調試階段,當然你可以在裏面寫更多的調試代碼。想象一下,只需要在我們想要調試的組件上加上 @debug就可以對該組件進行調試,而不需要在每次調試的時候寫很多冗餘代碼。(如果你還不知道怎麼使用HOC,請👇如何使用HOC)

渲染劫持

高階組件可以在render函數中做非常多的操作,從而控制原組件的渲染輸出。只要改變了原組件的渲染,我們都將它稱之爲一種 渲染劫持

實際上,上面的組合渲染和條件渲染都是 渲染劫持的一種,通過反向繼承,不僅可以實現以上兩點,還可直接 增強由原組件 render函數產生的 React元素

通過反向繼承實現

function hijackHOC(WrappedComponent) {

return class extends WrappedComponent {

render() {

const tree = super.render();

let newProps = {};

if (tree && tree.type === 'input') {

newProps = { value: '渲染被劫持了' };

}

const props = Object.assign({}, tree.props, newProps);

const newTree = React.cloneElement(tree, props, tree.props.children);

return newTree;

}

}

}

注意上面的說明我用的是 增強而不是 更改render函數內實際上是調用 React.creatElement產生的 React元素

雖然我們能拿到它,但是我們不能直接修改它裏面的屬性,我們通過 getOwnPropertyDescriptors函數來打印下它的配置項:

可以發現,所有的 writable屬性均被配置爲了 false,即所有屬性是不可變的。(對這些配置項有疑問,請👇defineProperty)

不能直接修改,我們可以藉助 cloneElement方法來在原組件的基礎上增強一個新組件:

React.cloneElement()克隆並返回一個新的 React元素,使用 element作爲起點。生成的元素將會擁有原始元素props與新props的淺合併。新的子級會替換現有的子級。來自原始元素的 key 和 ref 將會保留。

React.cloneElement()幾乎相當於:


 
  1. <element.type {...element.props} {...props}>{children}</element.type>

如何使用HOC

上面的示例代碼都寫的是如何聲明一個 HOCHOC實際上是一個函數,所以我們將要增強的組件作爲參數調用 HOC函數,得到增強後的組件。

class myComponent extends Component {

render() {

return (<span>原組件</span>)

}

}

export default inheritHOC(myComponent);

compose

在實際應用中,一個組件可能被多個 HOC增強,我們使用的是被所有的 HOC增強後的組件,借用一張 裝飾模式的圖來說明,可能更容易理解:

假設現在我們有 loggervisiblestyle等多個 HOC,現在要同時增強一個 Input組件:

logger(visible(style(Input)))

這種代碼非常的難以閱讀,我們可以手動封裝一個簡單的函數組合工具,將寫法改寫如下:

const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));

compose(logger,visible,style)(Input);

compose函數返回一個所有函數組合後的函數, compose(f,g,h)(...args)=>f(g(h(...args)))是一樣的。

很多第三方庫都提供了類似 compose的函數,例如 lodash.flowRightRedux提供的 combineReducers函數等。

Decorators

我們還可以藉助 ES7爲我們提供的 Decorators來讓我們的寫法變的更加優雅:

@logger

@visible

@style

class Input extends Component {

// ...

}

DecoratorsES7的一個提案,還沒有被標準化,但目前 Babel轉碼器已經支持,我們需要提前配置 babel-plugin-transform-decorators-legacy

"plugins": ["transform-decorators-legacy"]

還可以結合上面的 compose函數使用:

const hoc = compose(logger, visible, style);

@hoc

class Input extends Component {

// ...

}

HOC的實際應用

下面是一些我在公司項目中實際對 HOC的實際應用場景,由於文章篇幅原因,代碼經過很多簡化,如有問題歡迎在評論區指出:

日誌打點

實際上這屬於一類最常見的應用,多個組件擁有類似的邏輯,我們要對重複的邏輯進行復用, 官方文檔中 CommentList的示例也是解決了代碼複用問題,寫的很詳細,有興趣可以👇使用高階組件(HOC)解決橫切關注點。

某些頁面需要記錄用戶行爲,性能指標等等,通過高階組件做這些事情可以省去很多重複代碼。

function logHoc(WrappedComponent) {

return class extends Component {

componentWillMount() {

this.start = Date.now();

}

componentDidMount() {

this.end = Date.now();

console.log(`${WrappedComponent.dispalyName} 渲染時間:${this.end - this.start} ms`);

console.log(`${user}進入${WrappedComponent.dispalyName}`);

}

componentWillUnmount() {

console.log(`${user}退出${WrappedComponent.dispalyName}`);

}

render() {

return <WrappedComponent {...this.props} />

}

}

}

可用、權限控制

function auth(WrappedComponent) {

return class extends Component {

render() {

const { visible, auth, display = null, ...props } = this.props;

if (visible === false || (auth && authList.indexOf(auth) === -1)) {

return display

}

return <WrappedComponent {...props} />;

}

}

}

authList是我們在進入程序時向後端請求的所有權限列表,當組件所需要的權限不列表中,或者設置的 visiblefalse,我們將其顯示爲傳入的組件樣式,或者 null。我們可以將任何需要進行權限校驗的組件應用 HOC

@auth

class Input extends Component { ... }

@auth

class Button extends Component { ... }



<Button auth="user/addUser">添加用戶</Button>

<Input auth="user/search" visible={false} >添加用戶</Input>

雙向綁定

vue中,綁定一個變量後可實現雙向數據綁定,即表單中的值改變後綁定的變量也會自動改變。而 React中沒有做這樣的處理,在默認情況下,表單元素都是 非受控組件。給表單元素綁定一個狀態後,往往需要手動書寫 onChange方法來將其改寫爲 受控組件,在表單元素非常多的情況下這些重複操作是非常痛苦的。

我們可以藉助高階組件來實現一個簡單的雙向綁定,代碼略長,可以結合下面的思維導圖進行理解。

首先我們自定義一個 Form組件,該組件用於包裹所有需要包裹的表單組件,通過contex向子組件暴露兩個屬性:

  • model:當前 Form管控的所有數據,由表單 name和 value組成,如 {name:'ConardLi',pwd:'123'}。 model可由外部傳入,也可自行管控。

  • changeModel:改變 model中某個 name的值。

class Form extends Component {

static childContextTypes = {

model: PropTypes.object,

changeModel: PropTypes.func

}

constructor(props, context) {

super(props, context);

this.state = {

model: props.model || {}

};

}

componentWillReceiveProps(nextProps) {

if (nextProps.model) {

this.setState({

model: nextProps.model

})

}

}

changeModel = (name, value) => {

this.setState({

model: { ...this.state.model, [name]: value }

})

}

getChildContext() {

return {

changeModel: this.changeModel,

model: this.props.model || this.state.model

};

}

onSubmit = () => {

console.log(this.state.model);

}

render() {

return <div>

{this.props.children}

<button onClick={this.onSubmit}>提交</button>

</div>

}

}

下面定義用於雙向綁定的 HOC,其代理了表單的 onChange屬性和 value屬性:

  • 發生 onChange事件時調用上層 Form的 changeModel方法來改變 context中的 model

  • 在渲染時將 value改爲從 context中取出的值。

  • function proxyHoc(WrappedComponent) {
    
    return class extends Component {
    
    static contextTypes = {
    
    model: PropTypes.object,
    
    changeModel: PropTypes.func
    
    }
    
    
    
    onChange = (event) => {
    
    const { changeModel } = this.context;
    
    const { onChange } = this.props;
    
    const { v_model } = this.props;
    
    changeModel(v_model, event.target.value);
    
    if(typeof onChange === 'function'){onChange(event);}
    
    }
    
    
    
    render() {
    
    const { model } = this.context;
    
    const { v_model } = this.props;
    
    return <WrappedComponent
    
    {...this.props}
    
    value={model[v_model]}
    
    onChange={this.onChange}
    
    />;
    
    }
    
    }
    
    }
    
    @proxyHoc
    
    class Input extends Component {
    
    render() {
    
    return <input {...this.props}></input>
    
    }
    
    }

     

上面的代碼只是簡略的一部分,除了 input,我們還可以將 HOC應用在 select等其他表單組件,甚至還可以將上面的 HOC兼容到 span、table等展示組件,這樣做可以大大簡化代碼,讓我們省去了很多狀態管理的工作,使用如下:

export default class extends Component {

render() {

return (

<Form >

<Input v_model="name"></Input>

<Input v_model="pwd"></Input>

</Form>

)

}

}

表單校驗

基於上面的雙向綁定的例子,我們再來一個表單驗證器,表單驗證器可以包含驗證函數以及提示信息,當驗證不通過時,展示錯誤信息:


 
function validateHoc(WrappedComponent) {

return class extends Component {

constructor(props) {

super(props);

this.state = { error: '' }

}

onChange = (event) => {

const { validator } = this.props;

if (validator && typeof validator.func === 'function') {

if (!validator.func(event.target.value)) {

this.setState({ error: validator.msg })

} else {

this.setState({ error: '' })

}

}

}

render() {

return <div>

<WrappedComponent onChange={this.onChange} {...this.props} />

<div>{this.state.error || ''}</div>

</div>

}

}

}
  1.  

const validatorName = {

func: (val) => val && !isNaN(val),

msg: '請輸入數字'

}

const validatorPwd = {

func: (val) => val && val.length > 6,

msg: '密碼必須大於6位'

}

<HOCInput validator={validatorName} v_model="name"></HOCInput>

<HOCInput validator={validatorPwd} v_model="pwd"></HOCInput>

當然,還可以在 Form提交的時候判斷所有驗證器是否通過,驗證器也可以設置爲數組等等,由於文章篇幅原因,代碼被簡化了很多,有興趣的同學可以自己實現。

Redux的connect

redux中的 connect,其實就是一個 HOC,下面就是一個簡化版的 connect實現:


 
  1. export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {

  2. class Connect extends Component {

  3. static contextTypes = {

  4. store: PropTypes.object

  5. }

  6.  

  7. constructor () {

  8. super()

  9. this.state = {

  10. allProps: {}

  11. }

  12. }

  13.  

  14. componentWillMount () {

  15. const { store } = this.context

  16. this._updateProps()

  17. store.subscribe(() => this._updateProps())

  18. }

  19.  

  20. _updateProps () {

  21. const { store } = this.context

  22. let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {}

  23. let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {}

  24. this.setState({

  25. allProps: {

  26. ...stateProps,

  27. ...dispatchProps,

  28. ...this.props

  29. }

  30. })

  31. }

  32.  

  33. render () {

  34. return <WrappedComponent {...this.state.allProps} />

  35. }

  36. }

  37. return Connect

  38. }

代碼非常清晰, connect函數其實就做了一件事,將 mapStateToPropsmapDispatchToProps分別解構後傳給原組件,這樣我們在原組件內就可以直接用 props獲取 state以及 dispatch函數了。

使用HOC的注意事項

告誡—靜態屬性拷貝

當我們應用 HOC去增強另一個組件時,我們實際使用的組件已經不是原組件了,所以我們拿不到原組件的任何靜態屬性,我們可以在 HOC的結尾手動拷貝他們:


 
  1. function proxyHOC(WrappedComponent) {

  2. class HOCComponent extends Component {

  3. render() {

  4. return <WrappedComponent {...this.props} />;

  5. }

  6. }

  7. HOCComponent.staticMethod = WrappedComponent.staticMethod;

  8. // ...

  9. return HOCComponent;

  10. }

如果原組件有非常多的靜態屬性,這個過程是非常痛苦的,而且你需要去了解需要增強的所有組件的靜態屬性是什麼,我們可以使用 hoist-non-react-statics來幫助我們解決這個問題,它可以自動幫我們拷貝所有非 React的靜態方法,使用方式如下:


 
  1. import hoistNonReactStatic from 'hoist-non-react-statics';

  2. function proxyHOC(WrappedComponent) {

  3. class HOCComponent extends Component {

  4. render() {

  5. return <WrappedComponent {...this.props} />;

  6. }

  7. }

  8. hoistNonReactStatic(HOCComponent,WrappedComponent);

  9. return HOCComponent;

  10. }

告誡—傳遞refs

使用高階組件後,獲取到的 ref實際上是最外層的容器組件,而非原組件,但是很多情況下我們需要用到原組件的 ref

高階組件並不能像透傳 props那樣將 refs透傳,我們可以用一個回調函數來完成ref的傳遞:


 
  1. function hoc(WrappedComponent) {

  2. return class extends Component {

  3. getWrappedRef = () => this.wrappedRef;

  4. render() {

  5. return <WrappedComponent ref={ref => { this.wrappedRef = ref }} {...this.props} />;

  6. }

  7. }

  8. }

  9. @hoc

  10. class Input extends Component {

  11. render() { return <input></input> }

  12. }

  13. class App extends Component {

  14. render() {

  15. return (

  16. <Input ref={ref => { this.inpitRef = ref.getWrappedRef() }} ></Input>

  17. );

  18. }

  19. }

React16.3版本提供了一個 forwardRef API來幫助我們進行 refs傳遞,這樣我們在高階組件上獲取的 ref就是原組件的 ref了,而不需要再手動傳遞,如果你的 React版本大於 16.3,可以使用下面的方式:


 
  1. function hoc(WrappedComponent) {

  2. class HOC extends Component {

  3. render() {

  4. const { forwardedRef, ...props } = this.props;

  5. return <WrappedComponent ref={forwardedRef} {...props} />;

  6. }

  7. }

  8. return React.forwardRef((props, ref) => {

  9. return <HOC forwardedRef={ref} {...props} />;

  10. });

  11. }

告誡—不要在render方法內使用高階組件

React Diff算法的原則是:

  • 使用組件標識確定是卸載還是更新組件

  • 如果組件的和前一次渲染時標識是相同的,遞歸更新子組件

  • 如果標識不同卸載組件重新掛載新組件

每次調用高階組件生成的都是是一個全新的組件,組件的唯一標識響應的也會改變,如果在 render方法調用了高階組件,這會導致組件每次都會被卸載後重新掛載。

約定-不要改變原始組件

官方文檔對高階組件的說明:

高階組件就是一個沒有副作用的純函數。

我們再來看看純函數的定義:

如果函數的調用參數相同,則永遠返回相同的結果。它不依賴於程序執行期間函數外部任何狀態或數據的變化,必須只依賴於其輸入參數。 該函數不會產生任何可觀察的副作用,例如網絡請求,輸入和輸出設備或數據突變。

如果我們在高階組件對原組件進行了修改,例如下面的代碼:


 
  1. InputComponent.prototype.componentWillReceiveProps = function(nextProps) { ... }

這樣就破壞了我們對高階組件的約定,同時也改變了使用高階組件的初衷:我們使用高階組件是爲了 增強而非 改變原組件。

約定-透傳不相關的props

使用高階組件,我們可以代理所有的 props,但往往特定的 HOC只會用到其中的一個或幾個 props。我們需要把其他不相關的 props透傳給原組件,如下面的代碼:


 
  1. function visible(WrappedComponent) {

  2. return class extends Component {

  3. render() {

  4. const { visible, ...props } = this.props;

  5. if (visible === false) return null;

  6. return <WrappedComponent {...props} />;

  7. }

  8. }

  9. }

我們只使用 visible屬性來控制組件的顯示可隱藏,把其他 props透傳下去。

約定-displayName

在使用 ReactDeveloperTools進行調試時,如果我們使用了 HOC,調試界面可能變得非常難以閱讀,如下面的代碼:


 
  1. @visible

  2. class Show extends Component {

  3. render() {

  4. return <h1>我是一個標籤</h1>

  5. }

  6. }

  7. @visible

  8. class Title extends Component {

  9. render() {

  10. return <h1>我是一個標題</h1>

  11. }

  12. }

爲了方便調試,我們可以手動爲 HOC指定一個 displayName,官方推薦使用 HOCName(WrappedComponentName)


 
  1. static displayName = `Visible(${WrappedComponent.displayName})`

這個約定幫助確保高階組件最大程度的靈活性和可重用性。

使用HOC的動機

回顧下上文提到的 Mixin 帶來的風險:

  • Mixin 可能會相互依賴,相互耦合,不利於代碼維護

  • 不同的 Mixin中的方法可能會相互衝突

  • Mixin非常多時,組件是可以感知到的,甚至還要爲其做相關處理,這樣會給代碼造成滾雪球式的複雜性

HOC的出現可以解決這些問題:

  • 高階組件就是一個沒有副作用的純函數,各個高階組件不會互相依賴耦合

  • 高階組件也有可能造成衝突,但我們可以在遵守約定的情況下避免這些行爲

  • 高階組件並不關心數據使用的方式和原因,而被包裹的組件也不關心數據來自何處。高階組件的增加不會爲原組件增加負擔

HOC的缺陷

  • HOC需要在原組件上進行包裹或者嵌套,如果大量使用 HOC,將會產生非常多的嵌套,這讓調試變得非常困難。

  • HOC可以劫持 props,在不遵守約定的情況下也可能造成衝突。

Hooks

HooksReactv16.7.0-alpha中加入的新特性。它可以讓你在 class以外使用state和其他 React特性。

使用 Hooks,你可以在將含有 state的邏輯從組件中抽象出來,這將可以讓這些邏輯容易被測試。同時, Hooks可以幫助你在不重寫組件結構的情況下複用這些邏輯。所以,它也可以作爲一種實現 狀態邏輯複用的方案。

閱讀下面的章節使用Hook的動機你可以發現,它可以同時解決 MixinHOC帶來的問題。

官方提供的Hooks

State Hook

我們要使用 class組件實現一個 計數器功能,我們可能會這樣寫:


 
  1. export default class Count extends Component {

  2. constructor(props) {

  3. super(props);

  4. this.state = { count: 0 }

  5. }

  6. render() {

  7. return (

  8. <div>

  9. <p>You clicked {this.state.count} times</p>

  10. <button onClick={() => { this.setState({ count: this.state.count + 1 }) }}>

  11. Click me

  12. </button>

  13. </div>

  14. )

  15. }

  16. }

通過 useState,我們使用函數式組件也能實現這樣的功能:


 
  1. export default function HookTest() {

  2. const [count, setCount] = useState(0);

  3. return (

  4. <div>

  5. <p>You clicked {count} times</p>

  6. <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>

  7. Click me

  8. </button>

  9. </div>

  10. );

  11. }

useState是一個鉤子,他可以爲函數式組件增加一些狀態,並且提供改變這些狀態的函數,同時它接收一個參數,這個參數作爲狀態的默認值。

Effect Hook

Effect Hook 可以讓你在函數組件中執行一些具有 side effect(副作用)的操作

參數

useEffect方法接收傳入兩個參數:

  • 1.回調函數:在第組件一次 render和之後的每次 update後運行, React保證在 DOM已經更新完成之後纔會運行回調。

  • 2.狀態依賴(數組):當配置了狀態依賴項後,只有檢測到配置的狀態變化時,纔會調用回調函數。


 
  1. useEffect(() => {

  2. // 只要組件render後就會執行

  3. });

  4. useEffect(() => {

  5. // 只有count改變時纔會執行

  6. },[count]);

回調返回值

useEffect的第一個參數可以返回一個函數,當頁面渲染了下一次更新的結果後,執行下一次 useEffect之前,會調用這個函數。這個函數常常用來對上一次調用 useEffect進行清理。


 
  1. export default function HookTest() {

  2. const [count, setCount] = useState(0);

  3. useEffect(() => {

  4. console.log('執行...', count);

  5. return () => {

  6. console.log('清理...', count);

  7. }

  8. }, [count]);

  9. return (

  10. <div>

  11. <p>You clicked {count} times</p>

  12. <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>

  13. Click me

  14. </button>

  15. </div>

  16. );

  17. }

執行上面的代碼,並點擊幾次按鈕,會得到下面的結果:

注意,如果加上瀏覽器渲染的情況,結果應該是這樣的:


 
  1. 頁面渲染...1

  2. 執行... 1

  3. 頁面渲染...2

  4. 清理... 1

  5. 執行... 2

  6. 頁面渲染...3

  7. 清理... 2

  8. 執行... 3

  9. 頁面渲染...4

  10. 清理... 3

  11. 執行... 4

那麼爲什麼在瀏覽器渲染完後,再執行清理的方法還能找到上次的 state呢?原因很簡單,我們在 useEffect中返回的是一個函數,這形成了一個閉包,這能保證我們上一次執行函數存儲的變量不被銷燬和污染。

你可以嘗試下面的代碼可能更好理解


 
  1. var flag = 1;

  2. var clean;

  3. function effect(flag) {

  4. return function () {

  5. console.log(flag);

  6. }

  7. }

  8. clean = effect(flag);

  9. flag = 2;

  10. clean();

  11. clean = effect(flag);

  12. flag = 3;

  13. clean();

  14. clean = effect(flag);

  15.  

  16. // 執行結果

  17.  

  18. effect... 1

  19. clean... 1

  20. effect... 2

  21. clean... 2

  22. effect... 3

模擬componentDidMount

componentDidMount等價於 useEffect的回調僅在頁面初始化完成後執行一次,當useEffect的第二個參數傳入一個空數組時可以實現這個效果。


 
  1. function useDidMount(callback) {

  2. useEffect(callback, []);

  3. }

官方不推薦上面這種寫法,因爲這有可能導致一些錯誤。

模擬componentWillUnmount


 
  1. function useUnMount(callback) {

  2. useEffect(() => callback, []);

  3. }

不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 並不會阻滯瀏覽器渲染頁面。這讓你的 app 看起來更加流暢。

ref Hook

使用 useRefHook,你可以輕鬆的獲取到 domref


 
  1. export default function Input() {

  2. const inputEl = useRef(null);

  3. const onButtonClick = () => {

  4. inputEl.current.focus();

  5. };

  6. return (

  7. <div>

  8. <input ref={inputEl} type="text" />

  9. <button onClick={onButtonClick}>Focus the input</button>

  10. </div>

  11. );

  12. }

注意 useRef()並不僅僅可以用來當作獲取 ref使用,使用 useRef產生的 refcurrent屬性是可變的,這意味着你可以用它來保存一個任意值。

模擬componentDidUpdate

componentDidUpdate就相當於除去第一次調用的 useEffect,我們可以藉助 useRef生成一個標識,來記錄是否爲第一次執行:


 
  1. function useDidUpdate(callback, prop) {

  2. const init = useRef(true);

  3. useEffect(() => {

  4. if (init.current) {

  5. init.current = false;

  6. } else {

  7. return callback();

  8. }

  9. }, prop);

  10. }

使用Hook的注意事項

使用範圍

  • 只能在 React函數式組件或自定義 Hook中使用 Hook

Hook的提出主要就是爲了解決 class組件的一系列問題,所以我們能在 class組件中使用它。

聲明約束

  • 不要在循環,條件或嵌套函數中調用Hook。

Hook通過數組實現的,每次 useState 都會改變下標, React需要利用調用順序來正確更新相應的狀態,如果 useState 被包裹循環或條件語句中,那每就可能會引起調用順序的錯亂,從而造成意想不到的錯誤。

我們可以安裝一個 eslint插件來幫助我們避免這些問題。


 
  1. // 安裝

  2. npm install eslint-plugin-react-hooks --save-dev

  3. // 配置

  4. {

  5. "plugins": [

  6. // ...

  7. "react-hooks"

  8. ],

  9. "rules": {

  10. // ...

  11. "react-hooks/rules-of-hooks": "error"

  12. }

  13. }

自定義Hook

像上面介紹的 HOCmixin一樣,我們同樣可以通過自定義的 Hook將組件中類似的狀態邏輯抽取出來。

自定義 Hook非常簡單,我們只需要定義一個函數,並且把相應需要的狀態和 effect封裝進去,同時, Hook之間也是可以相互引用的。使用 use開頭命名自定義 Hook,這樣可以方便 eslint進行檢查。

下面我們看幾個具體的 Hook封裝:

日誌打點

我們可以使用上面封裝的生命週期 Hook


 
  1. const useLogger = (componentName, ...params) => {

  2. useDidMount(() => {

  3. console.log(`${componentName}初始化`, ...params);

  4. });

  5. useUnMount(() => {

  6. console.log(`${componentName}卸載`, ...params);

  7. })

  8. useDidUpdate(() => {

  9. console.log(`${componentName}更新`, ...params);

  10. });

  11. };

  12.  

  13. function Page1(props){

  14. useLogger('Page1',props);

  15. return (<div>...</div>)

  16. }

修改title

根據不同的頁面名稱修改頁面 title:


 
  1. function useTitle(title) {

  2. useEffect(

  3. () => {

  4. document.title = title;

  5. return () => (document.title = "主頁");

  6. },

  7. [title]

  8. );

  9. }

  10. function Page1(props){

  11. useTitle('Page1');

  12. return (<div>...</div>)

  13. }

雙向綁定

我們將表單 onChange的邏輯抽取出來封裝成一個 Hook,這樣所有需要進行雙向綁定的表單組件都可以進行復用:


 
  1. function useBind(init) {

  2. let [value, setValue] = useState(init);

  3. let onChange = useCallback(function(event) {

  4. setValue(event.currentTarget.value);

  5. }, []);

  6. return {

  7. value,

  8. onChange

  9. };

  10. }

  11. function Page1(props){

  12. let value = useBind('');

  13. return <input {...value} />;

  14. }

當然,你可以向上面的 HOC那樣,結合 contextform來封裝一個更通用的雙向綁定,有興趣可以手動實現一下。

使用Hook的動機

減少狀態邏輯複用的風險

HookMixin在用法上有一定的相似之處,但是 Mixin引入的邏輯和狀態是可以相互覆蓋的,而多個 Hook之間互不影響,這讓我們不需要在把一部分精力放在防止避免邏輯複用的衝突上。

在不遵守約定的情況下使用 HOC也有可能帶來一定衝突,比如 props覆蓋等等,使用 Hook則可以避免這些問題。

避免地獄式嵌套

大量使用 HOC的情況下讓我們的代碼變得嵌套層級非常深,使用 HOC,我們可以實現扁平式的狀態邏輯複用,而避免了大量的組件嵌套。

讓組件更容易理解

在使用 class組件構建我們的程序時,他們各自擁有自己的狀態,業務邏輯的複雜使這些組件變得越來越龐大,各個生命週期中會調用越來越多的邏輯,越來越難以維護。使用 Hook,可以讓你更大限度的將公用邏輯抽離,將一個組件分割成更小的函數,而不是強制基於生命週期方法進行分割。

使用函數代替class

相比函數,編寫一個 class可能需要掌握更多的知識,需要注意的點也越多,比如 this指向、綁定事件等等。另外,計算機理解一個 class比理解一個函數更快。 Hooks讓你可以在 classes之外使用更多 React的新特性。

理性的選擇

實際上, Hookreact16.8.0才正式發佈 Hook穩定版本,筆者也還未在生產環境下使用,目前筆者在生產環境下使用的最多的是 HOC

React官方完全沒有把 classesReact中移除的打算, class組件和 Hook完全可以同時存在,官方也建議避免任何“大範圍重構”,畢竟這是一個非常新的版本,如果你喜歡它,可以在新的非關鍵性的代碼中使用 Hook

小結

mixin已被拋棄, HOC正當壯年, Hook初露鋒芒,前端圈就是這樣,技術迭代速度非常之快,但我們在學習這些知識之時一定要明白爲什麼要學,學了有沒有用,要不要用。不忘初心,方得始終。

文中如有錯誤,歡迎指正,謝謝閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章