###寫在前面
老趙(Jeffrey Zhao) 是筆者敬佩的能做 20 個引體向上的前輩
當時還混部落格的時候有看過幾篇老趙的.Net 文章,後來因為選擇前端學習 JS 仔細看了 yield 系列文章,頓時高山仰止(人家那才叫程式設計呢,筆者這只能算是程式碼應用)
再後來 github 追隨了老趙(目前筆者唯二 follow 的人),多半因為技術敬佩,另一小半因為老趙的 學生資助計劃(雖然現在已經像 WindJS 一樣被歷史塵封,但應該向有擔當且做實事的人致敬)
##一.WindJS 的基本原理
Wind.js 的確是個「輪子」,但絕對不是「重新發明」的輪子。我不會掩飾對 Wind.js 的「自誇」:Wind.js 在 JavaScript 非同步程式設計領域絕對是一個創新,可謂前無來者。有朋友就評價說「在看到 Wind.js 之前,真以為這是不可能實現的」,因為 Wind.js 事實上是用類庫的形式「修補」了 JavaScript 語言,也正是這個原因,才能讓 JavaScript 非同步程式設計體驗獲得質的飛躍。
我們現在想要解決的是「流程控制」問題,那麼說起在 JavaScript 中進行流程控制,有什麼能比 JavaScript 語言本身更為現成的解決方案呢?在我看來,流程控制並非是一個理應使用類庫來解決的問題,它應該是語言的職責。只不過在某些時候,我們無法使用語言來表達邏輯或是控制流程,於是退而求其次地使用「類庫」來解決這方面的問題。
以上引自 專訪 Wind.js 作者老趙(上):緣由、思路及發展
基本原理從一個例子說起,如果我們希望給 O(n) 去重添上動畫效果,要怎麼做?
###0. 問題重述
輸入:一個陣列,元素型別為 Number|String(清晰起見,簡化問題)
輸出:一個不含重複元素的陣列
去重方法如下:
// O(n) 去重
var unique = function(arr) {
var dir = {};
var _arr = arr.slice();
var res = [];
_arr.forEach(function(item) {
// id = type + item,避免 String 鍵名衝突
var id = typeof item + item;
if (!dir[id]) {
dir[id] = true;
res.push(item);
}
});
return res;
};
// test
var arr = [1, 2, '1', 2, 3, 23, 1, '5'];
console.log(unique(arr));
// log print:
// [ 1, 2, '1', 3, 23, '5' ]
那麼如果現在要給去重的過程添上動畫,要怎麼做?
- 先去重並記錄過程細節,去重結束後執行動畫序列,動畫結束後繼續其它業務
似乎只有這一種穩妥的選擇,但如果問題不是簡單的去重,而是其它更複雜的(比如快排甚至退火演算法)呢?是不是要設計一個複雜的資料結構,儲存過程細節?考慮快排動畫,我們希望表達每趟操作的細節(2 個指標如何移動,如何比較,如何賦值...),就必須要用一個巨大的物件陣列把這些過程保留下來嗎?
純屬浪費,因為排序只想得到排序結果,而不是過程細節,我們辛苦記錄的過程細節事實上只用了一次就被丟棄了
換個角度,在排序過程中執行動畫才是最合理的,在排序過程中可以拿到所有細節,執行動畫,然後下一步排序
但問題是動畫在做,排序也在做,Step1 的動畫完成後,排序已經進行到 Step5 了。嗯,我們需要讓 JS 停下來
###1. 讓 JS 停下來
使用 yield 再簡單不過了,F12 輸入 function* + yield 語法,想怎麼暫停就怎麼暫停,關於 ES6 yield 的更多資訊,請檢視 [generator(生成器)_ES6 筆記 2](/articles/generator(生成器)-es6 筆記 2/)
注意,現在時間是6 年前(2010-6),JS 才剛剛迎來 ES5 時代,沒有 yield 可以用,當時大多數人和筆者一樣覺得 JS 是停不下來的(比如迴圈如何暫停)
確實,迴圈停不下來,但是遞迴完全可以暫停
模擬一個非同步動畫(CSS 動畫等等),如下:
var asyncAnim = function(str, callback) {
console.log(str);
setTimeout(function() {
// 耗時動畫
//...
callback();
}, 100);
};
然後迴圈改遞迴:
var uniqueWithAnim = function(arr, callback) {
var dir = {};
var _arr = arr.slice();
var res = [];
var animLock = false;
// 用遞迴代替迴圈
var i = 0, len = _arr.length;
var find = function() {
if (i === len) {
return callback(res);
}
var item = _arr[i];
var id = typeof item + item;
if (!dir[id]) {
dir[id] = true;
res.push(item);
// 動畫
animLock = true;
asyncAnim('push ' + item, function() {
// 動畫完成回撥
animLock = false;
find();
});
}
i++;
if (!animLock) {
find();
}
};
// 開始遞迴
find();
};
測試一下:
// test
uniqueWithAnim(arr, function() {
console.log('all done');
});
// log print:
// push 1
// (100ms later)push 2
// (100ms later)push 1
// (100ms later)push 3
// (100ms later)push 23
// (100ms later)push 5
// (100ms later)all done
效果是 JS 被停下來了,我們通過回撥把非同步邏輯與同步邏輯串起來了,但是需要改寫程式碼,一點都不美
###2. 模擬 yield
我們想要的是一種通用的能讓 JS 停下來的工具模式,所以需要模擬實現 yield
var w = function(fn) {
// $yield
var $yield = function(result, next) {
var res = {};
res.value = result;
if (typeof next === 'function') {
res.next = next;
}
return res;
};
return fn.bind(null, $yield);
};
w 是一個 wrapper,給 fn 注入引數,$yield 用來建立 step 鏈
試用一下:
var oneTwoThree = w(function($yield) {
return $yield(1, function() {
return $yield(2, function() {
return $yield(3);
});
});
});
console.log(oneTwoThree());
console.log(oneTwoThree().next());
console.log(oneTwoThree().next().next());
// log print:
// { value: 1, next: [Function] }
// { value: 2, next: [Function] }
// { value: 3 }
這就是一種簡單的 yield 實現方式,看起來也不美
###3. WindJS 原理
-
遞迴代替迴圈。把邏輯塊拆分為 step 鏈,延時呼叫下一個函式,就是所謂的暫停(
Sleep) -
「編譯」隱藏改寫程式碼細節。Wind 幫我們改寫原始碼,所以效果是我們可以以同步形式編寫非同步程式碼
Wind 不改變使用者的思維方式和編碼習慣,也不強加一堆 API,而是以「隱式」重寫原始碼的方式來實現,所以看起來像是修改了 JS 語言本身,以不變應萬變,不用像 async 模組一樣提供大而全的方案
P.S. 現在 ES7 已經吸納了這種方案(提供了 async&await),這樣看來,Wind 確實「修改」了 JS 語言本身
P.S. 如果框架都按這種思路來設計,FEers 就沒有那麼多東西需要學了...如果語言本身吸納各種特性趨於完美,就不需要什麼框架了,頂多還需要工程化工具(比如構建工具等等)
##二。「編譯」原始碼
「編譯」原始碼是 JS終極黑魔法,像一扇黑氣繚繞的大門,代表著無限可能
###1. Wind 編譯示例
讀取檔案觸發異常,示例如下:
var fs = require('fs');
var Wind = require('Wind');
// 任務模型捕獲非同步異常
var Task = Wind.Async.Task;
var Binding = Wind.Async.Binding;
var readFileAsync = Binding.fromStandard(fs.readFile);
var readFile = eval(Wind.compile('async', function() {
try {
var file = $await(readFileAsync('./nosuch.file', 'utf-8'));
} catch (err) {
console.log('catch error: ' + err);
}
}));
// 獲取任務物件
var task = readFile();
// 啟動任務
task.start();
編譯結果如下:
(function () {
var _builder_$0 = Wind.builders["async"];
return _builder_$0.Start(this,
_builder_$0.Try(
_builder_$0.Delay(
function () {
return _builder_$0.Bind(readFileAsync("./nosuch.file", "utf-8"), function (file) {
return _builder_$0.Normal();
});
}),
function (err) {
console.log("catch error: " + err);
return _builder_$0.Normal();
},
null
)
);
})
Wind 幫我們完成了改寫,把同步形式的程式碼改寫為非同步回撥形式
###2. 模擬捕獲非同步回撥中的異常
Wind 的 try...catch 能夠捕獲非同步回撥中的異常,看起來很神秘,其實原理很簡單
// 嘗試捕獲非同步回撥中的異常
var ex = {};
ex.Try = function(asyncFn, errHandler) {
asyncFn.call(null, errHandler);
};
// test
var _readFileSync = function(callback) {
fs.readFile('./nosuch.file', 'utf-8', callback);
};
ex.Try(_readFileSync, function(err) {
console.log('catch error: ' + err);
});
// log print:
// catch error: Error: ENOENT: no such file or directory, open 'E:\node\learn\async\wind\nosuch.file'
Wind 內部與上面的示例類似,只是我們被 compile 矇住了雙眼,才看不透這層神祕
###3.「編譯」
我們模擬的 ex.Try 看起來一點也不像 Wind 優雅的原生 try...catch,沒關係,我們也來試試「編譯」美容法:
ex.compile = function(fn) {
var rTry = /\s+ try\s*{([^}]+)}/m;
var rCatch = /\s+ catch[^)]+\)\s*{([^}]+)}/m;
var source = fn.toString();
// console.log(source);
// parse try block
var sourceTry = rTry.exec(source)[1].trim();
// console.log(sourceTry);
var sourceTry = sourceTry.replace(/^[^$]*\$await\s*\((.+)\)\s*\)/m, function(match, p1) {
// console.log(p1);
return '(function(callback) {\n' + p1 + ', callback);\n});';
});
// console.log(sourceTry);
var asyncFn = eval(sourceTry);
// parse catch block
var sourceCatch = rCatch.exec(source)[1].trim();
// console.log(sourceCatch);
sourceCatch = '(function(err) {\n' + sourceCatch + '\n});';
var errHandler = eval(sourceCatch);
return {
start: function() {
ex.Try(asyncFn, errHandler);
}
};
};
最後,測試效果:
// test
var t = ex.compile(function() {
try {
var file = $await(fs.readFile('./nosuch.file', 'utf-8'));
} catch (err) {
console.log('catch error: ' + err);
}
});
t.start();
// log print:...
形式和效果都完全一樣,這就是 Wind 的 try...catch 能夠捕獲非同步回撥異常的祕密
「編譯」完成的工��如下:
var file = $await(fs.readFile('./nosuch.file', 'utf-8'));
-->
var _readFileSync = function(callback) {
fs.readFile('./nosuch.file', 'utf-8', callback);
};
console.log('catch error: ' + err);
-->
function(err) {
console.log('catch error: ' + err);
}
說白了就是字串拼接,僅此而已
同樣的,Wind 能處理無限序列就沒什麼好奇怪的了,示例如下:
// infinite fib series
var fib = eval(Wind.compile("async", function () {
$await(Wind.Async.sleep(1000));
console.log(0);
$await(Wind.Async.sleep(1000));
console.log(1);
var a = 0, current = 1;
while (true) {
var b = a;
a = current;
current = a + b;
$await(Wind.Async.sleep(1000));
console.log(current);
}
}));
fib().start();
while(true) 死迴圈被替換成了含斷點的死遞迴,就像 ES6 的 yield:
// infinite fib series in es6 yield
var fib = (function* () {
yield 0;
yield 1;
var a = 0, current = 1;
while (true) {
var b = a;
a = current;
current = a + b;
yield current;
}
})();
fib.next(); // 0
fib.next(); // 1
fib.next(); // 1
##三。總結
Wind 的特點如下:
-
編譯原始碼
-
任務模型
-
以同步形式編寫非同步程式碼
適用場景:適用於從已有的以同步方式編寫的程式碼向 Node 遷移,能夠省去重寫程式碼的開銷
那麼,ES7 的 async&await 能否取代 Wind?
可以,因為實現原理與目標都是一致的
-
實現原理:
yield暫停 -
目標:以同步形式編寫非同步程式碼
ES7 的 async&await 從 promise,generator 一路輾轉走來,而 Wind 早在 5 年前就看到了這一天,並提前實現了願景
學習技術思想本身,而不是單純的程式碼應用,這才是程式設計
###參考資料
- 《深入淺出 NodeJS》
暫無評論,快來發表你的看法吧