面向切面编程(Aspect-oriented programming,AOP)是一种编程范式。作后端 Java web 的同窗,特别是用过 Spring 的同窗确定对它很是熟悉。AOP 是 Spring 框架里面其中一个重要概念。但是在 Javascript 中,AOP 是一个常常被忽视的技术点。javascript
假设你如今有一个牛逼的日历弹窗,有一天,老板让你统计一下天天这个弹窗里面某个按钮的点击数,因而你在弹窗里作了埋点;前端
过了一个星期,老板说用户反馈这个弹窗好慢,各类卡顿。你想看一下某个函数的平均执行时间,因而你又在弹窗里加上了性能统计代码。java
时间久了,你会发现你的业务逻辑里包含了大量的和业务无关的东西,即便是一些你已经封装过的函数。react
那么 AOP 就是为了解决这类问题而存在的。web
分离业务代码和数据统计代码(非业务代码),不管在什么语言中,都是AOP的经典应用之一。从核心关注点中分离出横切关注点,是 AOP 的核心概念。ajax
在前端的常见需求中,有如下一些业务可使用 AOP 将其从核心关注点中分离出来编程
提到 AOP 就要说到装饰器模式,AOP 常常会和装饰器模式混为一谈。json
在 ES6+ 以前,要使用装饰器模式,一般经过Function.prototype.before
作前置装饰,和Function.prototype.after
作后置装饰(见《Javascript设计模式和开发实践》)。后端
Javascript 引入的 Decorator ,和 Java 的注解在语法上很相似,不过在语义上没有一丁点关系。Decorator 提案提供了对 Javascript 的类和类里的方法进行装饰的能力。(尽管只是在编译时运行的函数语法糖)设计模式
由于在使用 React 的实际开发中有大量基于 Class 的 Component,因此我这里用 React 来举例。
好比如今页面中有一个button,点击这个button会弹出一个弹窗,与此同时要进行数据上报,来统计有多少用户点击了这个登陆button。
import React, { Component } from 'react';
import send from './send';
class Dialog extends Component {
constructor(props) {
super(props);
}
@send
showDialog(content) {
// do things
}
render() {
return (
<button onClick={() => this.showDialog('show dialog')}>showDialog</button>
)
}
}
export default Dialog;
复制代码
上面代码引用了@send
装饰器,他会修改这个 Class 上的原型方法,下面是@send
装饰器的实现
export default function send(target, name, descriptor) {
let oldValue = descriptor.value;
descriptor.value = function () {
console.log(`before calling ${name} with`, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
复制代码
在按钮点击后执行showDialog
前,能够执行咱们想要的切面操做,咱们能够将埋点,数据上报相关代码封装在这个装饰器里面来实现 AOP。
上面的send
这个装饰器实际上是一个前置装饰器,咱们能够将它再封装一下使它能够前置执行任意函数。
function before(beforeFn = function () { }) {
return function (target, name, descriptor) {
let oldValue = descriptor.value;
descriptor.value = function () {
beforeFn.apply(this, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
}
复制代码
这样咱们就可使用@before
装饰器在一个原型方法前切入任意的非业务代码。
function beforeLog() {
console.log(`before calling ${name} with`, arguments);
}
class Dialog {
...
@before(beforeLog)
showDialog(content) {
// do things
}
...
}
复制代码
和@before
装饰器相似,能够实现一个@after
后置装饰器,只是函数的执行顺序不同。
function after(afterFn = function () { }) {
return function (target, name, descriptor) {
let oldValue = descriptor.value;
descriptor.value = function () {
let ret = oldValue.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
return descriptor;
}
}
复制代码
有时候咱们想统计一段代码在用户侧的执行时间,可是又不想将打点代码嵌入到业务代码中,一样能够利用装饰器来作 AOP。
function measure(target, name, descriptor) {
let oldValue = descriptor.value;
descriptor.value = function () {
let ret = oldValue.apply(this, arguments);
performance.mark("startWork");
afterFn.apply(this, arguments);
performance.mark("endWork");
performance.measure("work", "startWork", "endWork");
performance
.getEntries()
.map(entry => JSON.stringify(entry, null, 2))
.forEach(json => console.log(json));
return ret;
};
return descriptor;
}
复制代码
在要统计执行时间的类方法前面加上@measure
就好了,这样作性能统计的代码就不会侵入到业务代码中。
class Dialog {
...
@measure
showDialog(content) {
// do things
}
...
}
复制代码
面向切面编程的重点就是将核心关注面分离出横切关注面,前端能够用 AOP 优雅的来组织数据上报、性能分析、统计函数的执行时间、动态改变函数参数、插件式的表单验证等代码。
《Javascript设计模式和开发实践》