JavaScript 組件之旅(二)編碼實現和演算法

來源:互聯網
上載者:User

首先,我們要考慮一下它的源檔案布局,也就是決定代碼如何拆分到獨立的檔案中去。為什麼要這麼做呢?還記得上期結尾處我提到這個組件會使用“外部代碼”嗎?為了區分代碼的用途,決定將代碼至少分成兩部分:外部代碼檔案和 Smart Queue 檔案。
區分用途只是其一,其二,分散到獨立檔案有利於代碼的維護。試想,以後的某一天你決定要在現有的隊列管理準系統之上,添加一些新的擴充功能,或是把它封裝成某個實現特定任務的組件,而又希望保持現有功能(內部實現)和調用方式(對外介面)不變,那麼將新的代碼寫到單獨的檔案是最好的選擇。

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

var SmartQueue = window.SmartQueue || {};SmartQueue.version = '0.1';

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

接著,按三個優先順序為 SmartQueue 建立三個隊列:

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

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

接下來,我們的主角 Task 隆重登場——怎麼 new 一個 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 : [];  };

裡面的具體細節就不說了,有必要的注釋,一般我們的代碼也能做到自我描述,後面代碼也是這樣。這裡告訴客戶(使用者):你想建立一個 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 對象三段式是什嗎?

  1. 定義對象的建構函式
  2. 在原型上定義屬性和方法
  3. 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_sortingvar _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);      }    }  };

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

圖片來自維基百科

  1. 訪問待排序的每一個節點

    1. 如果已經訪問過了,則返回
    2. 否則標記為已訪問
    3. 找出它串連(在這裡是依賴)的每個節點
    4. 跳到內層1遞迴訪問這些節點
    5. 訪問完了就把當前節點加入已排序列表
  2. 繼續訪問下一個

如果 A 依賴 B, B 依賴 C, C 依賴 A, 那麼這 3 個節點形成了循環相依性。 文中指出這個演算法並不能檢測出循環相依性。通過標記節點是否已訪問,可以解決循環相依性造成的遞迴死迴圈。我們來分析一下循環相依性的情境:

從節點 A 出發的時候,它被標記為已訪問,當從節點 C 再回到節點 A 的時候,它已經被訪問過了。不過這個時候 C 並不知道 A 是否在自己的上遊鏈上,所以不能直接判定發生了循環相依性,因為 A 可能是其他已“處理”(跑完了內層遞迴)過的節點。如果我們知道節點是不是第一次被訪問過,就可以判斷是哪一種情況。

改造一下上面的演算法,將“是否已訪問”改成“訪問計數” (task._visited++)。僅當節點被訪問過 1 次的時候 (task._visited === 1),才將其加入到已排序列表,全部遍曆完之後,如果待排序的節點數比已排序的多 (queue.length > _sorted[index].length),則表明待排序中多出的節點發生了循環相依性。

至此,隊列管理組件的編碼實現已經完成。什嗎?怎麼使用?很簡單啦:

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

更多功能,如任務的依賴,等待你去發掘哦。

本期貼出的代碼都是一些局部片段,部分 helper 方法代碼沒有貼出來。查看完整的代碼請訪問這裡。後面我們將介紹如何管理組件檔案,以及構建組件,下期不見不散哦。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.