因為工作的需要,我要在網頁端編寫一段腳本,把數據通過網頁批量提交到系統中去。所以我就想到了Greasemonkey插件,於是就開始動手寫,發現問題解決得很順利。但是在對腳本進行總結和整理的時候,我習慣性地問了自己一個問題:能不能再簡單點?
我的答案當然是“能”。
首先回顧我的數據批量提交的需求:我有一批用戶數據要插入到系統中,但是因為系統庫表結構不是行列式的,所以無法轉化為sql語句插入。要插入的數據有接近200條,就是傻呵呵地手工錄入到系統,估計也要1天的時間。作為程序員,當然不會干這么傻的事情,我一定要用程序來解決。這個編程的過程耗費了我1天的時間。相比手工錄入,我額外收入是這篇博文,絕對的合算!
編程平台選擇沒花費時間,直接選定基於Greasemonkey寫自己的腳本,瀏覽器當然是firefox了。腳本的工作過程:
- 在腳本中預先存放要插入的數據
- 模擬鼠標點擊,打開頁面中的輸入窗口
- 將數據錄入到輸入窗口,並模擬點擊“提交”按鈕,將數據提交到系統中。
- 依次循環,直到所有數據都處理完畢。
這里的技術難點在於:
- 打開輸入窗口,需要等待不定期的時間,視網絡情況而定。
- 提交數據到后台,需要等待處理完畢之后才可以循環下一個數據。
如果我是菜鳥的話,我當然直接寫一個類似這樣的應用邏輯:
1: for(var i = 0; i < dataArray.length; ++i)
2: {
3: clickButtonForInputWindow();
4: waitInputWindow();
5: enterInputData(dataArray[i]);
6: clickSubmitButton();
7: waitInputWindowClose();
8: }
實際上這樣寫所有瀏覽器都會陷入一片白屏,並在若干分鍾之后提示“沒有響應”而被強行終止掉。原因就是瀏覽器在調用javascript的時候,主界面是停止響應的,因為cpu交給js執行了,沒有時間去處理界面消息。
為了滿足“不鎖死”的要求,我們可以把腳本修改成這樣:
1: for(var i = 0; i < dataArray.length; ++i)
2: {
3: setTimeout(clickButtonForInputWindow);
4: …
5: setTimeout(waitInputWindowClose);
6: }
實際上setTimeout和setInterval是瀏覽器唯一可以支持異步的操作。如何更優雅地使用這兩個函數來實現異步操作呢?目前簡單的答案是老趙的Wind.js。雖然我沒有用過這個函數庫,但是光是$await調用,就是符合我一貫對簡潔的要求的。但是對於我這樣的單個文件的腳本來說,去網上下載一個外部js庫,明顯不如有一段支持異步操作的代碼拷貝過來的快和爽。
所以我決定另辟蹊徑,做一個不要編譯而且易用性還可以更能夠Copy&Paste的異步函數庫。
說異步之前,我們一起回憶一下同步操作的幾種結構類型:
- 順序:就是語句的先后順序執行
- 判斷:就是判斷語句
- 循環:嚴格來說應該是跳轉(goto),但大多數現代語言都取消了goto。循環其實應該是復合結構,是if和goto的組合體。
異步操作的難點在兩個地方:
- 異步的判斷:異步情況下的判斷基本都是檢測條件十分滿足,然后執行某些動作。
- 異步的順序:順序中的每一步操作之后都要交回控制權,等待在下一個時間片中繼續執行下一步。難點是如何保持順序性。尤其在兩個順序動作中間夾雜一個異步的循環的時候。
- 異步的循環:每次循環之后都交回控制權到瀏覽器,如此循環,直到運行結束。
最簡單的實現當然就是異步循環了,我的實現代碼如下:
1: function asyncWhile(fn, interval)
2: {
3: if( fn == null || (typeof(fn) != "string" && typeof(fn) != "function") )
4: return;
5: var wrapper = function()
6: {
7: if( (typeof(fn) == "function" ? fn() : eval(fn) ) !== false )
8: setTimeout(wrapper, interval == null? 1: interval);
9: }
10: wrapper();
11: }
實際上,“等待並執行”邏輯,根本上就是一個異步循環問題。這種情況的實現方法示例如下:
1: asyncWhile(function(){
2: if( xxxCondition == false )
3: return true; // 表示繼續循環
4: else
5: doSomeThing();
6: return false; // 表示不需要繼續循環了
7: });
對於非等待並執行的邏輯,簡單一個 setTimeout 就可以了。
異步容易,實現異步中的順序才叫難度呢。最早的起因是我要實現3步,但是第二部是一個異步的100多次的循環。也就是說,我要實現的3步操作,其實是103次的順序異步操作。為了一個如何在瀏覽器中實現可響應的等待,找破了腦袋,只找到一個firefox中的實現,還要申請特權調用。
最后想出了一個簡單的方法,就是引入了“執行鏈(Execution Chain)”的概念,同一個執行鏈的所有登記函數是順序的,不同執行鏈之間沒有任何關系。另外,不提供互斥(mutex)等概念,如果要同步,自行在代碼中檢查。
在同一個執行鏈中,保存一個執行令牌,只有令牌和函數序號匹配,才允許執行,這樣就保證了異步執行的順序性。
1: function asyncSeq(funcArray, chainName, abortWhenError)
2: {
3: if( typeof(funcArray) == "function" )
4: return asyncSeq([funcArray], chainName, abortWhenError);
5:
6: if( funcArray == null || funcArray.length == 0 )
7: return;
8:
9: if( chainName == null ) chainName = "__default_seq_chain__";
10: var tInfos = asyncSeq.chainInfos = asyncSeq.chainInfos || {};
11: var tInfo = tInfos[chainName] = tInfos[chainName] || {count : 0, currentIndex : -1, abort : false};
12:
13: for(var i = 0; i < funcArray.length; ++i)
14: {
15: asyncWhile(function(item, tIndex){
16: return function(){
17: if( tInfo.abort )
18: return false;
19: if( tInfo.currentIndex < tIndex )
20: return true;
21: else if( tInfo.currentIndex == tIndex )
22: {
23: try{
24: item();
25: }
26: catch(e){
27: if( abortWhenError ) tInfo.abort = true;
28: }
29: finally{
30: tInfo.currentIndex ++;
31: }
32: }
33: else
34: {
35: if( abortWhenError ) tInfo.abort = true;
36: }
37: return false;
38: };
39: }(funcArray[i], tInfo.count ++));
40: }
41:
42: setTimeout(function(){
43: if( tInfo.count > 0 && tInfo.currentIndex == -1 )
44: tInfo.currentIndex = 0;
45: },20); // 為了調試的原因,加了延遲啟動
46: }
由此,一個支持Copy&Paste的異步js函數庫就完成了。具體的使用例子如下:
1: function testAsync()
2: {
3: asyncSeq([function(){println("aSyncSeq -0 ");}
4: , function(){println("aSyncSeq -1 ");}
5: , function(){println("aSyncSeq -2 ");}
6: , function(){println("aSyncSeq -3 ");}
7: , function(){println("aSyncSeq -4 ");}
8: , function(){println("aSyncSeq -5 ");}
9: , function(){println("aSyncSeq -6 ");}
10: , function(){println("aSyncSeq -7 ");}
11: , function(){println("aSyncSeq -8 ");}
12: , function(){println("aSyncSeq -9 ");}
13: , function(){println("aSyncSeq -10 ");}
14: , function(){println("aSyncSeq -11 ");}
15: , function(){println("aSyncSeq -12 ");}
16: , function(){println("aSyncSeq -13 ");}
17: , function(){println("aSyncSeq -14 ");}
18: , function(){println("aSyncSeq -15 ");}
19: , function(){println("aSyncSeq -16 ");}
20: , function(){println("aSyncSeq -17 ");}
21: , function(){println("aSyncSeq -18 ");}
22: , function(){println("aSyncSeq -19 ");}
23: , function(){println("aSyncSeq -20 ");}
24: , function(){println("aSyncSeq -21 ");}
25: , function(){println("aSyncSeq -22 ");}
26: , function(){println("aSyncSeq -23 ");}
27: , function(){println("aSyncSeq -24 ");}
28: , function(){println("aSyncSeq -25 ");}
29: , function(){println("aSyncSeq -26 ");}
30: , function(){println("aSyncSeq -27 ");}
31: , function(){println("aSyncSeq -28 ");}
32: , function(){println("aSyncSeq -29 ");}
33: ]);
34:
35: asyncSeq([function(){println("aSyncSeq test-chain -a0 ");}
36: , function(){println("aSyncSeq test-chain -a1 ");}
37: , function(){println("aSyncSeq test-chain -a2 ");}
38: , function(){println("aSyncSeq test-chain -a3 ");}
39: , function(){println("aSyncSeq test-chain -a4 ");}
40: , function(){println("aSyncSeq test-chain -a5 ");}
41: , function(){println("aSyncSeq test-chain -a6 ");}
42: , function(){println("aSyncSeq test-chain -a7 ");}
43: , function(){println("aSyncSeq test-chain -a8 ");}
44: ], "test-chain");
45:
46: asyncSeq([function(){println("aSyncSeq -a0 ");}
47: , function(){println("aSyncSeq -a1 ");}
48: , function(){println("aSyncSeq -a2 ");}
49: , function(){println("aSyncSeq -a3 ");}
50: , function(){println("aSyncSeq -a4 ");}
51: , function(){println("aSyncSeq -a5 ");}
52: , function(){println("aSyncSeq -a6 ");}
53: , function(){println("aSyncSeq -a7 ");}
54: , function(){println("aSyncSeq -a8 ");}
55: ]);
56: }
57:
58: var textArea = null;
59:
60: function println(text)
61: {
62: if( textArea == null )
63: {
64: textArea = document.getElementById("text");
65: textArea.value = "";
66: }
67:
68: textArea.value = textArea.value + text + "\r\n";
69: }
最后,要向大家說一聲抱歉,很多只想拿代碼的朋友恐怕要失望了,如果你真的不知道怎么處理這些多余的行號,你可以學習一下正則表達式的替換,推薦用UltraEdit。