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 {}
复制代码
关注公众号《不同的前端》,以不同的视角学习前端,快速成长,一块儿把玩最新的技术、探索各类黑科技