AOP(面向切面編程)針對業務中的一些關鍵點/關鍵時刻所作的事情(即切面)進行抽離,抽離的是代碼執行的過程當中的某個關鍵步驟。簡單來講,AOP關注的是什麼時間點下的什麼行爲/定義。前端
OOP(面向對象編程)對於前端er應該都很熟悉了,咱們下面舉個例子來對比一下AOP和OOPreact
假設咱們有一個「車🚗」的類:面試
class Car {
constructor({ name, door, material, accelaration }) {
Object.assign(this, {
name,
door,
material,
accelaration
})
}
// 起步
start() {
console.log('start!')
}
// 行駛中
running() {
console.log(`${this.name} is running!`)
}
// 開門
open() {
console.log(`open the ${this.door}`)
}
// 加速
accelerate() {
console.log(`accelerate with ${this.accelaration}`)
}
}
複製代碼
而後有一個Lamborghini的類,繼承於Car類編程
class Lamborghini extends Car {
// Lamborghini路過的時候,擁有很高的回頭率,而且會被拍照
running() {
console.log(`${this.name} is running!`)
console.log('girls: "Ahh! Lamborghini is comming!"')
console.log('boys: "Look! Lamborghini is comming, let us take a photo"')
}
// Lamborghini開門的時候,你們都想看看車主到底是什麼樣的
open() {
console.log(`open the ${this.door}`)
console.log("who drive this?")
}
// Lamborghini加速的時候,巨大的聲浪吸引了你們的回頭
accelerate() {
console.log(`accelerate with ${this.accelaration}`)
console.log('~~~~~~~~~~~')
console.log("who's comming?")
}
}
const o = new Lamborghini({ name: 'Aventador', door: 'scissors door', material: 'carbon', accelaration: '3s 0-100' });
o.start();
o.running();
o.accelerate();
o.open();
複製代碼
另外有一個救護車類api
class ambulance extends Car {
// 救護車路過的時候,你們會讓開
running() {
console.log(`${this.name} is running!`)
console.log('bi~bu~, bi~bu~')
console.log('ambulance is comming, please go aside')
}
// 救護車開門的時候,醫生會下來拯救傷員
open() {
console.log(`open the ${this.door}`)
console.log("Are you ok?")
}
// 救護車加速的時候,沒什麼特別的
}
const c = new ambulance({ name: 'ambulance1', door: 'normal door', material: 'normal', accelaration: 'normal' });
c.start();
c.running();
c.accelerate();
c.open();
複製代碼
咱們能夠看見,OOP是經過繼承來複用一些和父類共有的屬性,若是有差別的話,那就在該子類的prototype上再定義差別之處。OOP是一種垂直上的代碼複用數組
AOP是面向切面、切點的編程,咱們須要找到切面、切點,並把有差別的特性注入到切點先後,實現水平上的代碼複用。babel
若是把上面的兩個子類改爲AOP實現,怎麼作呢?首先咱們能夠發現,每個子類不一樣的之處,只是父類的方法的一個修改。好比open方法是:app
// Lamborghini類open的時候
console.log(`open the ${this.door}`)
console.log("who drive this?")
// ambulance類open的時候
console.log(`open the ${this.door}`)
console.log("Are you ok?")
複製代碼
都有先open the ${this.door}
,那麼基於AOP的話,切點就是open the ${this.door}
,咱們要在open the door
後插入差別性的行爲:異步
function injectLamborghini(target) {
const { open } = target.prototype
target.prototype.open = function() {
open.call(this) // 公共特性open,也是切點
console.log("who drive this?") // 這就是差別性的行爲
}
return target
}
複製代碼
一樣的方法,咱們將其餘差別的特性注入到繼承父類的一個子類裏面,就是一個新的子類了:ide
function injectLamborghini(target) {
const { open, running, accelerate } = target.prototype
target.prototype.open = function() {
open.call(this) // 切點
console.log("who drive this?")
}
target.prototype.running = function() {
running.call(this) // 切點
console.log('girls: "Ahh! Lamborghini is comming!"')
console.log('boys: "Look! Lamborghini is comming, let us take a photo"')
}
target.prototype.accelerate = function() {
accelerate.call(this) // 切點
console.log('~~~~~~~~~~~')
console.log("who's comming?")
}
return target
}
const injectLamborghiniSubClass = injectLamborghini(class extends Car{})
const o = new injectLamborghiniSubClass({ name: 'Aventador', door: 'scissors door', material: 'carbon', accelaration: '3s 0-100' })
o.start();
o.running();
o.accelerate();
o.open();
// injectLamborghiniSubClass可使用裝飾器語法:
// 須要babel,能夠去本身的項目裏面試一下
@injectLamborghini
class Lamborghini extends Car{}
複製代碼
至於ambulance類如何改爲AOP風格來實現,相信你們應該內心有數了
一個異步請求,當請求返回的時候,拿到數據立刻setState並把loading組件換掉,很常規的操做。可是,當那個須要setState的組件被卸載的時候(切換路由、卸載上一個狀態組件)去setState就會警告:
若是要解決這個問題,咱們須要修改掛載、卸載、請求時的代碼
// 掛載
componentDidMount() {
this._isMounted = true;
}
// 卸載
componentWillUnmount() {
this._isMounted = false;
}
// 後面請求的時候
request(url)
.then(res => {
if (this._isMounted) {
this.setState(...)
}
})
複製代碼
可使用HOC來實現,也能夠基於裝飾器來實現AOP風格的代碼注入。使用裝飾器最終的表現就是,若是須要這個「不要對卸載的組件setState」功能的組件,加上一個裝飾器便可:
function safe(target) {
const {
componentDidMount,
componentWillUnmount,
setState,
} = target.prototype;
target.prototype.componentDidMount = function() {
componentDidMount.call(this); // 掛載的切點
this._isMounted = true;
}
target.prototype.componentWillUnmount = function() {
componentWillUnmount.call(this);// 卸載的切點
this._isMounted = false;
}
target.prototype.setState = function(...args) {
if (this._isMounted) { // 讓setstate只能在掛載後的元素進行
setState.call(this, ...args); // setstate的切點
}
}
}
// 使用的時候,只須要加一個safe的裝飾器
@safe
export default class Test extends Component {
// ...
}
複製代碼
函數組件內部狀態由hook維護,各類相似class組件的行爲均可以使用hook來模擬。並且之後整個項目全是函數組件是一個趨勢,沒有class如何使用AOP呢?
其實,hook已經天生自帶一絲的AOP的風格了,把一些邏輯寫好封裝到一個自定義hook裏面,須要使用的時候,往函數組件裏面插入該hook便可。
若是要在函數組件裏面基於AOP來複用代碼,首先,咱們要明確指出切點是哪裏。其次,咱們要對切點先後注入其餘代碼。最簡單的實現,就是使用發佈-訂閱模式往切點注入新的邏輯
// 自定義一個hook
function useAOP(opts = {}) {
const store = useRef({
...opts,
$$trigger(key, ...args) {
if (store[key]) {
store[key].apply(null, args);
}
}
}).current;
return store.$$trigger;
}
// 函數組件
function Test(props) {
const trigger = useAOP({
mount() {
console.log("did mount");
},
click() {
console.log('click')
}
});
useEffect(() => {
// 切點是組件掛載
trigger("mount");
}, [trigger]); // trigger確定是每次都同樣的,只會執行一次這個effect
// 切點是點擊的時候
return <div onClick={() => trigger('click')}>1</div>;
}
複製代碼
上面的實現,能夠支持依賴組件內部狀態的狀況。若是不須要依賴組件內部狀態,那麼咱們能夠直接在外面包一個函數,注入trigger到props裏面:
function createAOP(opts = {}) {
const store = {
...opts,
$$trigger(key, ...args) {
if (store[key]) {
store[key].apply(null, args);
}
}
};
return function(cpn) {
return function(...args) {
const props = args.shift(); // 給props注入trigger
// 注意,不能直接賦值哦,只能傳一個新的進去
return cpn.apply(null, [
{ ...props, $$trigger: store.$$trigger },
...args
]);
};
};
}
// 函數組件Test
function Test(props) {
const { $$trigger: trigger } = props;
useEffect(() => {
// 切點是組件掛載
trigger("mount");
}, [trigger]); // trigger確定是每次都同樣的,只會執行一次這個effect
// 切點是點擊的時候
return <div onClick={() => trigger('click')}>1</div>;
}
// 用的時候就用這個了
export default createAOP({
mount() {
console.log("did mount");
},
click() {
console.log("click");
}
})(Test)
複製代碼
若是有兩個頁面,頁面結構徹底不同,可是有幾個接口以及數據處理邏輯是徹底同樣的(增刪改)
// 有兩個頁面,操做的時候,請求的接口方法同樣
class A extends Component {
state = {
list: [{ info: "info1" }, { info: "info2" }]
};
add = () => {}
del = (index) => {}
edit = (index) => {}
render() {
// 刪除和修改的時候傳index進去處理某項數據
return (
<main> <button onClick={this.add}>新增</button> <ul> {this.state.list.map(({ info }, index) => ( <li> <a onClick={this.del.bind(this, index)}>刪除</a> <a onClick={this.edit.bind(this, index)}>修改</a> <h2>{info}</h2> </li> ))} </ul> </main>
);
}
}
class B extends Component {
state = {
list: [{ info: "不同的信息" }, { info: "不同的ui" }]
};
add = () => {}
del = (index) => {}
edit = (index) => {}
render() {
// 新增就新增,刪除和修改的時候傳index進去處理某項數據
return (
<section> {this.state.list.map(({ info }, index) => ( <p> <span onClick={this.del.bind(this, index)}>del</span> <a onClick={this.edit.bind(this, index)}>edit</a> <footer>{info}</footer> </p> ))} <a onClick={this.add}>+</a> </section>
);
}
}
複製代碼
通常狀況下,咱們多是把新增、修改、刪除單獨抽離出來,而後兩個組件裏面import進來,在class裏面新增這些方法,和state關聯起來(請求、請求成功、返回數據、setstate、作一些其餘的掛在this下的操做),這樣子咱們仍是作了一些相似且重複的事情。若是使用裝飾器爲這三個操做切點注入一些操做,那麼最後咱們只須要新增一行裝飾器代碼
// 僞代碼
function injectOperation(target) {
target.prototype.add = function(...args) {
// do something for this.state
request('/api/add', {
params: {
// ...
}
}).then(r => { // this已經綁的了,對state作一些事情 })
}
target.prototype.edit = function() {} // 相似的
target.prototype.del = function() {}
return target;
}
// example,組件內部再也不須要寫add、edit、del函數
@injectOperation
class A extends Component {}
複製代碼
關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技