學習es7的Decorator(順帶寫個react高階組件)

1. Decorator基本知識

在不少框架和庫中看到它的身影,尤爲是React和Redux,還有mobx中,那什麼是裝飾器呢。react

修飾器(Decorator)是一個函數,用來修改類的行爲。不是很理解這種抽象概念,仍是看代碼講解實際些。git

//定義一個函數,也就是定義一個Decorator,target參數就是傳進來的Class。
//這裏是爲類添加了一個靜態屬性
function addAge(target) {
  target.age = 2;
}

//在Decorator後面跟着Class,Decorator是函數的話,怎麼不是addAge(MyGeekjcProject)這樣寫呢?
//我只能這樣理解:由於語法就這樣,只要Decorator後面是Class,默認就已經把Class當成參數隱形傳進Decorator了(就是所謂的語法糖)。
@addAge
class MyGeekjcProject {}

console.log(MyGeekjcProject.age) // 2

1.1 傳參修飾類

但上面這樣只傳了一個Class做爲參數不夠靈活怎麼辦?
咱們能夠在外層套一個函數,只要最後返回的是一個Decorator便可,管你套多少個函數傳多少個參數都無所謂。github

function addAge(age) {
  return function(target) {
    target.age = age;
  }
}

//注意這裏,隱形傳入了Class,語法相似於addAge(2)(MyGeekjcProject)
@testable(2)
class MyGeekjcProject {}
MyGeekjcProject.age // 2

@addAge(3)
class MyGeekjcProject {}
MyGeekjcProject.age // 3

1.2 修飾類的屬性

下面是修改類的prototype對象編程

function description(target) {
  target.prototype.url = 'https://www.geekjc.com';
}

@description
class MyGeekjcProject {}

let geekjc = new MyGeekjcProject();
geekjc.url // https://www.geekjc.com

概念大概理解了,修飾器不只能夠修飾類,還能夠修飾類的屬性redux

//假如修飾類的屬性則傳入三個參數,對應Object.defineProperty()裏三個參數,具體不細說
//target爲目標對象,對應爲Class的實例
//name爲所要修飾的屬性名,這裏就是修飾器緊跟其後的name屬性
//descriptor爲該屬性的描述對象
//這裏的Decorator做用是使name屬性不可寫,並返回修改後的descriptor
function readonly(target, name, descriptor){
  descriptor.writable = false;
  return descriptor;
}

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

關於Object.defineProperty()能夠看文章學習Object.defineProperty緩存

再看一個複雜的例子app

//定義一個Class並在其add上使用了修飾器
class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

//定義一個修飾器
function log(target, name, descriptor) {
  //這裏是緩存舊的方法,也就是上面那個add()原始方法
  var oldValue = descriptor.value;

  //這裏修改了方法,使其做用變成一個打印函數
  //最後依舊返回舊的方法,真是巧妙
  descriptor.value = function() {
    console.log(`Calling "${name}" with`, arguments);
    return oldValue.apply(null, arguments);
  };

  return descriptor;
}

const math = new Math();
math.add(2, 4);

看完上面的代碼就知道decorator有什麼用了,修飾器不只能夠修飾類的行爲,還能夠修飾類的屬性。框架

2. react高階組件

更通俗地描述爲,高階組件經過包裹(wrapped)被傳入的React組件,通過一系列處理,最終返回一個相對加強(enhanced)的React組件,供其餘組件調用。編程語言

3. 實現一個高階組件

面咱們來實現一個最簡單的高階組件(函數),它接受一個React組件,包裹後而後返回。函數式編程

export default function withDescription(WrappedComponent) {
  return class HOC extends Component {
    render() {
      return <div>
        <div className="geekjc-description">
          極客教程(https://www.geekjc.com)致力於推廣各類編程語言技術,也爲了將來數字化世界,讓人更容易找到操做數字化的方式,爲了將來而生的編程學習平臺,
          大部分資源是徹底免費的,而且會根據當前互聯網的變化實時更新本站內容。
        </div>
        <WrappedComponent {...this.props}/>
      </div>
    }
  }
}

在其餘組件裏,咱們引用這個高階組件,用來強化它。

@withDescription
export default class Geekjc extends Component {
  render() {
    return (
      <div>
        我是一個普通組件
      </div>
    );
  }
}

在這裏使用了ES7裏的decorator(能夠看第一節的Decorator),來提高寫法上的優雅,可是實際上它只是一個語法糖,下面這種寫法也是能夠的。

const EnhanceDemo = withDescription(Geekjc);

隨後,觀察React組件樹發生了什麼變化,如圖所示,能夠發現Geekjc組件被HOC組件包裹起來了,符合了高階組件的預期,即組件是層層包裹起來的,如同洋蔥同樣。

2018102511.png

可是隨之帶來的問題是,若是這個高階組件被使用了屢次,那麼在調試的時候,將會看到一大堆HOC,因此這個時候須要作一點小優化,就是在高階組件包裹後,應當保留其原有名稱。

咱們改寫一下上述的高階組件代碼,增長了getDisplayName函數以及靜態屬性displayName,此時再去觀察DOM Tree

function getDisplayName(component) {
  return component.displayName || component.name || 'Component';
}
export default function (WrappedComponent) {
  return class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return <div>
        <div className="geekjc-description">
          極客教程(https://www.geekjc.com)致力於推廣各類編程語言技術,也爲了將來數字化世界,讓人更容易找到操做數字化的方式,爲了將來而生的編程學習平臺,
          大部分資源是徹底免費的,而且會根據當前互聯網的變化實時更新本站內容。
        </div>
        <WrappedComponent {...this.props}/>
      </div>
    }
  }
}

