《JavaScript设计模式与开发实践》曾探

定义

在**不改变原对象的基础上,在程序运行期间**,通过对其进行包装拓展(添加属性 或者方法〉使原有对象可以满足用户的更复杂需求。

class Person {
    wear() {
        console.log('人')
    }
}

class Glasses {
    target: any;
    constructor(person: any) {
        this.target = person;
    }
    wear() {
        this.target.wear();
        console.log('戴眼镜')
    }
}

class Hat {
    target: any;
    constructor(person: any) {
        this.target = person;
    }
    wear() {
        this.target.wear();
        console.log('戴帽子')
    }
}

let person = new Person();
person.wear();//人
person = new Glasses(person);
person.wear();//人 戴眼镜
person = new Hat(person);
person.wear(); //人 戴帽子

Glasses和Hat的构造函数都接收并保存person参数,在Glasses和Hat的wear方法中既有其本身逻辑,也调用了person的wear方法。

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象 之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire 方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。

因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透 明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象

为什么不用继承?

在面向对象语言中,给对象添加新功能常常使用继承的方式。但这种方式很不灵活,并且会带来几个问题。

一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之 改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。

装饰函数

如何为函数添加功能?直接修改函数?这是最次的方式。

在不修改源代码的情况下,为原函数添加新的功能。我们可以保留原函数的引用,在新的扩展函数中调用它。

let fun = () => {
    console.log('1')
}
let _old = fun;
fun = () => {
    _old();
    console.log('2')
}
fun()
//1
//2

但这样有时会出现this的指向问题

const classA = {
    char: 'a',
    fun: function () {
        console.log('this', this)
        console.log('1 ', this.char)
    }
}
classA.fun()
// this { char: 'a', fun: [Function: fun] }
// 1  a
let _old = classA.fun;
classA.fun = function () {
    _old()
    console.log('2')
}
classA.fun()
// this undefined
// TypeError: Cannot read properties of undefined(reading 'char')

当然,我们也可以使用call,bind,apply或者其它方式来解决这类问题

const classA = {
    char: 'a',
    fun: function () {
        console.log('this', this)
        console.log('1 ', this.char)
    }
}
classA.fun()
// this { char: 'a', fun: [Function: fun] }
// 1  a
let _old = classA.fun;
classA.fun = function () {
    _old.call(this);
    console.log('2')
}
classA.fun()
// this { char: 'a', fun: [Function(anonymous)] }
// 1  a
// 2

但实际上我们有一种更好的方式

AOP

AOP 面向切面编程(Aspect Oriented Programming)

通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术

在前端JS与TS中,我们同样可以使用AOP思想,对函数进行装饰

首先定义before与after函数

Function.prototype.before = function (beforefn) {
    const __self = this;
    // 保存原函数的引用 
    return function () { // 返回包含了原函数和新函数 
        beforefn.apply(this, arguments);
        // 新函数在原函数之前执行
        return __self.apply(this, arguments);
    }
}
Function.prototype.after = function (afterfn) {
    const __self = this;
    return function () {
        const ret = __self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
};

示例

funcA = funcA.before(() => console.log('before funcA'))
funcA()
// before funcA
// funcA
funcA = funcA.after(() => console.log('after funcA'))
const res = funcA()
// before funcA
// funcA
// after funcA
console.log(res)
// resA

如果是在TS下或者不想污染原生对象时,那就需要换一种写法

function before(fn: Function, beforefn: Function) {
    return function (this: any) {
        beforefn.apply(this, arguments);
        return fn.apply(this, arguments);
    }
}
function after(fn: Function, afterfn: Function) {
    return function (this: any) {
        const res = fn.apply(this, arguments);
        afterfn.apply(this, arguments);
        return res
    }
}

将原函数和新函数都作为参数传入

给出几个调用示例

let funcB = (arg: string) => {
    console.log('funcB ', arg)
}
funcB = after(funcB, () => console.log('after funcB'))
funcB('arg-b')
//funcB  arg-b
//after funcB
const classC = {
    char: 'C',
    funCC: (arg: string) => {
        console.log('funcC ', arg)
    }
}
classC.funCC = before(classC.funCC, () => console.log('before funcC'))
// classC.funCC = after(classC.funCC, () => console.log('after funcC'))
classC.funCC('arg-c')
// before funcC
// funcC  arg - c
class ClassD {
    char: string;
    constructor(char: string) {
        this.char = char;
    }
    funcD(arg: string) {
        console.log('funcD ' + arg + ' ' + this.char)
    }
}
const d1 = new ClassD('D1')
d1.funcD('print-d1')
//funcD print-d1 D1
d1.funcD = before(d1.funcD, () => console.log('before d1.funcD'))
d1.funcD('print-d1')
//before d1.funcD
//funcD print - d1 D1
const d2 = new ClassD('D2')
d1.funcD.call(d2, 'print-d2')
//before d1.funcD
//funcD print - d2 D2

注意,原函数和新函数使用的是同一个参数对象,因此我们可以在新函数中==对一部分参数进行修改==,并将新的参数传递给原函数

type ObjDemo = {
    a: string;
    [key: string]: any;
}
let funcE = (obj1: ObjDemo, arr2: number[], str: string) => {
    console.log('obj1', obj1);
    console.log('arr2', arr2)
    console.log('str', str)
}
funcE = before(funcE, (o1: ObjDemo, arr2: number[], str: string) => {
    console.log('change params')
    o1.a = 'AAAAA'
    o1.c = 'CCCCC'
    arr2[0] = 1
    o1 = { a: '111' }
    str = 'DDDDDD'
})
funcE({ a: 'a1' }, [2, 4, 6, 8], 'str')
// change params
// obj1 { a: 'AAAAA', c: 'CCCCC' }
// arr2[1, 4, 6, 8]
// str str

这里要注意一点,在新函数中对于形参的直接赋值都是不会生效的,比如上面这个例子中 obj1={a:‘111’} 和str=‘DDDDDD’ 对于参数的修改都没有成功。

这是因为在JavaScript中,函数传参传的是值而非引用。在函数内部直接赋值比如 o1 = { a: ‘111’ } 实际上是在函数内部修改了o1变量的指向,对于函数外了obj1指向并无影响。而 o1.a = ‘AAAAA’ 可以生效自然是因为此时o1尚未被覆盖,它与函数外的obj1正指向同一地址。

JS装饰器

  • 2015-3-24
    stage 1阶段,也是目前广为使用的用法,也基本等同于TS开启了experimentalDecorators的用法。
  • 2018-09
    进入到stage2阶段,用法和stage1很大不同
  • 2021-12
    针对stage2提案进行了一次修改。
  • 2022-03
    正式进入stage3。去掉了metadata部分,使用方式没有发生太大变化。

js原生目前不支持装饰器,只能通过babel体验装饰器这个新特性。

TS装饰器

注意:TS装饰器并不等同于JS装饰器,TS目前实现的装饰器是基于JS装饰器stage-1的语法。下文中TS编译成的JS目标版本是ES5

TS装饰器通过 @函数 的方式对对象进行包装并返回包装后的对象。可以装饰的内容包括

  • 类属性
  • 类方法
  • 参数
  • 访问器

image-20220816181406455

这是typescript/lib/lib.es5.d.ts中的一系列声明

装饰器本质就是编译时执行的函数。

指定TS的编译目标JS版本可以在tsconfig中配置,也可以使用命令编译

tsc --target ES5 --experimentalDecorators xxx.ts

类的装饰

image-20220824174229148

类的装饰器函数接收一个参数

  • target 即类本身

最简单的例子

@desc
class ClassA {
    char: string = 'A'
    constructor(char: string) {
        this.char = char
    }
}
function desc(target: any) {
    console.log(target.name)
}
//ClassA

如果想要向装饰器中传入参数,那么需要动态的生成装饰器

@descB
@descA('demoA')
class ClassA {
    char: string = 'A'
    constructor(char: string) {
        this.char = char
    }
}
function descA(text: string) {
    return function (target: any) {
        console.log(target.name + ' ' + text)
    }
}
function descB(target: any) {
    console.log(target.name + ' ' + 'demoB')
}

装饰器的调用顺序遵循就近原则,我们可以参考一下转译后的js代码

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    console.log('decorators', decorators)//decorators [ [Function: descB], [Function (anonymous)] ]
    console.log('target', target)//target [Function: ClassA]
    console.log('key', key)//key undefined
    console.log('desc', desc)//desc undefined
    var c = arguments.length, r = c < 3 ? target//参数数量小于3,是类装饰器,直接拿到target
        : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key)
            : desc, d;
    console.log('c', c)//c 2
    console.log('r', r)//r [Function: ClassA]
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
        r = Reflect.decorate(decorators, target, key, desc);
    else
        for (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i])//就近调用
                r = (c < 3 ? d(r)//r是传入装饰器的目标对象,这里的d即是装饰器函数的引用,如果d(r)返回了值,则用返回值覆盖原对象,
                    : c > 3 ? d(target, key, r)
                        : d(target, key)) || r;//如果没有返回,则使用原对象
    console.log('r', r)//r [Function: ClassA]
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var ClassA = /** @class */ (function () {
    function ClassA(char) {
        this.char = 'A';
        this.char = char;
    }
    ClassA = __decorate([
        descB,
        descA('demoA')
    ], ClassA);
    return ClassA;
}());
function descA(text) {
    return function (target) {
        console.log(target.name + ' ' + text);
    };
}
function descB(target) {
    console.log(target.name + ' ' + 'demoB');
}

