余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

ES6 系列之我们来聊聊装饰器(es7装饰器工作原理)

xiyangw 2022-11-26 17:03 14 浏览 0 评论

Decorator

装饰器主要用于:

  1. 装饰类
  2. 装饰方法或属性

装饰类

ES6 系列之我们来聊聊装饰器(es7装饰器工作原理)

@annotation
class MyClass { }
function annotation(target) {
 target.annotated = true;
}

装饰方法或属性

class MyClass {
 @readonly
 method() { }
}
function readonly(target, name, descriptor) {
 descriptor.writable = false;
 return descriptor;
}

Babel

安装编译

我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。

不过我们也可以选择本地编译:

npm init
npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

新建 .babelrc 文件

{
 "plugins": [
 ["@babel/plugin-proposal-decorators", { "legacy": true }],
 ["@babel/plugin-proposal-class-properties", {"loose": true}]
 ]
}

再编译指定的文件

babel decorator.js --out-file decorator-compiled.js

装饰类的编译

编译前:

@annotation
class MyClass { }
function annotation(target) {
 target.annotated = true;
}

编译后:

var _class;
let MyClass = annotation(_class = class MyClass {}) || _class;
function annotation(target) {
 target.annotated = true;
}

我们可以看到对于类的装饰,其原理就是:

@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;

装饰方法的编译

编译前:

class MyClass {
 @unenumerable
 @readonly
 method() { }
}
function readonly(target, name, descriptor) {
 descriptor.writable = false;
 return descriptor;
}
function unenumerable(target, name, descriptor) {
 descriptor.enumerable = false;
 return descriptor;
}

编译后:

var _class;
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) {
 /**
 * 第一部分
 * 拷贝属性
 */
 var desc = {};
 Object["ke" + "ys"](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;
 }
 /**
 * 第二部分
 * 应用多个 decorators
 */
 desc = decorators
 .slice()
 .reverse()
 .reduce(function(desc, decorator) {
 return decorator(target, property, desc) || desc;
 }, desc);
 /**
 * 第三部分
 * 设置要 decorators 的属性
 */
 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["define" + "Property"](target, property, desc);
 desc = null;
 }
 return desc;
}
let MyClass = ((_class = class MyClass {
 method() {}
}),
_applyDecoratedDescriptor(
 _class.prototype,
 "method",
 [readonly],
 Object.getOwnPropertyDescriptor(_class.prototype, "method"),
 _class.prototype
),
_class);
function readonly(target, name, descriptor) {
 descriptor.writable = false;
 return descriptor;
}

装饰方法的编译源码解析

我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。

Object.getOwnPropertyDescriptor()

在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:

Object.getOwnPropertyDescriptor() 方法返回指定对象上的一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

顺便注意这是一个 ES5 的方法。

举个例子:

const foo = { value: 1 };
const bar = Object.getOwnPropertyDescriptor(foo, "value");
// bar {
// value: 1,
// writable: true
// enumerable: true,
// configurable: true,
// }
const foo = { get value() { return 1; } };
const bar = Object.getOwnPropertyDescriptor(foo, "value");
// bar {
// get: /*the getter function*/,
// set: undefined
// enumerable: true,
// configurable: true,
// }

第一部分源码解析

在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:

// 拷贝一份 descriptor
var desc = {};
Object["ke" + "ys"](descriptor).forEach(function(key) {
 desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter
if ("value" in desc || desc.initializer) {
 desc.writable = true;
}

那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:

class MyClass {
 @readonly
 born = Date.now();
}
function readonly(target, name, descriptor) {
 descriptor.writable = false;
 return descriptor;
}
var foo = new MyClass();
console.log(foo.born);

Babel 就会编译为:

// ...
(_descriptor = _applyDecoratedDescriptor(_class.prototype, "born", [readonly], {
 configurable: true,
 enumerable: true,
 writable: true,
 initializer: function() {
 return Date.now();
 }
}))
// ...

此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。

第二部分源码解析

接下是应用多个 decorators:

/**
 * 第二部分
 * @type {[type]}
 */
desc = decorators
 .slice()
 .reverse()
 .reduce(function(desc, decorator) {
 return decorator(target, property, desc) || desc;
 }, desc);

对于一个方法应用了多个 decorator,比如:

class MyClass {
 @unenumerable
 @readonly
 method() { }
}

Babel 会编译为:

_applyDecoratedDescriptor(
 _class.prototype,
 "method",
 [unenumerable, readonly],
 Object.getOwnPropertyDescriptor(_class.prototype, "method"),
 _class.prototype
)

在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。

第三部分源码解析

/**
 * 第三部分
 * 设置要 decorators 的属性
 */
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["define" + "Property"](target, property, desc);
 desc = null;
}
return desc;

如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:

desc.initializer.call(context)

而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能

class MyClass {
 @readonly
 value = this.getNum() + 1;
 getNum() {
 return 1;
 }
}

最后无论是装饰方法还是属性,都会执行:

Object["define" + "Property"](target, property, desc);

由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。

应用

1.log

为一个方法添加 log 函数,检查输入的参数:

class Math {
 @log
 add(a, b) {
 return a + b;
 }
}
function log(target, name, descriptor) {
 var oldValue = descriptor.value;
 descriptor.value = function(...args) {
 console.log(`Calling ${name} with`, args);
 return oldValue.apply(this, args);
 };
 return descriptor;
}
const math = new Math();
// Calling add with [2, 4]
math.add(2, 4);

再完善点:

let log = (type) => {
 return (target, name, descriptor) => {
 const method = descriptor.value;
 descriptor.value = (...args) => {
 console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
 let ret;
 try {
 ret = method.apply(target, args);
 console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
 } catch (error) {
 console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
 }
 return ret;
 }
 }
};

2.autobind

class Person {
 @autobind
 getPerson() {
 return this;
 }
}
let person = new Person();
let { getPerson } = person;
getPerson() === person;
// true

我们很容易想到的一个场景是 React 绑定事件的时候:

class Toggle extends React.Component {
 @autobind
 handleClick() {
 console.log(this)
 }
 render() {
 return (
 <button onClick={this.handleClick}>
 button
 </button>
 );
 }
}

我们来写这样一个 autobind 函数:

const { defineProperty, getPrototypeOf} = Object;
function bind(fn, context) {
 if (fn.bind) {
 return fn.bind(context);
 } else {
 return function __autobind__() {
 return fn.apply(context, arguments);
 };
 }
}
function createDefaultSetter(key) {
 return function set(newValue) {
 Object.defineProperty(this, key, {
 configurable: true,
 writable: true,
 enumerable: true,
 value: newValue
 });
 return newValue;
 };
}
function autobind(target, key, { value: fn, configurable, enumerable }) {
 if (typeof fn !== 'function') {
 throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`);
 }
 const { constructor } = target;
 return {
 configurable,
 enumerable,
 get() {
 /**
 * 使用这种方式相当于替换了这个函数,所以当比如
 * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回
 * 所以这里做了 this 的判断
 */
 if (this === target) {
 return fn;
 }
 const boundFn = bind(fn, this);
 defineProperty(this, key, {
 configurable: true,
 writable: true,
 enumerable: false,
 value: boundFn
 });
 return boundFn;
 },
 set: createDefaultSetter(key)
 };
}

3.debounce

有的时候,我们需要对执行的方法进行防抖处理:

class Toggle extends React.Component {
 @debounce(500, true)
 handleClick() {
 console.log('toggle')
 }
 render() {
 return (
 <button onClick={this.handleClick}>
 button
 </button>
 );
 }
}

我们来实现一下:

function _debounce(func, wait, immediate) {
 var timeout;
 return function () {
 var context = this;
 var args = arguments;
 if (timeout) clearTimeout(timeout);
 if (immediate) {
 var callNow = !timeout;
 timeout = setTimeout(function(){
 timeout = null;
 }, wait)
 if (callNow) func.apply(context, args)
 }
 else {
 timeout = setTimeout(function(){
 func.apply(context, args)
 }, wait);
 }
 }
}
function debounce(wait, immediate) {
 return function handleDescriptor(target, key, descriptor) {
 const callback = descriptor.value;
 if (typeof callback !== 'function') {
 throw new SyntaxError('Only functions can be debounced');
 }
 var fn = _debounce(callback, wait, immediate)
 return {
 ...descriptor,
 value() {
 fn()
 }
 };
 }
}

4.time

用于统计方法执行的时间:

function time(prefix) {
 let count = 0;
 return function handleDescriptor(target, key, descriptor) {
 const fn = descriptor.value;
 if (prefix == null) {
 prefix = `${target.constructor.name}.${key}`;
 }
 if (typeof fn !== 'function') {
 throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
 }
 return {
 ...descriptor,
 value() {
 const label = `${prefix}-${count}`;
 count++;
 console.time(label);
 try {
 return fn.apply(this, arguments);
 } finally {
 console.timeEnd(label);
 }
 }
 }
 }
}

5.mixin

用于将对象的方法混入 Class 中:

const SingerMixin = {
 sing(sound) {
 alert(sound);
 }
};
const FlyMixin = {
 // All types of property descriptors are supported
 get speed() {},
 fly() {},
 land() {}
};
@mixin(SingerMixin, FlyMixin)
class Bird {
 singMatingCall() {
 this.sing('tweet tweet');
 }
}
var bird = new Bird();
bird.singMatingCall();
// alerts "tweet tweet"

mixin 的一个简单实现如下:

function mixin(...mixins) {
 return target => {
 if (!mixins.length) {
 throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
 }
 for (let i = 0, l = mixins.length; i < l; i++) {
 const descs = Object.getOwnPropertyDescriptors(mixins[i]);
 const keys = Object.getOwnPropertyNames(descs);
 for (let j = 0, k = keys.length; j < k; j++) {
 const key = keys[j];
 if (!target.prototype.hasOwnProperty(key)) {
 Object.defineProperty(target.prototype, key, descs[key]);
 }
 }
 }
 };
}

6.redux

实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了装饰器,就可以改写上面的代码。

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {};

相对来说,后一种写法看上去更容易理解。

7.注意

以上我们都是用于修饰类方法,我们获取值的方式为:

const method = descriptor.value;

但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:

const value = descriptor.initializer && descriptor.initializer();

参考

  1. ECMAScript 6 入门
  2. core-decorators
  3. ES7 Decorator 装饰者模式
  4. JS 装饰器(Decorator)场景实战

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

作者:冴羽

相关推荐

前后端分离 Vue + NodeJS(Koa) + MongoDB实践

作者:前端藏经阁转发链接:https://www.yuque.com/xwifrr/gr8qaw/vr51p4写在前面闲来无事,试了一下Koa,第一次搞感觉还不错,这个项目比较基础但还是比较完整了,...

MongoDB 集群如何工作?

一、什么是“MongoDB”?“MongoDB”是一个开源文档数据库,也是领先的“NoSQL”数据库,分别用“C++”“编程语言”编写,使用带有“Schema”的各种类似JSON的文档,是也分别被认为...

三部搭建mongo,和mongo UI界面

三步搭建mongo,和mongoUI界面安装首先你需要先有一个docker的环境检查你的到docker版本docker--versionDockerversion18.03.1-ce,b...

Mongodb 高可用落地方案

此落地方案,用于实现高可用。复制集这里部署相关的复制集,用于实现MongoDB的高可用。介绍MongoDB复制集用于提供相关的数据副本,当发生硬件或者服务中断的时候,将会从副本中恢复数据,并进行自动...

一次线上事故,我顿悟了MongoDB的精髓

大家好,我是哪吒,最近项目在使用MongoDB作为图片和文档的存储数据库,为啥不直接存MySQL里,还要搭个MongoDB集群,麻不麻烦?让我们一起,一探究竟,继续学习MongoDB分片的理论与实践,...

IDEA中安装MongoDB插件-再也无要nosql manager for mongodb

大家都知道MongoDB数据库作为典型的非关系型数据库被广泛使用,但基于MongoDB的可视化管理工具-nosqlmanagerformongodb也被用的较多,但此软件收费,所以国内的破解一般...

数据库监控软件Lepus安装部署详解

Lepus安装部署一、软件介绍Lepus是一套开源的数据库监控平台,目前已经支持MySQL、Oracle、SQLServer、MongoDB、Redis等数据库的基本监控和告警(MySQL已经支持复...

YAPI:从0搭建API文档管理工具

背景最近在找一款API文档管理工具,之前有用过Swagger、APIManager、Confluence,现在用的还是Confluence。我个人一直不喜欢用Swagger,感觉“代码即文档”,让代...

Mac安装使用MongoDB

下载MongoDB包:https://www.mongodb.com/download-center解压mongodb包手动解压到/usr/local/mongodb文件夹配置Mac环境变量打开环境...

保证数据安全,不可不知道的MongoDB备份与恢复

大家在项目中如果使用MongoDB作为NOsql数据库进行存储,那一定涉及到数据的备份与恢复,下面给大家介绍下:MongoDB数据备份方法在MongoDB中我们使用mongodump命令来备...

MongoDB数据备份、还原脚本和定时任务脚本

备注:mongodump和mongorestore命令需要在MongoDB的安装目录bin下备份脚本备份格式/usr/local/mongodb/bin/mongodump -h ...

等保2.0测评:mongoDB数据库

一、MongoDB介绍MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产...

MongoDB入门实操《一》

什么是MongoDBMongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之...

Python安装PyMongo的方法详细介绍

欢迎点击右上角关注小编,除了分享技术文章之外还有很多福利,私信学习资料可以领取包括不限于Python实战演练、PDF电子文档、面试集锦、学习资料等。前言本文主要给大家介绍的是关于安装PyMongo的...

第四篇:linux系统中mongodb的配置

建议使用普通用户进行以下操作。1、切换到普通用户odysee。2、准备mongodb安装包,自行去官网下载。3、解压安装包并重命名为mongodb4.04、配置mongodbcdmongod...

取消回复欢迎 发表评论: