前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门

面向切面编程的概念解读

  1. 什么是面向切面编程?
Aspect Oriented Programming (AOP),面向切面编程,是一个比较热门的话题。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。-- 面向切面编程_百度百科
  1. 用普通的大白话去解释上面比较官方的话
  • 什么是切面
比如说我们用刀切一块面包,那么切过之后在我们眼前的这个面就是所谓的切面
  • 面向切面比较热门是因为它有很多的优点,因为它可以进行那种无侵入的干扰
比如说还是我们有一块面包,切得时候不乱切,从他的开头或者结尾的地方进行切取,而不是从中间横切竖切,如果我们按照这种顺序来切就始终能保留着它有一块完整的面包,如果说乱切又横切又竖切这样的话这块面包就乱掉了,所以面向切面编程最大的优点就是它可以对业务进行无侵入的干扰
  • “AOP 主要的目的是针对业务处理过程中的切面进行提取”
这句话就是说:你想要吃面包就从两头切就好了,而不要从中间乱动
  • “它所面对的是处理过程中的某个步骤或阶段”
这句话是说:他所面对的肯定是我们当前需要处理的业务中的某个部分, 要对这个业务进行一系列的操作,以及对这个业务进行相应的提取和隔离,我们不对人家具体的业务进行干扰,我们只对这个业务进行操作,将这个业务放到开头或者结尾表面的位置
  • “以获得逻辑过程中各部分之间低耦合性的隔离效果”
这句话是说:如果你往人家的业务里面穿插了一些代码,比如说比较常用的例子:要做一个网页的性能检测,那么就要在我们的 html 代码里面穿插各种各样比较讨厌的节点,然后再加很多东西,或者说你用 js 来做,在 js 中埋下 N 多的埋点,然后再恰当的时机再 send 给服务器,如果说你把这个做成面向切面的话,就是不在里面进行埋点,而是在整体的业务逻辑上给它加对应的东西,然后这个东西在需要执行的函数之前或者之后执行,这样的话如果有一天进行了热插拔,将真正的函数给抽走了,那么这样对原来人家的逻辑也不会有任何的影响,那么你这个埋点的工作就和人家原来的执行函数没有任何相应的混淆,他也能进行这种隔离效果
  1. 用图片来解释上面的逻辑
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
上图的是一个地板,这个地板对向我们的这个面就是我们用刀把地板横割出来的一个面,那么如果说我们想测这个地板的高度和宽度的话,我们是不需要在它的里面打一个一个的小点然后一点点的那么去测的,我们最常用的方法肯定是:上下表面在上表面用米尺往下一钩、压紧了,然后一看显示的就是这个的高了,同样的道理在横向的方向这样一压尺子也就能知道这个的宽了,这个就是通过他的上下面、左右面就能拿到对应的想要拿到的数据了,这个例子就是所谓的面向切面编程了
JavaScript 面向切面编程的代码实战
  1. 在桌面新建一个 aoptest 的文件夹然后将其拖拽至 sublime 编辑器中,在 aoptest 文件夹下新建一个 index.html 和 aop.js 文件
    编辑 index.html 文件

前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 编辑 aop.js 文件
  • 写一个简单的函数
function test() { alert(2); };

  • 比如说现在的需求是:你要统计一下当前的所有的函数谁耗时最长,然后做相应的性能优化
  • 普通的方法
function test() { var start = new Date(); alert(2); var end = new Date(); console.log(end - start); };

  • 上面是因为 js 逻辑比较简单的,然后使用了上面的简单的方法,但是如果逻辑相当的复杂的话,你在人每个代码里都插一段这样的代码,这样做显然比较的不合适,而且搞不好人家声明了一个 start 你也声明了一个 start 这样变量冲突了,会把人家的变量给污染了,那样的话就影响了业务逻辑,所以说我们就要想一套无侵入的代码方式,直接能统计出来当前的 test() 以及里面所有的 function 的执行时间
