裝飾者模式的應用:react高階組件和ES6 裝飾器

一 裝飾者模式

優先使用對象組合而不是類繼承。 --《設計模式》

1.什麼是裝飾者模式javascript

定義:動態的給對象添加一些額外的屬性或行爲。相比於使用繼承,裝飾者模式更加靈活。html

2.裝飾者模式參與者java

Component:裝飾者和被裝飾者共同的父類,是一個接口或者抽象類,用來定義基本行爲
ConcreteComponent:定義具體對象,即被裝飾者
Decorator:抽象裝飾者,繼承自Component,從外類來擴展ConcreteComponent。對於ConcreteComponent來講,不須要知道Decorator的存在,Decorator是一個接口或抽象類
ConcreteDecorator:具體裝飾者,用於擴展ConcreteComponent
注:裝飾者和被裝飾者對象有相同的超類型,由於裝飾者和被裝飾者必須是同樣的類型,這裏利用繼承是爲了達到類型匹配,而不是利用繼承得到行爲。react

利用繼承設計子類,只能在編譯時靜態決定,而且全部子類都會繼承相同的行爲;利用組合的作法擴展對象,就能夠在運行時動態的進行擴展。裝飾者模式遵循開放-關閉原則:類應該對擴展開放,對修改關閉。利用裝飾者,咱們能夠實現新的裝飾者增長新的行爲而不用修改現有代碼,而若是單純依賴繼承,每當須要新行爲時,還得修改現有的代碼。git

3.javascript 如何使用裝飾者模式
javascript 動態語言的特性使得使用裝飾器模式十分的簡單,文章主要內容會介紹兩種使用裝飾者模式的實際例子。es6

二 react高階組件

咱們都知道高階函數是什麼, 高階組件實際上是差很少的用法,只不過傳入的參數變成了react組件,並返回一個新的組件.github

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

形如:編程

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高階組件是react應用中很重要的一部分,最大的特色就是重用組件邏輯。它並非由React API定義出來的功能,而是由React的組合特性衍生出來的一種設計模式。
若是你用過redux,那你就必定接觸太高階組件,由於react-redux中的connect就是一個高階組件。redux

先來一個最簡單的高階組件設計模式

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

class Usual extends Component {
  render() {
    console.log(this.props, 'props');
    return (
      <div>
        Usual
      </div>
    )
  }
}
export default simpleHoc(Usual);
import React, { Component } from 'react';

const simpleHoc = WrappedComponent => {
  console.log('simpleHoc');
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

export default simpleHoc;

組件Usual經過simpleHoc的包裝,打了一個log... 那麼形如simpleHoc就是一個高階組件了,經過接收一個組件class Usual,並返回一個組件class。 其實咱們能夠看到,在這個函數裏,咱們能夠作不少操做。 並且return的組件一樣有本身的生命週期,function,另外,咱們看到也能夠把props傳給WrappedComponent(被包裝的組件)。

實現高階組件的方法有兩種
 屬性代理(props proxy)。高階組件經過被包裹的 React 組件來操做 props。
 反向繼承(inheritance inversion)。高階組件繼承於被包裹的 React 組件。

屬性代理
引入裏咱們寫的最簡單的形式,就是屬性代理(Props Proxy)的形式。經過hoc包裝wrappedComponent,也就是例子中的Usual,原本傳給Usual的props,都在hoc中接受到了,也就是props proxy。 由此咱們能夠作一些操做

1.操做props
最直觀的就是接受到props,咱們能夠作任何讀取,編輯,刪除的不少自定義操做。包括hoc中定義的自定義事件,均可以經過props再傳下去。

import React, { Component } from 'react';

const propsProxyHoc = WrappedComponent => class extends Component {

  handleClick() {
    console.log('click');
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      handleClick={this.handleClick}
    />);
  }
};
export default propsProxyHoc;

而後咱們的Usual組件render的時候, console.log(this.props) 會獲得handleClick.

