從Mixins到HOC再到React Hooks

引言

咱們都知道在業務開發的過程當中,若是徹底不一樣的組件有類似的功能,這就會產生橫切關注點(cross-cutting concerns)問題。javascript

在React中,存在一些最佳實踐去處理橫切關注點的問題,能夠幫助咱們更好地進行代碼的邏輯複用。html

Mixins

針對這個問題,在使用createReactClass建立 React 組件的時候,引入 mixins 功能會是一個很好的解決方案。java

爲了在初始階段更加容易地適應和學習React,官方在 React 中包含了一些急救方案。mixin 系統是其中之一。react

因此咱們能夠將通用共享的方法包裝成Mixins方法,而後注入各個組件進行邏輯複用的實現。編程

原理

const mixin = function(obj, mixins) {
  const newObj = obj;
  newObj.prototype = Object.create(obj.prototype);
  for (let prop in mixins) {
    if (mixins.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixins[prop];
    }
  }
  return newObj;
}
複製代碼

上述代碼就實現了一個簡單的mixin函數,其實質就是將mixins中的方法遍歷賦值給newObj.prototype,從而實現mixin返回的函數建立的對象都有mixins中的方法,也就是把額外的功能都混入進去。設計模式

在咱們大體明白了mixin做用後,讓咱們來看看如何在React使用mixin。數組

應用

var RowMixin = {
  renderHeader: function() {
    return (
      <div className='row-header'> <h1> {this.getHeaderText()} </h1> </div>
    );
  }
};

var UserRow = React.createClass({
  mixins: [RowMixin], // 混入renderHeader方法
  getHeaderText: function() {
    return this.props.user.fullName;
  },
  render: function() {
    return (
      <div> {this.renderHeader()} <h2>{this.props.user.biography}</h2> </div>
    )
  }
}); 
複製代碼

使用React.createClass,官方提供了mixins的接入口。須要複用的代碼邏輯從這裏混入就能夠。app

這是ES5的寫法,實際上React16版本後就已經廢棄了。ide

ES6 自己是不包含任何 mixin 支持。所以,當你在 React 中使用 ES6 class 時,將不支持 mixins 。函數式編程

官方也發現了不少使用 mixins 而後出現了問題的代碼庫。而且不建議在新代碼中使用它們。

缺點

Mixins Considered Harmful

  • Mixins 引入了隱式的依賴關係(Mixins introduce implicit dependencies)

  • Mixins 引發名稱衝突(Mixins cause name clashes)

  • Mixins 致使滾雪球式的複雜性(Mixins cause snowballing complexity)

引自官方博客: reactjs.org/blog/2016/0…

官方博客裏面有一篇文章詳細描述了棄用的緣由。裏面列舉了三條罪狀,如上所述。

在實際開發的過程當中,咱們沒法預知別人往代碼裏mixin了什麼屬性和狀態。若是想要mixin本身的功能,可能會發生衝突,甚至須要去解耦以前的代碼。

這樣的方式同時也破壞了組件的封裝性,代碼之間的依賴是不可見的,給重構代碼也帶來了必定的難度。若是對組件進行修改,極可能會致使mixin方法錯誤或者失效。

在日後的開發維護過程當中,就致使了滾雪球式的複雜性。

名稱衝突

組件中含有多個mixin——

  • 不一樣的mixin中含有相同名字的非生命週期函數,React會拋出異常(不是後面的函數覆蓋前面的函>數)。

  • 不一樣的mixin中含有相同名字的生命週期函數,不會拋出異常,mixin中的相同的生命週期函數(除render方法)會按照createClass中傳入的mixins數組順序依次調用,所有調用結束後再調用組件內部的相同的聲明周期函數。

  • 不一樣的mixin中默認props或初始state中存在相同的key值時,React會拋出異常。

mixin裏面對不一樣狀況名稱衝突的處理,只有當相同名稱的生命週期函數,纔會按照聲明的順序調用,最後調用組件內部的同名函數。其餘狀況下都會拋出異常。

mixin這種混入模式,會給組件不斷增長新的方法和屬性,組件自己不只能夠感知,甚至須要作相關的處理(例如命名衝突、狀態維護),一旦混入的模塊變多時,整個組件就變的難以維護,也就是爲何如此多的React庫都採用高階組件的方式進行開發。

HOC

在mixin廢棄後,不少開源組件庫都是使用的高階組件寫法。

高階組件屬於函數式編程(functional programming)思想。

對於被包裹的組件時不會感知到高階組件的存在,而高階組件返回的組件會在原來的組件之上具備功能加強的效果。

高階函數

說到高階組件,先要說一下高階函數的定義。

在數學和計算機科學中,高階函數是至少知足下列一個條件的函數:

  • 接受一個或多個函數做爲輸入

  • 輸出一個函數

簡單地來講,高階函數就是接受函數做爲輸入或者輸出的函數。

const add = (x,y,f) => f(x)+f(y);
add(-5, 6, Math.abs);
複製代碼

高階組件

