在vue2中用装饰者模式

210 阅读12分钟

一、装饰者模式

1. 什么是装饰者模式?

2. 装饰者模式产生的背景

3. JS的特殊性

二、es7装饰器

1. babel支持

2. 装饰器的写法

3. 装饰器函数的几个参数

三、在vue组件中使用装饰器

1. 防止被装饰函数作用域丢失

2. 一些应用实例

3. 装饰器累加使用

四、装饰器应用场景分析

装饰者模式是面向对象语言(如Java)设计模式的一种。JS当中由于没有类的概念,(es6的class其实是一种function的封装,不同于Java中的类),不能称为面向对象语言,前端开发者一般也很少刻意在代码中应用装饰者模式。事实上,在JS中应用装饰者模式,是很有必要的。

一、装饰者模式

  1. 什么是装饰者模式?

装饰者模式是面向对象语言设计模式的一种。它允许在程序运行期间,动态地装饰一个对象的行为,而又不修改对象本身。

打个比方,装饰者模式非常类似于人们日常行为中的着装打扮,比如要去约会了,穿休闲装;要去面试了,穿正式装;超人和蜘蛛侠要去救人了,都要先换一套职业装(仿佛只有这样才能去救人,why?)。

人和衣服进行不同的组合,就获得各种BlingBling的能力了,可以去社交或者拯救世界。而人还是人,衣服还是衣服,在程序员眼里,这样的动态组合就是装饰者模式。

0

  1. 装饰者模式产生的背景

在面向对象语言中,由于封装、继承和多态的特性,想要给一个基础类增加新的行为,就得增加一个新的子类(除非重构这个基础类,这将会很危险)。随着业务量增加和时间推移,子类的数量将会庞大,代码耦合度极高,系统将极不稳定。

在程序设计中,应当保证系统的松耦合和高复用。尽量将功能(行为)独立拆分解耦,每种新的功能(行为)分离开来称为一个装饰者,当需要的时候,再将它们组合在一起使用。这样仅仅需要实现数个装饰者,就可以组合成很多个不同的功能。而不是创建大量的子类。

比如下面这个例子,可以看出装饰者模式的优秀。

一楼的咖啡厅共售卖10种品牌的咖啡。定义一个抽象基类(Coffee),Coffee定义抽象方法计算咖啡售价,为每种品牌的咖啡定义一个品牌类。品牌类都继承自基类(Coffee),实现计算咖啡售价方法。现在老板临时加需求说,给咖啡用3种不同的杯子装,连同杯子一块售卖,3种杯子的价格是不相同的。

如果使用继承的方式来,就要给每种品牌的咖啡创建子类,比如玻璃杯卡布奇诺、塑料杯卡布奇诺、生态杯卡布奇诺和玻璃杯拿铁、塑料杯拿铁、生态杯拿铁。总共需要10 x 3 = 30个子类。但是如果把玻璃杯、塑料杯和生态杯这些对象动态组合到咖啡上面,只需要额外增加3个类。

为了节约篇幅,我只粘贴了两个咖啡品牌类。

0

0

0

0

用装饰者模式:

  1. 新建3个杯子类

0

0

0

  1. 计算咖啡售价

0

可以看出来,上面的杯子类和咖啡品牌类都继承自咖啡基类,都实现了基类的抽象方法,杯子类和品牌类互相不干涉,它们之间的组合可以生产出n x m种不同的子类。节约了代码,降低了耦合,提高了代码复用,是不是很优秀?

  1. JS的特殊性

作为一门动态语言,JS可以在任意地方任何时候动态地增删改一个对象的属性和方法,而在Java、C#中,对象一旦创建,就不可更改了。像上面的问题,JS写一个保存所有咖啡对象的集合,一个保存所有杯子对象的集合,配合一个cost函数,随随便便就搞定了。

