溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

React深入分析從Mixin到HOC再到Hook

發(fā)布時(shí)間:2021-11-06 15:46:26 來(lái)源:億速云 閱讀:158 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要介紹“React深入分析從Mixin到HOC再到Hook”,在日常操作中,相信很多人在React深入分析從Mixin到HOC再到Hook問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”React深入分析從Mixin到HOC再到Hook”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

Mixin設(shè)計(jì)模式

React深入分析從Mixin到HOC再到Hook

Mixin(混入)是一種通過(guò)擴(kuò)展收集功能的方式,它本質(zhì)上是將一個(gè)對(duì)象的屬性拷貝到另一個(gè)對(duì)象上面去,不過(guò)你可以拷貝任意多個(gè)對(duì)象的任意個(gè)方法到一個(gè)新對(duì)象上去,這是繼承所不能實(shí)現(xiàn)的。它的出現(xiàn)主要就是為了解決代碼復(fù)用問(wèn)題。

很多開(kāi)源庫(kù)提供了Mixin的實(shí)現(xiàn),如Underscore的_.extend方法、JQuery的extend方法。

使用_.extend方法實(shí)現(xiàn)代碼復(fù)用:

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();

我們可以嘗試手動(dòng)寫一個(gè)簡(jiǎn)單的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方法將任意對(duì)象的任意方法擴(kuò)展到目標(biāo)對(duì)象上。

React中應(yīng)用Mixin

React也提供了Mixin的實(shí)現(xiàn),如果完全不同的組件有相似的功能,我們可以引入來(lái)實(shí)現(xiàn)代碼復(fù)用,當(dāng)然只有在使用createClass來(lái)創(chuàng)建React組件時(shí)才可以使用,因?yàn)樵赗eact組件的es6寫法中它已經(jīng)被廢棄掉了。

例如下面的例子,很多組件或頁(yè)面都需要記錄用戶行為,性能指標(biāo)等。如果我們?cè)诿總€(gè)組件都引入寫日志的邏輯,會(huì)產(chǎn)生大量重復(fù)代碼,通過(guò)Mixin我們可以解決這一問(wèn)題:

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帶來(lái)的危害

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

  •  Mixin 可能會(huì)相互依賴,相互耦合,不利于代碼維護(hù)

  •  不同的 Mixin 中的方法可能會(huì)相互沖突

  •  Mixin非常多時(shí),組件是可以感知到的,甚至還要為其做相關(guān)處理,這樣會(huì)給代碼造成滾雪球式的復(fù)雜性

React現(xiàn)在已經(jīng)不再推薦使用Mixin來(lái)解決代碼復(fù)用問(wèn)題,因?yàn)镸ixin帶來(lái)的危害比他產(chǎn)生的價(jià)值還要巨大,并且React全面推薦使用高階組件來(lái)替代它。另外,高階組件還能實(shí)現(xiàn)更多其他更強(qiáng)大的功能,在學(xué)習(xí)高階組件之前,我們先來(lái)看一個(gè)設(shè)計(jì)模式。

裝飾模式

React深入分析從Mixin到HOC再到Hook

裝飾者(decorator)模式能夠在不改變對(duì)象自身的基礎(chǔ)上,在程序運(yùn)行期間給對(duì)像動(dòng)態(tài)的添加職責(zé)。與繼承相比,裝飾者是一種更輕便靈活的做法。

高階組件(HOC)

React深入分析從Mixin到HOC再到Hook

高階組件可以看作React對(duì)裝飾模式的一種實(shí)現(xiàn),高階組件就是一個(gè)函數(shù),且該函數(shù)接受一個(gè)組件作為參數(shù),并返回一個(gè)新的組件。

高階組件(HOC)是React中的高級(jí)技術(shù),用來(lái)重用組件邏輯。但高階組件本身并不是React API。它只是一種模式,這種模式是由React自身的組合性質(zhì)必然產(chǎn)生的。

function visible(WrappedComponent) {    return class extends Component {      render() {        const { visible, ...props } = this.props;        if (visible === false) return null;        return <WrappedComponent {...props} />;      }    }  }

上面的代碼就是一個(gè)HOC的簡(jiǎn)單應(yīng)用,函數(shù)接收一個(gè)組件作為參數(shù),并返回一個(gè)新組件,新組建可以接收一個(gè)visible props,根據(jù)visible的值來(lái)判斷是否渲染Visible。

下面我們從以下幾方面來(lái)具體探索HOC。

React深入分析從Mixin到HOC再到Hook

HOC的實(shí)現(xiàn)方式