//无侵入式的写法 //给 Function 的 prototype 上绑定一个 before 方法,就是在你函数执行之前给你一个时间点 //参数 fn 是函数执行时一定要给的回调 Function.prototype.before = function(fn){ var __self = this; //拿到 这个 this,将这个 this 保存起来以便后面出现了问题还能拿到这个 this 的缓存 //要先让回调 fn 先执行,这样就是在 test 函数执行前先执行了 fn(); //下面 self 执行自己就可以了 再把参数也传过去 __self.apply(this,arguments); }; //给 Function 的 prototype 上绑定一个 after 方法,就是在你函数执行之后再给你一个时间点 //参数 fn 是函数执行时要给的回调 Function.prototype.after = function(fn){};

  • 写完上面的其实就可以执行 before 了,这里已经将之前的简单方法给注释掉了
test.before(function(){ //先执行一个较简单的 alert(1); })

  1. 在浏览器中打开 index.html 页面,会发现执行顺序确实是如我们写的那样的,先执行的是回调的 fn 再执行了 test
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 上面的就实现了无侵入式的代码,对他原来的代码逻辑没有做任何的处理,但是却是可以执行里面的相应的逻辑,而且在 before 之前就可以进行处理
  2. 接下来对代码进行一些修改
//写一个简单的函数 function test() { alert(2); return 'me test'; };

  • 下面在执行 before 的时候 实际上 __self 已经和之前的 test 没关系了,它相当于是 test 的另一个副本 这个时候就可以在下面执行他自己的时候将 'me test' return 出来
Function.prototype.before = function(fn){ var __self = this; //拿到 这个 this,将这个 this 保存起来以便后面出现了问题还能拿到这个 this 的缓存 //要让 fn 先执行,这样就是在 test 函数执行前先执行了 fn(); //下面 self 执行自己就可以了 再把参数也传过去 return __self.apply(this,arguments); };

  • 复制到浏览器的控制台中执行之后会发现这个 return 成功的将 test 中的 return 给 return 出来了
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 上面是 before 的实现逻辑,并没有写 after,下面就是 after 的具体实现
  • 编辑文件时先将之前调用的 test.befor() 给注释掉
  • 先完善 after 方法
//给 Function 的 prototype 上绑定一个 after 方法,就是在你函数执行之后再给你一个时间点 //参数 fn 是函数执行时要给的回调 Function.prototype.after = function(fn){ //after 和 before 的执行顺序刚好相反,要先执行本身 this 然后再执行回调 fn var __self = this; //跟 before一样要先把这个 this 存起来 //先执行它自己 __self.apply(this,arguments); //再执行回调 fn fn(); };

  • 接着在下面调用 after 方法
//下面执行的是 after test.after(function(){ //先执行一个较简单的 alert(3); });

  1. 接着就可以刷新浏览器来查看具体效果
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  • 上图的执行效果就证明这个 after 执行已经在 test 之后了
  1. 接着将之前的 test.before() 解注释再来查看执行效果
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  • 从上图可以看到执行顺序没问题了,但是出现了一个明显的问题:默认函数 test 被执行了两遍
  1. 下面就来优化一下执行两遍的问题,具体的思路是:
将 test 作为中转 before 回调和 before 一起送到 after 去 ,after 这个时候就可以写在前面了 ,也需要将 after 和 test 一起送到 before 去
  1. 对上面的思路具体的实现, before 方法
//我们是将所有的东西都绑到了 before 身上,before 和 after 都挂载到了 Function 的原型链上,所有说我们得要他进行这样链式的调用 //想要进行链式的调用首先修改 before Function.prototype.before = function(fn) { var __self = this; //拿到 这个 this,将这个 this 保存起来以便后面出现了问题还能拿到这个 this 的缓存 //在这里 return 一个 function 回去 这样的话当我执行完 before 之后 ,因为 before 和 after 是挂载到了原型链上 这样这个 return function 就可以执行 after 和before 了因为 after 和before 是挂载到了最顶层的原型链上 return function() { //这里面的 this 指向的是调用的函数 console.log(this); //这里在控制台输出的是 window,因为这整个 return function 作为一个闭包已经被 return 出去了 当下面的 before 执行完之后 这个闭包已经被暴露在最外层的环境里了,所以这个 this 指向了 window //要先让回调 fn 先执行,这样就是在 test 函数执行前先执行了 fn(); //下面 self 执行自己就可以了 再把参数也传过去 __self.apply(this, arguments); } }; //下面执行一下 before 查看效果 test.before(function(){ alert(1); })();

10.1 可以在控制台中看到此时的 this 指向的是 window
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png 10.2 上面的 this 已经指向了 window 了,如果是为了使 window 也能使用这个 fn 可以将 fn 也写成下面 __self 那样
fn.apply(this,arguments); __self.apply(this, arguments);

10.3 但是我们这里是需要保住之前执行的 this 也就是 test 函数 自己,所以需要用到上面缓存的 __self
fn.apply(__self,arguments); __self.apply(__self, arguments);

10.4 这个如果想要动态的来改变这个 this 值的话也可以将 __self 写成 this 这个默认指向的是 window
fn.apply(this,arguments);

  1. after 方法
    11.1 after 与 before 不一样 before 是先执行回调再执行本身,而 after 是先执行本身然后再执行回调
Function.prototype.after = function(fn){ var __self = this; return function(){ __self.apply(__self,arguments); //这个如果想要动态的来改变这个 this 值的话也可以将 __self 写成 this 这个默认指向的是 window fn.apply(this,arguments); } }

  1. 接下来就可以进行 链式 调用了
    12.1 这个整个链式调用的执行机制如下
挂载 self 指向的是 test 执行 before 回调 (注意此时 before 的回调是没有直接的执行的只是将这个 function 传给了 下面调用的 after) 执行 self(这个是 after 的 self ,指向的实际上是 before 返回的 function) after 自己执行回调
test.before(function(){ alert(1); //因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 }).after(function(){ alert(3); })();

  1. 这个时候刷新浏览器看下效果
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 接下来我们来动态的改变一下 after 和 before 的顺序,因为不是所有的人都会照着你写的这个顺序来的
test.after(function(){ alert(1); //因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 }).before(function(){ alert(3); })();

  1. 再在浏览器中查看
    15.1 这里输出的就变成了 3 2 1
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 实际上应该是不管怎么改都应该是按照 1 2 3 的顺序来执行的,因为我们在 before 和 after 中做了处理,所以在这里我们的回调函数也要做相应的改变
    16.1 这个整个链式调用的执行机制如下
挂载 self 指向的是 test 执行 after 回调 (注意此时 after 的回调是没有直接的执行的只是将这个 function 传给了 下面调用的 before) 执行 self(这个是 before 的 self ,指向的实际上是 after 返回的 function) before 自己执行回调(虽然说在 after 的回调里是先执行了 test 再执行了回调 ,但是在 before 中是先执行了自己的回调 然后才执行了 after 中的回调)
test.after(function(){ alert(3); //因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 }).before(function(){ alert(1); })();

  1. 再次刷新浏览器就会看到正确的执行顺序了
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 如果说你想要在执行的时候对一些 返回进行判断可以这样写
test.after(function(){ alert(3); }).before(function(){ alert(1); //在这里出错的话就 return 然后可以在 before 回调的时候判断 return false; })(); Function.prototype.before = function(fn) { var __self = this; return function() { //在这里做一个判断如果执行出错的话直接 return 这样就不会往下再执行了 if(fn.apply(this,arguments) == false){ return false; } __self.apply(__self, arguments); } };

  1. 刷新浏览器查看效果
    19.1 这个时候就会发现已经不会再往下执行的,这样是做了容错的处理
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 还有一个问题是 test 函数本身是 return 了一个字符串的,但是在反复的调用的时候这个给丢掉了,我们可以在 after 方法中(因为 after 是最后执行的),得到并 return 出来
    20.1 首先也需要在 before return function 中将这个字符串 return 出去 这样才能在 after 中执行的时候接收的到
Function.prototype.before = function(fn) { var __self = this; return function() { fn.apply(this,arguments) //在 test 函数执行完之后会有一个值 这个值一定要 return出来 这样才能在 after 中执行的时候接收的到 return __self.apply(__self, arguments); } };

20.2 这样就能在 after 中进行接收了
Function.prototype.after = function(fn){ var __self = this; return function(){ //获取 beforereturn 的 function 执行后的 return 值 var result = __self.apply(__self,arguments); //容错 if(result == false){ return false; }fn.apply(this,arguments); //最后将上面获取到的 返回值 result 给 return 出去 return result; }

【前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门】20.3 这里需要将之前调用的 before 中增加的 return false 注释掉,否则执行到最后返回的就是 容错时返回的 false 了
test.after(function(){ alert(3); }).before(function(){ alert(1); })();

  1. 将整个的代码复制到浏览器的控制台中进行输出查看最后的结果,会发现没问题了,返回的字符串也输出了
前端性能优化常用技术手段--JavaScript|前端性能优化常用技术手段--JavaScript 面向切面编程(AOP)入门
文章图片
image.png
  1. 这样就说明 before 和 after 顺序颠倒了也无所谓,只要执行的是相应的回调就好了,这个是利用了 JS 中的 this,你可以在调用时将这个 this 给存起来,甚至是用闭包进行更复杂的
总结
  • 上面就是面向切面编程的一个示例,面向切面编程是你进入到一个高级前端开发工程师必须要掌握的一个基本技能,这个是需要深入的去理解
  • 在 Java 、.NET 或者是 PHP 等很多编程语言里面,如果说你知道了面向切面编程这个思想,然后去实现,还是有很多的益处的。
  • 参考:AOP面向切面编程
  • aop.js 的源码(注释看起来难免会很乱,因为第一次接触,所以很多都需要思考的去理解)
//写一个简单的函数 function test() { alert(2); return 'me test'; }; //比如说现在的需求是:你要统计一下当前的所有的函数谁耗时最长,然后做相应的性能优化// //普通的方法 // function test() { //var start = new Date(); //alert(2); //var end = new Date(); //console.log(end - start); // }; //无侵入式的写法 // //给 Function 的 prototype 上绑定一个 before 方法,就是在你函数执行之前给你一个时间点 // //参数 fn 是函数执行时要给的回调 // Function.prototype.before = function(fn){ //var __self = this; //拿到 这个 this,将这个 this 保存起来以便后面出现了问题还能拿到这个 this 的缓存 ////要先让回调 fn 先执行,这样就是在 test 函数执行前先执行了 //fn(); ////下面 self 执行自己就可以了 再把参数也传过去这里的 return 可以拿到 test 中 return 的 'me test' 的 //return __self.apply(this,arguments); // }; //给 Function 的 prototype 上绑定一个 after 方法,就是在你函数执行之后再给你一个时间点 //参数 fn 是函数执行时要给的回调 // Function.prototype.after = function(fn){ ////after 和 before 的执行顺序刚好相反,要先执行本身 this 然后再执行回调 fn //var __self = this; //跟 before一样要先把这个 this 存起来 ////先执行它自己 //__self.apply(this,arguments); ////再执行回调 fn //fn(); // }; // //写完上面的其实就可以先执行 before 了 // test.before(function(){ ////先执行一个较简单的 //alert(1); // }); // //下面执行的是 after 可以先将 test.before() 注释查看 after 的执行效果 之后再解注释查看效果 之后会发现 test 默认执行了两遍的问题 // test.after(function(){ ////先执行一个较简单的 //alert(3); // }); //上面的代码执行之后会出现一个问题:默认函数 test 被执行 2 遍 //解决思路是 将 test 作为中转 //before: 回调和 before 一起送到 after 去 //after 这个时候就可以写在前面了也需要将 after 和 test 一起送到 before 去//下面是如何去实现上面的思路,先将上面的注释掉,只留下 test 函数//我们是将所有的东西都绑到了 before 身上,before 和 after 都挂载到了 Function 的原型链上,所有说我们得要他进行这样链式的调用 //想要进行链式的调用首先修改 before Function.prototype.before = function(fn) { var __self = this; //拿到 这个 this,将这个 this 保存起来以便后面出现了问题还能拿到这个 this 的缓存 //在这里 return 一个 function 回去 这样的话当我执行完 before 之后 ,因为 before 和 after 是挂载到了原型链上 这样这个 return function 就可以执行 after 和before 了因为 after 和before 是挂载到了最顶层的原型链上 return function() { // //这里面的 this 指向的是调用的函数 // // console.log(this); //这里在控制台输出的是 window,因为这整个 return function 作为一个闭包已经被 return 出去了 当下面的 before 执行完之后 这个闭包已经被暴露在最外层的环境里了,所以这个 this 指向了 window ////因为上面的 this 已经指向了 window 了为了使 window 也能使用这个 fn 可以将 fn 也写成下面 __self 那样 //fn.apply(this,arguments); ////下面 self 执行自己就可以了 再把参数也传过去 //__self.apply(this, arguments); // //但是我们这里是需要保住之前执行的 this 也就是 test 函数 自己,所以需要用到上面缓存的 __self //// fn.apply(__self,arguments); ////这个如果想要动态的来改变这个 this 值的话也可以将 __self 写成 this 这个默认指向的是 window //fn.apply(this,arguments)//在这里做一个判断如果执行出错的话直接 return 这样就不会往下再执行了 if(fn.apply(this,arguments) == false){ return false; } // __self.apply(__self, arguments); //在 test 函数执行完之后会有一个值 这个值一定要 return出来 这样才能在 after 中执行的时候接收的到 return __self.apply(__self, arguments); } }; // //下面执行一下 before 查看效果 // test.before(function(){ //alert(1); // })(); //接下来是同样的改写 after //after 与 before 不一样 before 是先执行回调再执行本身,而 after 是先执行本身然后再执行回调 Function.prototype.after = function(fn){ var __self = this; return function(){ // __self.apply(__self,arguments); //获取 beforereturn 的 function 执行后的 return 值 var result = __self.apply(__self,arguments); //容错 if(result == false){ return false; }//这个如果想要动态的来改变这个 this 值的话也可以将 __self 写成 this 这个默认指向的是 window fn.apply(this,arguments); //最后将上面获取到的 返回值 result 给 return 出去 return result; } }// //接下来就可以进行 链式 调用了 // //这个整个链式调用的执行机制如下 // //挂载 self 指向的是 test执行 before 回调 (注意此时 before 的回调是没有直接的执行的只是将这个 function 传给了 下面调用的 after)执行 self(这个是 after 的 self ,指向的实际上是 before 返回的 function)after 自己执行回调 // test.before(function(){ //alert(1); ////因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 // }).after(function(){ //alert(3); // })(); // // 接下来我们来动态的改变一下 after 和 before 的顺序,因为不是所有的人都会照着你写的这个顺序来的 // //这个会按照 3 2 1 的顺序来输出 // test.after(function(){ //alert(1); ////因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 // }).before(function(){ //alert(3); // })(); // //实际上应该是不管怎么改都应该是按照 1 2 3 的顺序来执行的,因为我们在 before 和 after 中做了处理,所以在这里我们的回调函数也要做相应的改变 // //这个的执行机制: // //挂载 self 指向的是 test执行 after 回调 (注意此时 after 的回调是没有直接的执行的只是将这个 function 传给了 下面调用的 before)执行 self(这个是 before 的 self ,指向的实际上是 after 返回的 function)before 自己执行回调(虽然说在 after 的回调里是先执行了 test 再执行了回调 ,但是在 before 中是先执行了自己的回调 然后才执行了 after 中的回调) // test.after(function(){ //alert(3); ////因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 // }).before(function(){ //alert(1); // })(); // 如果说你想要在执行的时候对一些 返回进行判断可以这样写 test.after(function(){ alert(3); //因为 after 是挂载到 Function 的原型链上的 而 before 执行最后返回的是一个 function 所以就可以直接调用 after 了 }).before(function(){ alert(1); //在这里出错的话就 return 然后可以在 before 回调的时候判断 // return false; })(); //还有一个问题是 test 函数本身是 return 了一个字符串的,但是在反复的调用的时候这个给丢掉了,我们可以在 after 方

    推荐阅读