導讀
前端發展速度非常之快,頁面和元件變得越來越複雜,如何更好的實現狀態邏輯複用
一直都是應用程式中重要的一部分,這直接關係著應用程式的質量以及維護的難易程度。
本文介紹了React
採用的三種實現狀態邏輯複用
的技術,並分析了他們的實現原理、使用方法、實際應用以及如何選擇使用他們。
本文略長,下面是本文的思維導圖,您可以從頭開始閱讀,也可以選擇感興趣的部分閱讀:
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 <h1>我是一個標籤</h1>
}
}
@visible
class Title extends Component {
render() {
return <h1>我是一個標題</h1>
}
}
複製程式碼
為了方便除錯,我們可以手動為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
的情況下讓我們的程式碼變得巢狀層級非常深,使用Hook
,我們可以實現扁平式的狀態邏輯複用,而避免了大量的元件巢狀。
讓元件更容易理解
在使用class
元件構建我們的程式時,他們各自擁有自己的狀態,業務邏輯的複雜使這些元件變得越來越龐大,各個生命週期中會呼叫越來越多的邏輯,越來越難以維護。使用Hook
,可以讓你更大限度的將公用邏輯抽離,將一個元件分割成更小的函式,而不是強制基於生命週期方法進行分割。
使用函式代替class
相比函式,編寫一個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祕密花園】,我們一起交流成長。