装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。这也是为什么说装饰器本质就是编译时执行的函数。

使用类装饰器替换原对象,如果返回的不是类,会报错

@changeToB
class ClassA {
    char: string = 'A'
}
function changeToB(target: any) {
    return class ClassB {
        char: string = 'B'
    }
}
const temp = new ClassA()
console.log(temp.char)//B
console.log(temp instanceof ClassA)//true

当然,我们也可以使用any进行强制替换,但并不建议这样做

@changeToB
class ClassA {
    char: string = 'A'
}
function changeToB(target: any): any {
    return 'ClassA is string'
}
console.log('ClassA', ClassA)//ClassA ClassA is string
const temp = new ClassA()//抛出异常 TypeError: ClassA is not a constructor

属性的装饰

image-20220824174251189

属性的装饰器接收两个参数

  • target 当修饰的属性是静态属性时,target为类,当修饰的属性是实例属性时,target为类的原型
  • prop 属性名字符串

一个简单的例子

function descAttr(target: any, prop: string) {
    console.log(`target`, target)
    console.log(`target type`, typeof target)
    if (typeof target === 'object') {
        console.log(`descAttr 实例属性变量名`, prop)
    }
    if (typeof target === 'function' && target[prop]) {
        console.log(`descAttr 静态属性`, target[prop])
    }
}

