Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

装饰器原理探究 #26

Open
FE-Sadhu opened this issue Jan 20, 2022 · 0 comments
Open

装饰器原理探究 #26

FE-Sadhu opened this issue Jan 20, 2022 · 0 comments
Labels
深入js笔记 about javascript

Comments

@FE-Sadhu
Copy link
Owner

js 装饰器可以装饰类、类方法、类属性,以例子来说用法如下:

@cls
class A {
  @dec
  a = 1;

  @decFn fn() {console.log(this.a)}
}

function dec(target, prop, descriptor) {
  // target --- A.protocol
  // prop --- a
  // descriptor --- {
  //        configurable: true,
  //        enumerable: true,
  //        writable: true,
  //        initializer: function initializer() {
  //          return 1; // 初始化时,会绑定实例作为 this 执行该函数,把返回值赋值给属性
  //        },
  //      }
  console.log(target, prop, descriptor);
}

function decFn(target, prop, descriptor) {
  // target --- A.protocol
  // prop --- fn
  // descriptor --- {configurable: true, enumerable: false, writable: true, value: Function}
  console.log(target, prop, descriptor);
}

function cls(target) {
  // target --- A
  console.log(target);
  // 可对类进行操作
  // 若有返回值,则返回值作为新类
}

cls 就是类的装饰器,dec 就是实例属性装饰器,decFn就是类方法装饰器。

js 装饰器原本设计是代码运行前执行的,可以做静态分析之类的事情。但是由于装饰器语法还处于提案中,并且语法可能在提案的不同阶段都会变(比如 babel legacy: true/false 编译出来的结果不同),不稳定,所以引擎还未去实现它。

所以现在想用装饰器语法,必须借助工具,比如 babel 转译、tsc 编译。转/编译后的装饰器,实际也是 runtime 阶段执行,只不过在修饰的类被实例化之前 invoke 。

接下来就分析 babel legacy: true 编译后的代码探究 js 装饰器的原理。

首先是对类的装饰:

let A = cls(_class = class A {
}) || _class;

很好理解,就是类自身传进装饰器内,让开发者可以自行操作该类,若有返回值则把类的引用替换成返回值的,否则保持类的引用。

然后是对类属性的装饰:

let A = cls(_class = (_class2 = class A {
  constructor() {
    _initializerDefineProperty(this, "a", _descriptor, this);
  }


}, (_descriptor = _applyDecoratedDescriptor(_class2.prototype, "a", [dec], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: function initializer() { 
  	// 实例属性的初始值
    return 1;
  }
})), _class2)) || _class;

_initializerDefineProperty 的行为就是在实例化时,给实例属性挂描述符。

function _initializerDefineProperty(target, property, descriptor, context) {
  // target --- 实例
  // property --- 属性
  // descriptor --- 描述符
  // context --- 实例
  if (!descriptor) return; // 装饰器有返回值,此处则不再挂描述符
  Object.defineProperty(target, property, {
    enumerable: descriptor.enumerable,
    configurable: descriptor.configurable,
    writable: descriptor.writable,
    value: descriptor.initializer
      ? descriptor.initializer.call(context)
      : void 0,
  });
}

_applyDecoratedDescriptor 的话主要目的就是执行装饰器。

function _applyDecoratedDescriptor(
  target, // 实例属性、类方法  --- 原型
  property, // 属性/方法名
  decorators, // [装饰器, ...]
  descriptor, // 描述符
  context, // 实例属性 --- undefined  类方法 --- 原型
) {
  var desc = {};
  Object.keys(descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  if ("value" in desc || desc.initializer) {
    desc.writable = true;
  }
  // 以上构建属性描述符
   
  // 以下执行装饰器,像洋葱一样内部先执行
  desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc; // 若装饰器有返回值,则把返回值作为描述符
    }, desc);
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    // 若属性装饰器走到此处,代表把装饰器的返回值作为了描述符,挂载在 原型 上
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

由上可知:

对类属性的装饰,也是在实例化前执行的。并且根据装饰器有没有返回值分情况对属性作操作:

  • 有返回值: 往类原型挂载属性,是赋值还是挂 getter setter 根据开发者来定
  • 无返回值: 往实例挂载属性,并赋初始化值

思考一个例子:

image.png

可见 getter setter 也是会按照原型链去找的。

最后是对类方法的修饰

let A =
  cls(
    (_class =
      ((_class2 = class A {
        constructor() {
          _initializerDefineProperty(this, "a", _descriptor, this);
        }

        fn() {
          console.log(this.a);
        }
      }),
      ((_descriptor = _applyDecoratedDescriptor(_class2.prototype, "a", [dec], {
        configurable: true,
        enumerable: true,
        writable: true,
        initializer: function initializer() {
          return 1;
        },
      })),
      _applyDecoratedDescriptor( // 对函数做处理
        _class2.prototype,
        "fn",
        [decFn],
        Object.getOwnPropertyDescriptor(_class2.prototype, "fn"), // {writable: true, enumerable: false, configurable: true, value: ƒ}
        _class2.prototype
      )),
      _class2))
  ) || _class;

经过对属性装饰器的分析,方法就很简单了,就是在实例化前执行装饰器,然后把类原型、方法名、方法描述符作为参数传进去,若有返回值,则把返回值当描述符挂在对象的方法名上,若无返回值,则挂方法原本的描述符。

@FE-Sadhu FE-Sadhu added the 深入js笔记 about javascript label Jan 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
深入js笔记 about javascript
Projects
None yet
Development

No branches or pull requests

1 participant