javascript的運行機制是單線程處理,即只有上一個任務完成后,才會執行下一個任務,這種機制也被稱為“同步”。
“同步”的最大缺點,就是如果某一任務運行時間較長,其后的任務就無法執行。這樣會阻塞頁面的渲染,導致頁面加載錯誤或是瀏覽器不響應進入假死狀態。
如果這段任務采用“異步”機制,那么它就會等到其他任務運行完后再執行,不會阻塞線程。
一、es6之前實現異步的方式:
最常用的方法是采用回調函數,但普通的回調函數并不能實現異步效果:
(1)同步回調
function testFn(data, callback){
callback(data);
}
testFn('0', function(data2){
for (var i=data2; i<300000000; i++);
console.log(1);
});
console.log(2);
等一秒鐘左右for循環結束,控制臺輸出1后,才會輸出2。這是因為 testFn 的回調函數 callback 是同步執行,所以for循環會阻塞后面的任務。
(2)異步回調
想要實現回調的異步執行,必須要借助js的其它方法,例如將上面 testFn 的回調函數放到延時器中調用,延遲的時間設置為0:
function testFn(data, callback){
setTimeout(function(){
callback(data)
},0);
}
這樣控制臺會先輸出2,接著大約一秒鐘后再輸出1,可見回調函數并沒有阻塞后面的任務,實現了異步效果。
除了使用定時器,實現異步執行的方法還有:事件監聽(例如點擊事件“click”)、requestAnimationFrame、XMLHttpRequest(jq中ajax方法的核心)、WebSocket、Worker以及Node.js 的 fs.readFIle等。
二、promise、generator和async/await:
es6 引入了 generator 和 promise,es7 又增加了async/await。這三種函數有一個重要的作用,就是解決回調函數的異步執行和嵌套問題。
(因為網絡上介紹這三種方法的文章很多,所以這里只介紹個人覺得相對常用的知識點)
(一)普通的回調嵌套:
定義一個函數,要求只有在獲取兩個數據(例如"data1"和"data2")后,才會執行“console.log”任務,通過對回調函數進行嵌套就可以達到目的:
function Test1(data, callback){
if(data){ // 本例是同步,要實現異步可以添加延時器
callback(data);
};
};
Test1("data1",function(){
Test1("data2",function(data2){
console.log(data2); // 如果不傳數據就不會執行回調函數
});
});
回調函數的執行需要依賴上一個函數,這樣的缺點是如果有多個回調函數,就需要嵌套很多層。這會使代碼的可讀性較差,并增加調試和維護的難度。
(二)promise
Promise是一個構造函數,它接收一個匿名函數作為參數:
new Promise(function(resolve, reject){
resolve(console.log(1));
reject(console.log(2));
console.log(3);
})
console.log(4);
// 輸出的順序為1、2、3、4
1、因為 promise 是構造函數(帶有屬性或方法的函數就叫構造函數),所以必須使用 new 實例化才能調用。而作為參數的匿名函數不需要額外調用就能執行。
2、匿名函數只能有 resolve 和 reject 兩個參數。結合if判斷使用的話,resolve 是條件正確時執行的方法,reject 則是錯誤時執行的方法。
3、將某一任務直接放到 resolve、reject 方法里,或者放到匿名函數里并不能實現異步效果。
function Test2(){
return new Promise(function(resolve, reject){
resolve(1);
});
};
Test2().then(function(num){
console.log(num);
return num;
}).then(function(num2){
console.log(num2);
})
console.log(2);
// 數字輸出的順序是2、1、1
4、為了防止匿名函數的自動執行,需要再定義一個函數,并將 promise 函數作為該函數的返回值。
5、將回調函數寫在 then 或 catch 方法里才能實現異步,then 接受的是 resolve 方法傳遞的數據,catch 則對應的是 reject。
6、resolve 只能向第一個 then 里的函數傳遞數據,后面 then 里的函數只能通過前一個 then 里函數的返回值獲取參數。
function pro1(){
return new Promise(function(resolve, reject){
resolve(1);
});
};
function pro2(){
return new Promise(function(resolve, reject){
});
};
Promise.all([ pro1(), pro2() ]).then().catch();
Promise.race([ pro1(), pro2() ]).then().catch();
7、all方法的作用:只有 pro1 和 pro2 都執行 resolve,Promise 才會執行 then 方法。如果其中有一個函數執行 reject,那么Promise 就執行catch方法。
8、race方法的作用:pro1 和 pro2 中只要有一個狀態發生改變,Promise的狀態就跟著發生改變,不論是resolve還是reject。
promise最大的作用
(一)能把嵌套改成鏈式調用。對上面的普通嵌套進行改造:
function Test2(data){
return new Promise(function(resolve, reject){
if(data){
resolve(data);
}
});
};
Test2(1).then(function( data1 ){
console.log( data1 );
return 2;
}).then(function( data2 ){
console.log( data2 );
})
console.log(3);
// 輸出結果為3、1、2
實現效果:
(1)先輸出3,表示 then 方法里的回調函數是異步執行。
(2)如果調用 Test2 時不傳數字“1”,就不會執行 resolve 方法,這樣即使第一個 then 方法里的函數有返回值“2”,也不會執行第二個 then 方法,實現了需求。
(二)解決 ajax 不能傳值給外部變量的問題
ajax 在 success 獲取的數值無法傳遞給外部變量,除非設置為同步模式,而 promise 的 resolve 方法可以解決這個問題。
設一個外部變量 outsideData:
var outsideData ;
function TestAjax(data){
return new Promise(function(resolve, reject){
$.ajax({
url: 'xxx.php';
type: 'GET',
datatype: 'json',
data: ' ',
success: function(res){
resolve(res.data)
}
});
});
};
TestAjax().then(function( data ){
outsideData = data;
})
(三)async/await
先說 async/await,是因為 async 函數是 Generator 函數的語法糖,容易理解,而且和 promise 函數也有很大的關聯。
用 async function 定義一個函數:
async function Test(){
return 1;
}
Test().then(function(num){
console.log(num);
});
console.log( Test() );
得到的結果是
1、async function 返回的是一個 promise 對象,所以該函數可以調用 then 方法,并因此實現異步執行效果。
async function Test(){
await console.log( 1 );
console.log( 2 );
await console.log( 3 );
}
Test();
console.log( 4 );
// 結果為1、4、2、3
2、await 可以代替 then 方法,以實現異步效果。
3、但第一個 await 是同步執行。不論跟的是 promise,還是其他的函數或方法。
4、第一個 await 后面的任務,不論有沒有 await 都是異步執行。但是如果要在 async 函數里再放一個函數,那前面就必須添加 await,否則會報錯。
function proFn(){
return new Promise(function(resolve, reject){
resolve(2);
});
};
async function Test(){
var data1 = await proFn();
console.log(1);
console.log(data1);
};
Test();
console.log(3);
// 結果為3、1、2
5、對于第一個 await,最好的方法是使其等于一個變量,然后對這個變量進行處理。
6、如果 await 后面跟的是 promise ,那么匿名函數必須執行 resolve 方法,否則 await 后面的任務就無法執行。
async/await 最大的作用就是替代 promise 的 then 方法:
function proFn(data){
return new Promise(function(){
if(data){
resolve(data);
};
});
};
async function Test(){
var data1 = await proFn(1);
var data1 = await proFn(2);
console.log(data1);
console.log(data2);
};
Test();
console.log(3);
// 結果為3、1、2
(1)async/await 將鏈式調用變得更加簡化。
(2)async/await 傳遞參數的方式也比 then 簡單,不需要通過 return 來傳遞參數。
(四)generator
和 promise、async/await 不同,generator 本身并不具有異步執行的功能。它在異步中的主要應用,是管理異步回調的執行流程。
generator 函數的特征是使用 * 和關鍵詞 yield:
function* Gen(){
yield 1;
}
var runGen = Gen();
console.log(runGen.next());
// 輸出結果為 {value: 1, done: false};
1、函數名后面加小括號并不能調用 generator 函數,只是創建了一個指針對象,next 方法才會執行函數。
2、next 方法返回的對象帶有兩個屬性,分別是“done”和“value”。done 的值表示 generator 函數是否運行完。value 對應的是 yield 后面的值。
function* Gen (num){
var num2 = yield console.log(num);
yield console.log(num2);
}
var runGen = Gen(1);
runGen.next(2);
runGen.next(3);
// 結果輸出1和3。
3、next 方法可以傳遞參數,該參數是上一個 yield 表達式的值。
之所以沒有輸出“2”,就是因為第一個 next 方法并沒有與之對應的"上一個 yield",所以傳參無效。
generator 函數管理流程的應用:
使用 generator 管理異步函數,需要用到三個知識點,thunk函數、next方法得到的done屬性、遞歸。
1、thunk函數
普通的多參數函數在調用時,需要一次性傳入多個數據。
thunk 函數則是把多個參數拆開,使得在調用時,數據可以分開傳入。
實現方法是定義一個函數,并且該函數的返回值也是一個函數,這樣就能將參數拆開放在兩個函數里:
// 普通的多參數函數:
function Test(data, callback){};
// 調用普通函數:
Test(data, callback);
// thunk函數:
function Test(data){
return function(callback){
callback();
}
}
// 調用thunk函數:
var runThunk = Test(data);
runThunk(callback);
將 callback 參數提取出來,放在作為返回值的匿名函數里,在調用該函數時 callback 和 data 所對應的數據就可以分兩步傳入。
2、通過 done 屬性控制流程:
generator 函數代替“嵌套”去控制流程的思路,就是通過上一個 yield 的執行情況,來決定下一個 next 方法是否執行,這需要用到 done 屬性:
function Test(){
setTimeout(function(){
console.log(1);
},0);
}
function* Gen(){
yield Test();
yield console.log(2);
}
var runGen = Gen();
var genObj = runGen.next();
if(!genObj.done){
runGen.next();
}
// 結果為2、1
(1)直接給 next 方法外面添加 if 判斷的缺點是,假如某個 yield 后面跟的是異步函數,那么其他 yield 所對應的非異步任務就會優先執行。
如果必須保證前一個任務運行完后,才會執行下一步,就需要把 next 方法放到 value 屬性里:
function Test2(){
return function(callback){
console.log(3);
callback();
}
}
function* Gen2(){
yield Test2();
yield console.log(4);
}
var runGen2 = Gen2();
var genObj2 = runGen2.next();
genObj2.value( function(){
runGen2.next();
} );
// 得到的結果是 3、4
(2)想在 value 里使用 next 方法,需要將 value 變成一個函數,這就要用到 thunk 函數。而 value 后面加一個小括號,就能調用作為返回值的函數了。
(3)如果把 next 方法直接放到 value 里,那么 next 方法得到的結果會被當成 value 的參數,先輸出。所以需要給 next 方法外面再包一層函數。
下面的函數就是最終形態:
function Test3(){
return function(callback){
setTimeout(function(){
console.log(5);
callback();
},0)
}
}
function* Gen3(){
yield Test3();
yield console.log(6);
}
var runGen3 = Gen3();
var genObj3 = runGen3.next();
genObj3.value( function(){
if(genObj3.done) return;
runGen3.next();
} );
// 結果為5、6
3、使用遞歸自動執行 generator 函數:
首先看看手動執行 generator 的例子:
// 為了簡潔,本例并沒有使用異步
function Thunk(num){
return function(callback){
console.log(num);
callback();
};
};
function* Gen(){
yield Thunk(1);
yield Thunk(2);
}
var runGen = Gen();
var runNext = runGen.next();
if(runNext.done) return;
runNext.value(function(){
var runNext2 = runGen.next();
if(runNext.done) return;
runNext2.value(function(){
});
});
// 結果輸出1、2
假如存在多個 yield,就需要寫很多 next,這會令代碼變得臃腫。通過觀察可以看出使用 next 方法的部分存在很大的重復性,所以可以使用遞歸(也就是函數內部調用自身)對其進行改造。
var runGen = Gen();
function next(err, data) {
var runNext = runGen.next(data);
if (runNext.done) return;
runNext.value(next);
}
next();
5、修改 promise 的鏈式調用
function Test(num){
return new Promise( function(resolve, reject){
resolve(num);
} );
}
function* Gen(){
yield Test(1);
yield Test(2);
}
var runGen = Gen();
function next(){
var genObj = runGen.next();
if(genObj.done) return;
genObj.value.then(function(num){
console.log(num);
next();
});
}
next();
console.log(3);
// 得到的結果為 3、1、2
總結:
關于es6的研究到此就告一段落了。個人覺得“類”和“箭頭函數”是一定要掌握的,因為這兩點能簡化代碼結構。至于異步,從例子的長短也能看出,generator 沒必要了解很深,還是交給 promise 和 ansyc/await 吧。
三、參考:
1、http://www.cnblogs.com/webeye/p/5383785.html (js同步的缺點)
2、http://blog.csdn.net/tywinstark/article/details/48447135 (15樓回復)
3、http://stackoverflow.com/questions/9516900/how-can-i-create-an-asynchronous-function-in-javascript (js實現異步的方法)
4、http://www.ruanyifeng.com/blog/2014/10/event-loop.html (js運行機制)
5、http://www.nowamagic.net/librarys/veda/detail/787 (瀏覽器假死原因)
6、https://segmentfault.com/a/1190000003096984 (異步回調的缺點)
7、https://www.oschina.net/translate/event-based-programming-what-async-has-over-sync (回調函數嵌套的缺點)
8、https://segmentfault.com/q/1010000002577322 (回調函數如何實現異步)
9、http://es6.ruanyifeng.com/#docs/promise (promise知識點)
10、http://www.cnblogs.com/lvdabao/p/es6-promise-1.html (promise需要放到另一個函數里)
11、https://segmentfault.com/a/1190000007535316(await知識點)
12、http://blog.rangle.io/javascript-asynchronous-options-2016/(generator不是異步)
13、http://es6.ruanyifeng.com/#docs/generator (generator知識點)
14、http://www.liaoxuefeng.com/wiki/ (generator函數的調用)
15、http://www.lxweimin.com/p/87183851756f (promise使用例子)
16、https://segmentfault.com/q/1010000011014844 (使用return返回ajax獲取的數值)