共计 7553 个字符,预计需要花费 19 分钟才能阅读完成。
产品上线事繁多,测试产品催不离。
休问 Bug 剩多少,眼圈如漆身如泥。
作为一个已经的 Java coder
, 当我第一次看到js
外面的装璜器 (Decorator
) 的时候,就马上想到了 Java
中的注解,当然在理论原理和性能下面,Java
的注解和 js
的装璜器还是有很大差异的。本文题目是Vue 中应用装璜器,我是认真的
,但本文将从装璜器的概念开发聊起,一起来看看吧。
通过本文内容,你将学到以下内容:
- 理解什么是装璜器
- 在办法应用装璜器
- 在
class
中应用装璜器 - 在
Vue
中应用装璜器
本文首发于公众号【前端有的玩】,不想当咸鱼,想要换工作,关注公众号,带你每日一起刷大厂面试题,关注
===
大厂offer
。
什么是装璜器
装璜器是 ES2016
提出来的一个提案,以后处于 Stage 2
阶段,对于装璜器的体验,能够点击 https://github.com/tc39/proposal-decorators 查看详情。装璜器是一种与类相干的语法糖,用来包装或者批改类或者类的办法的行为,其实装璜器就是设计模式中装璜者模式的一种实现形式。不过后面说的这些概念太干了,咱们用人话来翻译一下,举一个例子。
在日常开发写 bug
过程中,咱们常常会用到防抖和节流,比方像上面这样
class MyClass {follow = debounce(function() {console.log('我是子君,关注我哦') | |
}, 100) | |
} | |
const myClass = new MyClass() | |
// 屡次调用只会输入一次 | |
myClass.follow() | |
myClass.follow() |
下面是一个防抖的例子,咱们通过 debounce
函数将另一个函数包起来,实现了防抖的性能,这时候再有另一个需要,比方心愿在调用 follow
函数前后各打印一段日志,这时候咱们还能够再开发一个 log
函数,而后持续将 follow
包装起来
/** | |
* 最外层是防抖,否则 log 会被调用屡次 | |
*/ | |
class MyClass { | |
follow = debounce(log(function() {console.log('我是子君,关注我哦') | |
}), | |
100 | |
) | |
} |
下面代码中的 debounce
和log
两个函数,实质上是两个包装函数,通过这两个函数对原函数的包装,使原函数的行为产生了变动,而 js
中的装璜器的原理就是这样的,咱们应用装璜器对下面的代码进行革新
class MyClass {@debounce(100) | |
@log | |
follow() {console.log('我是子君,关注我哦') | |
} | |
} |
装璜器的模式就是 @ + 函数名
,如果有参数的话,前面的括号外面能够传参
在办法上应用装璜器
装璜器能够利用到 class
上或者 class
外面的属性下面,但个别状况下,利用到 class
属性下面的场景会比拟多一些,比方像下面咱们说的 log
,debounce
等等,都个别会利用到类属性下面,接下来咱们一起来具体看一下如何实现一个装璜器,并利用到类下面。在实现装璜器之前,咱们须要先理解一下属性描述符
理解一下属性描述符
在咱们定义一个对象外面的属性的时候,其实这个属性下面是有许多属性描述符的,这些描述符表明了这个属性能不能批改,能不能枚举,能不能删除等等,同时 ECMAScript
将这些属性描述符分为两类,别离是数据属性和拜访器属性,并且数据属性与拜访器属性是不能共存的。
数据属性
数据属性蕴含一个数据值的地位,在这个地位能够读取和写入值。数据属性蕴含了四个描述符,别离是
configurable
示意能不能通过
delete
删除属性,是否批改属性的其余描述符个性,或者是否将数据属性批改为拜访器属性。当咱们通过let obj = {name: ''}
申明一个对象的时候,这个对象外面所有的属性的configurable
描述符的值都是true
enumerable
示意能不能通过
for in
或者Object.keys
等形式获取到属性,咱们个别申明的对象外面这个描述符的值是true
, 然而对于class
类外面的属性来说,这个值是false
writable
示意是否批改属性的数据值,通过将这个批改为
false
, 能够实现属性只读的成果。value
示意以后属性的数据值,读取属性值的时候,从这里读取;写入属性值的时候,会写到这个地位。
拜访器属性
拜访器属性不蕴含数据值,他们蕴含了 getter
与setter
两个函数,同时 configurable
与enumerable
是数据属性与拜访器属性共有的两个描述符。
getter
在读取属性的时候调用这个函数,默认这个函数为
undefined
setter
在写入属性值的时候调用这个函数,默认这个函数为
undefined
理解了这六个描述符之后,你可能会有几个疑难:我如何去定义批改这些属性描述符?这些属性描述符与明天的文章主题有什么关系?接下来是揭晓答案的时候了。
应用Object.defineProperty
理解过 vue2.0
双向绑定原理的同学肯定晓得,Vue
的双向绑定就是通过应用 Object.defineProperty
去定义数据属性的 getter
与setter
办法来实现的,比方上面有一个对象
let obj = { | |
name: '子君', | |
officialAccounts: '前端有的玩' | |
} |
我心愿这个对象外面的用户名是不能被批改的,用 Object.defineProperty
该如何定义呢?
Object.defineProperty(obj,'name', { | |
// 设置 writable 是 false, 这个属性将不能被批改 | |
writable: false | |
}) | |
// 批改 obj.name | |
obj.name = "小人" | |
// 打印仍然是子君 | |
console.log(obj.name) |
通过 Object.defineProperty
能够去定义或者批改对象属性的属性描述符,然而因为数据属性与拜访器属性是互斥的,所以一次只能批改其中的一类,这一点须要留神。
定义一个防抖装璜器
装璜器实质上仍然是一个函数,不过这个函数的参数是固定的,如下是防抖装璜器的代码
/** | |
*@param wait 提早时长 | |
*/ | |
function debounce(wait) {return function(target, name, descriptor) {descriptor.value = debounce(descriptor.value, wait) | |
} | |
} | |
// 应用形式 | |
class MyClass {@debounce(100) | |
follow() {console.log('我是子君,我的公众号是【前端有的玩】,关注有惊喜哦') | |
} | |
} |
咱们逐行去剖析一下代码
- 首先咱们定义了一个
debounce
函数,同时有一个参数wait
,这个函数对应的就是在上面调用装璜器时应用的@debounce(100)
-
debounce
函数返回了一个新的函数,这个函数即装璜器的外围,这个函数有三个参数,上面逐个剖析target
: 这个类属性函数是在谁下面挂载的,如上例对应的是MyClass
类name
: 这个类属性函数的名称,对应下面的follow
descriptor
: 这个就是咱们后面说的属性描述符,通过间接descriptor
下面的属性,即可实现属性只读,数据重写等性能
- 而后第三行
descriptor.value = debounce(descriptor.value, wait)
, 后面咱们曾经理解到, 属性描述符下面的value
对应的是这个属性的值,所以咱们通过重写这个属性,将其用debounce
函数包装起来,这样在函数调用follow
时理论调用的是包装后的函数
通过下面的三步,咱们就实现了类属性下面可应用的装璜器,同时将其利用到了类属性下面
在 class
上应用装璜器
装璜器不仅能够利用到类属性下面,还能够间接利用到类下面,比方我心愿能够实现一个相似 Vue
混入那样的性能,给一个类混入一些办法属性,应该如何去做呢?
// 这个是要混入的对象 | |
const methods = {logger() {console.log('记录日志') | |
} | |
} | |
// 这个是一个登陆登出类 | |
class Login{login() {} | |
logout() {} | |
} |
如何将下面的 methods
混入到 Login
中,首先咱们先实现一个类装璜器
function mixins(obj) {return function (target) {Object.assign(target.prototype, obj) | |
} | |
} | |
// 而后通过装璜器混入 | |
@mixins(methods) | |
class Login{login() {} | |
logout() {} | |
} |
这样就实现了类装璜器。对于类装璜器,只有一个参数,即target
, 对应的就是这个类自身。
理解完装璜器,咱们接下来看一下如何在 Vue
中应用装璜器。
在 Vue
中应用装璜器
应用 ts
开发 Vue
的同学肯定对 vue-property-decorator
不会感到生疏,这个插件提供了许多装璜器,不便大家开发的时候应用,当然本文的中点不是这个插件。其实如果咱们的我的项目没有应用ts
,也是能够应用装璜器的,怎么用呢?
配置根底环境
除了一些老的我的项目,咱们当初个别新建 Vue
我的项目的时候,都会抉择应用脚手架 vue-cli3/4
来新建,这时候新建的我的项目曾经默认反对了装璜器,不须要再配置太多额定的货色,如果你的我的项目应用了 eslint
, 那么须要给eslint
配置以下内容。
parserOptions: { | |
ecmaFeatures:{ | |
// 反对装璜器 | |
legacyDecorators: true | |
} | |
} |
应用装璜器
尽管 Vue
的组件,咱们个别书写的时候 export
进来的是一个对象,然而这个并不影响咱们间接在组件中应用装璜器,比方就拿上例中的 log
举例。
function log() { | |
/** | |
* @param target 对应 methods 这个对象 | |
* @param name 对应属性办法的名称 | |
* @param descriptor 对应属性办法的修饰符 | |
*/ | |
return function(target, name, descriptor) {console.log(target, name, descriptor) | |
const fn = descriptor.value | |
descriptor.value = function(...rest) {console.log(` 这是调用办法【${name}】前打印的日志 `) | |
fn.call(this, ...rest) | |
console.log(` 这是调用办法【${name}】后打印的日志 `) | |
} | |
} | |
} | |
export default {created() {this.getData() | |
}, | |
methods: {@log() | |
getData() {console.log('获取数据') | |
} | |
} | |
} |
看了下面的代码,是不是发现在 Vue
中应用装璜器还是很简略的,和在 class
的属性下面应用的形式截然不同,但有一点须要留神,在 methods
外面的办法下面应用装璜器,这时候装璜器的 target
对应的是methods
。
除了在 methods
下面能够应用装璜器之外,你也能够在生命周期钩子函数下面应用装璜器,这时候 target
对应的是整个组件对象。
一些罕用的装璜器
上面小编列举了几个小编在我的项目中罕用的几个装璜器,不便大家应用
1. 函数节流与防抖
函数节流与防抖利用场景是比拟广的,个别应用时候会通过 throttle
或debounce
办法对要调用的函数进行包装,当初就能够应用上文说的内容将这两个函数封装成装璜器,防抖节流应用的是 lodash
提供的办法,大家也能够自行实现节流防抖函数哦
import {throttle, debounce} from 'lodash' | |
/** | |
* 函数节流装璜器 | |
* @param {number} wait 节流的毫秒 | |
* @param {Object} options 节流选项对象 | |
* [options.leading=true] (boolean): 指定调用在节流开始前。* [options.trailing=true] (boolean): 指定调用在节流完结后。*/ | |
export const throttle = function(wait, options = {}) {return function(target, name, descriptor) {descriptor.value = throttle(descriptor.value, wait, options) | |
} | |
} | |
/** | |
* 函数防抖装璜器 | |
* @param {number} wait 须要提早的毫秒数。* @param {Object} options 选项对象 | |
* [options.leading=false] (boolean): 指定在提早开始前调用。* [options.maxWait] (number): 设置 func 容许被提早的最大值。* [options.trailing=true] (boolean): 指定在提早完结后调用。*/ | |
export const debounce = function(wait, options = {}) {return function(target, name, descriptor) {descriptor.value = debounce(descriptor.value, wait, options) | |
} | |
} |
封装完之后,在组件中应用
import {debounce} from '@/decorator' | |
export default { | |
methods:{@debounce(100) | |
resize(){} | |
} | |
} |
2. loading
在加载数据的时候,为了个用户一个敌对的提醒,同时避免用户持续操作,个别会在申请前显示一个 loading, 而后在申请完结之后关掉 loading,个别写法如下
export default { | |
methods:{async getData() {const loading = Toast.loading() | |
try{const data = await loadData() | |
// 其余操作 | |
}catch(error){ | |
// 异样解决 | |
Toast.fail('加载失败'); | |
}finally{loading.clear() | |
} | |
} | |
} | |
} |
咱们能够把下面的 loading
的逻辑应用装璜器从新封装,如下代码
import {Toast} from 'vant' | |
/** | |
* loading 装璜器 | |
* @param {*} message 提示信息 | |
* @param {function} errorFn 异样解决逻辑 | |
*/ | |
export const loading = function(message = '加载中...', errorFn = function() {}) {return function(target, name, descriptor) { | |
const fn = descriptor.value | |
descriptor.value = async function(...rest) { | |
const loading = Toast.loading({ | |
message: message, | |
forbidClick: true | |
}) | |
try {return await fn.call(this, ...rest) | |
} catch (error) { | |
// 在调用失败,且用户自定义失败的回调函数时,则执行 | |
errorFn && errorFn.call(this, error, ...rest) | |
console.error(error) | |
} finally {loading.clear() | |
} | |
} | |
} | |
} |
而后革新下面的组件代码
export default { | |
methods:{@loading('加载中') | |
async getData() { | |
try{const data = await loadData() | |
// 其余操作 | |
}catch(error){ | |
// 异样解决 | |
Toast.fail('加载失败'); | |
} | |
} | |
} | |
} |
3. 确认框
当你点击删除按钮的时候,个别都须要弹出一个提示框让用户确认是否删除,这时候惯例写法可能是这样的
import {Dialog} from 'vant' | |
export default { | |
methods: {deleteData() { | |
Dialog.confirm({ | |
title: '提醒', | |
message: '确定要删除数据,此操作不可回退。' | |
}).then(() => {console.log('在这里做删除操作') | |
}) | |
} | |
} | |
} |
咱们能够把下面确认的过程提出来做成装璜器,如下代码
import {Dialog} from 'vant' | |
/** | |
* 确认提示框装璜器 | |
* @param {*} message 提示信息 | |
* @param {*} title 题目 | |
* @param {*} cancelFn 勾销回调函数 | |
*/ | |
export function confirm( | |
message = '确定要删除数据,此操作不可回退。', | |
title = '提醒', | |
cancelFn = function() {} | |
) {return function(target, name, descriptor) { | |
const originFn = descriptor.value | |
descriptor.value = async function(...rest) { | |
try { | |
await Dialog.confirm({ | |
message, | |
title: title | |
}) | |
originFn.apply(this, rest) | |
} catch (error) {cancelFn && cancelFn(error) | |
} | |
} | |
} | |
} |
而后再应用确认框的时候,就能够这样应用了
export default { | |
methods: { | |
// 能够不传参,应用默认参数 | |
@confirm() | |
deleteData() {console.log('在这里做删除操作') | |
} | |
} | |
} |
是不是霎时简略多了, 当然还能够持续封装很多很多的装璜器,因为文章内容无限,临时提供这三个。
装璜器组合应用
在下面咱们将类属性下面应用装璜器的时候,说道装璜器能够组合应用,在 Vue
组件下面应用也是一样的,比方咱们心愿在确认删除之后,调用接口时候呈现loading
,就能够这样写(肯定要留神程序)
export default { | |
methods: {@confirm() | |
@loading() | |
async deleteData() {await delete() | |
} | |
} | |
} |
本节定义的装璜器,均已利用到这个我的项目中 https://github.com/snowzijun/vue-vant-base, 这是一个基于
Vant
开发的开箱即用挪动端框架,你只须要fork
下来,无需做任何配置就能够间接进行业务开发,欢送应用,喜爱麻烦给一个star
。
我是子君,明天就写这么多,本文首发于【前端有的玩】,这是一个专一于前端技术,前端面试相干的公众号,同时关注之后即刻拉你退出前端交换群,咱们一起聊前端,欢送关注。
结语
不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。——文森特・梵高