class ClassC {
    @descAttr
    static char = 'C'
    @descAttr
    public str: string = 'D'
}
// target { } 实例属性传递的是原型
// target type object
// descAttr 实例属性变量名 str
// target[class ClassC] { char: 'C' }静态属性传递的target是类本身
// target type function
// descAttr 静态属性 C

注意,这里没有尝试打印实例属性的值,因为此时实例并没有创建。没有办法在定义一个原型对象的成员时描述一个实例属性。

注意,属性的装饰器函数本身是无返回值的,类型是void,在TS中使用返回值报错,但是如果使用any来强制加入返回,那么将返回一个固定对象,这个对象是一个描述符对象,会用来修改原属性的描述符对象,对于静态属性和实例属性都适用

function descAttr(target: any, prop: string): any {
    if (typeof target === 'object') {
        console.log(`descAttr 实例属性变量名`, prop)
        return {
            writable: false,
        }
    }
    if (typeof target === 'function' && target[prop]) {
        console.log(`descAttr 静态属性`, target[prop])
        return {
            writable: false,
        }
    }
}

class ClassC {
    @descAttr
    static char = 'C'
    @descAttr
    public str: string = 'D'
}

对上面的类分别进行调用

let tempF = new ClassC().str = 'F'

image-20220822181334345

ClassC.char = 'E'

image-20220822181432196

