源代码解析——core-decorators(decorate)

1,023 阅读3分钟

前言

这次要分析的源代码是 core-decorator 库中的一个函数:decorate。先简单介绍一下 core-decorators这个库是干嘛的, core-decorators是一个按照 JavaScript state-0 decorators 提案编写的一个装饰器库。(装饰器的标准可能会发生变化)。现目前在typescript中使用装饰器语法使用这个库是比较方便的。比如:

class Test {
    @decorate(memoize)
    public complicatedCalcs(): number {
        // do some complicated caculates
        return 123;
    }
}

通过decorate(memoize) 装饰过的函数就有了缓存的功能,在函数参数不变的情况下不会经过函数体内的逻辑而直接返回结果,这是一种常见的性能优化手段。

需要说明的是:这里的memoize不是 core-decorators库中提供的,而是来自于lodash中,我们使用了decorate这个函数将memoize变成了一个装饰器。 并且 core-decorators函数库的中的其他很多装饰器也都是依赖于decorate这个函数的。 decorate 函数可以将高阶函数都变为装饰器。今天我们就来解析一下 decorate 函数之中的秘密。

抽丝剥茧

和大多数的函数库类似,暴露出的函数的函数体很简单:

export default function decorate(...args) {
    return _decorate(handleDescriptor, args);
}

_decorate 是另一个基础函数:


export function decorate(handleDescriptor, entryArgs) {
    if (isDescriptor(entryArgs[entryArgs.length - 1])) {
        return handleDescriptor(...entryArgs, []);
    } else {
        // 方法装饰器
        return function() {
            return handleDescriptor(
                ...Array.prototype.slice.call(arguments),
                entryArgs
            );
        };
    }
}

为了减少复杂度,这里我们只看else分支中的语句。else分支中正是用于修饰类中的方法使用的方法装饰器。

所以,decorate 函数本身等价于:

export default function decorate(...args) {
    return function() {
        return handleDescriptor(
            ...Array.prototype.slice.call(arguments),
            entryArgs
        );
    };
}

这里需要插入一些关于typescript装饰器的相关知识。

当一个类中的方法被装饰器修饰时,会进行如下的步骤:

  1. 先对装饰器表达式本身求值
  2. 求值的结果会被当作函数,然后再进行调用。

我们还是以文章最开头的Test类为例子说明:

Typescript经过编译后:

var Test = /** @class */ (function() {
    function Test() {}
    Test.prototype.test = function() {
        console.log(123);
        return 123;
    };
    __decorate(
        [core_decorators_1.decorate(lodash_1.memoize)],
        Test.prototype,
        "test",
        null
    );
    return Test;
})();

从上面的代码中我们可以看出:core_decorators_1.decorate(lodash_1.memoize) 会在 __decorate 之前执行。简单的讲会发生下面这些事情:

  1. 先执行 core_decorators_1.decorate(lodash_1.memoize) 并返回一个新的函数,我们假设这个新函数为 midFunc.

其中midFunc 其实就是:

function() {
    return handleDescriptor(
        ...Array.prototype.slice.call(arguments),
        entryArgs
    );
};
  1. 然后将在执行上述的函数midFunc(target, key, descriptor),会得到一个 Descriptor 对象,再使用 Object.defineProperty 重新为该target重新设置 keyDescriptor 即:
Object.defineProperty(Test.prototype, "test", midFunc(Test.prototype, "test", null));

那么重点就在于 handleDescriptor这个函数了。源码如下:


function handleDescriptor(target, key, descriptor, [decorator, ...args]) {
    const { configurable, enumerable, writable } = descriptor;
    const originalGet = descriptor.get;
    const originalSet = descriptor.set;
    const originalValue = descriptor.value;
    const isGetter = !!originalGet;

    return {
        configurable,
        enumerable,
        get() {
            const fn = isGetter ? originalGet.call(this) : originalValue;
            const value = decorator.call(this, fn, ...args);

            if (isGetter) {
                return value;
            } else {
                const desc = {
                    configurable,
                    enumerable
                };

                desc.value = value;
                desc.writable = writable;

                defineProperty(this, key, desc);

                return value;
            }
        },
        set: isGetter ? originalSet : createDefaultSetter()
    };
}

我们看到 handleDescriptor 直接返回了一个对象,该对象是一个Descriptor。所以我们第一次访问 "test" 属性的时候,会直接进入该Descriptorget部分。其中的意思大概是:

  1. 先获取到要修饰的方法 fn
  2. 使用我们要装饰该函数的高阶函数decorator去修饰fn,返回的value也应该是一个函数。
  3. 重新给"test"属性设置Descriptor,这样我们再以下次访问"test" 属性时,则会直接访问到value这个函数了。

我们以@decorate(memoize) 为例,流程如下

总结

大概总结一下其中比较精髓的点:

  1. 直接返回了一个descriptor,在其中设置了getter。
  2. 在上述的getter中去执行我们要应用的高阶函数,然后重新给该属性设置(defineProperty)。
  3. 由于在上一步我们重新定义了descriptor,所以我们不会再进入到上面的getter中,而是进入到我们使用的高阶函数返回的函数中。

以上,大概就是decorate函数的执行流程了。如果你觉得文章对你有帮助,点个赞再走哦。