A higher-order component is a function that takes a component and returns a new component.

高階組件是一個接受組件而且返回新組件的函數,注意雖然名字叫高階組件但它自身是一個函數,它能夠加強它所包裹的組件功能,或者說賦予了它所包裹的組件一個新的功能。

它不是React API的一部分,源自於React生態,是官方推崇的複用組合的一種方式。它對應着設計模式中的裝飾者模式。

高階組件,主要有兩種方式處理包裹組件的方式,分別是屬性代理和反向繼承。

屬性代理(Props Proxy)

實質上是經過包裹原來的組件來操做props

  • 操做props

  • 得到refs引用

  • 抽象state

  • 用其餘元素包裹組件

export default function withHeader(WrappedComponent) {
  return class HOC extends Component {
    render() {
      const newProps = {
        test:'hoc'
      }
      // 透傳props,而且傳遞新的newProps
      return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } } } 複製代碼

屬性代理,其實是經過包裹原來的組件,來注入一些額外的props或者state。

爲了加強可維護性,有一些固有的約定,好比命名高階組件的時候須要使用withSomething的格式。

對於傳入的props最好直接透傳,不要破壞組件自己的屬性和狀態。

反向繼承(Inheritance Inversion)

  • 渲染劫持

  • 操做props和state

export default function (WrappedComponent) {
  return class Inheritance extends WrappedComponent {
    componentDidMount() {
      // 能夠方便地獲得state,作一些更深刻的修改。
      console.log(this.state);
    }
    render() {
      return super.render();
    }
  }
}
複製代碼

反向繼承能夠經過super關鍵字獲取到父類原型對象上的全部方法(父類實例上的屬性或方法則沒法獲取)。在這種方式中,它們的關係看上去被反轉(inverse)了。

反向繼承能夠劫持渲染,能夠進行延遲渲染/條件渲染等操做。

約定

  • 約定:將不相關的 props 傳遞給被包裹的組件

  • 約定:包裝顯示名稱以便輕鬆調試

  • 約定:最大化可組合性

// 而不是這樣...
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)
複製代碼

compose能夠幫助咱們組合任意個(包括0個)高階函數,例如compose(a,b,c)返回一個新的函數d,函數d依然接受一個函數做爲入參,只不過在內部會依次調用c,b,a,從表現層對使用者保持透明。 基於這個特性,咱們即可以很是便捷地爲某個組件加強或減弱其特徵,只須要去變動compose函數裏的參數個數便可。

應用場景

  • 模塊複用

  • 頁面鑑權

  • 日誌及性能打點

例子

export const withTimer = (interval) => (wrappedComponent) => {

  return class extends wrappedComponent {
    constructor(props) {
      super(props);
    }
    // 傳入endTime 計算剩餘時間戳
    endTimeStamp = DateUtils.parseDate(this.props.endTime).getTime();

    componentWillMount() {
      // 未過時則手動調用計時器 開始倒計時
      if (Date.now() < this.endTimeStamp) {
        this.onTimeChange();
        this.setState({expired: false});
        this.__timer = setInterval(this.onTimeChange, interval);
      }
    }

    componentWillUnmount() {
      // 清理計時器
      clearInterval(this.__timer);
    }

    onTimeChange = () => {
      const now = Date.now();
      // 根據剩餘時間戳計算出 時、分、秒注入到目標組件
      const ret = Helper.calc(now, this.endTimeStamp);
      if (ret) {
        this.setState(ret);
      } else {
        clearInterval(this.__timer);
        this.setState({expired: true});
      }
    }

    render() {
      // 反向繼承
      return super.render();
    }
  };
};

複製代碼
@withTimer()
export class Card extends React.PureComponent {
  render() {
    const {data, endTime} = this.props;
    // 直接取用hoc注入的狀態
    const {expired, minute, second} = this.state;
    // 略去render邏輯
    return (...);
  }
}


複製代碼

需求是須要進行定時器倒計時,不少組件都須要注入倒計時功能。那麼咱們把它提取爲一個高階組件。

這是一個反向繼承的方式,能夠拿到組件自己的屬性和狀態,而後把時分秒等狀態注入到了組件中。

原組件使用了ES7的裝飾器語法,就能夠增強它的功能。

組件自己只須要有一個endTime的屬性,而後高階組件就能夠計算出時分秒而且進行倒計時。

也就是說,高階組件賦予了原組件倒計時的功能。

注意

在使用高階組件寫法時,也有一些注意事項。

  • 不要在render函數中使用高階組件
render() {
  // 每次調用 render 函數都會建立一個新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做!
  return <EnhancedComponent />; } 複製代碼

若是在render函數中建立,每次都會從新渲染一個新的組件。這不只僅是性能問題,每次重置該組件的狀態,也可能會引發代碼邏輯錯誤。

  • 靜態方法必須複製
// 定義靜態函數
WrappedComponent.staticMethod = function() {/*...*/}
// 如今使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 加強組件沒有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼

當你將 HOC 應用於組件時,原始組件將使用容器組件進行包裝。這意味着新組件沒有原始組件的任何靜態方法。