这个例子转译后

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    console.log('decorators', decorators)//decorators [ [Function: descAttr] ]
    console.log('target', target)//target 实例属性时是原型{},静态属性时是[Function: ClassC] { char: 'C' }
    console.log('key', key)//key str/char
    console.log('desc', desc)//desc undefined
    var c = arguments.length, r = c < 3 ? target
        : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key)
            : desc, d;
    console.log('c', c)//c 4
    console.log('r', r)//r undefined
    console.log('desc-2', desc)//desc-2 undefined
    console.log(typeof Reflect.decorate === "function")//false
    if
        (typeof Reflect === "object" && typeof Reflect.decorate === "function")
        r = Reflect.decorate(decorators, target, key, desc);
    else
        for (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i])
                r = (c < 3 ? d(r)
                    : c > 3 ? d(target, key, r)//这里c=4执行r=d(target, key, r)||r ts中由于声明的原因只会接收前面两个参数target和key
                        : d(target, key)) || r;
    console.log('r-2', r)//r-2 { writable: false }
    //如果d(target, key, r)有返回,则会执行到Object.defineProperty(target, key, r),重新对对象中该属性的描述符进行定义
    //如果没有返回,则r=undefined 不会执行到Object.defineProperty(target, key, r)
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function descAttr(target, prop) {
    console.log("target", target);
    console.log("target type", typeof target);
    if (typeof target === 'object') {
        console.log("descAttr \u5B9E\u4F8B\u5C5E\u6027\u53D8\u91CF\u540D", prop);
        return {
            writable: false,
        };
    }
    if (typeof target === 'function' && target[prop]) {
        console.log("descAttr \u9759\u6001\u5C5E\u6027", target[prop]);
        return {
            writable: false,
        };
    }
}
var ClassC = /** @class */ (function () {
    function ClassC() {
        this.str = 'D';
    }
    ClassC.char = 'C';
    __decorate([
        descAttr
    ], ClassC.prototype, "str", void 0);
    __decorate([
        descAttr
    ], ClassC, "char", void 0);
    return ClassC;
}());

方法的装饰

image-20220824174312427

方法的装饰器接收三个参数

  • target 实例方法target为类原型,静态方法target为类
  • prop 被装饰的方法名字符串
  • desc 被装饰的方法的属性描述符,可以通过desc.value拿到该方法本身

方法装饰器可以返回一个对象,这个对象将作为方法的属性描述符

一个简单的例子

function descMethod(target: any, prop: any, desc: any) {
    console.log('target', target) //target {}/[class ClassE] { char_2: 'F' }
    console.log('prop', prop) //prop funcE/funcF
    console.log('desc', desc)
    // desc {
    //     value: [Function: funcE]/[Function: funcF],
    //     writable: true,
    //     enumerable: false,
    //     configurable: true
    // }
}
class ClassE {
    char = 'E'
    static char_2 = 'F'
    @descMethod
    static funcF(str: string) {
        console.log('funcFstr ', str)
        console.log('funcFchar_2', this.char_2)
    }
    @descMethod
    funcE(str: string) {
        console.log('funcEstr ', str)
        console.log('funcEchar', this.char)
    }
}
ClassE.funcF('strF')
// funcFstr  strF
// funcFchar_2 F

将descMethod修改一下

function descMethod(target: any, prop: any, desc: any) {
    desc.value = () => console.log('change func')
}
ClassE.funcF('strF')
//change func

这个例子转译后

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    console.log('decorators', decorators)//decorators [ [Function: descMethod] ]
    console.log('target', target)//target 实例属性时是原型{ funcE: [Function (anonymous)] },静态属性时是[Function: ClassE] { funcF: [Function (anonymous)], char_2: 'F' }
    console.log('key', key)//key funcE/funcF
    console.log('desc', desc)//desc null
    var c = arguments.length, r = c < 3 ? target//这里desc===null,执行desc = Object.getOwnPropertyDescriptor(target, key),
        : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key)//获取target中key属性的描述符对象
            : desc, d;
    console.log('c', c)//c 4
    console.log('r', r)
    // r {
    //     value: [Function(anonymous)],
    //     writable: true,
    //     enumerable: true,
    //     configurable: true
    // }
    console.log('desc-2', desc)
    // desc - 2 {
    //     value: [Function(anonymous)],
    //     writable: true,
    //     enumerable: true,
    //     configurable: true
    // }
    console.log(typeof Reflect.decorate === "function")//false
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
        r = Reflect.decorate(decorators, target, key, desc);
    else
        for
            (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i])
                r = (c < 3 ? d(r)
                    : c > 3 ? d(target, key, r)//c=4 执行r= d(target, key,r)||r  这里可以在d(target, key,r)中完全返回一个对象作为新描述符,也可以直接修改r
                        : d(target, key)) || r;
    //这里描述符对象已被修改
    r.value()//change func
    //执行到 Object.defineProperty(target, key, r) 重新定义描述符
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function descMethod(target, prop, desc) {
    desc.value = function () { return console.log('change func'); };
}
var ClassE = /** @class */ (function () {
    function ClassE() {
        this.char = 'E';
    }
    ClassE.funcF = function (str) {
        console.log('funcFstr ', str);
        console.log('funcFchar_2', this.char_2);
    };
    ClassE.prototype.funcE = function (str) {
        console.log('funcEstr ', str);
        console.log('funcEchar', this.char);
    };
    ClassE.char_2 = 'F';
    __decorate([
        descMethod
    ], ClassE.prototype, "funcE", null);
    __decorate([
        descMethod
    ], ClassE, "funcF", null);
    return ClassE;
}());
ClassE.funcF('strF');
// funcFstr  strF
// funcFchar_2 F

