React組件複用的方式

React組件複用的方式

現前端的工程化愈加重要,雖然使用Ctrl+CCtrl+V一樣可以完成需求,可是一旦面臨修改那就是一項龐大的任務,因而減小代碼的拷貝,增長封裝複用能力,實現可維護、可複用的代碼就變得尤其重要,在React中組件是代碼複用的主要單元,基於組合的組件複用機制至關優雅,而對於更細粒度的邏輯(狀態邏輯、行爲邏輯等),複用起來卻不那麼容易,很難把狀態邏輯拆出來做爲一個可複用的函數或組件,實際上在Hooks出現以前,都缺乏一種簡單直接的組件行爲擴展方式,對於MixinHOCRender Props都算是在既有(組件機制的)遊戲規則下探索出來的上層模式,一直沒有從根源上很好地解決組件間邏輯複用的問題,直到Hooks登上舞臺,下面咱們就來介紹一下MixinHOCRender PropsHooks四種組件間複用的方式。html

Mixin

固然React好久以前就再也不建議使用Mixin做爲複用的解決方案,可是如今依舊能經過create-react-class提供對Mixin的支持,此外注意在以ES6class方式聲明組件時是不支持Mixin的。
Mixins容許多個React組件之間共享代碼,它們很是相似於Python中的mixinsPHP中的traitsMixin方案的出現源自一種OOP直覺,只在早期提供了React.createClass() API(React v15.5.0正式廢棄,移至create-react-class)來定義組件,天然而然地,(類)繼承就成了一種直覺性的嘗試,而在JavaScript基於原型的擴展模式下,相似於繼承的Mixin方案就成了一個不錯的解決方案,Mixin主要用來解決生命週期邏輯和狀態邏輯的複用問題,容許從外部擴展組件生命週期,在Flux等模式中尤其重要,可是在不斷實踐中也出現了不少缺陷:前端

  • 組件與Mixin之間存在隱式依賴(Mixin常常依賴組件的特定方法,但在定義組件時並不知道這種依賴關係)。
  • 多個Mixin之間可能產生衝突(好比定義了相同的state字段)。
  • Mixin傾向於增長更多狀態,這下降了應用的可預測性,致使複雜度劇增。
  • 隱式依賴致使依賴關係不透明,維護成本和理解成本迅速攀升。
  • 難以快速理解組件行爲,須要全盤瞭解全部依賴Mixin的擴展行爲,及其之間的相互影響
  • 組件自身的方法和state字段不敢輕易刪改,由於難以肯定有沒有Mixin依賴它。
  • Mixin也難以維護,由於Mixin邏輯最後會被打平合併到一塊兒,很難搞清楚一個Mixin的輸入輸出。

毫無疑問,這些問題是致命的,因此,Reactv0.13.0放棄了Mixin靜態橫切(相似於繼承的複用),轉而走向HOC高階組件(相似於組合的複用)。react

示例

上古版本示例,一個通用的場景是: 一個組件須要按期更新,用setInterval()作很容易,但當不須要它的時候取消定時器來節省內存是很是重要的,React提供生命週期方法來告知組件建立或銷燬的時間,下面的Mixin,使用setInterval()並保證在組件銷燬時清理定時器。git

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};

