異步編程
由於js是單線程的運行環境,但是爲了解決線程阻塞的問題,所以就使用異步編程的方式。
callback
首先,最傳統的異步編程的方式就是使用回調函數。
回調函數其實就是一個普通函數,它的特別之處在於它的調用方式。在這裏我們還需要弄清楚函數調用和回調函數的調用方式的區別。
假設有兩個函數f1與f2,如果在滿足一定的條件下f2裏調用f1,則此調用方式爲函數調用;如果把f1當成f2的參數並且在滿足一樣的條件下使用參數f1,則此調用方式爲回調函數。
Talk is cheap,show you the code:
//這是函數調用
function f1() {
console.log('I am f1!');
}
function f2(count) {
console.log('I am f2!');
if (count > 1) {
f1();
}
}
f2(2);
//這是回調函數
function f1() {
console.log('I am f1!');
}
function f2(count,f) {
console.log('I am f2!');
if (count > 1) {
f();
}
}
f2(2,f1);
其實沒必要太過區分函數調用和回調函數的區別,因爲這兩種方式本質都一樣,並且可以相互轉換,只是使用回調函數的方式由於把函數作爲參數傳遞使得其靈活性變得更好。
正是由於回調函數可以回調的特性,我們就可以在同步代碼裏通過調用回調函數實現異步操作。
回調函數看似操作簡便,但是多層回調就存在一個非常致命的“回調地獄”問題:
f(text1, function (count) {
if (count > 1) {
f(text2, function (count) {
if (count > 1) {
f(text3, function (count) {
//...
})
}
})
}
})
這是一種類似遞歸的橫向嵌套回調函數擴展使得代碼很容易失控,所以就引入了Promise這種更加靈活更爲簡便的支持異步編程的對象。
Promise
promise是一個Promise類型的對象,簡單來說也是一個容器,其基本用法如下:
let promise = new Promise(function(resolve, reject) {
//code
});
它的參數resolve和reject是JavaScript本身提供的回調函數。 我們的代碼僅在執行器內部。
它的返回值promise有兩個屬性:state(初始值爲"pending")和result(初始值爲undefined)。
注意:Promise內部代碼只能執行一個resolve或者一個reject,一旦修改的狀態確定,則後面所有的resolve和reject都會被忽略。
返回的promise對象我們無法直接操作,我們可以藉助其一個重要的API:promise.then()。
promise.then(
function(result) { /* ...*/ },
function(error) { /* ... */ }
);
這個方法提供兩個參數,第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。其中,第二個函數是可選的。
Promise對象還有更多的API,例如.catch()、.finally()等等,這裏我不多做介紹。
下面將上面的回調函數的例子改寫成Promise版本:
function f1() {
console.log('I am f1!');
}
function f2(count) {
console.log('I am f2!');
if (count > 1) {
return new Promise(resolve => resolve());
}
}
f2(2).then(f1);
也許在這裏感覺用Promise和直接使用回調函數的方式差不多,那是因爲這裏代碼邏輯簡單。
更重要的一點,使用鏈式promise.then()的方式可以完美的解決“回調地獄”的問題:
new Promise(function(resolve, reject) {
}).then(function(result) { // (**)
}).then(function(result) { // (***)
}).then(function(result) {
});
注意:promise.then().then()與promise.then();promise.then();不一樣。因爲每次promise.then()都是返回一個新的promise對象。而我們所說的鏈式promise.then()是指第一種調用方式。
async/await
一句話描述,async/await就是Promise的語法糖,async/await與Promise相比,最明顯的特徵就是可以直接在同步代碼裏實現異步編程!
先簡單介紹一下async和await:
首先,它倆都是js的關鍵字,async應作用於function(注:es6的class本質上也是function)並且此function總是返回一個promise對象。而await作用於一個promise對象,其效果類似於promise.then()。
注意:await若處於一個function內,則此function一定有async修飾!
Talk is cheap,show you the code:
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));
return 10;
}
function f() {
wait().then(result => console.log(result));
}
f();
運行上述代碼,會在1秒後在控制檯輸出數字10。
此代碼是Promise與async/await的綜合案例,首先,在wait()函數內,由於此函數有async修飾,所以其返回的10不是Number類型而是Promise類型且此PromiseValue的值是10。正是由於wait()函數返回的是一個Promise對象,所以函數f裏可以直接使用wait().then()的方式進行調用。
此代碼還有另外一種更常見的編寫方式:
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));
return 10;
}
async function f() {
console.log(await wait());
}
f();
Generator
Generator是一個函數,正常的函數只能返回一個值或者沒有返回值,而Generator函數可以按需求一個接一個地返回多個值。
一個基本的Generator函數:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generate = generateSequence();
不難發現Generator函數與普通函數相比有兩個明顯的特徵:*以及yeild關鍵字。
直接調用Generator函數並不會運行此函數,而是得到一個指向其內部狀態的指針對象,真正運行Generator函數是通過調用其指針對象調用.next()方法來改變此指針對象的狀態。調用.next()方法後,它將一直執行直到最接近的yield語句。 然後函數執行暫停,並將產生的值返回到外部代碼。
.next()方法返回的結果有兩個屬性:value和done。
說道.next()方法,這不就是迭代器裏面的一個重要元素嗎?沒錯,其實Generator就是迭代器。所以Generator的最主要作用就是用於流程管理。
Generator函數本身單獨並不能實現異步編程,通過yield可以暫停函數執行的特性可以將Generator函數裏的Promise對象用yield修飾,這樣每次進行Generator函數的迭代我們都能得到Promise對象然後進行我們自己的異步邏輯並且還能暫停函數的執行。
下面給出阮一峯教程的一個用Generator 函數封裝的異步操作示例:
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
一個舉一反四的小案例
首先我們看一個不含異步操作的代碼:
function delay() {
console.log('延遲2秒');
}
function sequence() {
console.log('函數開始');
setTimeout(delay, 2000);
console.log('函數結束');
}
sequence();
運行這段代碼,結果顯示爲:函數開始,函數結束,延遲2秒。
現在,我們想要上面介紹過的四種方式來實現異步操作,使得其結果爲:函數開始,延遲2秒,函數結束。
首先,使用回調函數的方式:
function callbackDelay(delay) {
delay();
console.log('函數結束');
}
//callback
function callbackSequence() {
console.log('函數開始');
setTimeout(() => callbackDelay(delay), 2000);
}
callbackDelay();
此回調的條件就是延遲兩秒後再調用回調函數。
使用Promise的方式:
//promise
function promiseSequence() {
console.log('函數開始');
return new Promise(resolve => setTimeout(() => resolve(delay()), 2000));
}
let promise = promiseSequence();
promise.then(() => console.log('函數結束'));
將函數結束封裝在promise.then()內可確保此操作一定在延遲之後纔會進行。
使用async/await的方式:
//async/await
async function asyncSequence() {
console.log('函數開始');
await new Promise(resolve => setTimeout(() => resolve(delay()), 2000));
console.log('函數結束');
}
asyncSequence();
不愧是在同步代碼裏實現異步,簡簡單單隻需要在同步代碼邏輯上添加上相應的關鍵字即可。
最後,再看看Generator的方式:
//generator
async function* generateSequence() {
console.log('函數開始');
yield await new Promise(resolve => setTimeout(() => resolve(delay()), 2000));
console.log('函數結束');
}
let generate = generateSequence();
for await(let value of generate) {
}
乍一看,像是在async/await的基礎上進行的畫蛇添足,誰讓Generator是迭代器,對於只有單個promise的迭代用上Generator肯定是大材小用了。
結語
此篇文章主要是講述了callback,promise,async/await,generator這四種常見的異步編程的基本知識和彼此之間的使用關聯,而它們裏面所包含的細節知識點遠遠不止於此,更多的詳細內容可以參看我下面給出的兩個鏈接。
阮一峯es6教程:https://es6.ruanyifeng.com/
javascript.info教程:https://javascript.info/