参数的装饰

image-20220824174341224

接收3个参数

  • target 如果是实例方法,则为类的原型,如果是静态方法,则为类本身
  • prop 方法名称或者undefined 当修饰的是构造函数的参数时,prop是undefined
  • index 修饰的参数在原参数列表中的索引

一个简单的例子

function descParam(target: any, prop: any, index: any) {
    console.log('target', target)
    console.log(`这是${prop}方法的第${index}个参数`)
}
class ClassF {
    constructor(@descParam str: string) {

    }
    static staticFunc(@descParam str: string) {

    }
    func(str: string, @descParam str_1: string) {

    }
}
// target { }
// 这是func方法的第1个参数
// target[class ClassF]
// 这是staticFunc方法的第0个参数
// target[class ClassF]
// 这是undefined方法的第0个参数

转译后

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    console.log('decorators', decorators)//decorators [ [Function: descAttr] ]
    console.log('target', target)//target 实例属性时是原型 { func: [Function (anonymous)] },静态属性时是[Function: ClassF] { staticFunc: [Function (anonymous)] }
    console.log('key', key)//key func/staticFunc
    console.log('desc', desc)//desc null
    var c = arguments.length, r = c < 3 ? target
        : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key)
            : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
        r = Reflect.decorate(decorators, target, key, desc);
    else
        for (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i]) r = (c < 3 ? d(r)
                : c > 3 ? d(target, key, r)//执行—__param中返回的函数 只接收target和key参数,并且没有返回值
                    : d(target, key)) || r;
    console.log('r', r)
    // r {
    //     value: [Function(anonymous)],
    //     writable: true,
    //     enumerable: true,
    //     configurable: true
    // }
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    //这个函数没有返回值,参数装饰器的返回值也被忽略掉了
    return function (target, key) { decorator(target, key, paramIndex); }
};
function descParam(target, prop, index) {
    console.log('target', target);
    console.log("\u8FD9\u662F" + prop + "\u65B9\u6CD5\u7684\u7B2C" + index + "\u4E2A\u53C2\u6570");
}
var ClassF = /** @class */ (function () {
    function ClassF(str) {
    }
    ClassF.staticFunc = function (str) {
    };
    ClassF.prototype.func = function (str, str_1) {
    };
    __decorate([
        __param(1, descParam)//执行工厂函数 第一个参数是原参数索引,第二个是装饰器函数
    ], ClassF.prototype, "func", null);
    __decorate([
        __param(0, descParam)
    ], ClassF, "staticFunc", null);
    ClassF = __decorate([
        __param(0, descParam)
    ], ClassF);
    return ClassF;
}());
// target { }
// 这是func方法的第1个参数
// target[class ClassF]
// 这是staticFunc方法的第0个参数
// target[class ClassF]
// 这是undefined方法的第0个参数

访问器的装饰

同样接收3个参数

  • target 实例属性target为类原型,静态属性target为类
  • prop 被装饰的访问器所对应的属性名字符串
  • desc 被装饰的访问器的所对应的属性描述符
desc {
    get: [Function: get char],
    set: [Function: set char],
    enumerable: false,
    configurable: true
}

注意:不能同时装饰同一个属性的get和set访问器

否则抛出异常 error TS1207: Decorators cannot be applied to multiple get/set accessors of the same name.

image-20220830150814938

简单例子

function descGet(target: any, key: any, desc: any) {
    console.log('desc', desc)
}
class ClassG {
    private _char = 'F'
    @descGet
    get char(): string {
        return this._char
    }
    set char(val: string) {
        this._char = val
    }
}

