React HOC高階元件詳解
High Order Component(包裝元件,後面簡稱HOC),是React開發中提高元件複用性的高階技巧。HOC並不是React的API,他是根據React的特性形成的一種開發模式。
HOC具體上就是一個接受元件作為引數並返回一個新的元件的方法
const EnhancedComponent = higherOrderComponent(WrappedComponent) 複製程式碼
在React的第三方生態中,有非常多的使用,比如Redux的connect
方法或者React-Router的withrouter
方法。
舉個例子
我們有兩個元件:
// CommentList class CommentList extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { // "DataSource" is some global data source comments: DataSource.getComments() }; } componentDidMount() { // Subscribe to changes DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { // Clean up listener DataSource.removeChangeListener(this.handleChange); } handleChange() { // Update component state whenever the data source changes this.setState({ comments: DataSource.getComments() }); } render() { return ( <div> {this.state.comments.map((comment) => ( <Comment comment={comment} key={comment.id} /> ))} </div> ); } } 複製程式碼
// BlogPost class BlogPost extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { blogPost: DataSource.getBlogPost(props.id) }; } componentDidMount() { DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { DataSource.removeChangeListener(this.handleChange); } handleChange() { this.setState({ blogPost: DataSource.getBlogPost(this.props.id) }); } render() { return <TextBlock text={this.state.blogPost} />; } } 複製程式碼
他們雖然是兩個不同的元件,對DataSource的需求也不同,但是他們有很多的內容是相似的:
- 在元件渲染之後監聽DataSource
- 在監聽器裡面呼叫setState
- 在unmout的時候刪除監聽器
在大型的工程開發裡面,這種相似的程式碼會經常出現,那麼如果有辦法把這些相似程式碼提取並複用,對工程的可維護性和開發效率可以帶來明顯的提升。
使用HOC我們可以提供一個方法,並接受不了元件和一些元件間的區別配置作為引數,然後返回一個包裝過的元件作為結果。
function withSubscription(WrappedComponent, selectData) { // ...and returns another component... return class extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { data: selectData(DataSource, props) }; } componentDidMount() { // ... that takes care of the subscription... DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { DataSource.removeChangeListener(this.handleChange); } handleChange() { this.setState({ data: selectData(DataSource, this.props) }); } render() { // ... and renders the wrapped component with the fresh data! // Notice that we pass through any additional props return <WrappedComponent data={this.state.data} {...this.props} />; } }; } 複製程式碼
然後我們就可以通過簡單的呼叫該方法來包裝元件:
const CommentListWithSubscription = withSubscription( CommentList, (DataSource) => DataSource.getComments() ); const BlogPostWithSubscription = withSubscription( BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id) ); 複製程式碼
注意:在HOC中我們並沒有修改輸入的元件,也沒有通過繼承來擴充套件元件。HOC是通過組合的方式來達到擴充套件元件的目的,一個HOC應該是一個沒有副作用的方法。
在這個例子中我們把兩個元件相似的生命週期方法提取出來,並提供selectData作為引數讓輸入元件可以選擇自己想要的資料。因為withSubscription是個純粹的方法,所以以後如果有相似的元件,都可以通過該方法進行包裝,能夠節省非常多的重複程式碼。
不要修改原始元件,使用組合進行功能擴充套件
function logProps(InputComponent) { InputComponent.prototype.componentWillReceiveProps = function(nextProps) { console.log('Current props: ', this.props); console.log('Next props: ', nextProps); }; // The fact that we're returning the original input is a hint that it has // been mutated. return InputComponent; } // EnhancedComponent will log whenever props are received const EnhancedComponent = logProps(InputComponent); 複製程式碼
通過以上方式我們也可以達到擴充套件元件的效果,但是會存在一些問題
componentWillReceiveProps
修改原始元件的方式缺乏抽象化,使用者必須知道這個方法是如何實現的來避免上面提到的問題。
如果通過組合的方式來做,我們就可以避免這些問題
function logProps(InputComponent) { return class extends React.Component{ componentWillReceiveProps(nextProps) { console.log('Current props: ', this.props); console.log('Next props: ', nextProps); } render() { <InputComponent {...this.props} /> } } } // EnhancedComponent will log whenever props are received const EnhancedComponent = logProps(InputComponent); 複製程式碼
慣例:無關的props傳入到原始元件
HOC元件會在原始元件的基礎上增加一些擴充套件功能使用的props,那麼這些props就不應該傳入到原始元件(當然有例外,比如HOC元件需要使用原始元件指定的props),一般來說我們會這樣處理props:
render() { // Filter out extra props that are specific to this HOC and shouldn't be // passed through const { extraProp, ...passThroughProps } = this.props; // Inject props into the wrapped component. These are usually state values or // instance methods. const injectedProp = someStateOrInstanceMethod; // Pass props to wrapped component return ( <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); } 複製程式碼
extraProp
是HOC元件中要用的props,不用的剩下的props我們都認為是原始元件需要使用的props,如果是兩者通用的props你可以單獨傳遞。
慣例:包裝元件的顯示名稱來方便除錯
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription; } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } 複製程式碼
簡單來說就是通過手動指定displayName
來讓HOC元件能夠更方便得被react devtool觀察到
慣例:不要在render方法裡面呼叫HOC方法
render() { // A new version of EnhancedComponent is created on every render // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // That causes the entire subtree to unmount/remount each time! return <EnhancedComponent />; } 複製程式碼
一來每次呼叫enhance
返回的都是一個新的class,react的diffing演算法是根據元件的特徵來判斷是否需要重新渲染的,如果兩次render的時候元件之間不是(===)完全相等的,那麼會直接重新渲染,而部署根據props傳入之後再進行diff,對效能損耗非常大。並且重新渲染會讓之前的元件的state和children全部丟失。
二來React的元件是通過props來改變其顯示的,完全沒有必要每次渲染動態產生一個元件,理論上需要在渲染時自定義的引數,都可以通過事先指定好props來實現可配置。
靜態方法必須被拷貝
有時候會在元件的class上面外掛一下幫助方法,如果按照上面的方法進行包裝,那麼包裝之後的class就沒有來這些靜態方法,這時候為了保持元件使用的一致性,一般我們會把這些靜態方法拷貝到包裝後的元件上。
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // Must know exactly which method(s) to copy :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; } 複製程式碼
這個之適用於你已知輸入元件存在那些靜態方法的情況,如果需要可擴充套件性更高,那麼可以選擇使用第三方外掛ofollow,noindex">
hoist-non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics'; function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance; } 複製程式碼
ref
ref作為React中的特殊屬性--類似於key,並不屬於props,也就是說我們使用傳遞props的方式並不會把ref傳遞進去,那麼這時候如果我們在HOC元件上放一個ref,拿到的是包裝之後的元件而不是原始元件,這可能就會導致一些問題。
在React 16.3之後官方增加來一個React.forwardRef
方法來解決這個問題,具體可以參考這裡