屬性代理

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

function proxyHOC(WrappedComponent) {    return class extends Component {      render() {        return <WrappedComponent {...this.props} />;      }    }  }

對(duì)比原生組件增強(qiáng)的項(xiàng):

  •  可操作所有傳入的props

  •  可操作組件的生命周期

  •  可操作組件的static方法

  •  獲取refs

反向繼承

返回一個(gè)組件,繼承原組件,在render中調(diào)用原組件的render。由于繼承了原組件,能通過(guò)this訪問(wèn)到原組件的生命周期、props、state、render等,相比屬性代理它能操作更多的屬性。

function inheritHOC(WrappedComponent) {    return class extends WrappedComponent {      render() {        return super.render();      }    }  }

對(duì)比原生組件增強(qiáng)的項(xiàng):

  •  可操作所有傳入的props

  •  可操作組件的生命周期

  •  可操作組件的static方法

  •  獲取refs

  •  可操作state

  •  可以渲染劫持

HOC可以實(shí)現(xiàn)什么功能

組合渲染

可使用任何其他組件和原組件進(jìn)行組合渲染,達(dá)到樣式、布局復(fù)用等效果。

通過(guò)屬性代理實(shí)現(xiàn):

function stylHOC(WrappedComponent) {    return class extends Component {      render() {        return (<div>          <div className="title">{this.props.title}</div>          <WrappedComponent {...this.props} />        </div>);      }    }  }

通過(guò)反向繼承實(shí)現(xiàn):

function styleHOC(WrappedComponent) {    return class extends WrappedComponent {      render() {        return <div>          <div className="title">{this.props.title}</div>          {super.render()}        </div>      }    }  }

條件渲染

根據(jù)特定的屬性決定原組件是否渲染。

通過(guò)屬性代理實(shí)現(xiàn):

function visibleHOC(WrappedComponent) {    return class extends Component {      render() {        if (this.props.visible === false) return null;        return <WrappedComponent {...props} />;      }    }  }

通過(guò)反向繼承實(shí)現(xiàn):

function visibleHOC(WrappedComponent) {    return class extends WrappedComponent {      render() {        if (this.props.visible === false) {          return null        } else {          return super.render()        }      }    }  }

操作props

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

通過(guò)屬性代理實(shí)現(xiàn):

function proxyHOC(WrappedComponent) {    return class extends Component {      render() {        const newProps = {          ...this.props,          user: 'ConardLi'        }        return <WrappedComponent {...newProps} />;      }    }  }

獲取refs

高階組件中可獲取原組件的ref,通過(guò)ref獲取組件實(shí)力,如下面的代碼,當(dāng)程序初始化完成后調(diào)用原組件的log方法。(不知道refs怎么用,請(qǐng)?Refs & DOM)

通過(guò)屬性代理實(shí)現(xiàn):

function refHOC(WrappedComponent) {    return class extends Component {      componentDidMount() {        this.wapperRef.log()      }      render() {        return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;      }    }  }

這里注意:調(diào)用高階組件的時(shí)候并不能獲取到原組件的真實(shí)ref,需要手動(dòng)進(jìn)行傳遞,具體請(qǐng)看傳遞refs

狀態(tài)管理

將原組件的狀態(tài)提取到HOC中進(jìn)行管理,如下面的代碼,我們將Input的value提取到HOC中進(jìn)行管理,使它變成受控組件,同時(shí)不影響它使用onChange方法進(jìn)行一些其他操作。基于這種方式,我們可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的雙向綁定,具體請(qǐng)看雙向綁定。

通過(guò)屬性代理實(shí)現(xiàn):

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

上面的例子通過(guò)屬性代理利用HOC的state對(duì)原組件進(jìn)行了一定的增強(qiáng),但并不能直接控制原組件的state,而通過(guò)反向繼承,我們可以直接操作原組件的state。但是并不推薦直接修改或添加原組件的state,因?yàn)檫@樣有可能和組件內(nèi)部的操作構(gòu)成沖突。

通過(guò)反向繼承實(shí)現(xiàn):

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>        )      }    }  }

上面的HOC在render中將props和state打印出來(lái),可以用作調(diào)試階段,當(dāng)然你可以在里面寫更多的調(diào)試代碼。想象一下,只需要在我們想要調(diào)試的組件上加上@debug就可以對(duì)該組件進(jìn)行調(diào)試,而不需要在每次調(diào)試的時(shí)候?qū)懞芏嗳哂啻a。(如果你還不知道怎么使用HOC,請(qǐng)?如何使用HOC)

