Decorator(修饰器),用来修改原类/方法的功能。
我们对装饰器并不陌生,例如 dvajs的 @connect写法
// 将 model 和 component 串联起来
@connect(({ user }) => ({
currentUser: user.currentUser
}))
export default class BasicLayout extends React.PureComponent {
// ...
}
网上例举了很多假设场景,我这里属于实际应用到,所以写下文章记录下。看下面的例子就能懂了。
一、基本知识:
装饰器本身就是一个普通函数 可以装饰类、类的方法和属性 如果装饰方法,装饰器会先运行,完成后再运行原本方法
一、最简单的装饰器(装饰类):
注意:装饰类的时候,只在编译时候
const myDec = (target) => {
target.aaa = 1
}
@myDec
class test1 {
}
上面的效果等价于
class test1 {
static aaa = 1
}
二、装饰类里面的方法:
//装饰器
const changeParams = () => {
// 三个参数分别是:
// 装饰的方法的类本身(可以理解为class的this)
//装饰的方法名称
//可以理解为Object.defineProperty()
return (target, name, desc) => {
const fun = desc.value //保存一份源方法
//这里获取到原方法的参数!也可以用 arguments 取得全部参数
desc.value = function (params) {
//do something
return fun(params)
}
}
}
class test {
// 放在类里面的方法上面
@changeParams ()
someFunction () {
}
}
装饰器的基本写法,万变不离其中。
二、基本使用:
场景:某位A同学写的代码,myClass有很多方法getList1、getList2、getList3....,方法里面有很多复杂代码,最终目的需要把其中几个方法传进来的随机小写字母变为大写。在保持原方法不变情况下,使用装饰器来做。
1、需要修改的类里面的方法(使用前)
class myClass {
getList1(params) {
// some code
console.log('params', params)
return params
}
getList2(params) {
// some code
console.log('params', params)
return params
}
// ...some other method
}
const { getList1 , getList2 } = new myClass()
getList1('aaa')
getList2('abc')
//分别输出 aaa abc
2、使用装饰器修改(使用后)
/**
* 写一个装饰器
* @returns {(function(*=, *=, *=): void)|*}
*/
const changeParams = () => {
return (target, name, desc) => {
const fun = desc.value //保存源方法
desc.value = function (params) {
let translate = params.toUpperCase() //修改参数,大小写转换
return fun(translate) // 返回要运行的方法
}
}
}
class myClass {
@changeParams() //这里使用装饰器!
getList1(params) {
// some code
console.log('params', params)
return params
}
@changeParams() //这里使用装饰器!
getList2(params) {
// some code
console.log('params', params)
return params
}
// ...some other method
}
const { getList1, getList2 } = new myClass()
getList1('aaa')
getList2('abc')
//分别输出 AAA ABC
3、进阶使用(接上面的修改)
此时有个需求,我想getList1方法返回大写+小写,getList2返回大写,怎么办,我是不是还要再写一个装饰器?这时只需要给装饰器加个参数判断就可以
/**
* 写一个装饰器
* @returns {(function(*=, *=, *=): void)|*}
*/
const changeParams = (shouldUpAndLower) => {
return (target, name, desc) => {
const fun = desc.value
desc.value = function (params) {
let translate = shouldUpAndLower ? params.toUpperCase() + params : params.toUpperCase() //装饰器的参数在这里做判断
return fun(translate)
}
}
}
class myClass {
@changeParams(true) //这里使用装饰器!
getList1(params) {
// some code
console.log('params', params)
return params
}
@changeParams(false) //这里使用装饰器!
getList2(params) {
// some code
console.log('params', params)
return params
}
// ...some other method
}
const { getList1, getList2 } = new myClass()
getList1('aaa')
getList2('abc')
//分别输出 AAAaaa ABC
这样即可完成。
this指向问题:
注意,上面的myClass 中方法我们用的不是箭头函数,那么就有可能出现,使用this 时候 this丢失的情况。
例如假设原本方法getList1有用到类内部的this,此时会出现报错: TypeError: Cannot read property 'data' of undefined
解决办法是,在装饰器里绑定this,(或者将方法改为箭头函数,但是我们写装饰器的原则就是为了不改变原方法,所以不推荐)。
完善一下装饰器,添加this绑定
/**
* 写一个装饰器
* @returns {(function(*=, *=, *=): void)|*}
*/
const changeParams = () => {
return (target, name, desc) => {
const fun = desc.value
desc.value = function (params) {
let translate = params.toUpperCase()
return fun.call(target, translate) //绑定this,target指向方法的类本身
}
}
}
三、在项目中的实际应用场景例举:
1、前端传参需要将驼峰转为下划线传给后端,且需要将后端返回的数据下划线格式转为驼峰格式。 (备注:后端是能有这个功能直接转换的,这里是我前端做了转换,因为后端为了规范,原来的个别!接口数据格式需要将驼峰转为下划线,所以为了保持原本写的方法不变,这里采用装饰器语法再合适不过了。)
实例全部代码:
----server.js文件(没使用之前)
class server {
async getTableList(params) {
return await fetch(`/aaa/bbb/ccc`, { params: params })
}
// other fetch....
}
export const { getShiftList } = new server()
----server.js文件(使用之后)
import { objTranslate } from '@/util/utils' //引入装饰器方法
class server {
@objTranslate() //装饰器,只需要在想转换的接口前面加上装饰器就可以
async getTableList(params) {
return await fetch(`/aaa/bbb/ccc`, { params: params })
}
// other fetch....
}
export const { getShiftList } = new server()
-----utils文件
/**
* 是否需要转换数据,下划线/驼峰
* @param resTranslate 返回结果是否需要转换
* @param paramsTranslate 入参是否转换
* @returns {(function(*, *, *): void)|*}
*/
export const objTranslate = ({ resTranslate = true, paramsTranslate = true } = {}) => {
return function (target, name, desc) {
const fun = desc.value //记录原函数
desc.value = async function (params, params2) { //第二个参数一般接口自取字段,不需要处理
if (!params) {
return resTranslate ? await lineToCamel(fun.call(target)) : await fun.call(target)
}
const translateParams = paramsTranslate ? objectHumpToLine({ ...params }) : params
return resTranslate ? lineToCamel(await fun.call(target, translateParams, params2)) :
await fun.call(target, translateParams, params2)
}
}
}
/**
* 驼峰转下划线
* @param data
* @returns {{}|*}
*/
export function objectHumpToLine(data) {
if (typeof data != 'object' || !data) return data
if (Array.isArray(data)) {
return data.map(item => objectHumpToLine(item))
}
const newData = {}
for (let key in data) {
let newKey = key.replace(/([A-Z])/g, (p, m) => `_${m.toLowerCase()}`)
newData[newKey] = objectHumpToLine(data[key])
}
return newData
}
/**
* 下划线转驼峰
* @param data
* @returns {{}|*}
*/
export function lineToCamel(data) {
if (typeof data != 'object' || !data) return data
if (Array.isArray(data)) {
return data.map(item => lineToCamel(item))
}
const newData = {}
for (let key in data) {
let newKey = key.replace(/_([a-z])/g, (p, m) => m.toUpperCase())
newData[newKey] = lineToCamel(data[key])
}
return newData
}
2、部分接口数据需要先判断查询本地有无缓存,再做是否请求的动作(应用端时候,节流用) 不例举
注意:
如果装饰纯函数会有变量提升问题
这就是为什么你在react的纯函数组件里面不能使用@connect语法糖写法的原因!
总结:
优点:该语法最好的好处是不用修改原来的代码,就可以修改原来的方法。
缺点:刚接触有点隐涩难懂。