用 ngQueue 來完成需要排隊的非同步工作

$q promise 是使用 AngularJS 時相當重要的一環,熟悉並活用之後可以讓許多原本看似較雜亂的 event-base 程式碼看起來謹然有序(當然也不是所有的 event-base 程式都適合使用 promise 來實現)。

排隊的需求

有時候我們會碰到一些特別的情況:

一段一段的動作是依序進行,但觸發的時候卻又是一次性的(比如說在一個 loop 中一次排定 10 個非同步的工作,需要依序執行)。

用程式碼表示大概是這樣:

var taskIds = [1, 2, 3, 4, 5];

angular.forEach(taskIds, function (taskId) {

  // 以 $http 為例
  $http.get('/someUrl', {params: {taskId: taskId}});

});

console.log('工作順利完成!');

然而我們真正想做的事情是這樣:

$http.get('/someUrl', {params: {taskId: 1}})
.then(function () {
  return $http.get('/someUrl', {params: {taskId: 2}});
})
.then(function () {
  return $http.get('/someUrl', {params: {taskId: 3}});
})
.then(function () {
  return $http.get('/someUrl', {params: {taskId: 4}});
})
.then(function () {
  return $http.get('/someUrl', {params: {taskId: 5}});
})
.then(function () {
  console.log('工作順利完成!');
});

在上面的例子中,工作只有五項所以固然可以自己打一打程式碼解決,一般來說碰到的情況會有下面的困境:

  • 工作項目數未知(為動態的或為參數)
  • 同時可以進行的工作數不只一項(以上面為例即 1, 2 可以同時進行,而 3, 4 可以同時進行,最後再單獨執行 5 即可)

ngQueue

ngQueue 這個 module 就是為了因應上述的情況而誕生的,特色如下:

  • 支援同步 / 非同步工作
  • $q 結合,並提供傳入 context 及 arguments 的功能
  • 建立 queue 時可以指定可同時執行的工作數量

更多資訊可以去 http://github.com/pc035860/ngQueue 看看

開始使用 ngQueue

首先頁面內載入 ngQueue.js

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script src="http://pc035860.github.io/ngQueue/ngQueue.min.js"></script>

接著在 AngularJS app 中載入 ngQueue module

angular.module('myApp', ['ngQueue']);

排入工作

首先先用 ngQueue 提供的 $queueFactory 建立 queue instance

// 建立同時執行兩件工作的 queue
var queue = $queueFactory(2);

同步

接著可以排入同步的工作(即不是非同步的工作),範例中我們也嘗試傳入自訂的 context 以及 arguments。

queue.enqueue(function (inA, inB, inC) {

  console.log(this);  // {name: "context"}

  console.log(inA, inB, inC);  // hello world !

  doSomething();

// 傳入 context 以及 arguments
}, {name: 'context'}, ['hello', 'world', '!']);

非同步

此類工作與同步工作在排入時最大的區別在於,工作內容最後的 return 值必須是 promise

下面以兩種為例,分別具有各自的代表性:

  • 使用 $timeout$q - 自訂的 promise 工作
// $timeout delay
queue.enqueue(function () {
  var dfd = $q.defer();

  $timeout(function () {
    dfd.resolve();
    // or dfd.reject()
  }, 100);

  return dfd.promise;
});
  • 使用 $http - AngularJS 內建就會 return promise 的工作
// $http request
queue.enqueue(function () {

  return $http.get('/some/api/call')
    .success(function () {
      // do something if success
    })
    .error(function () {
      // do something if error
    });

});

Demo

這個 demo 演示了 ngQueue 處理排入的同步 / 非同步工作的實際情況。 可以透過上方的按鈕選擇排入「同步」、「非同步」以及「亂數 10 個」的工作,而 queue 的 instance 在建立的時候設定為同時能夠執行兩個工作,因此我們在 demo 中可以看到同時會有兩個數字在倒數。

任務完成

所以最後回頭修改本篇的第一個範例,得到我們使用 ngQueue 之後的版本。

需要注意的是,如果 taskId 不用 argument 的方式傳入 enqueue 的 function 的話,真的輪到該 function 執行的時候,會無法透過 closure 抓到正確的 taskId

var queue = $queueFactory(1),
    taskIds = [1, 2, 3, 4, 5];

angular.forEach(taskIds, function (taskId) {

  // enqueue with argument: taskId
  queue.enqueue(function (taskId) {

    // returns $http promise
    return $http.get('/someUrl', {params: {taskId: taskId}});

  }, null, [taskId]);

});

queue.enqueue(function () {
  // all tasks finished
  console.log('工作順利完成!')
});