JavaScript 組件之旅(二)編碼實(shí)現(xiàn)和算法
首先,我們要考慮一下它的源文件布局,也就是決定代碼如何拆分到獨(dú)立的文件中去。為什么要這么做呢?還記得上期結(jié)尾處我提到這個(gè)組件會(huì)使用“外部代碼”嗎?為了區(qū)分代碼的用途,決定將代碼至少分成兩部分:外部代碼文件和 Smart Queue 文件。
區(qū)分用途只是其一,其二,分散到獨(dú)立文件有利于代碼的維護(hù)。試想,以后的某一天你決定要在現(xiàn)有的隊(duì)列管理基本功能之上,添加一些新的擴(kuò)展功能,或是把它包裝成某個(gè)實(shí)現(xiàn)特定任務(wù)的組件,而又希望保持現(xiàn)有功能(內(nèi)部實(shí)現(xiàn))和調(diào)用方式(對(duì)外接口)不變,那么將新的代碼寫(xiě)到單獨(dú)的文件是最好的選擇。
嗯,下期會(huì)重點(diǎn)談?wù)勎募季值脑掝},現(xiàn)在要開(kāi)始切入正題了。第一步,當(dāng)然是要為組件創(chuàng)建自己的命名空間,組件所有的代碼都將限制在這個(gè)頂層命名空間內(nèi):
var SmartQueue = window.SmartQueue || {}; SmartQueue.version = '0.1';
初始化的時(shí)候,如果碰到命名空間沖突就把它拉過(guò)來(lái)用。通常這個(gè)沖突是由重復(fù)引用組件代碼導(dǎo)致的,因此“拉過(guò)來(lái)用”會(huì)將對(duì)象以同樣的實(shí)現(xiàn)重寫(xiě)一次;最壞的情況下,如果碰巧頁(yè)面上另一個(gè)對(duì)象也叫 SmartQueue, 那不好意思了,我會(huì)覆蓋你的實(shí)現(xiàn)——如果沒(méi)有進(jìn)一步的命名沖突,基本上兩個(gè)組件可以相安無(wú)事地運(yùn)行。同時(shí)順便給它一個(gè)版本號(hào)。
接著,按三個(gè)優(yōu)先級(jí)為 SmartQueue 創(chuàng)建三個(gè)隊(duì)列:
var Q = SmartQueue.Queue = [[], [], []];
每個(gè)都是空數(shù)組,因?yàn)檫€沒(méi)有任務(wù)加進(jìn)去嘛。又順便給它建個(gè)“快捷方式”,后面要訪問(wèn)數(shù)組直接寫(xiě) Q[n] 就可以啦。
接下來(lái),我們的主角 Task 隆重登場(chǎng)——怎么 new 一個(gè) Task, 定義在這里:
var T = SmartQueue.Task = function(fn, level, name, dependencies) { if(typeof fn !== FUNCTION) { throw new Error('Invalid argument type: fn.'); } this.fn = fn; this.level = _validateLevel(level) ? level : LEVEL_NORMAL; // detect type of name this.name = typeof name === STRING && name ? name : 't' + _id++; // dependencies could be retrieved as an 'Object', so use instanceof instead. this.dependencies = dependencies instanceof Array ? dependencies : []; };
里面的具體細(xì)節(jié)就不說(shuō)了,有必要的注釋?zhuān)话阄覀兊拇a也能做到自我描述,后面代碼也是這樣。這里告訴客戶(hù)(使用者):你想新建一個(gè) SmartQueue.Task 實(shí)例,就要至少傳一個(gè)參數(shù)給這個(gè)構(gòu)造函數(shù)(后 3 個(gè)都可以省略進(jìn)行缺省處理),否則拋出異常伺候。
但是這還不夠,有時(shí)候,客戶(hù)希望從已有 Task 克隆一個(gè)新實(shí)例,或是從一個(gè)“殘廢體”(具有部分 Task 屬性的對(duì)象)修復(fù)出“健康體”(真正的 Task 對(duì)象實(shí)例),通過(guò)上面的構(gòu)造方式就有點(diǎn)不爽了——客戶(hù)得這樣寫(xiě):
var task1 = new SmartQueue.Task(obj.fn, 1, '', obj.dependencies);
我很懶,我只想傳 fn 和 dependencies 兩個(gè)屬性,不想做額外的事情。好吧,我們來(lái)重構(gòu)一下構(gòu)造函數(shù):
var _setupTask = function(fn, level, name, dependencies) { if(typeof fn !== FUNCTION) { throw new Error('Invalid argument type: fn.'); } this.fn = fn; this.level = _validateLevel(level) ? level : LEVEL_NORMAL; // detect type of name this.name = typeof name === STRING && name ? name : 't' + _id++; // dependencies could be retrieved as an 'Object', so use instanceof instead. this.dependencies = dependencies instanceof Array ? dependencies : []; }; var T = SmartQueue.Task = function(task) { if(arguments.length > 1) { _setupTask.apply(this, arguments); } else { _setupTask.call(this, task.fn, task.level, task.name, task.dependencies); } // init context/scope and data for the task. this.context = task.context || window; this.data = task.data || {}; };
如此一來(lái),原來(lái)的構(gòu)造方式可以繼續(xù)工作,而上面的懶人可以這樣傳入一個(gè)“殘廢體”:
var task1 = new SmartQueue.Task({fn: obj.fn, dependencies: obj.dependencies});
當(dāng)構(gòu)造函數(shù)收到多個(gè)參數(shù)時(shí),按之前的方案等同處理;否則,視唯一的參數(shù)為 Task 對(duì)象或“殘廢體”。這里通過(guò) JavaScript 中的 apply
/call
方法將新實(shí)例傳給重構(gòu)出來(lái)的 _setupTask
方法,作為該方法的上下文 (context, 也有稱(chēng)為 scope), apply
/call
是 JavaScript 在方法之間傳遞上下文的法寶,要用心體會(huì)哦。同時(shí),允許用戶(hù)定義 task.fn
在執(zhí)行時(shí)的上下文,并將自定義的數(shù)據(jù)傳遞給執(zhí)行中的 fn.
經(jīng)典的 JavaScript 對(duì)象三段式是什么?
- 定義對(duì)象的構(gòu)造函數(shù)
- 在原型上定義屬性和方法
- new 對(duì)象,拿來(lái)用
所以,下面要為 SmartQueue.Task
對(duì)象的原型定義屬性和方法。上期分析過(guò) Task (任務(wù))有幾個(gè)屬性和方法,部分屬性我們已經(jīng)在 _setupTask
中定義了,下面是原型提供的屬性和方法:
T.prototype = { enabled: true, register: function() { var queue = Q[this.level]; if(_findTask(queue, this.name) !== -1) { throw new Error('Specified name exists: ' + this.name); } queue.push(this); }, changeTo: function(level) { if(!_validateLevel(level)) { throw new Error('Invalid argument: level'); } level = parseInt(level, 10); if(this.level === level) { return; } Q[this.level].remove(this); this.level = level; this.register(); }, execute: function() { if(this.enabled) { // pass context and data this.fn.call(this.context, this.data); } }, toString: function() { var str = this.name; if(this.dependencies.length) { str += ' depends on: [' + this.dependencies.join(', ') + ']'; } return str; } };
如你所見(jiàn),邏輯非常簡(jiǎn)單,也許你已經(jīng)在一分鐘內(nèi)掃過(guò)了代碼,嘴角不經(jīng)意間露出一絲心領(lǐng)神會(huì)。不過(guò),這里要說(shuō)的是簡(jiǎn)單而且通常最不被重視的 toString
方法。在一些高級(jí)語(yǔ)言中,為自定義對(duì)象實(shí)現(xiàn) toString
方法被作為最佳實(shí)踐準(zhǔn)則而推薦,為什么呢?因?yàn)?toString
可以很方便地在調(diào)試器中提供有用的信息,可以方便地將對(duì)象基本信息寫(xiě)入日志;在統(tǒng)一的編程模式中,實(shí)現(xiàn) toString
可以讓你少寫(xiě)一些代碼。
嗯,我們繼續(xù)推進(jìn),我們要實(shí)現(xiàn) SmartQueue 的具體功能。上期分析過(guò),SmartQueue 只有一個(gè)實(shí)例,因此我們決定直接在 SmartQueue 下面創(chuàng)建方法:
SmartQueue.init = function() { Q.forEach(function(queue) { queue.length = 0; }); };
這里用到 JavaScript 1.6 為 Array 對(duì)象提供的遍歷方法 forEach
. 之所以這樣寫(xiě)是因?yàn)槲覀兗俣ā巴獠看a”已經(jīng)在前面運(yùn)行過(guò)了。設(shè)置 Array 對(duì)象的 length
屬性為 0
導(dǎo)致,它被清空并且釋放所有的項(xiàng)(數(shù)組單元)。
最后一個(gè)方法 fire
, 是整個(gè)組件最主要的方法,它負(fù)責(zé)對(duì)所有任務(wù)隊(duì)列進(jìn)行排序,并逐個(gè)執(zhí)行。由于代碼稍長(zhǎng)了一點(diǎn),這里只介紹排序使用的算法和實(shí)現(xiàn)方式,完整代碼在這里。
var _dirty = true, // A flag indicates weather the Queue need to be fired. _sorted = [], index; // Sort all Queues. // ref: http://en.wikipedia.org/wiki/Topological_sorting var _visit = function(queue, task) { if(task._visited >= 1) { task._visited++; return; } task._visited = 1; // find out and visit all dependencies. var dependencies = [], i; task.dependencies.forEach(function(dependency) { i = _findTask(queue, dependency); if(i != -1) { dependencies.push(queue[i]); } }); dependencies.forEach(function(t) { _visit(queue, t); }); if(task._visited === 1) { _sorted[index].push(task); } }, _start = function(queue) { queue.forEach(function(task) { _visit(queue, task); }); }, _sort = function(suppress) { for(index = LEVEL_LOW; index <= LEVEL_HIGH; index++) { var queue = Q[index]; _sorted[index] = []; _start(queue); if(!suppress && queue.length > _sorted[index].length) { throw new Error('Cycle found in queue: ' + queue); } } };
我們將按任務(wù)指定的依賴(lài)關(guān)系對(duì)同一優(yōu)先級(jí)內(nèi)的任務(wù)進(jìn)行排序,確保被依賴(lài)的任務(wù)在設(shè)置依賴(lài)的任務(wù)之前運(yùn)行。這是一個(gè)典型的深度優(yōu)先的拓?fù)渑判?/FONT>問(wèn)題,維基百科提供了一個(gè)深度優(yōu)先排序算法,大致描述如下:

圖片來(lái)自維基百科
- 訪問(wèn)待排序的每一個(gè)節(jié)點(diǎn)
- 如果已經(jīng)訪問(wèn)過(guò)了,則返回
- 否則標(biāo)記為已訪問(wèn)
- 找出它連接(在這里是依賴(lài))的每個(gè)節(jié)點(diǎn)
- 跳到內(nèi)層1遞歸訪問(wèn)這些節(jié)點(diǎn)
- 訪問(wèn)完了就把當(dāng)前節(jié)點(diǎn)加入已排序列表
- 繼續(xù)訪問(wèn)下一個(gè)
如果 A 依賴(lài) B, B 依賴(lài) C, C 依賴(lài) A, 那么這 3 個(gè)節(jié)點(diǎn)形成了循環(huán)依賴(lài)。 文中指出這個(gè)算法并不能檢測(cè)出循環(huán)依賴(lài)。通過(guò)標(biāo)記節(jié)點(diǎn)是否已訪問(wèn),可以解決循環(huán)依賴(lài)造成的遞歸死循環(huán)。我們來(lái)分析一下循環(huán)依賴(lài)的場(chǎng)景:
從節(jié)點(diǎn) A 出發(fā)的時(shí)候,它被標(biāo)記為已訪問(wèn),當(dāng)從節(jié)點(diǎn) C 再回到節(jié)點(diǎn) A 的時(shí)候,它已經(jīng)被訪問(wèn)過(guò)了。不過(guò)這個(gè)時(shí)候 C 并不知道 A 是否在自己的上游鏈上,所以不能直接判定發(fā)生了循環(huán)依賴(lài),因?yàn)?A 可能是其他已“處理”(跑完了內(nèi)層遞歸)過(guò)的節(jié)點(diǎn)。如果我們知道節(jié)點(diǎn)是不是第一次被訪問(wèn)過(guò),就可以判斷是哪一種情況。
改造一下上面的算法,將“是否已訪問(wèn)”改成“訪問(wèn)計(jì)數(shù)” (task._visited++
)。僅當(dāng)節(jié)點(diǎn)被訪問(wèn)過(guò) 1 次的時(shí)候 (task._visited === 1
),才將其加入到已排序列表,全部遍歷完之后,如果待排序的節(jié)點(diǎn)數(shù)比已排序的多 (queue.length > _sorted[index].length
),則表明待排序中多出的節(jié)點(diǎn)發(fā)生了循環(huán)依賴(lài)。
至此,隊(duì)列管理組件的編碼實(shí)現(xiàn)已經(jīng)完成。什么?怎么使用?很簡(jiǎn)單啦:
var t1 = new SmartQueue.Task(function() { alert("Hello, world!"); }), t2 = new SmartQueue.Task(function() { alert("High level task has name"); }, 2, 'myname'); t1.register(); t2.register(); SmartQueue.fire();
更多功能,如任務(wù)的依賴(lài),等待你去發(fā)掘哦。
本期貼出的代碼都是一些局部片段,部分 helper 方法代碼沒(méi)有貼出來(lái)。查看完整的代碼請(qǐng)?jiān)L問(wèn)這里。后面我們將介紹如何管理組件文件,以及構(gòu)建組件,下期不見(jiàn)不散哦。
相關(guān)文章
使用純JS實(shí)現(xiàn)checkbox的框選效果(鼠標(biāo)拖拽多選)
最近做了一個(gè)用js實(shí)現(xiàn)鼠標(biāo)拖拽多選的功能,于是整理了一下思路,寫(xiě)了一個(gè)小demo,下面這篇文章主要給大家介紹了關(guān)于如何使用純JS實(shí)現(xiàn)checkbox的框選效果(鼠標(biāo)拖拽多選)的相關(guān)資料,需要的朋友可以參考下2022-05-05javascript判斷機(jī)器是否聯(lián)網(wǎng)的2種方法
只有機(jī)器已經(jīng)聯(lián)網(wǎng)以后,web應(yīng)用才能啟動(dòng),下面使用javascript判斷機(jī)器是否聯(lián)網(wǎng),具體判斷代碼如下,有此需求的朋友可以參考下2013-08-08關(guān)于Javascript模塊化和命名空間管理的問(wèn)題說(shuō)明
最近閑下來(lái)的時(shí)候,稍微想了想這個(gè)問(wèn)題。關(guān)于Javascript模塊化和命名空間管理2010-12-12dateformat.js超輕量級(jí)的JS日期處理庫(kù)的使用
dateformat.js 是一個(gè)非常簡(jiǎn)潔、輕量級(jí)、不到 5kb 的很簡(jiǎn)潔的 Javascript 庫(kù),本文主要介紹了dateformat.js超輕量級(jí)的JS日期處理庫(kù)的使用,感興趣的可以了解一下2023-12-12JavaScript實(shí)現(xiàn)數(shù)組對(duì)象轉(zhuǎn)換為鍵值對(duì)的四種方式
本文探討了將包含 {icon: "abc", url: "123"} 形式對(duì)象的數(shù)組轉(zhuǎn)換為鍵值對(duì)形式的四種方法,并從實(shí)現(xiàn)方式的簡(jiǎn)潔性、可讀性和性能角度進(jìn)行了分析比較,感興趣的朋友可以參考下2024-02-02微信小程序之滑動(dòng)頁(yè)面隱藏和顯示組件功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了微信小程序之滑動(dòng)頁(yè)面隱藏和顯示組件功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06