您好,登錄后才能下訂單哦!
這篇文章主要介紹“React深入分析從Mixin到HOC再到Hook”,在日常操作中,相信很多人在React深入分析從Mixin到HOC再到Hook問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”React深入分析從Mixin到HOC再到Hook”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
Mixin設計模式
Mixin(混入)是一種通過擴展收集功能的方式,它本質上是將一個對象的屬性拷貝到另一個對象上面去,不過你可以拷貝任意多個對象的任意個方法到一個新對象上去,這是繼承所不能實現的。它的出現主要就是為了解決代碼復用問題。
很多開源庫提供了Mixin的實現,如Underscore的_.extend方法、JQuery的extend方法。
使用_.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中的高級技術,用來重用組件邏輯。但高階組件本身并不是React API。它只是一種模式,這種模式是由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中進行管理,如下面的代碼,我們將Input的value提取到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> ) } } }
上面的HOC在render中將props和state打印出來,可以用作調試階段,當然你可以在里面寫更多的調試代碼。想象一下,只需要在我們想要調試的組件上加上@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() 幾乎相當于:
<element.type {...element.props} {...props}>{children}</element.type>
如何使用HOC
上面的示例代碼都寫的是如何聲明一個HOC,HOC實際上是一個函數,所以我們將要增強的組件作為參數調用HOC函數,得到增強后的組件。
class myComponent extends Component { render() { return (<span>原組件</span>) } } export default inheritHOC(myComponent);
compose
在實際應用中,一個組件可能被多個HOC增強,我們使用的是被所有的HOC增強后的組件,借用一張裝飾模式的圖來說明,可能更容易理解:
假設現在我們有logger,visible,style等多個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.flowRight,Redux提供的combineReducers函數等。
Decorators
我們還可以借助ES7為我們提供的Decorators來讓我們的寫法變的更加優雅:
@logger @visible @style class Input extends Component { // ... }
Decorators是ES7的一個提案,還沒有被標準化,但目前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是我們在進入程序時向后端請求的所有權限列表,當組件所需要的權限不列表中,或者設置的visible是false,我們將其顯示為傳入的組件樣式,或者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> } } }
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實現:
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函數其實就做了一件事,將mapStateToProps和mapDispatchToProps分別解構后傳給原組件,這樣我們在原組件內就可以直接用props獲取state以及dispatch函數了。
使用HOC的注意事項
告誡—靜態屬性拷貝
當我們應用HOC去增強另一個組件時,我們實際使用的組件已經不是原組件了,所以我們拿不到原組件的任何靜態屬性,我們可以在HOC的結尾手動拷貝他們:
function proxyHOC(WrappedComponent) { class HOCComponent extends Component { render() { return <WrappedComponent {...this.props} />; } } HOCComponent.staticMethod = WrappedComponent.staticMethod; // ... return HOCComponent; }
如果原組件有非常多的靜態屬性,這個過程是非常痛苦的,而且你需要去了解需要增強的所有組件的靜態屬性是什么,我們可以使用hoist-non-react-statics來幫助我們解決這個問題,它可以自動幫我們拷貝所有非React的靜態方法,使用方式如下:
import hoistNonReactStatic from 'hoist-non-react-statics'; function proxyHOC(WrappedComponent) { class HOCComponent extends Component { render() { return <WrappedComponent {...this.props} />; } } hoistNonReactStatic(HOCComponent,WrappedComponent); return HOCComponent; }
告誡—傳遞refs
使用高階組件后,獲取到的ref實際上是最外層的容器組件,而非原組件,但是很多情況下我們需要用到原組件的ref。
高階組件并不能像透傳props那樣將refs透傳,我們可以用一個回調函數來完成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版本提供了一個forwardRef API來幫助我們進行refs傳遞,這樣我們在高階組件上獲取的ref就是原組件的ref了,而不需要再手動傳遞,如果你的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} />; }); }
告誡—不要在render方法內使用高階組件
React Diff算法的原則是:
使用組件標識確定是卸載還是更新組件
如果組件的和前一次渲染時標識是相同的,遞歸更新子組件
如果標識不同卸載組件重新掛載新組件
每次調用高階組件生成的都是是一個全新的組件,組件的唯一標識響應的也會改變,如果在render方法調用了高階組件,這會導致組件每次都會被卸載后重新掛載。
約定-不要改變原始組件
官方文檔對高階組件的說明:
高階組件就是一個沒有副作用的純函數。
我們再來看看純函數的定義:
如果函數的調用參數相同,則永遠返回相同的結果。它不依賴于程序執行期間函數外部任何狀態或數據的變化,必須只依賴于其輸入參數。
該函數不會產生任何可觀察的副作用,例如網絡請求,輸入和輸出設備或數據突變。
如果我們在高階組件對原組件進行了修改,例如下面的代碼:
InputComponent.prototype.componentWillReceiveProps = function(nextProps) { ... }
這樣就破壞了我們對高階組件的約定,同時也改變了使用高階組件的初衷:我們使用高階組件是為了增強而非改變原組件。
約定-透傳不相關的props
使用高階組件,我們可以代理所有的props,但往往特定的HOC只會用到其中的一個或幾個props。我們需要把其他不相關的props透傳給原組件,如下面的代碼:
function visible(WrappedComponent) { return class extends Component { render() { const { visible, ...props } = this.props; if (visible === false) return null; return <WrappedComponent {...props} />; } } }
我們只使用visible屬性來控制組件的顯示可隱藏,把其他props透傳下去。
約定-displayName
在使用React Developer Tools進行調試時,如果我們使用了HOC,調試界面可能變得非常難以閱讀,如下面的代碼:
@visible class Show extends Component { render() { return <h2>我是一個標簽</h2> } } @visible class Title extends Component { render() { return <h2>我是一個標題</h2> } }
為了方便調試,我們可以手動為HOC指定一個displayName,官方推薦使用HOCName(WrappedComponentName):
static displayName = `Visible(${WrappedComponent.displayName})`
這個約定幫助確保高階組件***程度的靈活性和可重用性。
使用HOC的動機
回顧下上文提到的 Mixin 帶來的風險:
Mixin 可能會相互依賴,相互耦合,不利于代碼維護
不同的 Mixin 中的方法可能會相互沖突
Mixin非常多時,組件是可以感知到的,甚至還要為其做相關處理,這樣會給代碼造成滾雪球式的復雜性
而HOC的出現可以解決這些問題:
高階組件就是一個沒有副作用的純函數,各個高階組件不會互相依賴耦合
高階組件也有可能造成沖突,但我們可以在遵守約定的情況下避免這些行為
高階組件并不關心數據使用的方式和原因,而被包裹的組件也不關心數據來自何處。高階組件的增加不會為原組件增加負擔
HOC的缺陷
HOC需要在原組件上進行包裹或者嵌套,如果大量使用HOC,將會產生非常多的嵌套,這讓調試變得非常困難。
HOC可以劫持props,在不遵守約定的情況下也可能造成沖突。
Hooks
Hooks是React v16.7.0-alpha中加入的新特性。它可以讓你在class以外使用state和其他React特性。
使用Hooks,你可以在將含有state的邏輯從組件中抽象出來,這將可以讓這些邏輯容易被測試。同時,Hooks可以幫助你在不重寫組件結構的情況下復用這些邏輯。所以,它也可以作為一種實現狀態邏輯復用的方案。
閱讀下面的章節使用Hook的動機你可以發現,它可以同時解決Mixin和HOC帶來的問題。
官方提供的Hooks
State Hook
我們要使用class組件實現一個計數器功能,我們可能會這樣寫:
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> ) } }
通過useState,我們使用函數式組件也能實現這樣的功能:
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是一個鉤子,他可以為函數式組件增加一些狀態,并且提供改變這些狀態的函數,同時它接收一個參數,這個參數作為狀態的默認值。
Effect Hook
Effect Hook 可以讓你在函數組件中執行一些具有 side effect(副作用)的操作
參數
useEffect方法接收傳入兩個參數:
1.回調函數:在第組件一次render和之后的每次update后運行,React保證在DOM已經更新完成之后才會運行回調。
2.狀態依賴(數組):當配置了狀態依賴項后,只有檢測到配置的狀態變化時,才會調用回調函數。
useEffect(() => { // 只要組件render后就會執行 }); useEffect(() => { // 只有count改變時才會執行 },[count]);
回調返回值
useEffect的***個參數可以返回一個函數,當頁面渲染了下一次更新的結果后,執行下一次useEffect之前,會調用這個函數。這個函數常常用來對上一次調用useEffect進行清理。
export default function HookTest() { const [count, setCount] = useState(0); useEffect(() => { console.log('執行...', 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> ); }
執行上面的代碼,并點擊幾次按鈕,會得到下面的結果:
注意,如果加上瀏覽器渲染的情況,結果應該是這樣的:
頁面渲染...1 執行... 1 頁面渲染...2 清理... 1 執行... 2 頁面渲染...3 清理... 2 執行... 3 頁面渲染...4 清理... 3 執行... 4
那么為什么在瀏覽器渲染完后,再執行清理的方法還能找到上次的state呢?原因很簡單,我們在useEffect中返回的是一個函數,這形成了一個閉包,這能保證我們上一次執行函數存儲的變量不被銷毀和污染。
你可以嘗試下面的代碼可能更好理解。
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); // 執行結果 effect... 1 clean... 1 effect... 2 clean... 2 effect... 3
模擬componentDidMount
componentDidMount等價于useEffect的回調僅在頁面初始化完成后執行一次,當useEffect的第二個參數傳入一個空數組時可以實現這個效果。
function useDidMount(callback) { useEffect(callback, []); }
官方不推薦上面這種寫法,因為這有可能導致一些錯誤。
模擬componentWillUnmount
function useUnMount(callback) { useEffect(() => callback, []); }
不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不會阻滯瀏覽器渲染頁面。這讓你的 app 看起來更加流暢。
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()并不僅僅可以用來當作獲取ref使用,使用useRef產生的ref的current屬性是可變的,這意味著你可以用它來保存一個任意值。
模擬componentDidUpdate
componentDidUpdate就相當于除去***次調用的useEffect,我們可以借助useRef生成一個標識,來記錄是否為***次執行:
function useDidUpdate(callback, prop) { const init = useRef(true); useEffect(() => { if (init.current) { init.current = false; } else { return callback(); } }, prop); }
使用Hook的注意事項
使用范圍
只能在React函數式組件或自定義Hook中使用Hook。
Hook的提出主要就是為了解決class組件的一系列問題,所以我們能在class組件中使用它。
聲明約束
不要在循環,條件或嵌套函數中調用Hook。
Hook通過數組實現的,每次 useState 都會改變下標,React需要利用調用順序來正確更新相應的狀態,如果 useState 被包裹循環或條件語句中,那每就可能會引起調用順序的錯亂,從而造成意想不到的錯誤。
我們可以安裝一個eslint插件來幫助我們避免這些問題。
// 安裝 npm install eslint-plugin-react-hooks --save-dev // 配置 { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error" } }
自定義Hook
像上面介紹的HOC和mixin一樣,我們同樣可以通過自定義的Hook將組件中類似的狀態邏輯抽取出來。
自定義Hook非常簡單,我們只需要定義一個函數,并且把相應需要的狀態和effect封裝進去,同時,Hook之間也是可以相互引用的。使用use開頭命名自定義Hook,這樣可以方便eslint進行檢查。
下面我們看幾個具體的Hook封裝:
日志打點
我們可以使用上面封裝的生命周期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
根據不同的頁面名稱修改頁面title:
function useTitle(title) { useEffect( () => { document.title = title; return () => (document.title = "主頁"); }, [title] ); } function Page1(props){ useTitle('Page1'); return (<div>...</div>) }
雙向綁定
我們將表單onChange的邏輯抽取出來封裝成一個Hook,這樣所有需要進行雙向綁定的表單組件都可以進行復用:
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} />; }
當然,你可以向上面的HOC那樣,結合context和form來封裝一個更通用的雙向綁定,有興趣可以手動實現一下。
使用Hook的動機
減少狀態邏輯復用的風險
Hook和Mixin在用法上有一定的相似之處,但是Mixin引入的邏輯和狀態是可以相互覆蓋的,而多個Hook之間互不影響,這讓我們不需要在把一部分精力放在防止避免邏輯復用的沖突上。
在不遵守約定的情況下使用HOC也有可能帶來一定沖突,比如props覆蓋等等,使用Hook則可以避免這些問題。
避免地獄式嵌套
大量使用HOC的情況下讓我們的代碼變得嵌套層級非常深,使用HOC,我們可以實現扁平式的狀態邏輯復用,而避免了大量的組件嵌套。
讓組件更容易理解
在使用class組件構建我們的程序時,他們各自擁有自己的狀態,業務邏輯的復雜使這些組件變得越來越龐大,各個生命周期中會調用越來越多的邏輯,越來越難以維護。使用Hook,可以讓你更大限度的將公用邏輯抽離,將一個組件分割成更小的函數,而不是強制基于生命周期方法進行分割。
使用函數代替class
相比函數,編寫一個class可能需要掌握更多的知識,需要注意的點也越多,比如this指向、綁定事件等等。另外,計算機理解一個class比理解一個函數更快。Hooks讓你可以在classes之外使用更多React的新特性。
理性的選擇
實際上,Hook在react 16.8.0才正式發布Hook穩定版本,筆者也還未在生產環境下使用,目前筆者在生產環境下使用的最多的是`HOC`。
React官方完全沒有把classes從React中移除的打算,class組件和Hook完全可以同時存在,官方也建議避免任何“大范圍重構”,畢竟這是一個非常新的版本,如果你喜歡它,可以在新的非關鍵性的代碼中使用Hook。
到此,關于“React深入分析從Mixin到HOC再到Hook”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。