渲染劫持

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

實(shí)際上,上面的組合渲染和條件渲染都是渲染劫持的一種,通過(guò)反向繼承,不僅可以實(shí)現(xiàn)以上兩點(diǎn),還可直接增強(qiáng)由原組件render函數(shù)產(chǎn)生的React元素。

通過(guò)反向繼承實(shí)現(xiàn):

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;      }    }  }

注意上面的說(shuō)明我用的是增強(qiáng)而不是更改。render函數(shù)內(nèi)實(shí)際上是調(diào)用React.creatElement產(chǎn)生的React元素:

React深入分析從Mixin到HOC再到Hook

雖然我們能拿到它,但是我們不能直接修改它里面的屬性,我們通過(guò)getOwnPropertyDescriptors函數(shù)來(lái)打印下它的配置項(xiàng):

React深入分析從Mixin到HOC再到Hook

可以發(fā)現(xiàn),所有的writable屬性均被配置為了false,即所有屬性是不可變的。(對(duì)這些配置項(xiàng)有疑問(wèn),請(qǐng)?defineProperty)

不能直接修改,我們可以借助cloneElement方法來(lái)在原組件的基礎(chǔ)上增強(qiáng)一個(gè)新組件:

React.cloneElement()克隆并返回一個(gè)新的React元素,使用 element 作為起點(diǎn)。生成的元素將會(huì)擁有原始元素props與新props的淺合并。新的子級(jí)會(huì)替換現(xiàn)有的子級(jí)。來(lái)自原始元素的 key 和 ref 將會(huì)保留。

React.cloneElement() 幾乎相當(dāng)于:

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

如何使用HOC

上面的示例代碼都寫的是如何聲明一個(gè)HOC,HOC實(shí)際上是一個(gè)函數(shù),所以我們將要增強(qiáng)的組件作為參數(shù)調(diào)用HOC函數(shù),得到增強(qiáng)后的組件。

class myComponent extends Component {    render() {      return (<span>原組件</span>)    }  }  export default inheritHOC(myComponent);

compose

在實(shí)際應(yīng)用中,一個(gè)組件可能被多個(gè)HOC增強(qiáng),我們使用的是被所有的HOC增強(qiáng)后的組件,借用一張裝飾模式的圖來(lái)說(shuō)明,可能更容易理解:

React深入分析從Mixin到HOC再到Hook

假設(shè)現(xiàn)在我們有l(wèi)ogger,visible,style等多個(gè)HOC,現(xiàn)在要同時(shí)增強(qiáng)一個(gè)Input組件:

logger(visible(style(Input)))

這種代碼非常的難以閱讀,我們可以手動(dòng)封裝一個(gè)簡(jiǎn)單的函數(shù)組合工具,將寫法改寫如下:

const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));  compose(logger,visible,style)(Input);

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

很多第三方庫(kù)都提供了類似compose的函數(shù),例如lodash.flowRight,Redux提供的combineReducers函數(shù)等。

Decorators

我們還可以借助ES7為我們提供的Decorators來(lái)讓我們的寫法變的更加優(yōu)雅:

@logger  @visible  @style  class Input extends Component {    // ...  }

Decorators是ES7的一個(gè)提案,還沒(méi)有被標(biāo)準(zhǔn)化,但目前Babel轉(zhuǎn)碼器已經(jīng)支持,我們需要提前配置babel-plugin-transform-decorators-legacy:

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

還可以結(jié)合上面的compose函數(shù)使用:

const hoc = compose(logger, visible, style);  @hoc  class Input extends Component {    // ...  }

HOC的實(shí)際應(yīng)用

下面是一些我在生產(chǎn)環(huán)境中實(shí)際對(duì)HOC的實(shí)際應(yīng)用場(chǎng)景,由于文章篇幅原因,代碼經(jīng)過(guò)很多簡(jiǎn)化,如有問(wèn)題歡迎在評(píng)論區(qū)指出:

日志打點(diǎn)

實(shí)際上這屬于一類最常見(jiàn)的應(yīng)用,多個(gè)組件擁有類似的邏輯,我們要對(duì)重復(fù)的邏輯進(jìn)行復(fù)用,官方文檔中CommentList的示例也是解決了代碼復(fù)用問(wèn)題,寫的很詳細(xì),有興趣可以?使用高階組件(HOC)解決橫切關(guān)注點(diǎn)。

某些頁(yè)面需要記錄用戶行為,性能指標(biāo)等等,通過(guò)高階組件做這些事情可以省去很多重復(fù)代碼。

function logHoc(WrappedComponent) {    return class extends Component {      componentWillMount() {        this.start = Date.now();      }      componentDidMount() {        this.end = Date.now();        console.log(`${WrappedComponent.dispalyName} 渲染時(shí)間:${this.end - this.start} ms`);        console.log(`${user}進(jìn)入${WrappedComponent.dispalyName}`);      }      componentWillUnmount() {        console.log(`${user}退出${WrappedComponent.dispalyName}`);      }      render() {        return <WrappedComponent {...this.props} />      }    }  }

可用、權(quán)限控制

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是我們?cè)谶M(jìn)入程序時(shí)向后端請(qǐng)求的所有權(quán)限列表,當(dāng)組件所需要的權(quán)限不列表中,或者設(shè)置的visible是false,我們將其顯示為傳入的組件樣式,或者null。我們可以將任何需要進(jìn)行權(quán)限校驗(yàn)的組件應(yīng)用HOC:

@auth  class Input extends Component {  ...  }  @auth  class Button extends Component {  ...  }  <Button auth="user/addUser">添加用戶</Button>  <Input auth="user/search" visible={false} >添加用戶</Input>

雙向綁定

在vue中,綁定一個(gè)變量后可實(shí)現(xiàn)雙向數(shù)據(jù)綁定,即表單中的值改變后綁定的變量也會(huì)自動(dòng)改變。而React中沒(méi)有做這樣的處理,在默認(rèn)情況下,表單元素都是非受控組件。給表單元素綁定一個(gè)狀態(tài)后,往往需要手動(dòng)書寫onChange方法來(lái)將其改寫為受控組件,在表單元素非常多的情況下這些重復(fù)操作是非常痛苦的。

我們可以借助高階組件來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的雙向綁定,代碼略長(zhǎng),可以結(jié)合下面的思維導(dǎo)圖進(jìn)行理解。

React深入分析從Mixin到HOC再到Hook

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

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

  •  changeModel:改變model中某個(gè)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屬性:

  •  發(fā)生onChange事件時(shí)調(diào)用上層Form的changeModel方法來(lái)改變context中的model。

  •  在渲染時(shí)將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>    }  }

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

export default class extends Component {    render() {      return (        <Form >          <Input v_model="name"></Input>          <Input v_model="pwd"></Input>        </Form>      )    }  }

表單校驗(yàn)

基于上面的雙向綁定的例子,我們?cè)賮?lái)一個(gè)表單驗(yàn)證器,表單驗(yàn)證器可以包含驗(yàn)證函數(shù)以及提示信息,當(dāng)驗(yàn)證不通過(guò)時(shí),展示錯(cuò)誤信息:

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>      }    }  }
const validatorName = {    func: (val) => val && !isNaN(val),    msg: '請(qǐng)輸入數(shù)字'  }  const validatorPwd = {    func: (val) => val && val.length > 6,    msg: '密碼必須大于6位'  }  <HOCInput validator={validatorName} v_model="name"></HOCInput>  <HOCInput validator={validatorPwd} v_model="pwd"></HOCInput>

當(dāng)然,還可以在Form提交的時(shí)候判斷所有驗(yàn)證器是否通過(guò),驗(yàn)證器也可以設(shè)置為數(shù)組等等,由于文章篇幅原因,代碼被簡(jiǎn)化了很多,有興趣的同學(xué)可以自己實(shí)現(xiàn)。

Redux的connect

React深入分析從Mixin到HOC再到Hook

redux中的connect,其實(shí)就是一個(gè)HOC,下面就是一個(gè)簡(jiǎn)化版的connect實(shí)現(xiàn):

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {    class Connect extends Component {      static contextTypes = {        store: PropTypes.object      }      constructor () {        super()        this.state = {          allProps: {}        }      }      componentWillMount () {        const { store } = this.context        this._updateProps()        store.subscribe(() => this._updateProps())      }      _updateProps () {        const { store } = this.context        let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {}         let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {}         this.setState({          allProps: {            ...stateProps,            ...dispatchProps,            ...this.props          }        })      }      render () {        return <WrappedComponent {...this.state.allProps} />      }    }    return Connect  }

代碼非常清晰,connect函數(shù)其實(shí)就做了一件事,將mapStateToProps和mapDispatchToProps分別解構(gòu)后傳給原組件,這樣我們?cè)谠M件內(nèi)就可以直接用props獲取state以及dispatch函數(shù)了。

使用HOC的注意事項(xiàng)

告誡&mdash;靜態(tài)屬性拷貝