这就是JS相比其它静态语言的巨大优势,比如在Vuejs中常用 Vue.use(element-ui) 来给Vue组件对象添加使用element-ui的功能。看起来像一个插件,插上即用,内部实现原理是给Vue组件的原型对象(Vue.prototype)增加一些行为(使用element-ui)。

虽然可以随时随地给对象增删改属性和方法很方便,但这种做法也是很不严谨的,它破坏了程序的封装性。比如在项目里给axios添加拦截器,修改了axios的请求和响应行为,后来来了新的需求,你觉得之前的拦截器行为与新需求不符,不想axios再拦截你的请求时,就会很麻烦。由于之前添加拦截器时污染了axios对象,现在你要么重新选择一个新的ajax库,要么在不影响原功能的情况下修改以前的拦截器代码。(好在axios提供了创建实例的方法,把拦截器装在各自的实例上,避免污染整个axios对象。)

像最初的给htmlElement绑定事件,就不得不用addEventListner来代替onClick以避免任意增删改属性方法带来的不稳定性了。

动态增删改对象的属性和方法是一把双刃剑,好在我们已经习惯它的利,而且它带给我们的利大于弊。

那么,JS和装饰者模式是否般配呢?

JS没有类式继承的强继承关系,没有封装类的强限制,原型链继承可以完美解决重写方法的需求。JS能应用装饰者模式的是,解耦和代码高复用性,以及一种开发理念 —— 先关注程序主流程开发,再装饰主流程中的重要步骤。

比如,从server端拿到一组流量桶数据,需求是流量桶按媒体分组,媒体按a-z排序,单个媒体组内的流量桶按orderId排序,最后还有流量桶按媒体名称着色。面对这一系列需求,如果在每一次ajax请求后都写这么多处理逻辑,将给二次开发带来巨大的麻烦。代码耦合性很强,复用率很低。而且一旦有新的业务变更,恐怕就要重写这个方法。这里就可以将功能拆分解耦,再分别装饰到ajax请求上,让代码结构清晰,一目了然。而且开发者能轻易看出来,程序的主流程是什么。

0

二、es7装饰器

在es6中,ECMA定义了class(类),在es7中定义了decorator(装饰器),为我们在JS中使用装饰者模式带来了技术支持。下面看看如何在JS中使用es7装饰器decorator。

  1. babel支持

npm安装babel-plugin-transform-decorators-legacy,并在.babelrc文件里plugins里添加babel-plugin-transform-decorators-legacy以获得babel对装饰器的支持。

  1. 装饰器的写法

0

0

总结:

  1. 装饰器可以装饰类的属性和方法。不能装饰函数(babel报错,提示装饰器只能装饰类)。

  2. 装饰器分方法装饰器和属性装饰器。

  3. 装饰器可以装饰对象字面量。但是和装饰类时有所不同。

0

  1. 装饰器函数的几个参数

  2. target 在属性装饰器中,它是类本身。在方法装饰器中它是类的原型对象。

0

在装饰对象字面量是它是对象本身,babel转化后如下:

0

  1. prop是被装饰对象的被装饰属性名

  2. descriptor属性描述符

descriptor顾明思议,描述属性。它分为数据描述符(value, writable,)和存取描述符(set, get),它们共有4个键值,共享2个键值(configurable, enumerable)。数据描述符和存取描述符只能存在一种,默认是数据描述符。比如上例的descriptor.value等于foo的函数体本身。

所以,我们在装饰器函数里引用原对象的行为,也就是descriptor.value,然后再调用它,并添加装饰行为(逻辑),以此达到装饰的目的。具体代码如下:

0

descriptor打印结果如下:

0

三、在vue组件中使用装饰器

  1. 防止被装饰函数作用域丢失

由于Javascript采用的是静态作用域,它的函数执行时作用域和定义时作用域可能会不同。函数的执行作用域跟定义(调用)它的对象有关。

