面向切面編程(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設計模式和開發實踐》