你可使用hoist-non-react-statics自動拷貝全部非 React 靜態方法:

  • Refs不會被傳遞

通常來講,高階組件能夠傳遞全部的props屬性給包裹的組件,可是不能傳遞 refs 引用。由於並非像 key 同樣,refs 是一個僞屬性,React 對它進行了特殊處理。

若是你向一個由高級組件建立的組件的元素添加 ref 應用,那麼 ref 指向的是最外層容器組件實例的,而不是包裹組件。

React Hooks

在不編寫class的狀況下使用state以及其餘的React特性。

Hook是一些可讓你在函數組件hook react state及生命週期等特性的函數。它不能在class組件中使用。

動機

  • 在組件之間複用狀態邏輯

    • render props

      任何被用於告知組件須要渲染什麼內容的函數props在技術上均可以被成爲稱爲render prop

      若是在render方法裏建立匿名函數,那麼使用render prop會抵消使用React.PureComponent帶來的優點。 須要把render方法建立爲實例函數,或者做爲全局變量傳入。

    • hoc

    • providers

    • consumers

    這些抽象層組成的組件會造成嵌套地獄,所以React須要爲共享狀態邏輯提供更好的原生途徑。

  • 加強代碼可維護性

  • class難以理解

React社區接受了React hooks的提案,這將減小編寫 React 應用時須要考慮的概念數量。

Hooks 可使得你始終使用函數,而沒必要在函數、類、高階組件和 reader props之間不斷切換。

Hooks

  • 基礎 Hook

    • useState

    • useEffect

      啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。

    • useContext

  • 額外的 Hook

    • useReducer

    • useCallback

    • useMemo

    • useRef

    • useImperativeHandle

    • useLayoutEffect

    • useDebugValue

  • 自定義Hook useSomething

    自定義Hook是一種重用狀態邏輯的機制,全部的state和反作用都是徹底隔離的。

官方已經棄用了一些生命週期,useEffect至關於componentDidMountcomponentDidUpdatecomponentWillUnmount

除了官方提供的Hook API之外,你可使用自定義Hook。

自定義 Hook 不須要具備特殊的標識。咱們能夠自由的決定它的參數是什麼,以及它應該返回什麼(若是須要的話)。

換句話說,它就像一個正常的函數。可是它的名字應該始終以 use 開頭,這樣能夠一眼看出其符合 Hook 的規則。

動畫、訂閱聲明、計時器是自定義Hook的一些經常使用操做。

接下來,咱們來用React Hook改寫一下以前的高階組件demo。

例子

export function useTimer(endTime, interval, callback) {
  interval = interval || 1000;
  
  // 使用useState Hook get/set狀態
  const [expired, setExpired] = useState(true);
  const endTimeStamp = DateUtils.parseDate(endTime).getTime();

  function _onTimeChange () {
    const now = Date.now();
    // 計算時分秒
    const ret = Helper.calc(now, endTimeStamp);
    if (ret) {
      // 回調傳出所需的狀態
      callback({...ret, expired});
    } else {
      clearInterval(this.__timer);
      setExpired(true);
      callback({expired});
    }
  }

  // 使用useEffect代替生命週期的調用
  useEffect(() => {
    if (Date.now() < endTimeStamp) {
      _onTimeChange();
      setExpired(false);
      this.__timer = setInterval(_onTimeChange, interval);
    }

    return () => {
      // 清除計時器
      clearInterval(this.__timer);
    }
  })
} 
複製代碼
export function Card (props) {
  const {data, endTime} = props;
  const [expired, setExpired] = useState(true);
  const [minute, setMinute] = useState(0);
  const [second, setSecond] = useState(0);

  useTimer(endTime, 1000, ({expired, minute, second}) => {
    setExpired(expired);
    setMinute(minute);
    setSecond(second);
  });
  return (...);
複製代碼

自定義Hook除了命名須要遵循規則,參數傳入和返回結果均可以根據具體狀況來定。

這裏,我在定時器每秒返回後傳出了一個callback,把時分秒等參數傳出。

除此以外能夠看到沒有class的生命週期,使用useEffect來完成反作用的操做。

約定

使用一個eslint-plugin-react-hooksESLint插件來強制執行這些規則

  1. 只在最頂層使用 Hook

不要在循環條件嵌套函數中調用 Hook, 確保老是在React 函數的最頂層調用他們。

由於React是根據你聲明的順序去調用hooks的,若是不在最頂層調用,那麼不能保證每次渲染的順序都是相同的。

遵照規則,React 纔可以在屢次的 useState 和 useEffect 調用之間保持 hook 狀態的正確。

  1. 只在 React 函數中調用 Hook

    • 在 React 的函數組件中調用 Hook

    • 在自定義 Hook 中調用其餘 Hook

參考

React官方文檔

Mixins Considered Harmful

深刻淺出React高階組件

React 高階組件(HOC)入門指南

Making Sense of React Hooks

相關文章
相關標籤/搜索