2018102512.png

此時,本來組件的名稱正確地顯示在了DOM Tree上。

這個簡單的例子裏高階組件只作了一件事,那即是爲被包裹的組件添加一個標題樣式。這個高階組件能夠用到任何一個須要添加此邏輯的組件上,只須要被此高階組件修飾便可。

由此能夠看出,高階組件的主要功能是封裝並抽離組件的通用邏輯,讓此部分邏輯在組件間更好地被複用。

4. 高階組件的進階用法

4.1 組件參數

仍是以上述例子爲例,此高階組件僅僅只是展現了關於網站的描述,可是爲了更好的抽象,此描述應當能夠被參數化,以下方式調用。

@withDescription('歡迎你們訪問收藏(https://www.geekjc.com)') 
export default class Geekjc extends Component {
  render() {
    return (
      //...
    );
  }
}

withDescription須要被改寫成以下形式,它接受一個參數,而後返回一個高階組件(函數)。

export default function (description) {
  return function (WrappedComponent) {
    return class HOC extends Component {
      render() {
        return <div>
          <div className="geekjc-description">
            {description
              ?description
              : '我是描述'}
          </div>
          <WrappedComponent {...this.props}/>
        </div>
      }
    }
  }
}

使用ES6寫法能夠更加簡潔。

export default(description) => (WrappedComponent) => class HOC extends Component {
  render() {
    return <div>
      <div className="geekjc-description">
        {description
          ? description
          : '我是描述'}
      </div>
      <WrappedComponent {...this.props}/>
    </div>
  }
}

如圖能夠看到,傳入的參數已經反映在DOM Tree裏了。

2018102513.png

柯里化 Curry
概念:只傳遞函數的一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
函數簽名:fun(params)(otherParams)
應用:在React裏,經過柯里化,咱們能夠經過傳入不一樣的參數來獲得不一樣的高階組件。

4.2 基於屬性代理的方式

屬性代理是最多見的高階組件的使用方式,上述描述的高階組件就是這種方式。它經過作一些操做,將被包裹組件的props和新生成的props一塊兒傳遞給此組件,這稱之爲屬性代理。

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

4.3 基於反向繼承的方式

這種方式返回的React組件繼承了被傳入的組件,因此它可以訪問到的區域、權限更多,相比屬性代理方式,它更像打入組織內部,對其進行修改。具體的能夠參考附錄裏提供的連接進行深刻學習。

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

4.4 組合多個高階組件

上述高階組件爲React組件加強了一個功能,若是須要同時增長多個功能須要怎麼作?這種場景很是常見,例如我既須要增長一個組件標題,又須要在此組件未加載完成時顯示Loading。

@withDescription
@withLoading
class Demo extends Component{

}

使用compose能夠簡化上述過程,也能體現函數式編程的思想。

const enhance = compose(withHeader,withLoading);
@enhance
class Demo extends Component{

}
組合 Compose
compose能夠幫助咱們組合任意個(包括0個)高階函數,例如compose(a,b,c)返回一個新的函數d,函數d依然接受一個函數做爲入參,只不過在內部會依次調用c,b,a,從表現層對使用者保持透明。
基於這個特性,咱們即可以很是便捷地爲某個組件加強或減弱其特徵,只須要去變動compose函數裏的參數個數即可。
compose函數實現方式有不少種,這裏推薦其中一個recompact.compose,詳情見下方參考類庫,也能夠看我以前寫的一篇文章 reduce與redux中compose函數

5. 與父組件區別

高階組件做爲一個函數,它能夠更加純粹地關注業務邏輯層面的代碼,好比數據處理,數據校驗,發送請求等,能夠改善目前代碼裏業務邏輯和UI邏輯混雜在一塊兒的現狀。父組件則是UI層的東西,咱們先前常常把一些業務邏輯處理放在父組件裏,這樣會形成父組件混亂的狀況。爲了代碼進一步解耦,能夠考慮使用高階組件這種模式。

6. 開源的高階組件使用

6.1 recompact

recompact提供了一系列使用的高階組件,能夠加強組件的行爲,能夠利用此庫學習高階組件的寫法。

import recompact from 'recompact'
import { pure, withProps } from 'recompact'

const enhance = recompact.compose(
  withProps({ className: 'beautiful' }),
  pure,
)
@enhance
class Demo extends Component{

}

6.2 React Sortable

經過使用此庫提供的高階組件,能夠方便地讓列表元素可拖動。

7. 參考

相關文章
相關標籤/搜索