前言
本来是想分析mobx原理的,但是因为太菜了没办法一下子看懂mobx源码MobX 中是使用装饰器设计模式实现观察值的,要先掌握装饰器,才能进一步去理解观察值。因此今天就先缕一缕Decorator装饰器这个东东。
啥是装饰器呀
装饰器模式
说到装饰器,就不得不说说装饰器模式。它是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构,是作为对现有类的一个包装。
一般来说,在代码设计中,我们应当遵循「多用组合,少用继承」的原则。通过装饰器模式动态地给一个对象添加一些额外的职责。
使用了装饰器的类可以理解为穿上了机甲的tony stark,钢铁侠本质是一个人,机甲通过“装饰”来增强这个人的能力,不过再怎么装饰他还是一个人
装饰器Decorator
装饰器是es7 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。
下面是一个使用装饰器的简单例子,这个 @readonly 可以将 count 属性设置为只读。可以看出来,装饰器大大提高了代码的简洁性和可读性。
class Person {
@readonly count = 0;
}
but,装饰器目前还是一个stage-2提案,没有被浏览器所支持,因此如果要使用的话需要在编译器中启用当前实验性的装饰器语法
- 在 tsconfig.json 中启用编译器选项 "experimentalDecorators": true 。
- npm i --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
- 在.babelrc配置:
{
"plugins": [
["@babel/plugin-proposal-decorators", {
"legacy": true
}],
["@babel/plugin-proposal-class-properties", {
"loose": true
}]
]
}
装饰器有什么优点
为什么要使用 Decorator,其实就是介绍到 AOP 范式的最大特点了:非侵入式增强。
非侵入式增强,顾名思义,不会对原代码有侵入性。比如我们想做一个通用的组件,这个组件会提供一些基础的功能,我想在这个组件的基础上做一些改造和增强,装饰器模型就是一个很好的选择
此时有的人就会问了:“你这个场景,用继承也可以啊”
确实,但是继承会导致类与类之间耦合度较高,而装饰器相比生成子类更为灵活
缺点
兼容性是真的不行,使用的话还需安装配置babel插件,麻烦
装饰器的原理
Object.defineProperty
Decorator其实就是一个语法糖,背后其实就是利用es5的Object.defineProperty(target,name,descriptor)
其中最核心的其实是 descriptor —— 属性描述符 。
属性描述符总共分两种:数据描述符(Data descriptor)和 访问器描述符(Accessor descriptor)。
描述符必须是两种形式之一,但不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
- configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false
- enumerable
当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
- value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
- writable
当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
存取描述符同时具有以下可选键值:
- get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。
- set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常
描述符的api使用
数据描述符:
Object.getOwnPropertyDescriptor(user,'name');
// 输出
/**
{
"value": "张三",
"writable": true,
"enumerable": true,
"configurable": true
}
**/
访问器描述符:
var anim = {
get age() { return 5; }
};
Object.getOwnPropertyDescriptor(anim, "age");
// 输出
/**
{
configurable: true,
enumerable: true,
get: /*the getter function*/,
set: undefined
}
**/
伪代码实现
举个🌰,我们要实现一个Person类:
class Person{
say(){
console.log('哈哈,前端真简单!');
}
}
执行上面的代码,大致代码如下:
Object.defineProperty(Person.prototype,'say',{
value:function(){console.log('哈哈,前端真简单!')},
enumerable:false,
configurable:true,
writable:true
})
如果我们利用装饰器来修饰他
class Person{
@readonly
say(){console.log('感觉头有点凉')}
}
在这种装饰器的属性,会在Object.defineProperty为Person.prototype注册say属性之前,执行以下代码:
let descriptor = {
value:specifiedFunction,
enumerable:false,
configurable:true,
writeable:true
};
descriptor = readonly(Person.prototype,'say',descriptor)||descriptor;
Object.defineProperty(Person.prototype,'say',descriptor);
从上面的伪代码我们可以看出,Decorator只是在Object.defineProperty为Person.prototype注册属性之前,执行了一个装饰函数,其属于一个类对Object.defineProperty的拦截。所以它和Object.defineProperty具有一致的形参:
-
obj:作用的目标对象
-
prop:作用的属性名
-
descriptor:针对该属性的描述符
如何使用一个装饰器
看到这里,相信有的读者会产生疑问:上面的伪代码的实现,readonly是一个怎样的函数呢?我怎么做才能自己写一个可以使用的装饰器?
一般来说,装饰器有两种:类装饰器、类属性装饰器
使用类装饰器
话不多说,直接给示例: 1:
const withSpeak = (constructor) => {
const prototype = constructor.prototype;
prototype.speak = function() {
console.log('I can speak ', this.language);
}
}
@withSpeak
class Student {
constructor(language) {
this.language = language;
}
}
const student1 = new Student('Chinese');
const student2 = new Student('English');
student1.speak(); // I can speak Chinese
student2.speak(); // I can speak Englist
2:
@name
@seal
class Person {
sayHello() {
console.log(`hello ,my name is ${this.name}`)
}
}
function name(constructor) {
Object.defineProperty(constructor.prototype,'name',{
value:'小明'
})
}
function seal(constructor) {
let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHello')
Object.defineProperty(constructor.prototype, 'sayHello', {
...descriptor,
writable: false
})
}
new Person().sayHello()
new Person().sayHello = 1;// Cannot assign to read only property 'sayHello' of object '#<Person>'
装饰函数传入你要修饰的类的constructor,拿到constructor后可以在类的原型上进行修改,也可以利用Object.getOwnPropertyDescriptor、Object.defineProperty修改类的描述
使用类属性装饰器
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly name = 'person'
}
const person = new Person();
person.name = 'tom';
亦或者是mobx
import {
observable,
action,
autorun
} from 'mobx'
class Store {
@observable count = 1;
@action
changeCount(count) {
this.count = count;
}
}
const store = new Store();
autorun(() => {
console.log('count is ', store.count);
})
store.changeCount(10); // 修改 count 的值,会引起 autorun 中的函数自动执行。
类属性装饰器可以用在类的属性、方法、get/set 函数中,装饰函数一般会接收三个参数:
- target:被修饰的类
- name:类成员的名字
- descriptor:属性描述符,对象会将这个参数传给 Object.defineProperty
babel是怎么实现一个装饰器的
未完待续。。。