2.refs獲取組件實例
當咱們包裝Usual的時候,想獲取到它的實例怎麼辦,能夠經過引用(ref),在Usual組件掛載的時候,會執行ref的回調函數,在hoc中取到組件的實例。

import React, { Component } from 'react';

const refHoc = WrappedComponent => class extends Component {

  componentDidMount() {
    console.log(this.instanceComponent, 'instanceComponent');
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      ref={instanceComponent => this.instanceComponent = instanceComponent}
    />);
  }
};

export default refHoc;

3.抽離state
這裏不是經過ref獲取state, 而是經過 { props, 回調函數 } 傳遞給wrappedComponent組件,經過回調函數獲取state。這裏用的比較多的就是react處理表單的時候。一般react在處理表單的時候,通常使用的是受控組件(文檔),即把input都作成受控的,改變value的時候,用onChange事件同步到state中。固然這種操做經過Container組件也能夠作到,具體的區別放到後面去比較。看一下代碼就知道怎麼回事了:

import React, { Component } from 'React';
const MyContainer = (WrappedComponent) => class extends Component {
    constructor(props) { 
        super(props); 
        this.state = {
              name: '', 4 
        };
        this.onNameChange = this.onNameChange.bind(this); 
    }
    onNameChange(event) { 
        this.setState({
            name: event.target.value, 
        })
    }
    render() {
        const newProps = {
            name: {
                value: this.state.name, 
                onChange: this.onNameChange,
            },
        } 
        return <WrappedComponent {...this.props} {...newProps} />; 
    }
}

在這個例子中,咱們把 input 組件中對 name prop 的 onChange 方法提取到高階組件中,這樣就有效地抽象了一樣的 state 操做。

反向繼承

const MyContainer = (WrappedComponent) => 
class extends WrappedComponent {
    render() {
        return super.render();
    } 
}

正如所見,高階組件返回的組件繼承於 WrappedComponent。由於被動地繼承了 WrappedCom- ponent,全部的調用都會反向,這也是這種方法的由來。
這種方法與屬性代理不太同樣。它經過繼承 WrappedComponent 來實現,方法能夠經過 super 來順序調用。由於依賴於繼承的機制,HOC 的調用順序和隊列是同樣的:

didmount→HOC didmount→(HOCs didmount)→will unmount→HOC will unmount→(HOCs will unmount)

在反向繼承方法中,高階組件可使用 WrappedComponent 引用,這意味着它可使用WrappedComponent 的 state、props 、生命週期和 render 方法。但它不能保證完整的子組件樹被解析。

1.渲染劫持
渲染劫持指的就是高階組件能夠控制 WrappedComponent 的渲染過程,並渲染各類各樣的結 果。咱們能夠在這個過程當中在任何 React 元素輸出的結果中讀取、增長、修改、刪除 props,或 讀取或修改 React 元素樹,或條件顯示元素樹,又或是用樣式控制包裹元素樹。
正如以前說到的,反向繼承不能保證完整的子組件樹被解析,這意味着將限制渲染劫持功能。 渲染劫持的經驗法則是咱們能夠操控 WrappedComponent 的元素樹,並輸出正確的結果。但若是 元素樹中包括了函數類型的 React 組件,就不能操做組件的子組件。
咱們先來看條件渲染的示例:

const MyContainer = (WrappedComponent) => 
class extends WrappedComponent {
  render() {
    if (this.props.loggedIn) {
        return super.render(); 
    } else {
        return null;
     }
   }
 }

第二個示例是咱們能夠對 render 的輸出結果進行修改:

const MyContainer = (WrappedComponent) => class extends WrappedComponent {
  render() {
    const elementsTree = super.render();
    let newProps = {};
    if (elementsTree && elementsTree.type === 'input') { 
        newProps = {value: 'may the force be with you'};
    }
    const props = Object.assign({}, elementsTree.props, newProps);
    const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children); 
    return newElementsTree;
  } 
}