var TickTock = React.createClass({
  mixins: [SetIntervalMixin], // 引用 mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // 調用 mixin 的方法
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

ReactDOM.render(
  <TickTock />,
  document.getElementById("example")
);

HOC

Mixin以後,HOC高階組件擔起重任,成爲組件間邏輯複用的推薦方案,高階組件從名字上就透漏出高級的氣息,實際上這個概念應該是源自於JavaScript的高階函數,高階函數就是接受函數做爲輸入或者輸出的函數,能夠想到柯里化就是一種高階函數,一樣在React文檔上也給出了高階組件的定義,高階組件是接收組件並返回新組件的函數。具體的意思就是: 高階組件能夠看做React對裝飾模式的一種實現,高階組件就是一個函數,且該函數接受一個組件做爲參數,並返回一個新的組件,他會返回一個加強的React組件,高階組件可讓咱們的代碼更具備複用性,邏輯性與抽象性,能夠對render方法進行劫持,也能夠控制propsstate等。
對比MixinHOCMixin是一種混入的模式,在實際使用中Mixin的做用仍是很是強大的,可以使得咱們在多個組件中共用相同的方法,但一樣也會給組件不斷增長新的方法和屬性,組件自己不只能夠感知,甚至須要作相關的處理(例如命名衝突、狀態維護等),一旦混入的模塊變多時,整個組件就變的難以維護,Mixin可能會引入不可見的屬性,例如在渲染組件中使用Mixin方法,給組件帶來了不可見的屬性props和狀態state,而且Mixin可能會相互依賴,相互耦合,不利於代碼維護,此外不一樣的Mixin中的方法可能會相互衝突。以前React官方建議使用Mixin用於解決橫切關注點相關的問題,但因爲使用Mixin可能會產生更多麻煩,因此官方如今推薦使用HOC。高階組件HOC屬於函數式編程functional programming思想,對於被包裹的組件時不會感知到高階組件的存在,而高階組件返回的組件會在原來的組件之上具備功能加強的效果,基於此React官方推薦使用高階組件。
HOC雖然沒有那麼多致命問題,但也存在一些小缺陷:github

  • 擴展性限制: HOC並不能徹底替代Mixin,一些場景下,Mixin能夠而HOC作不到,好比PureRenderMixin,由於HOC沒法從外部訪問子組件的State,同時經過shouldComponentUpdate濾掉沒必要要的更新,所以,React在支持ES6Class以後提供了React.PureComponent來解決這個問題。
  • Ref傳遞問題: Ref被隔斷,Ref的傳遞問題在層層包裝下至關惱人,函數Ref可以緩解一部分(讓HOC得以獲知節點建立與銷燬),以至於後來有了React.forwardRef API
  • WrapperHell: HOC氾濫,出現WrapperHell(沒有包一層解決不了的問題,若是有,那就包兩層),多層抽象一樣增長了複雜度和理解成本,這是最關鍵的缺陷,而HOC模式下沒有很好的解決辦法。

示例

具體而言,高階組件是參數爲組件,返回值爲新組件的函數,組件是將props轉換爲UI,而高階組件是將組件轉換爲另外一個組件。HOCReact的第三方庫中很常見,例如ReduxconnectRelaycreateFragmentContainer算法

// 高階組件定義
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中修改組件原型,而應該使用組合的方式,經過將組件包裝在容器組件中實現功能。一般狀況下,實現高階組件的方式有如下兩種:編程

  • 屬性代理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} 
            />;
        }
    }
}

或者咱們的目的是將其使用其餘組件包裹起來用以達成佈局或者是樣式的目的。babel

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        render() {
            return (
                <div class="layout">
                    <WrappedComponent  {...this.props} />
                </div>
            );
        }
    }
}

反向繼承

反向繼承是指返回的組件去繼承以前的組件,在反向繼承中咱們能夠作很是多的操做,修改stateprops甚至是翻轉Element Tree,反向繼承有一個重要的點,反向繼承不能保證完整的子組件樹被解析,也就是說解析的元素樹中包含了組件(函數類型或者Class類型),就不能再操做組件的子組件了。
當咱們使用反向繼承實現高階組件的時候能夠經過渲染劫持來控制渲染,具體是指咱們能夠有意識地控制WrappedComponent的渲染過程,從而控制渲染控制的結果,例如咱們能夠根據部分參數去決定是否渲染組件。

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends WrappedComponent {
        render() {
            return this.props.isRender && super.render();  
        }
    }
}

甚至咱們能夠經過重寫的方式劫持原組件的生命週期。

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends WrappedComponent {
        componentDidMount(){
          // ...
        }
        render() {
            return super.render();  
        }
    }
}

因爲其實是繼承關係,咱們能夠去讀取組件的propsstate,若是有必要的話,甚至能夠修改增長、修改和刪除propsstate,固然前提是修改帶來的風險須要你本身來控制。在一些狀況下,咱們可能須要爲高階屬性傳入一些參數,那咱們就能夠經過柯里化的形式傳入參數,配合高階組件能夠完成對組件的相似於閉包的操做。

const HOCFactoryFactory = (params) => {
    // 此處操做params
    return (WrappedComponent) => {
        return class EnhancedComponent extends WrappedComponent {
            render() {
                return params.isRender && this.props.isRender && super.render();
            }
        }
    }
}

注意

不要改變原始組件

不要試圖在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加強以前那樣使用了,更嚴重的是,若是你再用另外一個一樣會修改componentDidUpdateHOC加強它,那麼前面的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} />;
    }
  }
}

過濾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一般能夠接收多個參數,好比在RelayHOC額外接收了一個配置對象用於指定組件的數據依賴。

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工具函數,包括lodashReduxRamda

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)

不要在render方法中使用HOC

Reactdiff算法使用組件標識來肯定它是應該更新現有子樹仍是將其丟棄並掛載新子樹,若是從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";

Refs不會被傳遞

雖然高階組件的約定是將全部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} />;
  });
}

Render Props

HOC同樣,Render Props也是一直以來都存在的元老級模式,render props指在一種React組件之間使用一個值爲函數的props共享代碼的簡單技術,具備render props的組件接收一個函數,該函數返回一個React元素並調用它而不是實現一個本身的渲染邏輯,render props是一個用於告知組件須要渲染什麼內容的函數props,也是組件邏輯複用的一種實現方式,簡單來講就是在被複用的組件中,經過一個名爲render(屬性名也能夠不是render,只要值是一個函數便可)的prop屬性,該屬性是一個函數,這個函數接受一個對象並返回一個子組件,會將這個函數參數中的對象做爲props傳入給新生成的組件,而在使用調用者組件這裏,只須要決定這個組件在哪裏渲染以及該以何種邏輯渲染並傳入相關對象便可。
對比HOCRender Props,技術上,兩者都基於組件組合機制,Render Props擁有與HOC 同樣的擴展能力,稱之爲Render Props,並非說只能用來複用渲染邏輯,而是表示在這種模式下,組件是經過render()組合起來的,相似於HOC 模式下經過Wrapperrender()創建組合關係形式上,兩者很是相像,一樣都會產生一層Wrapper,而實際上Render Props HOC 甚至可以相互轉換。
一樣,Render Props也會存在一些問題:

  • 數據流向更直觀了,子孫組件能夠很明確地看到數據來源,但本質上Render Props是基於閉包實現的,大量地用於組件的複用將不可避免地引入了callback hell問題。
  • 丟失了組件的上下文,所以沒有this.props屬性,不能像HOC那樣訪問this.props.children

示例

<!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 MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0,  y: 0, }
  }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }
  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)} {/* Render Props */}
      </div>
    )
  }
}

class MouseLocation extends React.Component {
  render() {
    return (
      <>
        <h1>請在此處移動鼠標</h1>
        <p>當前鼠標的位置是: x:{this.props.mouse.x} y:{this.props.mouse.y}</p>
      </>
    )
  }
}

ReactDOM.render(
  <MouseTracker render={mouse => <MouseLocation mouse={mouse} />}></MouseTracker>, 
  document.getElementById("root")
);
</script>

</html>

Hooks

代碼複用的解決方案層出不窮,可是總體來講代碼複用仍是很複雜的,這其中很大一部分緣由在於細粒度代碼複用不該該與組件複用捆綁在一塊兒,HOCRender Props 等基於組件組合的方案,至關於先把要複用的邏輯包裝成組件,再利用組件複用機制實現邏輯複用,天然就受限於組件複用,於是出現擴展能力受限、Ref 隔斷、Wrapper Hell等問題,那麼咱們就須要有一種簡單直接的代碼複用方式,函數,將可複用邏輯抽離成函數應該是最直接、成本最低的代碼複用方式,但對於狀態邏輯,仍然須要經過一些抽象模式(如Observable)才能實現複用,這正是Hooks的思路,將函數做爲最小的代碼複用單元,同時內置一些模式以簡化狀態邏輯的複用。比起上面提到的其它方案,Hooks讓組件內邏輯複用再也不與組件複用捆綁在一塊兒,是真正在從下層去嘗試解決(組件間)細粒度邏輯的複用問題此外,這種聲明式邏輯複用方案將組件間的顯式數據流與組合思想進一步延伸到了組件內。
檔案Hooks也並不是完美,只是就目前而言,其缺點以下:

  • 額外的學習成本,主要在於Functional ComponentClass Component之間的比較上。
  • 寫法上有限制(不能出如今條件、循環中),而且寫法限制增長了重構成本。
  • 破壞了PureComponentReact.memo淺比較的性能優化效果,爲了取最新的propsstate,每次render()都要從新建立事件處函數。
  • 在閉包場景可能會引用到舊的stateprops值。
  • 內部實現上不直觀,依賴一份可變的全局狀態,再也不那麼pure
  • React.memo並不能徹底替代shouldComponentUpdate(由於拿不到state change,只針對props change)。
  • useState API設計上不太完美。

示例

<!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">
  const {useState, useEffect} = React;

  function useMouseLocation(location){
    return (
      <>
        <h1>請在此處移動鼠標</h1>
        <p>當前鼠標的位置是: x:{location.x} y:{location.y}</p>
      </>
    );
  }

  function MouseTracker(props){
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);

    function handleMouseMove(event){
        setX(event.clientX);
        setY(event.clientY);
    }
    return (
      <div onMouseMove={handleMouseMove}>
        {useMouseLocation({x, y})}
      </div>
    )
  }

  ReactDOM.render(
    <MouseTracker/>, 
    document.getElementById("root")
  );
</script>

</html>

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/38136388
https://juejin.cn/post/6844903910470057997
https://juejin.cn/post/6844903850038525959
https://my.oschina.net/u/4663041/blog/4588963
https://zh-hans.reactjs.org/docs/hooks-intro.html
https://zh-hans.reactjs.org/docs/hooks-effect.html
https://react-cn.github.io/react/docs/reusable-components.html
http://www.ayqy.net/blog/react%E7%BB%84%E4%BB%B6%E9%97%B4%E9%80%BB%E8%BE%91%E5%A4%8D%E7%94%A8/
相關文章
相關標籤/搜索