萬盛學電腦網

 萬盛學電腦網 >> 腳本專題 >> javascript >> JavaScript組件:編碼實現和算法

JavaScript組件:編碼實現和算法

   話說上期我們討論了隊列管理組件的設計,並且給它取了個響亮而獨特的名字:Smart Queue. 這次,我們要將之前的設計成果付諸實踐,用代碼來實現它。

  首先,我們要考慮一下它的源文件布局,也就是決定代碼如何拆分到獨立的文件中去。為什麼要這麼做呢?還記得上期結尾處我提到這個組件會使用“外部代碼”嗎?為了區分代碼的用途,決定將代碼至少分成兩部分:外部代碼文件和 Smart Queue 文件。

  區分用途只是其一,其二,分散到獨立文件有利於代碼的維護。試想,以後的某一天你決定要在現有的隊列管理基本功能之上,添加一些新的擴展功能,或是把它包裝成某個實現特定任務的組件,而又希望保持現有功能(內部實現)和調用方式(對外接口)不變,那麼將新的代碼寫到單獨的文件是最好的選擇。

  嗯,下期會重點談談文件布局的話題,現在要開始切入正題了。第一步,當然是要為組件創建自己的命名空間,組件所有的代碼都將限制在這個頂層命名空間內:

  var SmartQueue = window.SmartQueue || {};

  SmartQueue.version = '0.1';

  初始化的時候,如果碰到命名空間沖突就把它拉過來用。通常這個沖突是由重復引用組件代碼導致的,因此“拉過來用”會將對象以同樣的實現重寫一次;最壞的情況下,如果碰巧頁面上另一個對象也叫 SmartQueue, 那不好意思了,我會覆蓋你的實現——如果沒有進一步的命名沖突,基本上兩個組件可以相安無事地運行。同時順便給它一個版本號。

  接著,按三個優先級為 SmartQueue 創建三個隊列:

  var Q = SmartQueue.Queue = [[], [], []];

  每個都是空數組,因為還沒有任務加進去嘛。又順便給它建個“快捷方式”,後面要訪問數組直接寫 Q[n] 就可以啦。

  接下來,我們的主角 Task 隆重登場——怎麼 new 一個 Task, 定義在這裡:

  裡面的具體細節就不說了,有必要的注釋,一般我們的代碼也能做到自我描述,後面代碼也是這樣。這裡告訴客戶(使用者):你想新建一個 SmartQueue.Task 實例,就要至少傳一個參數給這個構造函數(後 3 個都可以省略進行缺省處理),否則拋出異常伺候。

  但是這還不夠,有時候,客戶希望從已有 Task 克隆一個新實例,或是從一個“殘廢體”(具有部分 Task 屬性的對象)修復出“健康體”(真正的 Task 對象實例),通過上面的構造方式就有點不爽了——客戶得這樣寫:

  var task1 = new SmartQueue.Task(obj.fn, 1, '', obj.dependencies);

  我很懶,我只想傳 fn 和 dependencies 兩個屬性,不想做額外的事情。好吧,我們來重構一下構造函數:

  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 || {};

  };

  如此一來,原來的構造方式可以繼續工作,而上面的懶人可以這樣傳入一個“殘廢體”:

  var task1 = new SmartQueue.Task({fn: obj.fn, dependencies: obj.dependencies});

  當構造函數收到多個參數時,按之前的方案等同處理;否則,視唯一的參數為 Task 對象或“殘廢體”。這裡通過 JavaScript 中的 apply/call 方法將新實例傳給重構出來的 _setupTask 方法,作為該方法的上下文 (context, 也有稱為 scope), apply/call 是 JavaScript 在方法之間傳遞上下文的法寶,要用心體會哦。同時,允許用戶定義 task.fn 在執行時的上下文,並將自定義的數據傳遞給執行中的 fn.

  經典的 JavaScript 對象三段式是什麼?

  定義對象的構造函數

  在原型上定義屬性和方法

  new 對象,拿來用

  所以,下面要為 SmartQueue.Task 對象的原型定義屬性和方法。上期分析過 Task (任務)有幾個屬性和方法,部分屬性我們已經在 _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;

  }

  };

  如你所見,邏輯非常簡單,也許你已經在一分鐘內掃過了代碼,嘴角不經意間露出一絲心領神會。不過,這裡要說的是簡單而且通常最不被重視的 toString 方法。在一些高級語言中,為自定義對象實現 toString 方法被作為最佳實踐准則而推薦,為什麼呢?因為 toString 可以很方便地在調試器中提供有用的信息,可以方便地將對象基本信息寫入日志;在統一的編程模式中,實現 toString 可以讓你少寫一些代碼。

  嗯,我們繼續推進,我們要實現 SmartQueue 的具體功能。上期分析過,SmartQueue 只有一個實例,因此我們決定直接在 SmartQueue 下面創建方法:

  SmartQueue.init = function() {

  Q.forEach(function(queue) {

  queue.length = 0;

  });

  };

  這裡用到 JavaScript 1.6 為 Array 對象提供的遍歷方法 forEach. 之所以這樣寫是因為我們假定“外部代碼”已經在前面運行過了。設置 Array 對象的 length 屬性為 0 導致,它被清空並且釋放所有的項(數組單元)。

  最後一個方法 fire, 是整個組件最主要的方法,它負責對所有任務隊列進行排序,並逐個執行。由於代碼稍長了一點,這裡只介紹排序使用的算法和實現方式,完整代碼在這裡。

  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);

  }

  }

  };

  我們將按任務指定的依賴關系對同一優先級內的任務進行排序,確保被依賴的任務在設置依賴的任務之前運行。這是一個典型的深度優先的拓撲排序問題,維基百科提供了一個深度優先排序算法,

copyright © 萬盛學電腦網 all rights reserved