在代碼中,異步編程的直接體現就是回調。異步編程依托于回調來實現,但不能說使用了回調后程序就異步化了。我們首先可以看看以下代碼。
~~~
function heavyCompute(n, callback) {
var count = 0,
i, j;
for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
}
callback(count);
}
heavyCompute(10000, function (count) {
console.log(count);
});
console.log('hello');
-- Console ------------------------------
100000000
hello
~~~
可以看到,以上代碼中的回調函數仍然先于后續代碼執行。JS本身是單線程運行的,不可能在一段代碼還未結束運行時去運行別的代碼,因此也就不存在異步執行的概念。
但是,如果某個函數做的事情是創建一個別的線程或進程,并與JS主線程并行地做一些事情,并在事情做完后通知JS主線程,那情況又不一樣了。我們接著看看以下代碼。
~~~
setTimeout(function () {
console.log('world');
}, 1000);
console.log('hello');
-- Console ------------------------------
hello
world
~~~
這次可以看到,回調函數后于后續代碼執行了。如同上邊所說,JS本身是單線程的,無法異步執行,因此我們可以認為`setTimeout`這類JS規范之外的由運行環境提供的特殊函數做的事情是創建一個平行線程后立即返回,讓JS主進程可以接著執行后續代碼,并在收到平行進程的通知后再執行回調函數。除了`setTimeout`、`setInterval`這些常見的,這類函數還包括NodeJS提供的諸如`fs.readFile`之類的異步API。
另外,我們仍然回到JS是單線程運行的這個事實上,這決定了JS在執行完一段代碼之前無法執行包括回調函數在內的別的代碼。也就是說,即使平行線程完成工作了,通知JS主線程執行回調函數了,回調函數也要等到JS主線程空閑時才能開始執行。以下就是這么一個例子。
~~~
function heavyCompute(n) {
var count = 0,
i, j;
for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
}
}
var t = new Date();
setTimeout(function () {
console.log(new Date() - t);
}, 1000);
heavyCompute(50000);
-- Console ------------------------------
8520
~~~
可以看到,本來應該在1秒后被調用的回調函數因為JS主線程忙于運行其它代碼,實際執行時間被大幅延遲。