当我们使用MobX的时候,首先要声明一个store, 用来保存状态,它的最基本的语法 如下:
class Todo { @observable title = ""; @observable finished = false; }
其中 @observable 是装饰器写法, title= ''; 是实例属性的新的写法,这两个语法是es7 中的提案,但都没有被采纳, 之所以被使用,是因为有babel 时进行转译。
首先看一下类的实例属性的新写法, 这是ES7 中关于静态属性的一个提案, 在这个提案对实例属性和静态属性规定了新的写法。
1,对于实例属性来说, 如果在使用的时候,就要对它进行初始化,那么可以直接在类的定义中用赋值方式进行初始化。
class Car { color = 'red'; }
这样,我们就声明了一个实例属性color,并且它的初始值为 'red'; 如果用以前的写法,我们要在类中定义constructor构造函数,然后在构造函数中写实例属性,
class Car { constructor() { this.color = 'red'; } }
可以发现,新的ES7 提案,大大简化了写法。
2, 不光是实例属性,实例方法也可以用这种赋值的方法进行初始化, 如果学习过react, 你可以已经见过这种写法了。
class Car { color = 'blue'; // 直接用赋值的方法,对实例方法进行初始化 getColor = () => { return this.color; } }
3, 静态属性就是在实例属性的前面加上static关键字, 它表示这个属性属于整个类,用类名加属性名进行读取。
class Car { static country = 'china'; }
4,静态方法,就是在方法前面加上static, 也示表示它属于这个类。
class Car { static country = 'china'; static getCountry = () => { return Car.country; } }
现在就用babel进行转译,使它能够在生产环境中使用,为了通用性,我们使用webpack 进行配置。现在是webpack4, 我们也使用一下。新建mobx文件夹,npm init -y 生成package.json文件,安装依赖, 这里要注意,webpack4 把命令行抽离成了一个单独的包webpack-cli,所以安装webpack时,要安装webpack-cli, 否则不能使用webpack打包命令。
npm install webpack webpack-cli webpack-cli --save-dev. 对类进行转译,用到一个babel核心插件 babel-plugin-transform-class-properties. npm install babel-core babel-loader babel-plugin-transform-class-properties --save-dev. 再来写webpack.config.js, 这里比原来要简单,因为webpack4 提出一个零配置概念。默认入口文件 src/index.js, 输出文件 dist/main.js 文件,
同时提供了一个mode 配置选项,用来表示生产环境还是测试环境,那么webpack.config.js中只写module就可以了。
const path = require('path'); module.exports = { module: { rules:[ { test: /\.js$/, include: path.join(__dirname, 'src'), use: { loader: 'babel-loader', options: { plugins: ['transform-class-properties'] } } } ] } }
现在建立入口文件, 新建src目录,再在里面新建index.js文件, 把上面的代码都写到里面。
class Car { color = 'red'; getColor = () => { return this.color; } static country = 'china'; static getCountry = () => { return Car.country; } } console.log(new Car().getColor()) console.log(Car.getCountry())
现在就执行打包命令了, 在package.json文件中的scripts字段中,写上 "build": "webpack --mode production", --mode production , 就是在webpack4要求指定的mode 参数。npm run build 执行打包命令,完成后,可以看到多了个dist目录,里面有一个main.js文件,因为它是一个js 文件,我们直接用node 执行一下就好了,node main.js 可以看到l输出了 red, china,打包没有问题,说明也编译成功了。
现在再来看装饰器,它的主要作用是来装饰一个类或类中的属性或方法,也就是说,它只能作用在class上,目的呢,则是对这个类,或属性方法,进行功能增强。怎么进行增强呢? 这就要说到装饰器的本质,它本身是一个函数,当js在进行编译的时候,会为这个函数注入默认的参数,如果作用到类上,则这个函数自动会获取到一个target的参数,表示这个类本身。
如果作用属性或方法上,则会为函数注入3个参数,target, key, descriptor, target 表示这个装饰的函数所属的对象,key 表示装饰的函数名称,descriptor, 装饰的函数的描述符,这三个参数和Obeject.defineProperty 的三个属性一致,为哪个对象的,哪个属性,设置怎么样的descriptor. 当函数获取到参数以后,就可以在函数内作任意的操作,所以可以进行增强。装饰器的语法是怎么样的,它是怎么作用到类上。语法其实很简单,就是在函数名的前面加@, 然后放到类或方法的上面,和java的注解一样。现在我们来体验一个装饰器,不过还是要先配置一个webpack, 装饰器的编译依赖一个核心插件,babel-plugin-transform-decorators-legacy. npm install 安装完成后,在webapck.config.js中的plugins进行配置。
好了,基础搭建已经完成,我们就来学习装饰器了,首先装饰器要作用在类上,我们要先定义在一个类,名字就叫Numberic 好了。其次,装饰器是一个函数,它有默认参数, 那我们先声明两个函数, 一个作用到类上,一个作用到方法上,最后,装饰器作用到类中,用通过@函数名实现了。那基本框架如下:
@time class Numberic { @readonly
} // 由于time函数,是装饰类的,所以它自动会获取到一个target参数,我们可以打印一下target,就是类本身 function time(target) { console.log(target); } // 由于readonly是作用到类的方法上的,所以它获取到3个参数。 function readonly(target, key, descriptor) { console.log(target); console.log(key); console.log(descriptor); }
这两个装饰器time, readonly 要对它装饰的类或函数起到什么增强作用呢? time, 我们来计算类的原型上所有方法的执行时间. readonly, 则表示这个方法不可以被复写。
先来看time是怎么写的?
function time(target) { const desc = Object.getOwnPropertyNames(target.prototype); for (let key of desc) { if (key === 'constructor') { continue; } const func = (target.prototype)[key]; if (typeof func === 'function') { Object.defineProperty(target.prototype, key, { value: function(...args) { console.time('time'); let ret = func.apply(this, args); console.timeEnd('time'); return ret; } }) } } }
在类中定义的方法,实际上都是定义在类的原型上,所以要对定义在类中所有方法都进行增强,就要在原型上先获得这些方法, 由于装饰的函数,自动会获得target, 所以target.prototype就可以获得原型。由于原型是一个对象,所以获取它上面的方法,就要获取到对象的属性,然后进行遍历。所以time函数的第一个句,就是用getOwnPropertyNames获取整个原型对象的属性名,这里打印一下,可以知道结果,是一个数组["constructor", "add"], 注意这里不要使用keys 方法,因为Object.keys()获取的是一个对象上的可枚举属性,而在原型上定义的方法,都是不可以枚举的,如果使用Object.keys(), 这里将会得到空数组。Object.getOwnProperyNames 可以获取到对象的非枚举属性,所以在这里使用没有问题。
然后使用了for of 对["constructor", "add"]数组进行编历,首先要去除构造函数,因为构造函数用来创建对象的,所以使用了 if (key === 'constructor') ,那么剩下的key值,就是我们想要的了,比如在这里key 为add. 我们声明了一个func变量来获取到属性值,这里也可以打印一下, 它是一个函数。
现在获取到了原型对象上的所有属性或对应的属性值,那现在就要对它进行增强,这时你会看到, 我们是用Object.defineProperty 重新给属性定义了一个新的函数,key 就是指代我们的add, value 就是给add 重新定义的方法,在value中,我们是用apply 方法,调用了原方法func.apply ,所以原有的方法,不会消失,正常使用。但是我们在方法的调用前后,分别调用了console.time 和console.timeend 来计算时间,所以我们对原来的函数进行了增强。
这时我们声明一个Numberic 对象,来体验一下time 装饰器。
let obj = new Numberic(); console.log(obj.add(1000));
这时控制台上输出结果,同时输出了计算时间,看来我们的装饰器time 起作用了。
所谓的装饰器,就是获取到对象原来定义属性,或属性值,然后用Object.defineProperty 方法重新定义新的属性或属性值,在新的属性值里面,调用原有方法的同时,增加新的功能,这样,原的方法调用了,同时增加了新的功能,所以增强了。
现在我们再来看一个readonly装饰器,它不可复写,就设置它的属性描述符writable: false.就好了, 由于它是装饰单个方法,所以它自动会获取到descriptor 属性,它就是属性描述符。
function readonly(target, key, descriptor) { console.log(descriptor); }
这时可以看到add完整的属性描述符,writable 为true, 表示可以复写,那直接修改为false 就可以了。
function readonly(target, key, descriptor) { descriptor.writable = false; }
readonly 装饰器就写完了。我们来测试一下, 组obj增加一个add 方法,
obj.add = function() { console.log('add'); }
如果如被复写,它就是调用我们新写的方法,输出add.
console.log(obj.add(1000));
可以看到,控制台仍然输出的是1000的和,表示add函数并没有被复写。