當(dāng)我們應(yīng)用HOC去增強(qiáng)另一個(gè)組件時(shí),我們實(shí)際使用的組件已經(jīng)不是原組件了,所以我們拿不到原組件的任何靜態(tài)屬性,我們可以在HOC的結(jié)尾手動(dòng)拷貝他們:

function proxyHOC(WrappedComponent) {    class HOCComponent extends Component {      render() {        return <WrappedComponent {...this.props} />;      }    }    HOCComponent.staticMethod = WrappedComponent.staticMethod;    // ...     return HOCComponent;  }

如果原組件有非常多的靜態(tài)屬性,這個(gè)過(guò)程是非常痛苦的,而且你需要去了解需要增強(qiáng)的所有組件的靜態(tài)屬性是什么,我們可以使用hoist-non-react-statics來(lái)幫助我們解決這個(gè)問(wèn)題,它可以自動(dòng)幫我們拷貝所有非React的靜態(tài)方法,使用方式如下:

import hoistNonReactStatic from 'hoist-non-react-statics';  function proxyHOC(WrappedComponent) {    class HOCComponent extends Component {      render() {        return <WrappedComponent {...this.props} />;      }    }    hoistNonReactStatic(HOCComponent,WrappedComponent);    return HOCComponent;  }

告誡&mdash;傳遞refs

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

高階組件并不能像透?jìng)鱬rops那樣將refs透?jìng)?,我們可以用一個(gè)回調(diào)函數(shù)來(lái)完成ref的傳遞:

function hoc(WrappedComponent) {    return class extends Component {      getWrappedRef = () => this.wrappedRef;      render() {        return <WrappedComponent ref={ref => { this.wrappedRef = ref }} {...this.props} />;      }    }  }  @hoc  class Input extends Component {    render() { return <input></input> }  }  class App extends Component {    render() {      return (        <Input ref={ref => { this.inpitRef = ref.getWrappedRef() }} ></Input>      );    }  }

React 16.3版本提供了一個(gè)forwardRef API來(lái)幫助我們進(jìn)行refs傳遞,這樣我們?cè)诟唠A組件上獲取的ref就是原組件的ref了,而不需要再手動(dòng)傳遞,如果你的React版本大于16.3,可以使用下面的方式:

function hoc(WrappedComponent) {    class HOC extends Component {      render() {        const { forwardedRef, ...props } = this.props;        return <WrappedComponent ref={forwardedRef} {...props} />;      }    }    return React.forwardRef((props, ref) => {      return <HOC forwardedRef={ref} {...props} />;    });  }

告誡&mdash;不要在render方法內(nèi)使用高階組件

React Diff算法的原則是:

  •  使用組件標(biāo)識(shí)確定是卸載還是更新組件

  •  如果組件的和前一次渲染時(shí)標(biāo)識(shí)是相同的,遞歸更新子組件

  •  如果標(biāo)識(shí)不同卸載組件重新掛載新組件

每次調(diào)用高階組件生成的都是是一個(gè)全新的組件,組件的唯一標(biāo)識(shí)響應(yīng)的也會(huì)改變,如果在render方法調(diào)用了高階組件,這會(huì)導(dǎo)致組件每次都會(huì)被卸載后重新掛載。

約定-不要改變?cè)冀M件

官方文檔對(duì)高階組件的說(shuō)明:

高階組件就是一個(gè)沒(méi)有副作用的純函數(shù)。

我們?cè)賮?lái)看看純函數(shù)的定義:

如果函數(shù)的調(diào)用參數(shù)相同,則永遠(yuǎn)返回相同的結(jié)果。它不依賴于程序執(zhí)行期間函數(shù)外部任何狀態(tài)或數(shù)據(jù)的變化,必須只依賴于其輸入?yún)?shù)。

該函數(shù)不會(huì)產(chǎn)生任何可觀察的副作用,例如網(wǎng)絡(luò)請(qǐng)求,輸入和輸出設(shè)備或數(shù)據(jù)突變。

如果我們?cè)诟唠A組件對(duì)原組件進(jìn)行了修改,例如下面的代碼:

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

這樣就破壞了我們對(duì)高階組件的約定,同時(shí)也改變了使用高階組件的初衷:我們使用高階組件是為了增強(qiáng)而非改變?cè)M件。

約定-透?jìng)鞑幌嚓P(guān)的props

使用高階組件,我們可以代理所有的props,但往往特定的HOC只會(huì)用到其中的一個(gè)或幾個(gè)props。我們需要把其他不相關(guān)的props透?jìng)鹘o原組件,如下面的代碼:

