日前發佈的dojo 1.7版本對其源碼進行了很大的變動。在邁向2.0版本之際,dojo提供了許多新的功能,也對許多已有的功能進行了修改,具體來講新版本更好地支持AMD規範、提供了新的事件處理系統(計劃在2.0版本中替換dojo.connect API)和DOM查詢模塊、更新對象存儲相關接口(ObjectStore)等。在本文中咱們將會介紹在dojo 1.7版本中新增的面向方面編程(AOP)功能以及其實現原理。編程
AOP即面向方面編程,是面向對象編程思想的延續。利用該思想剝離一些通用的業務,能夠有效下降業務邏輯間的耦合度,提升程序的可重用性。隨着Java領域中Spring框架的流行,其倡導的AOP理念被更多的人所熟識(要注意的是Spring並非該思想的獨創者,但說Spring框架的流行讓更多人瞭解和學習了AOP思想並不爲過)。由於Java爲靜態語言,因此在實現AOP功能時較爲複雜,通常採起兩種方式即動態代理和字節碼生成技術(如CGLib)來實現該功能。在JavaScript領域,由於之前模塊化需求並非很強烈,因此AOP的理念並無被普遍引入進來。可是隨着RIA技術的發展,愈來愈多的業務邏輯在前臺完成,JavaScript代碼的組織和可維護性愈加重要,正是在這樣的背景下,出現了不少JavaScript模塊化管理的類庫,而dojo也在這方面積極探索,新版本的dojo已經更好地支持AMD規範,並提供了面向方面編程的支持。數據結構
在面向方面編程功能推出以前,dojo能夠經過使用connect方法來實現相似的功能。connect方法主要能夠實現兩類功能即爲dom對象綁定事件和爲已有的方法添加後置方法。已經有很多文章分析dojo的connect方法的使用和原理,再加上dojo計劃在未來版本中移除該API,因此在此不對這個方法進行更細緻的分析了。app
在dojo 1.7的版本中新增了aspect模塊,該模塊主要用來實現AOP的功能。藉助於此項功能能夠爲某個對象的方法在運行時添加before、after或around類型的加強(advice,即要執行的切面方法)。爲了介紹此功能,咱們先用dojo的類機制聲明一個簡單的類:框架
define("com.levinzhang.Person", ["dojo/_base/declare"], function (declare) { declare("com.levinzhang.Person", null, { name: null, age: null, constructor: function (name, age) { this.name = name; this.age = age; }, getName: function () { return this.name; }, getAge: function () { return this.age; }, sayMyself: function () { alert("Person's name is " + this.getName() + "!"); } }); })
這裏聲明類的方法,與咱們在介紹類機制時略有不一樣,由於dojo從1.6版本開始支持AMD規範,經過define方法來聲明模塊及其依賴關係。有了類之後,咱們須要建立一個實例,以下:dom
dojo.require("com.levinzhang.Person"); var person = new com.levinzhang.Person("levin",30);
如今咱們要藉助dojo的aspect模塊爲這個類的實例添加AOP功能。假設咱們須要在sayMyself方法的調用先後分別添加對另外一個方法的調用(即所謂的加強advice),示例代碼以下:模塊化
var aspect = dojo.require("dojo.aspect"); //引入aspect模塊 //聲明在person 的sayMyself方法調用前要調用的方法 var signal = aspect.before(person, "sayMyself", function () { alert("調用了before"); }); //聲明在person 的sayMyself方法調用後要調用的方法 aspect.after(person, "sayMyself", function () { alert("調用了after"); }); //此時調用sayMyself方法將會前後打印出: //「調用了before」、「Person's name is levin!」、「調用了after」 //即按照before、目標方法、after的順序執行 person.sayMyself();
在以上的代碼片斷中,咱們使用了aspect的before和after方法實現了在目標方法先後添加advice。在調用before和after方法後將會返回一個signal對象,這個對象記錄了目標advice並提供了移除方法,如要移除上文添加的before advice,只需執行如下代碼:學習
signal.remove(); //移除前面添加的beforeadvice //此時調用sayMyself方法將會前後打印出: // 「Person's name is levin!」、「調用了after」 //即經過aspect.before添加的方法已經被移除 person.sayMyself();
除了before和after類型的advice,dojo還支持around類型的advice,在這種狀況下,須要返回一個function,在這個function中能夠添加任意的業務邏輯代碼並調用目標方法,示例代碼以下:ui
var signal = aspect.around(person, "sayMyself", function (original) { return function () { alert("before the original method"); original.apply(person, arguments); //調用目標方法,即原始的sayMyself方法 alert("after the original method"); } }); //此時調用sayMyself方法將會前後打印出: //「before the original method」、「Person's name is levin!」、「after the original method」 person.sayMyself();
從上面的示例代碼咱們能夠看到,around類型的advice會有更多對業務邏輯的控制權,原始的目標方法會以參數的形式傳遞進來,以便在advice中進行調用。this
經過對以上幾種類型advice使用方式的介紹,咱們能夠看到dojo的AOP功能在JavaScript中實現了AOP Alliance所倡導的advice類型。須要指出的是,每種類型的advice都可添加多個,dojo會按照添加的順序依次執行。spa
瞭解了dojo AOP功能的基本語法後,讓咱們分析一下其實現原理。dojo aspect模塊的實如今dojo/aspect.js文件中,整個文件的代碼數在100行左右,所以其實現是至關簡潔高效的。
經過var aspect = dojo.require("dojo.aspect");方法引入該模塊時,會獲得一個簡單的JavaScript對象,咱們調用aspect.before、aspect.around、aspect.after時,均會調用該文件中定義的aspect方法所返回的function。
define([], function () { …… return { before: aspect("before"), around: aspect("around"), after: aspect("after") }; });
如今咱們看一下aspect方法的實現:
function aspect(type) { //對於不一樣類型的advice均返回此方法,只不過type參數會有所不一樣 return function (target, methodName, advice, receiveArguments) { var existing = target[methodName], dispatcher; if (!existing || existing.target != target) { //通過AOP處理的方法均會被一個新的方法所替換,也就是這裏的dispatcher dispatcher = target[methodName] = function () { // before advice var args = arguments; //獲得第一個before類型的advice var before = dispatcher.before; while (before) { //調用before類型的advice args = before.advice.apply(this, args) || args; //找到下一個before類型的advice before = before.next; } //調用around類型的advice if (dispatcher.around) { 調用dispatcher.around的advice方法 var results = dispatcher.around.advice(this, args); } //獲得第一個after類型的advice var after = dispatcher.after; while (after) { //調用after類型的advice results = after.receiveArguments ? after.advice.apply(this, args) || results : after.advice.call(this, results); //找到下一個after類型的advice after = after.next; } return results; }; if (existing) { //設置最初的around類型的advice,即調用目標方法 dispatcher.around = { advice: function (target, args) { return existing.apply(target, args); } }; } dispatcher.target = target; } //對於不一樣類型的advice,通用advise方法來修改dispatcher,即對象的同名方法 var results = advise((dispatcher || existing), type, advice, receiveArguments); advice = null; return results; }; }
咱們能夠看到,在第一次調用aspect方法時,原有的目標方法會被替換成dispatcher方法,而在這個方法中會按照內部的數據結構,依次調用各類類型的advice和最初的目標方法。而構建和調整這個內部數據結構是經過advise方法來實現的:
function advise(dispatcher, type, advice, receiveArguments) { var previous = dispatcher[type]; //獲得指定類型的前一個advice var around = type == "around"; var signal; if (around) { //對around類型的advice,只需調用advice方法,並將上一個advice(有可能即爲//目標方法)做爲參數傳入便可 var advised = advice(function () { return previous.advice(this, arguments); }); //構建返回的對象,即aspect.around方法的返回值 signal = { //移除方法 remove: function () { signal.cancelled = true; }, advice: function (target, args) { //即爲真正執行的around方法 return signal.cancelled ? previous.advice(target, args) : //取消,跳至下一個 advised.apply(target, args); // 調用前面的advised方法 } }; } else { // 對於after或before類型的advice,構建移除方法 signal = { remove: function () { var previous = signal.previous; var next = signal.next; if (!next && !previous) { delete dispatcher[type]; } else { if (previous) { previous.next = next; } else { dispatcher[type] = next; } if (next) { next.previous = previous; } } }, advice: advice, receiveArguments: receiveArguments }; } if (previous && !around) { if (type == "after") { //將新增的advice加到列表的尾部 var next = previous; while (next) { //移到鏈表尾部 previous = next; next = next.next; } previous.next = signal; signal.previous = previous; } else if (type == "before") { //將新增的advice添加到起始位置 dispatcher[type] = signal; signal.next = previous; previous.previous = signal; } } else { // around類型的advice或第一個advice dispatcher[type] = signal; } return signal; }
以上,咱們分析了dojo的aspect模塊的使用以及實現原理,儘管這種將靜態語言編程風格移植到腳本語言中的作法可否被你們接受並普遍使用尚有待時間的檢驗,但這種嘗試和實現方式仍是很值得借鑑的。