前端發展速度很是之快,頁面和組件變得愈來愈複雜,如何更好的實現狀態邏輯複用
一直都是應用程序中重要的一部分,這直接關係着應用程序的質量以及維護的難易程度。html
本文介紹了React
採用的三種實現狀態邏輯複用
的技術,並分析了他們的實現原理、使用方法、實際應用以及如何選擇使用他們。前端
本文略長,下面是本文的思惟導圖,您能夠從頭開始閱讀,也能夠選擇感興趣的部分閱讀:vue
Mixin
(混入)是一種經過擴展收集功能的方式,它本質上是將一個對象的屬性拷貝到另外一個對象上面去,不過你能夠拷貝任意多
個對象的任意個
方法到一個新對象上去,這是繼承
所不能實現的。它的出現主要就是爲了解決代碼複用問題。react
不少開源庫提供了Mixin
的實現,如Underscore
的_.extend
方法、JQuery
的extend
方法。git
使用_.extend
方法實現代碼複用:es6
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
方法:github
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
方法將任意對象的任意方法擴展到目標對象上。web
React
也提供了Mixin
的實現,若是徹底不一樣的組件有類似的功能,咱們能夠引入來實現代碼複用,固然只有在使用createClass
來建立React
組件時纔可使用,由於在React
組件的es6
寫法中它已經被廢棄掉了。算法
例以下面的例子,不少組件或頁面都須要記錄用戶行爲,性能指標等。若是咱們在每一個組件都引入寫日誌的邏輯,會產生大量重複代碼,經過Mixin
咱們能夠解決這一問題:npm
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>) } });
React
官方文檔在Mixins Considered Harmful一文中提到了Mixin
帶來了危害:
Mixin
可能會相互依賴,相互耦合,不利於代碼維護 Mixin
中的方法可能會相互衝突Mixin
很是多時,組件是能夠感知到的,甚至還要爲其作相關處理,這樣會給代碼形成滾雪球式的複雜性React
如今已經再也不推薦使用Mixin
來解決代碼複用問題,由於Mixin
帶來的危害比他產生的價值還要巨大,而且React
全面推薦使用高階組件來替代它。另外,高階組件還能實現更多其餘更強大的功能,在學習高階組件以前,咱們先來看一個設計模式。
裝飾者(decorator
)模式可以在不改變對象自身的基礎上,在程序運行期間給對像動態的添加職責。與繼承相比,裝飾者是一種更輕便靈活的作法。
高階組件能夠看做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
。
函數返回一個咱們本身定義的組件,而後在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
可以使用任何其餘組件和原組件進行組合渲染,達到樣式、佈局複用等效果。
經過屬性代理實現
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
進行特殊的操做。
經過屬性代理實現
function proxyHOC(WrappedComponent) { return class extends Component { render() { const newProps = { ...this.props, user: 'ConardLi' } return <WrappedComponent {...newProps} />; } } }
高階組件中可獲取原組件的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);
上面的例子經過屬性代理利用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
函數,獲得加強後的組件。
class myComponent extends Component { render() { return (<span>原組件</span>) } } export default inheritHOC(myComponent);
在實際應用中,一個組件可能被多個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
函數等。
咱們還能夠藉助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
的實際應用場景,因爲文章篇幅緣由,代碼通過不少簡化,若有問題歡迎在評論區指出:
實際上這屬於一類最多見的應用,多個組件擁有相似的邏輯,咱們要對重複的邏輯進行復用,
官方文檔中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
,其實就是一個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
的結尾手動拷貝他們:
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; }
使用高階組件後,獲取到的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} />; }); }
React
Diff
算法的原則是:
每次調用高階組件生成的都是是一個全新的組件,組件的惟一標識響應的也會改變,若是在render
方法調用了高階組件,這會致使組件每次都會被卸載後從新掛載。
官方文檔對高階組件的說明:
高階組件就是一個沒有反作用的純函數。
咱們再來看看純函數的定義:
若是函數的調用參數相同,則永遠返回相同的結果。它不依賴於程序執行期間函數外部任何狀態或數據的變化,必須只依賴於其輸入參數。
該函數不會產生任何可觀察的反作用,例如網絡請求,輸入和輸出設備或數據突變。
若是咱們在高階組件對原組件進行了修改,例以下面的代碼:
InputComponent.prototype.componentWillReceiveProps = function(nextProps) { ... }
這樣就破壞了咱們對高階組件的約定,同時也改變了使用高階組件的初衷:咱們使用高階組件是爲了加強
而非改變
原組件。
使用高階組件,咱們能夠代理全部的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
透傳下去。
在使用React Developer Tools
進行調試時,若是咱們使用了HOC
,調試界面可能變得很是難以閱讀,以下面的代碼:
@visible class Show extends Component { render() { return <h1>我是一個標籤</h1> } } @visible class Title extends Component { render() { return <h1>我是一個標題</h1> } }
爲了方便調試,咱們能夠手動爲HOC
指定一個displayName
,官方推薦使用HOCName(WrappedComponentName)
:
static displayName = `Visible(${WrappedComponent.displayName})`
這個約定幫助確保高階組件最大程度的靈活性和可重用性。
回顧下上文提到的 Mixin
帶來的風險:
Mixin
可能會相互依賴,相互耦合,不利於代碼維護 Mixin
中的方法可能會相互衝突Mixin
很是多時,組件是能夠感知到的,甚至還要爲其作相關處理,這樣會給代碼形成滾雪球式的複雜性
而HOC
的出現能夠解決這些問題:
HOC
須要在原組件上進行包裹或者嵌套,若是大量使用HOC
,將會產生很是多的嵌套,這讓調試變得很是困難。HOC
能夠劫持props
,在不遵照約定的狀況下也可能形成衝突。
Hooks
是React v16.7.0-alpha
中加入的新特性。它可讓你在class
之外使用state
和其餘React
特性。
使用Hooks
,你能夠在將含有state
的邏輯從組件中抽象出來,這將可讓這些邏輯容易被測試。同時,Hooks
能夠幫助你在不重寫組件結構的狀況下複用這些邏輯。因此,它也能夠做爲一種實現狀態邏輯複用
的方案。
閱讀下面的章節使用Hook的動機你能夠發現,它能夠同時解決Mixin
和HOC
帶來的問題。
咱們要使用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 可讓你在函數組件中執行一些具備 side effect(反作用)的操做
參數
useEffect
方法接收傳入兩個參數:
render
和以後的每次update
後運行,React
保證在DOM
已經更新完成以後纔會運行回調。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 看起來更加流暢。
使用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); }
React
函數式組件或自定義Hook
中使用Hook
。Hook
的提出主要就是爲了解決class
組件的一系列問題,因此咱們能在class
組件中使用它。
Hook
經過數組實現的,每次 useState
都會改變下標,React
須要利用調用順序來正確更新相應的狀態,若是 useState
被包裹循環或條件語句中,那每就可能會引發調用順序的錯亂,從而形成意想不到的錯誤。
咱們能夠安裝一個eslint
插件來幫助咱們避免這些問題。
// 安裝 npm install eslint-plugin-react-hooks --save-dev // 配置 { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error" } }
像上面介紹的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
:
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
和Mixin
在用法上有必定的類似之處,可是Mixin
引入的邏輯和狀態是能夠相互覆蓋的,而多個Hook
之間互不影響,這讓咱們不須要在把一部分精力放在防止避免邏輯複用的衝突上。
在不遵照約定的狀況下使用HOC
也有可能帶來必定衝突,好比props
覆蓋等等,使用Hook
則能夠避免這些問題。
大量使用HOC
的狀況下讓咱們的代碼變得嵌套層級很是深,使用HOC
,咱們能夠實現扁平式的狀態邏輯複用,而避免了大量的組件嵌套。
在使用class
組件構建咱們的程序時,他們各自擁有本身的狀態,業務邏輯的複雜使這些組件變得愈來愈龐大,各個生命週期中會調用愈來愈多的邏輯,愈來愈難以維護。使用Hook
,可讓你更大限度的將公用邏輯抽離,將一個組件分割成更小的函數,而不是強制基於生命週期方法進行分割。
相比函數,編寫一個class
可能須要掌握更多的知識,須要注意的點也越多,好比this
指向、綁定事件等等。另外,計算機理解一個函數比理解一個class
更快。Hooks
讓你能夠在classes
以外使用更多React
的新特性。
實際上,Hook
在react 16.8.0
才正式發佈Hook
穩定版本,筆者也還未在生產環境下使用,目前筆者在生產環境下使用的最多的是`HOC
`。
React
官方徹底沒有把classes
從React
中移除的打算,class
組件和Hook
徹底能夠同時存在,官方也建議避免任何「大範圍重構」,畢竟這是一個很是新的版本,若是你喜歡它,能夠在新的非關鍵性的代碼中使用Hook
。
mixin
已被拋棄,HOC
正當壯年,Hook
初露鋒芒,前端圈就是這樣,技術迭代速度很是之快,但咱們在學習這些知識之時必定要明白爲何要學,學了有沒有用,要不要用。不忘初心,方得始終。
文中若有錯誤,歡迎在評論區指正,謝謝閱讀。
想閱讀更多優質文章,或者須要文章中思惟導圖源文件可關注個人github博客,歡迎star✨。
推薦關注個人微信公衆號【code祕密花園】,咱們一塊兒交流成長。