最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从babel的编译结果来学习装饰器

    正文概述 掘金(HsuYang)   2021-01-15   559

    1、先决条件

    因此本文只阐述类和方法的装饰器

    2、项目配置

    本文的项目配置将会采用笔者在之前的文章中所用到的配置。从babel编译结果来学习ES6-class

    3、类的装饰

    3.1、 基础

    我们来编写一个简单的装饰器

    function Wrap(target) {
      return target;
    }
    
    @Wrap
    class Polygon {}
    

    运行

    npm run build 
    

    我们将会得到如下结果:

    "use strict";
    
    var _class;
    
    function _classCallCheck(instance, Constructor) {
      if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
      }
    }
    
    function Wrap(target) {
      return target;
    }
    
    var Polygon =
      Wrap(
        (_class = function Polygon() {
          _classCallCheck(this, Polygon);
        })
      ) || _class; 
    

    从babel转码的结果,我们可以知道,如果类装饰器不返回内容,将不会对类进行修改。并且我们还可以知道,如果在别的地方引用此类的话,类装饰器已经执行了。如果类装饰器返回内容的话(且为真),那么我们的这个类就被改写了。实际例子:vue-class-component的Component装饰器。

    3.2、多个装饰器装饰器同一个类

    后续的操作如上,将不会赘述。 源码:

    
    function Wrap(target) {
      return target;
    }
    
    function Component(target) {
      return target;
    }
    
    function Decs(target) {
      return target;
    }
    
    @Wrap
    @Component
    @Decs
    class Polygon {}
    

    转码结果:

    
    function Wrap(target) {
      return target;
    }
    
    function Component(target) {
      return target;
    }
    
    function Decs(target) {
      return target;
    }
    
    var Polygon =
      Wrap(
        (_class =
          Component(
            (_class =
              Decs(
                (_class = function Polygon() {
                  _classCallCheck(this, Polygon);
                })
              ) || _class)
          ) || _class)
      ) || _class;
    

    从这个结果,我们可以得出结论,离class越近的装饰器越先执行。

    3.3 接受参数的装饰器

    当我们的类装饰器需要获得参数的时候,在装饰器外面再封装一层函数。 我们先来一个错误的示范,各位读者请看,此时Component并没有写成函数的执行形式,我们来看转码结果: 源码:

    
    function Wrap(target) {
      return target;
    }
    
    function Component(options = {}) {
      return function (target) {
        return target;
      };
    }
    
    function Decs(target) {
      return target;
    }
    
    @Wrap
    @Component
    @Decs
    class Polygon {}
    

    转码结果:

    function Wrap(target) {
      return target;
    }
    
    function Component() {
      var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
      return function (target) {
        return target;
      };
    }
    
    function Decs(target) {
      return target;
    }
    
    var Polygon =
      Wrap(
        (_class =
          Component(
            (_class =
              Decs(
                (_class = function Polygon() {
                  _classCallCheck(this, Polygon);
                })
              ) || _class)
          ) || _class)
      ) || _class;
    

    从转码结果来看,因为我们的粗心,此时Component返回的是function (target) { return target; };后面的装饰器所接收的结果均会是这个function,这样,我们的原始类已经被修改了。如果我们的类装饰器需要接收参数,一定要写成函数的执行形式,否则会给程序造成潜在的bug,这个小坑大家一定要注意。 笔者喜欢一个设计API的原则——恶心自己,成全别人。这句话的意思就是说,在设计API的时候给用户尽可能的提供提供便利性,而在设计API的时候增加很多额外的判断,这大概是老祖宗流传下来的哲学吧。

    
    function anonymous(target) {
    	return target;
    }
    
    function Component() {
    	var isFunc = arguments.length > 0 && typeof arguments[0] === 'function';
        return isFunc ? anonymouse(arguments[0]) : anonymous;
    }
    

    容错能容的了一时,但是容不了一世,如:

     @Component(function(){ })
     class Polygon {}
    

    此时我们的程序就傻眼了,反而还出现了bug,因此,对于我们开发者来说,更多的还是要仔细阅读API才是最终的解决方案。 正确编写可接受参数的装饰器之后,转码的结果:

    var Polygon =
      ((_dec = Component({
        age: "26",
        name: "hsuyang",
      })),
      Wrap(
        (_class =
          _dec(
            (_class =
              Decs(
                (_class = function Polygon() {
                  _classCallCheck(this, Polygon);
                })
              ) || _class)
          ) || _class)
      ) || _class);
    

    4、 方法或属性装饰器

    其中descriptor的描述如下:

    interface PropertyDescriptor {
        configurable?: boolean;
        enumerable?: boolean;
        value?: any;
        writable?: boolean;
        get?(): any;
        set?(v: any): void;
    }
    

    由于装饰器在装饰属性和方法上还是有一定的区别的,因此笔者把这里单独拆开讲

    4.1 属性装饰器

    属性装饰器babel对于属性的初始化,运用了一个叫initializer的方法(笔者最开始学习的时候也是比较懵逼,难道阮一峰老师错了,结果阮一峰老师没有错,只是自己的见识还不够?)。 源码:

    function readonly(target, name, descriptor) {
      descriptor.writable = false;
      descriptor.value = function () {
        console.log("hsuyang");
      };
      return descriptor;
    }
    
    class Person {
      @readonly
      name1;
    
      @readonly
      name2 = "I love microsoft";
    }
    

    转码结果:

    "use strict";
    
    var _class, _descriptor, _descriptor2, _temp;
    
    function _initializerDefineProperty(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 });
    }
    
    function _classCallCheck(instance, Constructor) {
      if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
      }
    }
    
    function _defineProperty(obj, key, value) {
      if (key in obj) {
        Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
      } else {
        obj[key] = value;
      }
      return obj;
    }
    
    function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
      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;
    }
    
    function _initializerWarningHelper(descriptor, context) {
      throw new Error("Decorating class property failed. Please ensure that " + "proposal-class-properties is enabled and runs after the decorators transform.");
    }
    
    function readonly(target, name, descriptor) {
      descriptor.writable = false;
    
      descriptor.value = function () {
        console.log("hsuyang");
      };
    
      return descriptor;
    }
    
    var Person =
      ((_class =
        ((_temp = function Person() {
          _classCallCheck(this, Person);
    
          _initializerDefineProperty(this, "name1", _descriptor, this);
    
          _initializerDefineProperty(this, "name2", _descriptor2, this);
        }),
        _temp)),
      ((_descriptor = _applyDecoratedDescriptor(_class.prototype, "name1", [readonly], {
        configurable: true,
        enumerable: true,
        writable: true,
        initializer: null,
      })),
      (_descriptor2 = _applyDecoratedDescriptor(_class.prototype, "name2", [readonly], {
        configurable: true,
        enumerable: true,
        writable: true,
        initializer: function initializer() {
          return "I love microsoft";
        },
      }))),
      _class);
    
    

    代码虽然多,但是核心就在于_applyDecoratedDescriptor和_initializerDefineProperty这两个方法。

    function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
      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) {
          // 如果装饰器不返回任何东西,则默认采用这个descriptor,因为JS语言的特性,desc是一个对象,当我们把这个对象传递到函数内部执行之后,这个对象已经被修改了,因此,属性装饰器,其实不管是否return descriptor,效果都是一样的。
          return decorator(target, property, desc) || desc;
        }, desc);
      // initializer 是专门用来处理在class中对 属性或者方法进行赋值语句操作的
      // 如果赋值语句存在,则属性的值等于赋值语句的结果
      if (context && desc.initializer !== void 0) {
        //并且把赋值上下文的this绑定进去
        desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
        //当处理完initializer之后,被处理成了undefined。这很重要
        desc.initializer = undefined;
      }
      //如果,没有赋值语句,则按照普通的Object.defineProperty处理。对于Object.defineProperty不懂的读者请移步MDN。
      if (desc.initializer === void 0) {
        Object.defineProperty(target, property, desc);
        desc = null;
      }
      return desc;
    }
    
    function _initializerDefineProperty(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
      });
    }
    

    各位尤其要注意这个_initializerDefineProperty方法,想要通过属性装饰器,通过修改descriptor.value,给属性设置值,是行不通的。因为之前在处理initializer的时候,处理完之后,已经被设置成了undefined,你想要通过initializer去改变属性的值,也是徒劳的。

    还有另外一个奇葩操作,笔者在之前的开发中也是这样干过?,但是其实是不推荐的。

    
    function Autowired(target, name, descriptor) {
      //想给类的某个字段设置一个属性值。
      target[name] = {};
    }
    
    class Person {
      @Autowired
      name1;
    }
    

    笔者在前文讲解babel转码class的时候说过,属性,不管是在构造器里面,还是外面,其都是定义在类的实例上的,而以上的赋值语句,我们其实是赋值在类的原形对象上的,当我们的类初始化之后,这个对象是拥有name1这个属性的,只不过是undefined罢了,而虽然我们在这个对象的原形上定义了name1,但是因为自身拥有name1属性,则自动忽略了原形对象上的name1属性。

    因此,我们也可以得出一个小小的结论,想要通过属性装饰器去修改属性的值,是不可取的手段,自欺欺人而已?。

    4.2 方法装饰器

    相对于属性装饰,方法装饰的坑要少的多。 源码:

    function log(target, name, descriptor) {
      var oldValue = descriptor.value;
    
      descriptor.value = function () {
        console.log(`Calling ${name} with`, arguments);
        return oldValue.apply(this, arguments);
      };
    
      return descriptor;
    }
    
    class Person {
      @log
      calc() {}
    }
    

    转码结果:

    //多余的代码就不必要贴出来了,关键就是,我们的方法装饰器,不会经过_initializerDefineProperty,因此,方法就可以通过value修改,而增加一些额外的操作(比如在方法执行器做什么事,方法执行完成之后做什么事)。
    var Person =
      ((_class = /*#__PURE__*/ (function () {
        function Person() {
          _classCallCheck(this, Person);
        }
    
        _createClass(Person, [
          {
            key: "calc",
            value: function calc() {},
          },
        ]);
    
        return Person;
      })()),
      _applyDecoratedDescriptor(_class.prototype, "calc", [log], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),
      _class);
    

    4.3 多个属性或者方法叠加使用

    源码:

    function log(target, name, descriptor) {
      var oldValue = descriptor.value;
    
      descriptor.value = function () {
        console.log(`Calling ${name} with`, arguments);
        return oldValue.apply(this, arguments);
      };
    
      return descriptor;
    }
    
    function log2(target, name, descriptor) {
      return descriptor;
    }
    
    class Person {
      @log
      @log2
      calc() {}
    }
    

    转码结果:

    //...省略代码
    // 将装饰器的顺序颠倒,并且迭代执行
    desc = decorators
        .slice()
        .reverse()
        .reduce(function (desc, decorator) {
          return decorator(target, property, desc) || desc;
        }, desc);
    // ...省略代码
    
    // 依次读取装饰器
    _applyDecoratedDescriptor(_class.prototype, "calc", [log, log2], Object.getOwnPropertyDescriptor(_class.prototype, "calc"), _class.prototype),
    
    

    由此可以得出结论,属性或方法装饰器跟类的装饰器的执行顺序是一致的,离的近装饰器的先执行。

    4.4 带参数的属性或方法装饰器

    带参数的属性或方法装饰器,和带参数的类的装饰器类似,如:

    function Prop(options = {}) {
        //做一些操作
        return function(target, prop, descriptor){
        	//做一些操作
            return descriptor;
        }
    }
    

    此处将不再赘述。

    5、总结

    1、装饰器总是在类定义的时候就已经执行了;

    2、类装饰器只接收一个参数,这个参数是被装饰的类本身,装饰器如果有为真的返回值,则这个值用作覆盖类,如果没有的话,就是类的本身。

    3、属性或方法装饰器接收三个参数,分别是target,prop,descriptor。对于babel转码的结果,属性装饰器还有一个额外的临时属性叫作initializer,用来初始属性的值。属性装饰器可以选择返回或不返回descriptor效果是一致的,因为JS对象在传递进入函数的时候,修改之后的效果将会反应在对象上,但是建议返回descriptor。

    4、永远不要尝试用属性装饰器去改写属性的值。

    5、带参数的装饰器一定要写成函数的执行形式。

    由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com?


    起源地下载网 » 从babel的编译结果来学习装饰器

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元