资讯专栏INFORMATION COLUMN

装饰者模式的应用:react高阶组件和ES6 装饰器

YuboonaZhang / 1954人阅读

摘要:装饰者模式参与者装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为定义具体对象,即被装饰者抽象装饰者,继承自,从外类来扩展。三装饰器高阶组件可以看做是装饰器模式在的实现。

一 装饰者模式
优先使用对象组合而不是类继承。 --《设计模式》

1.什么是装饰者模式

定义:动态的给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。

2.装饰者模式参与者

Component:装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为
ConcreteComponent:定义具体对象,即被装饰者
Decorator:抽象装饰者,继承自Component,从外类来扩展ConcreteComponent。对于ConcreteComponent来说,不需要知道Decorator的存在,Decorator是一个接口或抽象类
ConcreteDecorator:具体装饰者,用于扩展ConcreteComponent
注:装饰者和被装饰者对象有相同的超类型,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。

利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:类应该对扩展开放,对修改关闭。利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。

3.javascript 如何使用装饰者模式
javascript 动态语言的特性使得使用装饰器模式十分的简单,文章主要内容会介绍两种使用装饰者模式的实际例子。

二 react高阶组件

我们都知道高阶函数是什么, 高阶组件其实是差不多的用法,只不过传入的参数变成了react组件,并返回一个新的组件.

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就是一个高阶组件。

先来一个最简单的高阶组件

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

class Usual extends Component {
  render() {
    console.log(this.props, "props");
    return (
      
Usual
) } } export default simpleHoc(Usual); import React, { Component } from "react"; const simpleHoc = WrappedComponent => { console.log("simpleHoc"); return class extends Component { render() { return } } } 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 ();
  }
};
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 ( 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 ; 
    }
}

在这个例子中,我们把 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 (
        

HOC Debugger Component

Props

{JSON.stringify(this.props, null, 2)}

State

{JSON.stringify(this.state, null, 2)}
{super.render()}
); } }

在这个例子中,显示了 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 (
      
Usual
) } } //simple-hoc const simpleHoc = WrappedComponent => { console.log("simpleHoc"); return class extends Component { render() { return } } }

和高阶组件是同样的效果。

类的装饰

@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 式继承

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108764.html

相关文章

  • 装饰模式能做什么?

    摘要:自行车的基类如下其它方法那么我们可以先创建一个装饰者模式基类这个基类其实没有做什么事情,它只是接受一个实例,实现其对应的方法,并且将调用其方法返回而已。 showImg(https://segmentfault.com/img/bVbs3pt?w=650&h=651); 什么是装饰者模式 装饰者模式是一种为函数或类增添特性的技术,它可以让我们在不修改原来对象的基础上,为其增添新的能力和...

    Ilikewhite 评论0 收藏0
  • React深入】从Mixin到HOC再到Hook

    摘要:与继承相比,装饰者是一种更轻便灵活的做法。它只是一种模式,这种模式是由自身的组合性质必然产生的。对比原生组件增强的项可操作所有传入的可操作组件的生命周期可操作组件的方法获取反向继承返回一个组件,继承原组件,在中调用原组件的。 导读 前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。 本...

    fox_soyoung 评论0 收藏0
  • redux原理分析

    摘要:介绍是一个针对应用的可预测的状态管理器。中的设计模式装饰者模式定义装饰者模式用于给对象动态地增加职责。连接操作不会改变原来的组件类,而是返回一个新的已与连接的组件类。的这行代码表示它对的数据进行订阅。 redux介绍 redux是一个针对JavaScript应用的可预测的状态管理器。 redux中的设计模式 装饰者模式 定义:装饰者模式用于给对象动态地增加职责。 我们来看看redux最...

    jubincn 评论0 收藏0
  • React复习进阶 - 高阶组件使用/配置装饰模式/新版context使用

    摘要:所谓高阶组件即使是接受一个组件作为参数返回一个新组件的函数用于提高组件的自身能力提高组件复用性普通高阶组件函数将父级属性向下传递并追加新属性为添加样式和木偶组件传入一个组件返回一个函数式组件高阶组件木偶组件我是本体是同样还可以为增加生命周期 所谓高阶组件即使是接受一个组件作为参数, 返回一个新组件的函数, 用于提高组件的自身能力, 提高组件复用性 1.普通高阶组件 HOC函数将父级属性...

    canger 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<