在Vue组件的方法上使用装饰器,装饰器函数的参数target是methods对象。因为装饰器函数是一个立即执行的高阶函数,在它执行的时候,Vue组件还未被实例化(Javascript作用域中是由上向下,由内向外执行)。但是被装饰器函数返回的新函数,它的作用域会被Vue绑定到Vue组件实例上(在方法调用之前,Vue已经绑定完成了)。所以原对象的方法调用时,作用域绑定到新函数的作用域上即可。

Vue绑定方法作用域到组件实例上

0

在下面的图中,如果不修正作用域到Vue组件实例上,原来的方法afterClick就会因为作用域错误而发生错误。

0

0

  1. 一些应用实例

  2. 装饰器应用之表单验证

在一个Web项目中,可能存在非常多的表单,如注册、登录、修改用户信息等。在表单数据提交给后台之前,常常要做一些校验,比如创建广告计划时,代码如下:

0

改造后,

0

表单装饰器写法如下:

0

这里,装饰器函数外面再包一个函数,它接受el-form的ref值作为参数。因为el-form的表单校验是调用this.$refs[ref值].validate(),该方法返回一个promise对象。在这里拿到ref值后,校验合法性,如果所有form校验结果都通过,则执行原对象的函数(即发送ajax),否则弹层提示校验非法,函数不在继续往下执行。

对于一些页面中,组合了多个el-form的情况,可以在每个应用el-form的组件中,分别定义一个validate方法和getData方法,用于校验表单和获取表单提交的数据。每个应用el-form的组件写法如下:

0

例如下面,我将创建策略配置的页面拆分成2个包含el-form的组件,一个是基础信息,一个是流量桶数据。添加装饰器,先验证基础信息,再验证流量桶数据,都通过后再汇总两个表单提交的数据,一起提交到server端。而el-from各自的校验规则都保存在封装他们的组件内部,而对外只用暴露2个方法,增强了组件的封装性。一次校验多个el-form的代码如下:

0

  1. 提交前确认装饰器

比如一些后台项目,操作谨慎,常常需要二次确认弹框。按钮很多,比如回滚、全量发布、灰度发布、开启实验、停止实验等等。

0

通常,我要写大量重复无意义的confirm调用代码,代码如下:

0

使用装饰器后,节省了大量代码,而且辨识度更高,逻辑结构更清晰。代码如下:

0

  1. 装饰器累加使用

装饰器可以累加使用。比如可以先验证再弹确认框 ,也可以先弹确认框再验证数据合法性,只需要调整下装饰器的顺序就行了。装饰器由上向下执行。

0

它们其实就是,从上向下依次执行装饰器。先执行confirm,confirm内部再调用validateData()的返回

0

四、装饰器应用场景分析

装饰者模式是AOP(面向切面编程)的一种,它针对业务处理中的切面进行提取,把处理过程看成一个个切面,并以此作为切入点进行编程。比如,银行的取款、查询、转账接口都需要先验证用户的权限,我们可以在这三个接口调用之前作为一个切入点,编写校验用户权限的逻辑,而不必关心程序是从哪里来要到哪里去。

0

在实际业务场景中,装饰者模式常用来:

1.方法执行前校验(author校验、form数据校验、二次确认)

  1. 打点记录、统计数据

  2. 动态地植入一些属性和方法参数

  3. 给方法装饰成防抖和节流行为

  4. ajax调用后弹层

参考资料:

  1. zhuanlan.zhihu.com/p/20743493 —— JS 5种不同的方法实现装饰者模式(译)

  2. www.runoob.com/design-patt… —— 装饰器模式

  3. blog.csdn.net/a553181867/… —— 学习、探究Java装饰者模式

  4. blog.csdn.net/Grit_ICPC/a… —— AOP之代理模式与装饰者模式

  5. segmentfault.com/a/119000000… —— Javascript深入词法作用域与动态作用域

  6. es6.ruanyifeng.com/#docs/decor… —— ECMAScript 6入门之装饰器