高階組件HOC
即Higher Order Component
是React
中用於複用組件邏輯的一種高級技巧,HOC
自身不是React API
的一部分,它是一種基於React
的組合特性而造成的設計模式。html
高階組件從名字上就透漏出高級的氣息,實際上這個概念應該是源自於JavaScript
的高階函數,高階函數就是接受函數做爲輸入或者輸出的函數,能夠想到柯里化就是一種高階函數,一樣在React
文檔上也給出了高階組件的定義,高階組件是接收組件並返回新組件的函數。react
A higher-order component is a function that takes a component and returns a new component.
具體而言,高階組件是參數爲組件,返回值爲新組件的函數,組件是將props
轉換爲UI
,而高階組件是將組件轉換爲另外一個組件。HOC
在React
的第三方庫中很常見,例如Redux
的connect
和Relay
的createFragmentContainer
。git
// 高階組件定義 const higherOrderComponent = (WrappedComponent) => { return class EnhancedComponent extends React.Component { // ... render() { return <WrappedComponent {...this.props} />; } }; } // 普通組件定義 class WrappedComponent extends React.Component{ render(){ //.... } } // 返回被高階組件包裝過的加強組件 const EnhancedComponent = higherOrderComponent(WrappedComponent);
在這裏要注意,不要試圖以任何方式在HOC
中修改組件原型,而應該使用組合的方式,經過將組件包裝在容器組件中實現功能。一般狀況下,實現高階組件的方式有如下兩種:github
Props Proxy
。Inheritance Inversion
。例如咱們能夠爲傳入的組件增長一個存儲中的id
屬性值,經過高階組件咱們就能夠爲這個組件新增一個props
,固然咱們也能夠對在JSX
中的WrappedComponent
組件中props
進行操做,注意不是操做傳入的WrappedComponent
類,咱們不該該直接修改傳入的組件,而能夠在組合的過程當中對其操做。算法
const HOC = (WrappedComponent, store) => { return class EnhancedComponent extends React.Component { render() { const newProps = { id: store.id } return <WrappedComponent {...this.props} {...newProps} />; } } }
咱們也能夠利用高階組件將新組件的狀態裝入到被包裝組件中,例如咱們可使用高階組件將非受控組件轉化爲受控組件。編程
class WrappedComponent extends React.Component { render() { return <input name="name" />; } } const HOC = (WrappedComponent) => { return class EnhancedComponent extends React.Component { constructor(props) { super(props); this.state = { name: "" }; } render() { const newProps = { value: this.state.name, onChange: e => this.setState({name: e.target.value}), } return <WrappedComponent {...this.props} {...newProps} />; } } }
或者咱們的目的是將其使用其餘組件包裹起來用以達成佈局或者是樣式的目的。設計模式
const HOC = (WrappedComponent) => { return class EnhancedComponent extends React.Component { render() { return ( <div class="layout"> <WrappedComponent {...this.props} /> </div> ); } } }
反向繼承是指返回的組件去繼承以前的組件,在反向繼承中咱們能夠作很是多的操做,修改state
、props
甚至是翻轉Element Tree
,反向繼承有一個重要的點,反向繼承不能保證完整的子組件樹被解析,也就是說解析的元素樹中包含了組件(函數類型或者Class
類型),就不能再操做組件的子組件了。
當咱們使用反向繼承實現高階組件的時候能夠經過渲染劫持來控制渲染,具體是指咱們能夠有意識地控制WrappedComponent
的渲染過程,從而控制渲染控制的結果,例如咱們能夠根據部分參數去決定是否渲染組件。數組
const HOC = (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { render() { return this.props.isRender && super.render(); } } }
甚至咱們能夠經過重寫的方式劫持原組件的生命週期。babel
const HOC = (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { componentDidMount(){ // ... } render() { return super.render(); } } }
因爲其實是繼承關係,咱們能夠去讀取組件的props
和state
,若是有必要的話,甚至能夠修改增長、修改和刪除props
和state
,固然前提是修改帶來的風險須要你本身來控制。在一些狀況下,咱們可能須要爲高階屬性傳入一些參數,那咱們就能夠經過柯里化的形式傳入參數,配合高階組件能夠完成對組件的相似於閉包的操做。閉包
const HOCFactoryFactory = (params) => { // 此處操做params return (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { render() { return params.isRender && this.props.isRender && super.render(); } } } }
使用Mixin
與HOC
均可以用於解決橫切關注點相關的問題。
Mixin
是一種混入的模式,在實際使用中Mixin
的做用仍是很是強大的,可以使得咱們在多個組件中共用相同的方法,但一樣也會給組件不斷增長新的方法和屬性,組件自己不只能夠感知,甚至須要作相關的處理(例如命名衝突、狀態維護等),一旦混入的模塊變多時,整個組件就變的難以維護,Mixin
可能會引入不可見的屬性,例如在渲染組件中使用Mixin
方法,給組件帶來了不可見的屬性props
和狀態state
,而且Mixin
可能會相互依賴,相互耦合,不利於代碼維護,此外不一樣的Mixin
中的方法可能會相互衝突。以前React
官方建議使用Mixin
用於解決橫切關注點相關的問題,但因爲使用Mixin
可能會產生更多麻煩,因此官方如今推薦使用HOC
。
高階組件HOC
屬於函數式編程functional programming
思想,對於被包裹的組件時不會感知到高階組件的存在,而高階組件返回的組件會在原來的組件之上具備功能加強的效果,基於此React
官方推薦使用高階組件。
不要試圖在HOC
中修改組件原型,或以其餘方式改變它。
function logProps(InputComponent) { InputComponent.prototype.componentDidUpdate = function(prevProps) { console.log("Current props: ", this.props); console.log("Previous props: ", prevProps); }; // 返回原始的 input 組件,其已經被修改。 return InputComponent; } // 每次調用 logProps 時,加強組件都會有 log 輸出。 const EnhancedComponent = logProps(InputComponent);
這樣作會產生一些不良後果,其一是輸入組件再也沒法像HOC
加強以前那樣使用了,更嚴重的是,若是你再用另外一個一樣會修改componentDidUpdate
的HOC
加強它,那麼前面的HOC
就會失效,同時這個HOC
也沒法應用於沒有生命週期的函數組件。
修改傳入組件的HOC
是一種糟糕的抽象方式,調用者必須知道他們是如何實現的,以免與其餘HOC
發生衝突。HOC
不該該修改傳入組件,而應該使用組合的方式,經過將組件包裝在容器組件中實現功能。
function logProps(WrappedComponent) { return class extends React.Component { componentDidUpdate(prevProps) { console.log("Current props: ", this.props); console.log("Previous props: ", prevProps); } render() { // 將 input 組件包裝在容器中,而不對其進行修改,Nice! return <WrappedComponent {...this.props} />; } } }
HOC
爲組件添加特性,自身不該該大幅改變約定,HOC
返回的組件與原組件應保持相似的接口。HOC
應該透傳與自身無關的props
,大多數HOC
都應該包含一個相似於下面的render
方法。
render() { // 過濾掉額外的 props,且不要進行透傳 const { extraProp, ...passThroughProps } = this.props; // 將 props 注入到被包裝的組件中。 // 一般爲 state 的值或者實例方法。 const injectedProp = someStateOrInstanceMethod; // 將 props 傳遞給被包裝組件 return ( <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); }
並非全部的HOC
都同樣,有時候它僅接受一個參數,也就是被包裹的組件。
const NavbarWithRouter = withRouter(Navbar);
HOC
一般能夠接收多個參數,好比在Relay
中HOC
額外接收了一個配置對象用於指定組件的數據依賴。
const CommentWithRelay = Relay.createContainer(Comment, config);
最多見的HOC
簽名以下,connect
是一個返回高階組件的高階函數。
// React Redux 的 `connect` 函數 const ConnectedComment = connect(commentSelector, commentActions)(CommentList); // connect 是一個函數,它的返回值爲另一個函數。 const enhance = connect(commentListSelector, commentListActions); // 返回值爲 HOC,它會返回已經鏈接 Redux store 的組件 const ConnectedComment = enhance(CommentList);
這種形式可能看起來使人困惑或沒必要要,但它有一個有用的屬性,像connect
函數返回的單參數HOC
具備簽名Component => Component
,輸出類型與輸入類型相同的函數很容易組合在一塊兒。一樣的屬性也容許connect
和其餘HOC
承擔裝飾器的角色。此外許多第三方庫都提供了compose
工具函數,包括lodash
、Redux
和Ramda
。
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // 你能夠編寫組合工具函數 // compose(f, g, h) 等同於 (...args) => f(g(h(...args))) const enhance = compose( // 這些都是單參數的 HOC withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)
React
的diff
算法使用組件標識來肯定它是應該更新現有子樹仍是將其丟棄並掛載新子樹,若是從render
返回的組件與前一個渲染中的組件相同===
,則React
經過將子樹與新子樹進行區分來遞歸更新子樹,若是它們不相等,則徹底卸載前一個子樹。
一般在使用的時候不須要考慮這點,但對HOC
來講這一點很重要,由於這表明着你不該在組件的render
方法中對一個組件應用HOC
。
render() { // 每次調用 render 函數都會建立一個新的 EnhancedComponent // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做! return <EnhancedComponent />; }
這不只僅是性能問題,從新掛載組件會致使該組件及其全部子組件的狀態丟失,若是在組件以外建立HOC
,這樣一來組件只會建立一次。所以每次render
時都會是同一個組件,通常來講,這跟你的預期表現是一致的。在極少數狀況下,你須要動態調用HOC
,你能夠在組件的生命週期方法或其構造函數中進行調用。
有時在React
組件上定義靜態方法頗有用,例如Relay
容器暴露了一個靜態方法getFragment
以方便組合GraphQL
片斷。可是當你將HOC
應用於組件時,原始組件將使用容器組件進行包裝,這意味着新組件沒有原始組件的任何靜態方法。
// 定義靜態函數 WrappedComponent.staticMethod = function() {/*...*/} // 如今使用 HOC const EnhancedComponent = enhance(WrappedComponent); // 加強組件沒有 staticMethod typeof EnhancedComponent.staticMethod === "undefined" // true
爲了解決這個問題,你能夠在返回以前把這些方法拷貝到容器組件上。
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // 必須準確知道應該拷貝哪些方法 :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
但要這樣作,你須要知道哪些方法應該被拷貝,你可使用hoist-non-react-statics
依賴自動拷貝全部非React
靜態方法。
import hoistNonReactStatic from "hoist-non-react-statics"; function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance; }
除了導出組件,另外一個可行的方案是再額外導出這個靜態方法。
// 使用這種方式代替... MyComponent.someFunction = someFunction; export default MyComponent; // ...單獨導出該方法... export { someFunction }; // ...並在要使用的組件中,import 它們 import MyComponent, { someFunction } from "./MyComponent.js";
雖然高階組件的約定是將全部props
傳遞給被包裝組件,但這對於refs
並不適用,那是由於ref
實際上並非一個prop
,就像key
同樣,它是由React
專門處理的。若是將ref
添加到HOC
的返回組件中,則ref
引用指向容器組件,而不是被包裝組件,這個問題能夠經過React.forwardRef
這個API
明確地將refs
轉發到內部的組件。。
function logProps(Component) { class LogProps extends React.Component { componentDidUpdate(prevProps) { console.log('old props:', prevProps); console.log('new props:', this.props); } render() { const {forwardedRef, ...rest} = this.props; // 將自定義的 prop 屬性 「forwardedRef」 定義爲 ref return <Component ref={forwardedRef} {...rest} />; } } // 注意 React.forwardRef 回調的第二個參數 「ref」。 // 咱們能夠將其做爲常規 prop 屬性傳遞給 LogProps,例如 「forwardedRef」 // 而後它就能夠被掛載到被 LogProps 包裹的子組件上。 return React.forwardRef((props, ref) => { return <LogProps {...props} forwardedRef={ref} />; }); }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>React</title> </head> <body> <div id="root"></div> </body> <script src="https://unpkg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> class WrappedComponent extends React.Component { render() { return <input name="name" />; } } const HOC = (WrappedComponent) => { return class EnhancedComponent extends React.Component { constructor(props) { super(props); this.state = { name: "" }; } render() { const newProps = { value: this.state.name, onChange: e => this.setState({name: e.target.value}), } return <WrappedComponent {...this.props} {...newProps} />; } } } const EnhancedComponent = HOC(WrappedComponent); const HOC2 = (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { render() { return this.props.isRender && super.render(); } } } const EnhancedComponent2 = HOC2(WrappedComponent); var vm = ReactDOM.render( <> <EnhancedComponent /> <EnhancedComponent2 isRender={true} /> </>, document.getElementById("root") ); </script> </html>
https://github.com/WindrunnerMax/EveryDay
https://juejin.cn/post/6844903477798256647 https://juejin.cn/post/6844904050236850184 https://zh-hans.reactjs.org/docs/higher-order-components.htm