在這個例子中,WrappedComponent 的渲染結果中,頂層的 input 組件的 value 被改寫爲 may the force be with you。所以,咱們能夠作各類各樣的事,甚至能夠反轉元素樹,或是改變元素 樹中的 props。這也是 Radium 庫構造的方法。

2.控制state
高階組件能夠讀取、修改或刪除 WrappedComponent 實例中的 state,若是須要的話,也能夠 增長 state。但這樣作,可能會讓 WrappedComponent 組件內部狀態變得一團糟。大部分的高階組 件都應該限制讀取或增長 state,尤爲是後者,能夠經過從新命名 state,以防止混淆。
咱們來看一個例子:

const MyContainer = (WrappedComponent) => class extends WrappedComponent {
 render() { 
    return (
        <div>
        <h2>HOC Debugger Component</h2>
        <p>Props</p> 
        <pre>{JSON.stringify(this.props, null, 2)}</pre> 
        <p>State</p>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>                 
        {super.render()}
        </div> );
     } 
 }

在這個例子中,顯示了 WrappedComponent 的 props 和 state,以方便咱們在程序中去調試它們。

三 ES6 裝飾器

高階組件能夠看作是裝飾器模式(Decorator Pattern)在React的實現。即容許向一個現有的對象添加新的功能,同時又不改變其結構,屬於包裝模式(Wrapper Pattern)的一種
ES7中添加了一個decorator的屬性,使用@符表示,能夠更精簡的書寫。那上面的例子就能夠改爲:

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

@simpleHoc
export default class Usual extends Component {
  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}

