高階元件的定義類比於高階函數的定義。高階函數接收函數作為參數,並且傳回值也是一個函數。類似的,高階元件接收React元件作為參數,並且傳回一個新的React元件。 高階元件本質上也是函數,並不是一個元件,這一點一定不要弄錯。
為什麼React引入高階元件的概念?它到底有何威力?讓我們先透過一個簡單的例子來說明一下。
假設有一個元件MyComponent
,需要從LocalStorage
取得數據,然後渲染資料到介面。我們可以這樣寫元件程式碼:
import React, { Component } from 'react' class MyComponent extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { return <p>{this.state.data}</p> } }
程式碼很簡單,但當有其他元件也需要從LocalStorage
取得相同的資料展示出來時,需要在每個元件都重複componentWillMount
中的程式碼,這顯然是很冗餘的。下面讓我們來看看使用高階元件可以怎麼改寫這部分程式碼。
import React, { Component } from 'react' function withPersistentData(WrappedComponent) { return class extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return <wrappedcomponent></wrappedcomponent> } } } class MyComponent2 extends Component { render() { return <p>{this.props.data}</p> } } const MyComponentWithPersistentData = withPersistentData(MyComponent2)
withPersistentData
就是一個高階元件,它傳回一個新的元件,在新元件的componentWillMount
中統一處理從LocalStorage
中取得資料的邏輯,然後將取得的資料以屬性的方式傳遞給被包裝的元件WrappedComponent
,這樣在WrappedComponent
中就可以直接使用this.props. data
取得需要展示的資料了,如MyComponent2
所示。當有其他的元件也需要這段邏輯時,繼續使用withPersistentData
這個高階元件包裝這些元件就可以了。
透過這個例子,可以看出高階元件的主要功能是封裝並分離元件的通用邏輯,讓通用邏輯在元件間更好地被重複使用。高階組件的這種實現方式,本質上是一個裝飾者設計模式。
高階元件的參數並非只能是一個元件,它還可以接收其他參數。例如,元件MyComponent3
需要從LocalStorage中取得key等於name的數據,而不是上面範例中寫死的key等於data的數據,withPersistentData
這個高階元件就不滿足我們的需求了。我們可以讓它接收額外的一個參數,來決定從LocalStorage
中取得哪個資料:
import React, { Component } from 'react' function withPersistentData(WrappedComponent, key) { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return <wrappedcomponent></wrappedcomponent> } } } class MyComponent2 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } class MyComponent3 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data'); const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');
新版本的withPersistentData
就滿足我們取得不同key的值的需求了。高階元件中的參數當然也可以是函數,我們將在下一節進一步說明。
高階元件最常見的函式簽章形式是這樣的:
HOC([param])([WrappedComponent])
用這種形式改寫withPersistentData
,如下:
import React, { Component } from 'react' const withPersistentData = (key) => (WrappedComponent) => { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return <wrappedcomponent></wrappedcomponent> } } } class MyComponent2 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } class MyComponent3 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2); const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);
實際上,此時的withPersistentData
和我們最初對高階組件的定義已經不同。它已經變成了一個高階函數,但這個高階函數的回傳值是一個高階元件。 HOC([param])([WrappedComponent])
這種形式中,HOC([param])
才是真正的高階元件,我們可以把它看成高階組件的變種形式。這種形式的高階組件因其特有的便利性——結構清晰(普通參數和被包裹組件分離)、易於組合,並大量出現在第三方庫中。如react-redux中的connect就是一個典型。 connect的定義如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(WrappedComponent)
這個函數會將一個React元件連接到Redux 的 store。在連接的過程中,connect透過函數類型的參數mapStateToProps
,從全域store中取出目前元件所需的state,並將state轉換成目前元件的props;同時透過函數類型的參數 mapDispatchToProps
,把目前元件用到的Redux的action creators,以props的方式傳遞給目前元件。
例如,我們把元件ComponentA連接到Redux上的寫法類似於:
const ConnectedComponentA = connect(mapStateToProps, mapDispatchToProps)(ComponentA);
我們可以把它拆分來看:
// connect 是一个函数,返回值enhance也是一个函数 const enhance = connect(mapStateToProps, mapDispatchToProps); // enhance是一个高阶组件 const ConnectedComponentA = enhance(ComponentA);
当多个函数的输出和它的输入类型相同时,这些函数是很容易组合到一起使用的。例如,有f,g,h三个高阶组件,都只接受一个组件作为参数,于是我们可以很方便的嵌套使用它们:f( g( h(WrappedComponent) ) )
。这里可以有一个例外,即最内层的高阶组件h可以有多个参数,但其他高阶组件必须只能接收一个参数,只有这样才能保证内层的函数返回值和外层的函数参数数量一致(都只有1个)。
例如我们将connect和另一个打印日志的高阶组件withLog
联合使用:
const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA));
这里我们定义一个工具函数:compose(...functions)
,调用compose(f, g, h)
等价于 (...args) => f(g(h(...args)))
。用compose
函数我们可以把高阶组件嵌套的写法打平:
const enhance = compose( connect(mapStateToProps), withLog ); const ConnectedComponentA = enhance(ComponentA);
像Redux等很多第三方库都提供了compose
的实现,compose
结合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。
有些同学可能会觉得高阶组件有些类似父组件的使用。例如,我们完全可以把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,但是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。
1)不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等(===
),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。
2)如果需要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。因为高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics可以帮助我们方便的拷贝组件所有的自定义静态方法。有兴趣的同学可以自行了解。
3)Refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是ref
并不会传递给被包装组件。如果你在高阶组件的返回组件中定义了ref
,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果你希望获取被包装组件的引用,你可以把ref
的回调函数定义成一个普通属性(给它一个ref以外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:
function FocusInput({ inputRef, ...rest }) { return <input>; } //enhance 是一个高阶组件 const EnhanceInput = enhance(FocusInput); // 在一个组件的render方法中... return (<enhanceinput> { this.input = input } }>) // 让FocusInput自动获取焦点 this.input.focus();</enhanceinput>
React 深入系列7:React 常用模式
我的新书《React进阶之路》已上市,请大家多多支持!
链接:京东 当当
React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。
高阶组件是React 中一个很重要且比较复杂的概念,高阶组件在很多第三方库(如Redux)中都被经常使用。在项目中用好高阶组件,可以显著提高代码质量。
高階組件的定義類比於高階函數的定義。高階函數接收函數作為參數,並且傳回值也是一個函數。類似的,高階元件接收React元件作為參數,並且傳回一個新的React元件。 高階元件本質上也是一個函數,並不是一個元件,這一點一定不要弄錯。
為什麼React引入高階元件的概念?它到底有何威力?讓我們先透過一個簡單的例子來說明一下。
假設有一個元件MyComponent
,需要從LocalStorage
取得數據,然後渲染資料到介面。我們可以這樣寫元件程式碼:
import React, { Component } from 'react' class MyComponent extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { return <p>{this.state.data}</p> } }
程式碼很簡單,但當有其他元件也需要從LocalStorage
取得相同的資料展示出來時,需要在每個元件都重複componentWillMount
中的程式碼,這顯然是很冗餘的。下面讓我們來看看使用高階元件可以怎麼改寫這部分程式碼。
import React, { Component } from 'react' function withPersistentData(WrappedComponent) { return class extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return <wrappedcomponent></wrappedcomponent> } } } class MyComponent2 extends Component { render() { return <p>{this.props.data}</p> } } const MyComponentWithPersistentData = withPersistentData(MyComponent2)
withPersistentData
就是一個高階元件,它傳回一個新的元件,在新元件的componentWillMount
中統一處理從LocalStorage
中取得資料的邏輯,然後將取得的資料以屬性的方式傳遞給被包裝的元件WrappedComponent
,這樣在WrappedComponent
中就可以直接使用this.props. data
取得需要展示的資料了,如MyComponent2
所示。當有其他的元件也需要這段邏輯時,繼續使用withPersistentData
這個高階元件包裝這些元件就可以了。
透過這個例子,可以看出高階元件的主要功能是封裝並分離元件的通用邏輯,讓通用邏輯在元件間更好地被重複使用。高階組件的這種實現方式,本質上是一個裝飾者設計模式。
高階元件的參數並非只能是一個元件,它還可以接收其他參數。例如,元件MyComponent3
需要從LocalStorage中取得key等於name的數據,而不是上面範例中寫死的key等於data的數據,withPersistentData
這個高階元件就不滿足我們的需求了。我們可以讓它接收額外的一個參數,來決定從LocalStorage
中取得哪個資料:
import React, { Component } from 'react' function withPersistentData(WrappedComponent, key) { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return <wrappedcomponent></wrappedcomponent> } } } class MyComponent2 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } class MyComponent3 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data'); const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');
新版本的withPersistentData
就滿足我們取得不同key的值的需求了。高階元件中的參數當然也可以是函數,我們將在下一節進一步說明。
高階元件最常見的函式簽章形式是這樣的:
HOC([param])([WrappedComponent])
用這種形式改寫withPersistentData
,如下:
import React, { Component } from 'react' const withPersistentData = (key) => (WrappedComponent) => { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return <wrappedcomponent></wrappedcomponent> } } } class MyComponent2 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } class MyComponent3 extends Component { render() { return <p>{this.props.data}</p> } //省略其他逻辑... } const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2); const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);
實際上,此時的withPersistentData
和我們最初對高階組件的定義已經不同。它已經變成了一個高階函數,但這個高階函數的回傳值是一個高階元件。 HOC([param])([WrappedComponent])
這種形式中,HOC([param])
才是真正的高階元件,我們可以把它看成高階組件的變種形式。這種形式的高階組件因其特有的便利性——結構清晰(普通參數和被包裹組件分離)、易於組合,並大量出現在第三方庫中。如react-redux中的connect就是一個典型。 connect的定義如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(WrappedComponent)
這個函數會將一個React元件連接到Redux 的 store。在連接的過程中,connect透過函數類型的參數mapStateToProps
,從全域store中取出目前元件所需的state,並將state轉換成目前元件的props;同時透過函數類型的參數 mapDispatchToProps
,把目前元件用到的Redux的action creators,以props的方式傳遞給目前元件。
例如,我們把元件ComponentA連接到Redux上的寫法類似於:
const ConnectedComponentA = connect(mapStateToProps, mapDispatchToProps)(ComponentA);
我們可以把它拆分來看:
// connect 是一个函数,返回值enhance也是一个函数 const enhance = connect(mapStateToProps, mapDispatchToProps); // enhance是一个高阶组件 const ConnectedComponentA = enhance(ComponentA);
當多個函數的輸出和它的輸入類型相同時,這些函數是很容易組合到一起使用的。例如,有f,g,h三個高階元件,都只接受一個元件作為參數,於是我們可以很方便的巢狀使用它們:f( g( h(WrappedComponent) ) )
。這裡可以有一個例外,即最內層的高階元件h可以有多個參數,但其他高階元件必須只能接收一個參數,只有這樣才能確保內層的函數回傳值和外層的函數參數數量一致(都只有1個)。
例如我們將connect和另一個列印日誌的高階元件withLog
結合使用:
const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA));
这里我们定义一个工具函数:compose(...functions)
,调用compose(f, g, h)
等价于 (...args) => f(g(h(...args)))
。用compose
函数我们可以把高阶组件嵌套的写法打平:
const enhance = compose( connect(mapStateToProps), withLog ); const ConnectedComponentA = enhance(ComponentA);
像Redux等很多第三方库都提供了compose
的实现,compose
结合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。
有些同学可能会觉得高阶组件有些类似父组件的使用。例如,我们完全可以把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,但是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。
1)不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等(===
),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。
2)如果需要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。因为高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics可以帮助我们方便的拷贝组件所有的自定义静态方法。有兴趣的同学可以自行了解。
3)Refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是ref
并不会传递给被包装组件。如果你在高阶组件的返回组件中定义了ref
,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果你希望获取被包装组件的引用,你可以把ref
的回调函数定义成一个普通属性(给它一个ref以外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:
function FocusInput({ inputRef, ...rest }) { return <input>; } //enhance 是一个高阶组件 const EnhanceInput = enhance(FocusInput); // 在一个组件的render方法中... return (<enhanceinput> { this.input = input } }>) // 让FocusInput自动获取焦点 this.input.focus();</enhanceinput>
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
EasyCanvas绘图库在Pixeler项目开发中使用实战总结
以上是React高階組件使用詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!