序
上一篇提到了模塊化的思想,并且引入了按需加載模塊的技術,然而對于實作細節卻沒有講解,因為按需加載有點復雜,如果不引入觀察者模式的設計思想,就會比較難實作,
使用觀察者模式,我們實作dom事件類似的注冊觸發,這種方式可以很好的解耦,讓模塊間沒有依賴,我們先討論一下觀察者模式是怎么回事,我們先引入幾個場景:
- 如果兩個模塊之間互動,通常我們用a變數和b變數參考,如果a更改了b的屬性,可以通過代碼b.property = someValue; 如果a要更改b的行為,那么可以通過b.doSomething(); 更改屬性還好,因為b是個物件,更改屬性不會報錯,如果觸發方法,b沒有dosomething的方法,那么代碼就會報錯,這個場景只有在a和b完全知道對方的時候可以使用,比如上一個例子,名為static的Page物件更改顯示頁面的方法(currentPage),因為無法知道currentPage是什么,這種場景不適合直接使用;
- 如果一個模塊發生了變動,它要讓另一個模塊做一系列的事情,這種場景比上一個場景更加嚴苛,更加不適合直接使用;
- 如果一個模塊發生了變動,讓另一個模塊更改事情,同時另一個模塊要求當前模塊發生互動,可能雙方需要互動一系列操作,這種場景很明顯也是無法做到的,
我們聯想一下dom事件,當dom被點擊的時候,如果監聽了點擊事件,那就觸發了點擊的回呼方法,如果沒有注冊,啥事情也不會發生,這時我們假設兩個模塊注冊事件和觸發事件,試一下來解決上述的場景問題:
- 如果模塊a需要模塊b發生變化,那么在a中呼叫b的觸發事件,b.dispatchEvent(eventname, data); data是傳遞的資料如果b監聽了b.addEventListener(eventName, fn); 那么會呼叫fn的回呼方法,因為fn是在b的作用域定義的,它可以訪問在b模塊下的私有變數,如果b沒有監聽eventName事件,那么什么都不會發生;
- 如果b監聽了若干個b.addEventListener(eventName, fn1); b.addEventListener(eventName, fn2); ...
那么當a中呼叫b.dispatchEvent的時候,就會觸發b的一系列方法; - 在監聽的回呼函式中,可以呼叫a.dispatchEvent方法,同理,a中的也可以呼叫b中的回呼方法,
通過以上談論,如果使用事件的方式是可以完美的解決之前的幾個場景問題,
需求
開發一個觀察者模式的物件,該物件有以下特點:
- 可以注冊事件,將名稱和回呼方法們管理起來,有時候回呼方法只觸發一次也很常用;
- 可以觸發事件,針對名稱以及一個物件,讓回呼方法們依次觸發;
- 可以移除某個回呼方法,也可以針對名稱移除該名稱對應的所有回呼方法;
- 可以銷毀物件的destroy方法,
實作
代碼如下
function EventDispatcher() {
this.listeners = {};
}
EventDispatcher.prototype = {
constructor: EventDispatcher,
// 訂閱自定義事件
addEventListener: function (type, listener) {
this._addEventListener(type, listener);
},
// 訂閱某個型別的事件
_addEventListener: function (type, listener, isOnce) {
var listeners = this.listeners;
if (typeof listeners[type] == "undefined") {
listeners[type] = [];
}
if (isOnce) listener.once = true;
listeners[type].push(listener);
},
// 注冊一次系結事件,觸發后自動清除
addOnce: function (type, listener) {
this._addEventListener(type, listener, true);
},
// 移除某個事件型別的訂閱事件
removeEventListener: function (type, listener) {
var listeners = this.listeners;
var handlers = listeners[type];
if (Array.isArray(handlers)) {
var i = handlers.indexOf(listener);
handlers.splice(i, 1);
}
},
// 根據名稱移除所有的回呼函式
clearListenerByType: function (type) {
var handlers = this.listeners[type];
if (Array.isArray(handlers)) handlers.length = 0;
},
// 觸發某個事件型別
dispatchEvent: function (event) {
var listeners = this.listeners;
if (!event.target) {
event.target = this;
}
var handlers = listeners[event.type];
// 如果有事件注冊
if (Array.isArray(handlers)) {
var onceIndexs = [];
for (var i = 0, len = handlers.length; i < len; i++) {
handlers[i](event);
if (handlers[i].once) {
onceIndexs.push(i);
}
}
// 移除一次觸發就銷毀的回呼方法
if (onceIndexs.length > 0) {
for (var i = onceIndexs.length - 1; i >= 0; i--) {
handlers.splice(onceIndexs[i], 1);
}
}
}
},
// 銷毀自定義事件
destroy: function () {
for (var str in this.listeners) {
this.listeners[str].length = 0;
delete this.listeners[str];
}
}
};
可以通過繼承或者組合方式來使用這個物件
var a = {
eventDispatcher: new EventDispatcher(),
attachDiyEvent: function (type, fn) {
this.eventDispatcher.addEventListener(type, fn);
},
dispatchEvent: function (data) {
this.eventDispatcher.dispatchEvent(data)
}
}
如果每個模塊都擁有EventDispatcher物件,就可以使用自定義事件進行跨模塊互動了,上一篇說的App物件,Page物件,以及后面的Component和PopUp物件,都是間接繼承了EventDispatcher物件,
實作按需加載
實作方法 App.require(list, bk),App.define(name, obj), 思路如下:
- require方法第一個引數中的name必須要在該name配置項的js中,有對應的App.define(name, obj),
就是說 App.require(["strTool"], function (strTool) {});
存在配置項 { strTool: { js: "/public/services/strTool.js "}}
在/public/services/strTool.js中必須要有定義 App.define("strTool", {});
App.require代表陣列全部被require后,才會執行回呼方法, App.define代表某一個模塊已經被加載了; - 如果陣列中加載的模塊都沒有參考別的模塊,等模塊全部加載完,執行回呼是沒有問題的,如果引入的模塊還參考了其它模塊,比如App.require(["m1", "m2"], function (m1, m2) {}); m2中還參考了其它模塊, 因為m2的定義是在回呼函式中定義的,所以當m2的其它模塊全部引入后,才會觸發m2的參考完成,因此這個場景也是成立的,同理,只要不存在回圈參考,或自參考, 就不會出現加載不出來的問題;
- 因為參考是異步的,存在一個檔案同時被多個檔案同時參考的情況,為了避免js被多次加載,需要做一下判斷,可以用上面介紹的觀察者模式,
實作App.require方法
App.require = function (list, bk) {
if (typeof list === "function") return list.call(this);
var len = list.length, mods = [], that = this;
if (len == 0) bk.call(this);
var func = function (obj, index) {
mods[index] = obj;
if (--len === 0) bk.apply(that, mods);
}
list.forEach(function (item, index) {
// 獲取對應的name的js檔案,內由App.define(name, obj)方法
var config = that._contains(item);
// 這個方法要定義自定義事件,在App.define觸發回呼,所有觸發后才會全部加載完畢
loadUnit._getUnit({ name: item, js: config.js }, function (obj) {
func(obj, index);
})
})
};
實作App.define方法
App.define = function (name, bk) {
if (typeof bk === "function") loadUnit.units[name] = bk();
else loadUnit.units[name] = bk;
// 告訴模塊我已經加載完了,
loadUnit.dispatchEvent({ type: name, detail: { component: loadUnit.units[name] }})
}
通過繼承的方式,擁有自定義事件的所有方法,實作LoadUnit物件
function LoadUnit() {
EventDispatcher.call(this);
// 存已經加載的模塊,如果模塊已加載,就不再加載js,直接回傳結果
this.units = {};
// 存放某個模塊的加載狀態,如果正在加載中,某個模塊名為true,否則為false或undefined
this.loadObj = {};
}
LoadUnit.prototype = create(EventDispatcher.prototype, {
constructor: LoadUnit,
_getUnit: function (config, next) {
var that = this;
var name = config.name, url = config.js;
// 如果已經加載鍋,直接回傳加載完畢
if (this.units[name]) next(this.units[name], config);
else {
// 監聽觸發一次的事件,再第二點里面觸發,觸發后才算加載完畢,
this.addOnce(name, function (obj) {
next(obj.detail.component, obj.detail.config || config);
});
// 如果不是加載狀態,那就加載js,反之不加載
if (!this.loadObj[name]) {
this.loadObj[name] = true;
var script = document.createElement("script"),
head = document.head;
script.src = https://www.cnblogs.com/stringWeb/p/url;
script.onload = function () {
that.loadObj[name] = false;
}
head.appendChild(script);
}
}
}
};
// 創建一個loadUnit,來管理模塊化
var loadUnit = new LoadUnit();
后面的pageLoadUnit, componentLoadUnit, popUpLoadUnit都是LoadUnit的實體物件,
簡單案例
為了更好的理解將上一章節的內容代碼拷貝下來,并作以下修改
-
修改static.js檔案
getDomObj: function () { this.attachDom(".page-container", "pageContainer") .attachDom("header", "header") .attachDiyEvent("changelayout", this.changeLayoutHandler) .attachDom("footer", "footer"); }, changeLayoutHandler: function (ev) { this.domList.header.textContent = ev.header; this.domList.footer.textContent = ev.footer; } -
修改home.js, second.js檔案
與app.staticPage互動改為自定義事件互動
app.staticPage.dispatchEvent("changelayout", { header: "次頁", footer: "從首頁跳轉到次頁" }); -
查看效果(主要針對移動端,可以通過手機端查看或者用瀏覽器模擬移動端)
將代碼放在可以訪問的服務器或本地服務上,啟動服務,通過瀏覽器訪問,訪問地址
結語
模塊開發是當前web開發中多人合作開發的主流方式,如何去管理模塊,以及有序的準確的加載是非常重要的,自定義事件是框架的核心之一,熟悉原理并熟練使用會讓專案更容易維護,
推廣
底層框架開源地址:https://gitee.com/string-for-100w/string
演示網站: https://www.renxuan.tech/
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/172605.html
標籤:JavaScript
上一篇:JavaScript高級程式設計(第4版)pdf 電子書
下一篇:1.框架概覽