//simple-hoc
const simpleHoc = WrappedComponent => {
  console.log('simpleHoc');
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

和高階組件是一樣的效果。

類的裝飾

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

上面代碼中,@testable 就是一個裝飾器。它修改了 MyTestableClass這 個類的行爲,爲它加上了靜態屬性isTestable。testable 函數的參數 target 是 MyTestableClass 類自己。

若是以爲一個參數不夠用,能夠在裝飾器外面再封裝一層函數。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上面代碼中,裝飾器 testable 能夠接受參數,這就等於能夠修改裝飾器的行爲。

方法的裝飾
裝飾器不只能夠裝飾類,還能夠裝飾類的屬性。

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

上面代碼中,裝飾器 readonly 用來裝飾「類」的name方法。
裝飾器函數 readonly 一共能夠接受三個參數。

function readonly(target, name, descriptor){
  // descriptor對象原來的值以下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 相似於
Object.defineProperty(Person.prototype, 'name', descriptor);

裝飾器第一個參數是 類的原型對象,上例是 Person.prototype,裝飾器的本意是要「裝飾」類的實例,可是這個時候實例還沒生成,因此只能去裝飾原型(這不一樣於類的裝飾,那種狀況時target參數指的是類自己);
第二個參數是 所要裝飾的屬性名
第三個參數是 該屬性的描述對象
另外,上面代碼說明,裝飾器(readonly)會修改屬性的 描述對象(descriptor),而後被修改的描述對象再用來定義屬性。

四 更加抽象的裝飾

ES5 中,mixin 爲 object 提供功能「混合」能力,因爲 JavaScript 的原型繼承機制,經過 mixin 一個或多個對象到構造器的 prototype上,可以間接提供爲「類」的實例混合功能的能力。

下面是例子:

function mixin(...objs){
    return objs.reduce((dest, src) => {
        for (var key in src) {
            dest[key] = src[key]
        }
        return dest;    
    });
}

function createWithPrototype(Cls){
    var P = function(){};
    P.prototype = Cls.prototype;
    return new P();
}

function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
}

function Employee(name, age, gender, level, salary){
    Person.call(this, name, age, gender);
    this.level = level;
    this.salary = salary;
}

Employee.prototype = createWithPrototype(Person);

mixin(Employee.prototype, {
    getSalary: function(){
        return this.salary;
    }
});

function Serializable(Cls, serializer){
    mixin(Cls, serializer);
    this.toString = function(){
        return Cls.stringify(this);
    } 
}

mixin(Employee.prototype, new Serializable(Employee, {
        parse: function(str){
            var data = JSON.parse(str);
            return new Employee(
                data.name,
                data.age,
                data.gender,
                data.level,
                data.salary
            );
        },
        stringify: function(employee){
            return JSON.stringify({
                name: employee.name,
                age: employee.age,
                gender: employee.gender,
                level: employee.level,
                salary: employee.salary
            });
        }
    })
);

從必定程度上,mixin 彌補了 JavaScript 單一原型鏈的缺陷,能夠實現相似於多重繼承的效果。在上面的例子裏,咱們讓 Employee 「繼承」 Person,同時也「繼承」 Serializable。有趣的是咱們經過 mixin Serializable 讓 Employee 擁有了 stringify 和 parse 兩個方法,同時咱們改寫了 Employee 實例的 toString 方法。

咱們能夠以下使用上面定義的類:

var employee = new Employee("jane",25,"f",1,1000);
var employee2 = Employee.parse(employee+""); //經過序列化反序列化複製對象

console.log(employee2, 
    employee2 instanceof Employee,    //true 
    employee2 instanceof Person,    //true
    employee == employee2);        //false

ES6 中的 mixin 式繼承
在 ES6 中,咱們能夠採用全新的基於類繼承的 「mixin」 模式設計更優雅的「語義化」接口,這是由於 ES6 中的 extends 能夠繼承動態構造的類,這一點和其餘的靜態聲明類的編程語言不一樣,在說明它的好處以前,咱們先看一下 ES6 中如何更好地實現上面 ES5 代碼裏的 Serializable:

用繼承實現 Serializable

class Serializable{
  constructor(){
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

class Person extends Serializable{
  constructor(name, age, gender){
    super();
    Object.assign(this, {name, age, gender});
  }
}

class Employee extends Person{
  constructor(name, age, gender, level, salary){
    super(name, age, gender);
    this.level = level;
    this.salary = salary;
  }
  static stringify(employee){
    let {name, age, gender, level, salary} = employee;
    return JSON.stringify({name, age, gender, level, salary});
  }
  static parse(str){
    let {name, age, gender, level, salary} = JSON.parse(str);
    return new Employee(name, age, gender, level, salary);
  }
}

let employee = new Employee("jane",25,"f",1,1000);
let employee2 = Employee.parse(employee+""); //經過序列化反序列化複製對象

console.log(employee2, 
  employee2 instanceof Employee,  //true 
  employee2 instanceof Person,  //true
  employee == employee2);   //false
上面的代碼,咱們用 ES6 的類繼承實現了 Serializable,與 ES5 的實現相比,它很是簡單,首先咱們設計了一個 Serializable 類:

class Serializable{
  constructor(){
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

它檢查當前實例的類上是否有定義 stringify 和 parse 靜態方法,若是有,使用靜態方法重寫 toString 方法,若是沒有,則在實例化對象的時候拋出一個異常。

這麼設計挺好的,但它也有不足之處,首先注意到咱們將 stringify 和 parse 定義到 Employee 上,這沒有什麼問題,可是若是咱們實例化 Person,它將報錯:

let person = new Person("john", 22, "m");
//Uncaught ReferenceError: Please define stringify method to the Class!

這是由於咱們沒有在 Person 上定義 parse 和 stringify 方法。由於 Serializable 是一個基類,在只支持單繼承的 ES6 中,若是咱們不須要 Person 可序列化,只須要 Person 的子類 Employee 可序列化,靠這種繼承鏈是作不到的。

另外,如何用 Serializable 讓 JS 原生類的子類(好比 Set、Map)可序列化?

因此,咱們須要考慮改變一下咱們的設計模式:

用 mixin 實現 Serilizable

const Serializable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

class Person {
  constructor(name, age, gender){
    Object.assign(this, {name, age, gender});
  }
}

class Employee extends Serializable(Person){
  constructor(name, age, gender, level, salary){
    super(name, age, gender);
    this.level = level;
    this.salary = salary;
  }
  static stringify(employee){
    let {name, age, gender, level, salary} = employee;
    return JSON.stringify({name, age, gender, level, salary});
  }
  static parse(str){
    let {name, age, gender, level, salary} = JSON.parse(str);
    return new Employee(name, age, gender, level, salary);
  }
}

let employee = new Employee("jane",25,"f",1,1000);
let employee2 = Employee.parse(employee+""); //經過序列化反序列化複製對象

console.log(employee2, 
  employee2 instanceof Employee,  //true 
  employee2 instanceof Person,  //true
  employee == employee2);   //false

在上面的代碼裏,咱們改變了 Serializable,讓它成爲一個動態返回類型的函數,而後咱們經過 class Employ extends Serializable(Person) 來實現可序列化,在這裏咱們沒有可序列化 Person 自己,而將 Serializable 在語義上變成一種修飾,即 Employee 是一種可序列化的 Person。因而,咱們要 new Person 就不會報錯了:

let person = new Person("john", 22, "m"); 
//Person {name: "john", age: 22, gender: "m"}

這麼作了以後,咱們還能夠實現對原生類的繼承,例如:

繼承原生的 Set 類

const Serializable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

class MySet extends Serializable(Set){
  static stringify(s){
    return JSON.stringify([...s]);
  }
  static parse(data){
    return new MySet(JSON.parse(data));
  }
}

let s1 = new MySet([1,2,3,4]);
let s2 = MySet.parse(s1 + "");
console.log(s2,         //Set{1,2,3,4}
            s1 == s2);  //false

經過 MySet 繼承 Serializable(Set),咱們獲得了一個可序列化的 Set 類!一樣咱們還能夠實現可序列化的 Map:

class MyMap extends Serializable(Map){
    ...
    static stringify(map){
        ...
    }
    static parse(str){
        ...
    }
}

若是不用 mixin 模式而使用繼承,咱們就得分別定義不一樣的類來對應 Set 和 Map 的繼承,而用了 mixin 模式,咱們構造出了通用的 Serializable,它能夠用來「修飾」任何對象。

咱們還能夠定義其餘的「修飾符」,而後將它們組合使用,好比:

const Serializable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    if(typeof this.constructor.stringify !== "function"){
      throw new ReferenceError("Please define stringify method to the Class!");
    }
    if(typeof this.constructor.parse !== "function"){
      throw new ReferenceError("Please define parse method to the Class!");
    }
  }
  toString(){
    return this.constructor.stringify(this);
  }
}

const Immutable = Sup => class extends Sup {
  constructor(...args){
    super(...args);
    Object.freeze(this);
  }
}

class MyArray extends Immutable(Serializable(Array)){
  static stringify(arr){
    return JSON.stringify({Immutable:arr});
  }
  static parse(data){
    return new MyArray(...JSON.parse(data).Immutable);
  }
}

let arr1 = new MyArray(1,2,3,4);
let arr2 = MyArray.parse(arr1 + "");
console.log(arr1, arr2, 
    arr1+"",     //{"Immutable":[1,2,3,4]}
    arr1 == arr2);

arr1.push(5); //throw Error!

上面的例子裏,咱們經過 Immutable 修飾符定義了一個不可變數組,同時經過 Serializable 修飾符修改了它的序列化存儲方式,而這一切,經過定義 class MyArray extends Immutable(Serializable(Array)) 來實現。

五 參考

react高階組件
類的裝飾器:ES6 中優雅的 mixin 式繼承

相關文章
相關標籤/搜索