function visible(WrappedComponent) {    return class extends Component {      render() {        const { visible, ...props } = this.props;        if (visible === false) return null;        return <WrappedComponent {...props} />;      }    }  }

我們只使用visible屬性來(lái)控制組件的顯示可隱藏,把其他props透?jìng)飨氯ァ?/p>

約定-displayName

在使用React Developer Tools進(jìn)行調(diào)試時(shí),如果我們使用了HOC,調(diào)試界面可能變得非常難以閱讀,如下面的代碼:

@visible  class Show extends Component {    render() {      return <h2>我是一個(gè)標(biāo)簽</h2>    }  }  @visible  class Title extends Component {    render() {      return <h2>我是一個(gè)標(biāo)題</h2>    }  }

React深入分析從Mixin到HOC再到Hook

為了方便調(diào)試,我們可以手動(dòng)為HOC指定一個(gè)displayName,官方推薦使用HOCName(WrappedComponentName):

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

React深入分析從Mixin到HOC再到Hook

這個(gè)約定幫助確保高階組件***程度的靈活性和可重用性。

使用HOC的動(dòng)機(jī)

回顧下上文提到的 Mixin 帶來(lái)的風(fēng)險(xiǎn):

  •  Mixin 可能會(huì)相互依賴,相互耦合,不利于代碼維護(hù)

  •  不同的 Mixin 中的方法可能會(huì)相互沖突

  •  Mixin非常多時(shí),組件是可以感知到的,甚至還要為其做相關(guān)處理,這樣會(huì)給代碼造成滾雪球式的復(fù)雜性

React深入分析從Mixin到HOC再到Hook

而HOC的出現(xiàn)可以解決這些問(wèn)題:

  •  高階組件就是一個(gè)沒(méi)有副作用的純函數(shù),各個(gè)高階組件不會(huì)互相依賴耦合

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

  •  高階組件并不關(guān)心數(shù)據(jù)使用的方式和原因,而被包裹的組件也不關(guān)心數(shù)據(jù)來(lái)自何處。高階組件的增加不會(huì)為原組件增加負(fù)擔(dān)

HOC的缺陷

  •  HOC需要在原組件上進(jìn)行包裹或者嵌套,如果大量使用HOC,將會(huì)產(chǎn)生非常多的嵌套,這讓調(diào)試變得非常困難。

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

Hooks

React深入分析從Mixin到HOC再到Hook

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

使用Hooks,你可以在將含有state的邏輯從組件中抽象出來(lái),這將可以讓這些邏輯容易被測(cè)試。同時(shí),Hooks可以幫助你在不重寫組件結(jié)構(gòu)的情況下復(fù)用這些邏輯。所以,它也可以作為一種實(shí)現(xiàn)狀態(tài)邏輯復(fù)用的方案。

閱讀下面的章節(jié)使用Hook的動(dòng)機(jī)你可以發(fā)現(xiàn),它可以同時(shí)解決Mixin和HOC帶來(lái)的問(wèn)題。

官方提供的Hooks

State Hook

我們要使用class組件實(shí)現(xiàn)一個(gè)計(jì)數(shù)器功能,我們可能會(huì)這樣寫:

export default class Count extends Component {    constructor(props) {      super(props);      this.state = { count: 0 }    }    render() {      return (        <div>          <p>You clicked {this.state.count} times</p>          <button onClick={() => { this.setState({ count: this.state.count + 1 }) }}>            Click me          </button>        </div>      )    }  }

通過(guò)useState,我們使用函數(shù)式組件也能實(shí)現(xiàn)這樣的功能:

export default function HookTest() {    const [count, setCount] = useState(0);    return (      <div>        <p>You clicked {count} times</p>        <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>          Click me          </button>      </div>    );  }

useState是一個(gè)鉤子,他可以為函數(shù)式組件增加一些狀態(tài),并且提供改變這些狀態(tài)的函數(shù),同時(shí)它接收一個(gè)參數(shù),這個(gè)參數(shù)作為狀態(tài)的默認(rèn)值。

Effect Hook

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

參數(shù)

useEffect方法接收傳入兩個(gè)參數(shù):

  •  1.回調(diào)函數(shù):在第組件一次render和之后的每次update后運(yùn)行,React保證在DOM已經(jīng)更新完成之后才會(huì)運(yùn)行回調(diào)。

  •  2.狀態(tài)依賴(數(shù)組):當(dāng)配置了狀態(tài)依賴項(xiàng)后,只有檢測(cè)到配置的狀態(tài)變化時(shí),才會(huì)調(diào)用回調(diào)函數(shù)。 