转译后

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target
        : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key)
            : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
        r = Reflect.decorate(decorators, target, key, desc);
    else
        for (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i])
                r = (c < 3 ? d(r)
                    : c > 3 ? d(target, key, r)
                        : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function descGet(target, key, desc) {
    console.log('desc', desc);
}
var ClassG = /** @class */ (function () {
    function ClassG() {
        this._char = 'F';
    }
    Object.defineProperty(ClassG.prototype, "char", {
        get: function () {
            return this._char;
        },
        set: function (val) {
            this._char = val;
        },
        enumerable: false,
        configurable: true
    });
    __decorate([
        descGet
    ], ClassG.prototype, "char", null);
    return ClassG;
}());

访问器装饰函数和前面几个装饰器的运作原理相似,同样可以返回一个新对象作为相应属性的属性描述符,也同样可以直接修改传递进来的描述符

TS装饰器应用实例

异常捕获装饰器catchError

装饰器设计

export type CatchErrorOption = {
    ignore?: boolean;//是否忽略异常
    catchFunc?: (e: any, ...args: any) => any | ((e: any, ...args: any) => void) | undefined;//回调函数
    finallyFunc?: Function | undefined;
}
export function catchError(option?: CatchErrorOption) {
    const { ignore = false, catchFunc, finallyFunc } = option || {}
    return function (target: any, prop: any, desc: any) {
        const oldFunc = desc.value
        desc.value = function () {
            let res;
            try {
                res = oldFunc.call(this, ...arguments)
                //如果无异常,使用原函数的返回
                if (res !== undefined) return res
            } catch (e) {
                if (!ignore && typeof catchFunc === 'function') {
                    res = catchFunc.call(this, e, ...arguments)
                }
                //如果ignore为false,并且catchError有返回,使用catchError的返回值
                if (res !== undefined) return res
            } finally {
                if (typeof finallyFunc === 'function') {
                    res = finallyFunc.call(this, ...arguments)
                }
                //finallyFunc有返回,使用finallyFunc的返回值,否则使用前面的返回值
                if (res !== undefined) return res
            }
        }
    }
}

使用示例1

import { catchError } from "./catchError"
function catchCb(e: any, ...args: any) {
    console.log('catchError:', e)
    console.log('catchError ', args)
    return 'catchFunc'
}
function finallyCb(str: string, index: number) {
    console.log(`finallyFunc  str:${str} index:${index}`)
    return 'finallyFunc'
}
class ClassDemo_1 {
    @catchError({ catchFunc: catchCb, finallyFunc: finallyCb })
    funcA(str: string, index: number) {
        console.log(`str:${str}  index:${index}`)
   shu     throw ('this is an error A')
    }
}
const classDemo_1 = new ClassDemo_1()
let res = classDemo_1.funcA('A', 1)
console.log('res ', res)
console.log('end')

输出:

str:A  index: 1
catchError: this is an error A
catchError['A', 1]
finallyFunc  str:A index: 1
res  finallyFunc
end

使用示例2 读取未定义变量的属性出现的异常 以及this获取

import { catchError } from "./catchError"
function catchCb(this: any, e: any, ...args: any) {
    console.log('catchError:', e)
    console.log('catchError ', args)
    console.log('c', this.c)
    return 'catchFunc'
}
function finallyCb(...args: any) {
    console.log(`finallyFunc  args: `, ...args)
    return 'finallyFunc'
}
class ClassDemo_1 {
    c = 'c'
    @catchError({ ignore: false, catchFunc: catchCb, finallyFunc: finallyCb })
    funcA(obj: any) {
        console.log(' obj.char.a', obj.char.a)
        return obj.char.a
    }
}
const classDemo_1 = new ClassDemo_1()
let res = classDemo_1.funcA({ str: 'strTest' })
console.log('res ', res)
console.log('end')

ignore为true时

// finallyFunc  args: { str: 'strTest' }
// res  finallyFunc
// end

ignore为false时

image-20220831140635036

loading装饰器

实现一个loading装饰器

  • loading装饰器会向它所装饰的函数挂载一个loading属性,可以通过xxx.prototype.loading或者xxx.loading的方式调用
  • loading装饰器会侦测所装饰的函数内部的异步操作,在异步操作开始时将loading置为true,在异步操作结束后将loading置为false
  • 被装饰的函数上挂载的loading属性变化时,通知上下文中的组件进行刷新

原理

  • TS装饰器模式
  • Promise
  • 属性描述符
  • this与上下文
  • AOP

获取原方法的引用,在装饰器内修改原方法的描述符,在原方法调用之前向其上挂载属性loading,并置为true,保留Promise的then接口原引用,并重写then接口,在then接口内对onrejected和onfulfilled进行包装,并将包装后的回调函数作为参数传入原then接口中使用

实现代码

export function loading(this: any, ...rest: any): any {
    //this 为上下文组件
    //以@loading()形式调用
    if (rest.length === 0) return _loading
    //以@loading形式调用
    if (rest.length === 3 && typeof rest[0][rest[1]] === 'function') _loading.call(this, rest[0], rest[1], rest[2])
}
function _loading(target: any, prop: string, desc: any) {
    if (typeof target[prop] !== 'function') throw ('This is not a function')
    const oldFunc = desc.value
    desc.value = function () {
        const context = this
        //获取原型
        const _prototype = desc.value?.prototype || context[prop].prototype || {}
        //获取目标
        const origin = desc.value || context[prop] || {}
        //保留原生引用
        let originalPromiseThen = Promise.prototype.then;
        //向原型和目标上挂载属性 如果有需求也可以挂载其它属性或对象 如开始,结束时间,运行时长等进行埋点
        _prototype.loading = true
        origin.loading = true
        //针对react 进行刷新调用
        context.forceUpdate && context.forceUpdate()
        // 重写Promise的then接口 针对await和then链式调用
        Promise.prototype.then = function (onfulfilled: ((value: any) => any), onrejected: ((value: any) => any)): Promise<any> {
            //对回调进行包装
            function wrappedCallback(this: any, func: Function) {
                const self = this
                return function () {
                    _prototype.loading = false
                    origin.loading = false
                    //针对react 进行刷新调用
                    context.forceUpdate && context.forceUpdate()
                    //@ts-ignore
                    return func.call(self, ...arguments)
                }
            };
            // 触发原来的回调
            return originalPromiseThen.call(this, wrappedCallback(onfulfilled), wrappedCallback(onrejected));
        };
        // 返回原函数值 并恢复Promise的then接口
        const res = oldFunc.call(this, ...arguments)
        Promise.prototype.then = originalPromiseThen
        return res
    }
}

使用示例

页面中使用

import React, { Component } from 'react';
import { Button } from 'antd'
import { Bind } from 'lodash-decorators'
import { loading } from '@/utils/loading'
interface IState {
    str1: string;
    str2: string;
    str3: string;
}
export default class Index extends Component<any, IState>{
    constructor(props: any) {
        super(props)
        this.state = {
            str1: '测试字符串1',
            str2: '测试字符串2',
            str3: '测试字符串3',
        }
    }
	//内部Promise链
    @Bind
    @loading
    loadDataFunc_1() {
        fetchData(this.state.str1).then((resp: any) => {
            this.setState({ str1: resp })
        })
    }
    //内部await
    @Bind
    @loading()
    async loadDataFunc_2() {
        const newStr = Array.from(this.state.str2).reverse().join('')
        await new Promise((resolve, reject) => setTimeout(() => {
            this.setState({ str2: newStr })
            //必须要有resolve或者reject
            resolve(newStr)
        }, 3000))
    }
    //还原Promise
    @Bind
    loadDataFunc_3() {
        fetchData(this.state.str3).then((resp: any) => {
            this.setState({ str3: resp })
        })
    }
    render(): React.ReactNode {
        return <React.Fragment>
            <h1>loading装饰器-内部Promise链</h1>
            <Button onClick={this.loadDataFunc_1} loading={this.loadDataFunc_1.prototype.loading} >加载数据</Button>
            <h1>{this.loadDataFunc_1.prototype.loading ? '加载中' : '加载完成'}</h1>
            <h1>{this.state.str1}</h1>
            <div style={{ height: '100px', width: '100%' }} />
            <h1>loading装饰器-内部await</h1>
            <Button onClick={this.loadDataFunc_2} loading={this.loadDataFunc_2.prototype.loading} >加载数据</Button>
            <h1>{this.loadDataFunc_2.prototype.loading ? '加载中' : '加载完成'}</h1>
            <h1>{this.state.str2}</h1>
            <div style={{ height: '100px', width: '100%' }} />
            <h1>loading装饰器-还原Promise</h1>
            <Button onClick={this.loadDataFunc_3} loading={
                //@ts-ignore
                this.loadDataFunc_3.loading
            } >加载数据</Button>
            <h1>{this.loadDataFunc_3.prototype.loading ? '加载中' : '加载完成'}</h1>
            <h1>{this.state.str3}</h1>
        </React.Fragment>
    }
}
//接口模拟
async function fetchData(base: string) {
    return new Promise(resolve => setTimeout(() => resolve(Array.from(base).reverse().join('')), 3000))
}

效果

loading-demo