現實開發中,要按順序執行一系列的同步非同步作業又是很常見的。還是用百度Hi網頁版中的例子,我們先要非同步擷取連絡人清單,然後再非同步擷取每一個連絡人的具體資訊,而且後者是分頁擷取的,每次請求發送10個連絡人的名稱然後取回對應的具體資訊。這就是多個需要順序執行的非同步請求。
為此,我們需要設計一種新的操作方式來最佳化代碼可讀性,讓順序非同步作業代碼看起來和傳統的順序同步作業碼一樣優雅。
傳統做法
大多數程式員都能夠很好的理解順序執行的代碼,例如這樣子的:
複製代碼 代碼如下:var firstResult = firstOperation(initialArgument);
var secondResult = secondOperation(firstResult);
var finalResult = thirdOperation(secondResult);
alert(finalResult);
其中先執行的函數為後執行的函數提供所需的資料。然而使用我們的非同步呼叫架構後,同樣的邏輯必須變成這樣子: 複製代碼 代碼如下:firstAsyncOperation(initialArgument).addCallback(function(firstResult) {
secondAsyncOperation(firstResult).addCallback(function(secondResult) {
thirdAsyncOperation(secondResult).addCallback(function(finalResult) {
alert(finalResult);
});
});
});
鏈式寫法
我認為上面的代碼實在是太不美觀了,並且希望能夠改造為jQuery風格的鏈式寫法。為此,我們先構造一個用例: 複製代碼 代碼如下:Async.go(initialArgument)
.next(firstAsyncOperation)
.next(secondAsyncOperation)
.next(thirdAsyncOperation)
.next(function(finalResult) { alert(finalResult); })
在這個用例當中,我們在go傳入初始化資料,然後每一個next後面傳入一個資料處理函數,這些處理函數按順序對資料進行處理。
同步並存
上面的用例調用到的全部都是非同步函數,不過我們最好能夠相容同步函數,讓使用者無需關心函數的具體實現,也能使用這項功能。為此我們再寫一個這樣的用例: 複製代碼 代碼如下:Async.go(0)
.next(function(i) { alert(i); return i + 1; })
.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 1000);
return operation;
})
.next(function(i) { alert(i); return i + 1; })
.next(function(i) { alert(i); return i; });
在上述用例中,我們期待能夠看到0, 1, 2, 3的提示資訊序列,並且1和2之間間隔為1000毫秒。
非同步本質
一個鏈式調用,本質上也是一個非同步呼叫,所以它返回的也是一個Operation執行個體。這個執行個體自然也有result、state和completed這幾個欄位,並且當整個鏈式調用完成時,result等於最後一個調用返回的結果,而completed自然是等於true。
我們可以擴充一下上一個用例,得到如下用例代碼: 複製代碼 代碼如下:var chainOperation = Async.go(0)
.next(function(i) { alert(i); return i + 1; })
.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 1000);
return operation;
})
.next(function(i) { alert(i); return i + 1; })
.next(function(i) { alert(i); return i; });
setTiemout(function() { alert(chainOperation.result; }, 2000);
把鏈式調用的返回儲存下來,在鏈式調用完成時,它的result應該與最後一個操作的返回一致。在上述用例中,也就是3。
調用時機
儘管我們提供了一種鏈式調用方式,但是使用者不一定會按照這種固定的方式來調用,所以我們仍然要考慮相容使用者的各種可能用法,例如說非同步地用next往調用鏈添加操作: 複製代碼 代碼如下:var chainOperation = Async.go(0);
chainOperation.next(function(i) { alert(i); return i + 1; });
setTimeout(function() {
chainOperation.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 2000);
return operation;
})
}, 1000);
setTimeout(function() {
chainOperation.next(function(i) { alert(i); return i + 1; });
}, 2000);
在這個用例當中,使用者每隔1000毫秒添加一個操作,而其中第二個操作耗時2000毫秒。也就是說,添加第三個操作時第二個操作還沒返回。作為一個健壯的架構,必須要能相容這樣的使用方式。
此外我們還要考慮,使用者可能想要先構造調用鏈,然後再執行調用鏈。這時候使用者就會先使用next方法添加操作,再使用go方法執行。 複製代碼 代碼如下:var chainOperation = Async
.chain(function(i) { alert(i); return i + 1; })
.next(function(i) {
alert(i);
var operation = new Async.Operation();
setTimeout(function() { operation.yield(i + 1); }, 2000);
return operation;
})
.go(0)
setTimeout(function() {
chainOperation.next(function(i) { alert(i); return i + 1; })
}, 1000);
在上述用例中,使用者通過chain和next添加了頭同步操作和非同步作業各一個,然後用go執行調用鏈,在調用鏈執行完畢之前又用next非同步追加了一個操作。一個健壯的架構,在這樣的用例當中應該能夠如同使用者所期望的那樣提示0, 1, 2。
小結
針對鏈式調用的需求,我們設計了如此多的用例,包括各種奇怪的非同步呼叫方式。最終如何?這樣的功能呢?