useEffect(() => {     // 只要組件render后就會(huì)執(zhí)行   });   useEffect(() => {     // 只有count改變時(shí)才會(huì)執(zhí)行   },[count]);

回調(diào)返回值

useEffect的***個(gè)參數(shù)可以返回一個(gè)函數(shù),當(dāng)頁(yè)面渲染了下一次更新的結(jié)果后,執(zhí)行下一次useEffect之前,會(huì)調(diào)用這個(gè)函數(shù)。這個(gè)函數(shù)常常用來(lái)對(duì)上一次調(diào)用useEffect進(jìn)行清理。

export default function HookTest() {    const [count, setCount] = useState(0);    useEffect(() => {      console.log('執(zhí)行...', count);      return () => {        console.log('清理...', count);      }    }, [count]);    return (      <div>        <p>You clicked {count} times</p>        <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>          Click me          </button>      </div>    );  }

執(zhí)行上面的代碼,并點(diǎn)擊幾次按鈕,會(huì)得到下面的結(jié)果:

React深入分析從Mixin到HOC再到Hook

注意,如果加上瀏覽器渲染的情況,結(jié)果應(yīng)該是這樣的:

頁(yè)面渲染...1   執(zhí)行... 1   頁(yè)面渲染...2   清理... 1   執(zhí)行... 2   頁(yè)面渲染...3   清理... 2   執(zhí)行... 3   頁(yè)面渲染...4   清理... 3   執(zhí)行... 4

那么為什么在瀏覽器渲染完后,再執(zhí)行清理的方法還能找到上次的state呢?原因很簡(jiǎn)單,我們?cè)趗seEffect中返回的是一個(gè)函數(shù),這形成了一個(gè)閉包,這能保證我們上一次執(zhí)行函數(shù)存儲(chǔ)的變量不被銷毀和污染。

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

var flag = 1;      var clean;      function effect(flag) {        return function () {          console.log(flag);        }      }      clean = effect(flag);      flag = 2;      clean();      clean = effect(flag);      flag = 3;      clean();      clean = effect(flag);      // 執(zhí)行結(jié)果      effect... 1      clean... 1      effect... 2      clean... 2      effect... 3

模擬componentDidMount

componentDidMount等價(jià)于useEffect的回調(diào)僅在頁(yè)面初始化完成后執(zhí)行一次,當(dāng)useEffect的第二個(gè)參數(shù)傳入一個(gè)空數(shù)組時(shí)可以實(shí)現(xiàn)這個(gè)效果。

function useDidMount(callback) {    useEffect(callback, []);  }

官方不推薦上面這種寫法,因?yàn)檫@有可能導(dǎo)致一些錯(cuò)誤。

模擬componentWillUnmount

function useUnMount(callback) {    useEffect(() => callback, []);  }

不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不會(huì)阻滯瀏覽器渲染頁(yè)面。這讓你的 app 看起來(lái)更加流暢。

ref Hook

使用useRef Hook,你可以輕松的獲取到dom的ref。

export default function Input() {    const inputEl = useRef(null);    const onButtonClick = () => {      inputEl.current.focus();    };    return (      <div>        <input ref={inputEl} type="text" />        <button onClick={onButtonClick}>Focus the input</button>      </div>    );  }

注意useRef()并不僅僅可以用來(lái)當(dāng)作獲取ref使用,使用useRef產(chǎn)生的ref的current屬性是可變的,這意味著你可以用它來(lái)保存一個(gè)任意值。

模擬componentDidUpdate

componentDidUpdate就相當(dāng)于除去***次調(diào)用的useEffect,我們可以借助useRef生成一個(gè)標(biāo)識(shí),來(lái)記錄是否為***次執(zhí)行:

function useDidUpdate(callback, prop) {    const init = useRef(true);    useEffect(() => {      if (init.current) {        init.current = false;      } else {        return callback();      }    }, prop);  }

使用Hook的注意事項(xiàng)

使用范圍

  •  只能在React函數(shù)式組件或自定義Hook中使用Hook。

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

聲明約束

  •  不要在循環(huán),條件或嵌套函數(shù)中調(diào)用Hook。

Hook通過(guò)數(shù)組實(shí)現(xiàn)的,每次 useState 都會(huì)改變下標(biāo),React需要利用調(diào)用順序來(lái)正確更新相應(yīng)的狀態(tài),如果 useState 被包裹循環(huán)或條件語(yǔ)句中,那每就可能會(huì)引起調(diào)用順序的錯(cuò)亂,從而造成意想不到的錯(cuò)誤。

我們可以安裝一個(gè)eslint插件來(lái)幫助我們避免這些問(wèn)題。

// 安裝  npm install eslint-plugin-react-hooks --save-dev  // 配置  {    "plugins": [      // ...      "react-hooks"    ],    "rules": {      // ...      "react-hooks/rules-of-hooks": "error"    }  }

自定義Hook

像上面介紹的HOC和mixin一樣,我們同樣可以通過(guò)自定義的Hook將組件中類似的狀態(tài)邏輯抽取出來(lái)。

自定義Hook非常簡(jiǎn)單,我們只需要定義一個(gè)函數(shù),并且把相應(yīng)需要的狀態(tài)和effect封裝進(jìn)去,同時(shí),Hook之間也是可以相互引用的。使用use開(kāi)頭命名自定義Hook,這樣可以方便eslint進(jìn)行檢查。

下面我們看幾個(gè)具體的Hook封裝:

日志打點(diǎn)

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

const useLogger = (componentName, ...params) => {    useDidMount(() => {      console.log(`${componentName}初始化`, ...params);    });    useUnMount(() => {      console.log(`${componentName}卸載`, ...params);    })    useDidUpdate(() => {      console.log(`${componentName}更新`, ...params);    });  };  function Page1(props){    useLogger('Page1',props);    return (<div>...</div>)  }

修改title

根據(jù)不同的頁(yè)面名稱修改頁(yè)面title:

function useTitle(title) {    useEffect(      () => {        document.title = title;        return () => (document.title = "主頁(yè)");      },      [title]    );  }  function Page1(props){    useTitle('Page1');    return (<div>...</div>)  }

雙向綁定

我們將表單onChange的邏輯抽取出來(lái)封裝成一個(gè)Hook,這樣所有需要進(jìn)行雙向綁定的表單組件都可以進(jìn)行復(fù)用:

function useBind(init) {    let [value, setValue] = useState(init);    let onChange = useCallback(function(event) {      setValue(event.currentTarget.value);    }, []);    return {      value,      onChange    };  }  function Page1(props){    let value = useBind('');    return <input {...value} />;  }

當(dāng)然,你可以向上面的HOC那樣,結(jié)合context和form來(lái)封裝一個(gè)更通用的雙向綁定,有興趣可以手動(dòng)實(shí)現(xiàn)一下。

使用Hook的動(dòng)機(jī)

減少狀態(tài)邏輯復(fù)用的風(fēng)險(xiǎn)

Hook和Mixin在用法上有一定的相似之處,但是Mixin引入的邏輯和狀態(tài)是可以相互覆蓋的,而多個(gè)Hook之間互不影響,這讓我們不需要在把一部分精力放在防止避免邏輯復(fù)用的沖突上。

在不遵守約定的情況下使用HOC也有可能帶來(lái)一定沖突,比如props覆蓋等等,使用Hook則可以避免這些問(wèn)題。

避免地獄式嵌套

大量使用HOC的情況下讓我們的代碼變得嵌套層級(jí)非常深,使用HOC,我們可以實(shí)現(xiàn)扁平式的狀態(tài)邏輯復(fù)用,而避免了大量的組件嵌套。

讓組件更容易理解

在使用class組件構(gòu)建我們的程序時(shí),他們各自擁有自己的狀態(tài),業(yè)務(wù)邏輯的復(fù)雜使這些組件變得越來(lái)越龐大,各個(gè)生命周期中會(huì)調(diào)用越來(lái)越多的邏輯,越來(lái)越難以維護(hù)。使用Hook,可以讓你更大限度的將公用邏輯抽離,將一個(gè)組件分割成更小的函數(shù),而不是強(qiáng)制基于生命周期方法進(jìn)行分割。

使用函數(shù)代替class

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

理性的選擇

實(shí)際上,Hook在react 16.8.0才正式發(fā)布Hook穩(wěn)定版本,筆者也還未在生產(chǎn)環(huán)境下使用,目前筆者在生產(chǎn)環(huán)境下使用的最多的是`HOC`。

React官方完全沒(méi)有把classes從React中移除的打算,class組件和Hook完全可以同時(shí)存在,官方也建議避免任何“大范圍重構(gòu)”,畢竟這是一個(gè)非常新的版本,如果你喜歡它,可以在新的非關(guān)鍵性的代碼中使用Hook。

到此,關(guān)于“React深入分析從Mixin到HOC再到Hook”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI