異步流程控制(Callback 、Promise、Generator + co、async + await)

一. Callback (回調(diào)函數(shù))

1.定義:把函數(shù)當作變量傳到另一個函數(shù)里,傳進去之后執(zhí)行甚至返回等待之后的執(zhí)行。
2.一個簡單的例子

function add_callback(p1, p2 ,callback) {
var my_number = p1 + p2;
callback(my_number);
}
add_callback(5, 15, function(num){
console.log("call " + num);
});

3. error first

1.回調(diào)函數(shù)的第一個參數(shù)保留給一個錯誤error對象,如果有錯誤發(fā)生,錯誤將通過第一個參數(shù)err返回。
2.回調(diào)函數(shù)的第二個參數(shù)為成功響應的數(shù)據(jù)保留,如果沒有錯誤發(fā)生,err將被設置為null, 成功的數(shù)據(jù)將從第二個參數(shù)返回。

4.callback hell

JavaScript 是由事件驅(qū)動的異步編程,一個異步的操作,我們在調(diào)用他的時候,不會馬上得到結(jié)果,而是會繼續(xù)執(zhí)行后面的代碼。這樣,如果我們需要在查到結(jié)果之后才做某些事情的話,就需要把相關(guān)的代碼寫在回調(diào)里面,如果涉及到多個這樣的異步操作,就勢必會陷入到回調(diào)地獄中去。eg:
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

二. Promise(為了解決callback hell的問題)

1. Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強大。ES6將其寫進了語言標準,統(tǒng)一了用法,原生提供了Promise對象。
2. Promise A+規(guī)范:

Promise 規(guī)范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升級版 Promise/A+,最終 ES6 中采用了 Promise/A+ 規(guī)范。Promise/A+ 官網(wǎng):https://promisesaplus.com/

3.promise對象的特點

1.對象的狀態(tài)不受外界影響,promise對象代表一個異步操作,有三種狀態(tài),pending(進行中)、fulfilled(已成功)、rejected(已失?。?。只有異步操作的結(jié)果,可以決定當前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài),這也是promise這個名字的由來“承諾”;
2.一旦狀態(tài)改變就不會再變,任何時候都可以得到這個結(jié)果,promise對象的狀態(tài)改變,只有兩種可能:從pending變?yōu)閒ulfilled,從pending變?yōu)閞ejected。這時就稱為resolved(已定型)。如果改變已經(jīng)發(fā)生了,你再對promise對象添加回調(diào)函數(shù),也會立即得到這個結(jié)果,這與事件(event)完全不同,事件的特點是:如果你錯過了它,再去監(jiān)聽是得不到結(jié)果的。

4. 創(chuàng)造了一個Promise實例

var promise = new Promise( function( resolve, reject) {
/some code
if(//異步操作成功){
resolve(value);
}else{
reject(error);
}
});

5.Promise 方法:

Promise.prototype.then = function() {}
Promise.prototype.catch = function() {}
Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}
1.promise.then方法返回promise的結(jié)果,then 的第一個參數(shù)是處理正確時的返回值的函數(shù),第二個參數(shù)是處理錯誤時的返回的error的函數(shù)
promise.then(function(value) {
// success
}, function(error) {
// failure
});

  1. promise.catch可以捕獲promise返回的錯誤
    var promise = new Promise(function(resolve, reject) {
    throw new Error('test');
    });
    promise.catch(function(error) {
    console.log(error);
    });
  2. Promise.resolve 是將一個值包裹成 promise對象,狀態(tài)是fulfilled
    Promise.resolve('haha')
    等價于
    new Promise(function (resolve, reject) {resolve('haha')})
  3. Promise.reject 也是將一個值包裹成 promise對象,只不過狀態(tài)是 rejected
    5.Promise.all()用于將多個Promise實例,包裝成一個新的Promise實例。
    var promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
    var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('hehe')
    }, 2000)
    })
    var start = Date.now()
    Promise.all([promise1, promise2])
    .then((res) => {
    console.log(Date.now() - start)
    console.log(res)
    })
    (1)只有promise1、promise2的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled,此時promise1、promise2的返回值組成一個數(shù)組,傳遞給回調(diào)函數(shù)。
    (2)只要promise1、promise2之中有一個被rejected,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值,會傳遞給新的Promise實例回調(diào)函數(shù)。
    6.Promise.race方法同樣是將多個Promise實例,包裝成一個新的Promise實例。但是只要promise1、promise2之中有一個實例率先改變狀態(tài),新的Promise實例的狀態(tài)就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給新的Promise實例的回調(diào)函數(shù)。
    var promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
    var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('hehe')
    }, 2000)
    })
    var start = Date.now()
    Promise.race([promise1, promise2])
    .then((res) => {
    console.log(Date.now() - start)
    console.log(res)
    })
6.擴展
  1. then 方法可以被同一個 promise 調(diào)用多次,原理在于:第一次調(diào)用之前,狀態(tài)就已經(jīng)從pengding->fulfilled,同時內(nèi)部保留了一個值,這時多次調(diào)用.then,都會返回內(nèi)部的那個值
    var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
    var start = Date.now()
    promise.then((res) => {
    console.log(res, Date.now() - start)
    })
    promise.then((res) => {
    console.log(res, Date.now() - start)
    })
    promise.then((res) => {
    console.log(res, Date.now() - start)
    })
  2. 在Promise構(gòu)造函數(shù)里throw一個error,相當于 reject(error)
  3. promise的 then和catch都是可以鏈式調(diào)用的,下一個then的值是上一個promise變成fulfilled的返回值(返回值可以是promise,也可以是任意值(這個任意值內(nèi)部是把它包裝成promise)。如果是promise,會等待該promise返回(狀態(tài)變更)),eg:
    var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
    var start = Date.now()
    promise
    .then((res) => {
    console.log(res, Date.now() - start)
    })
    .then((res) => {
    console.log(res, Date.now() - start)
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('hehe')
    }, 2000)
    })
    })
    .then((res) => {
    console.log(res, Date.now() - start)
    })
  4. 構(gòu)造函數(shù)resolve 或 reject 只執(zhí)行一次,多次調(diào)用沒有任何作用
    var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    reject('error')
    })
    var start = Date.now()
    promise.then(() => {
    console.log('then', Date.now() - start)
    })
    promise.catch((e) => {
    console.error(e)
    console.error('catch', Date.now() - start)
    })
  5. 值穿透,then的參數(shù)正常情況下使用接收函數(shù),如果傳遞一個非函數(shù),則忽略,下一個then使用的是當前then的上一個返回值,也就是會跳過這個then
    var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
    reject('haha')
    }, 1000)
    })
    promise2
    .catch(1)
    .catch('hehe')
    .catch((error) => {
    return console.log(1, error)
    })
    .catch((error) => {
    return console.log(2, error)
    })
  6. promise拋錯(rejected),則跳過then,被最近的一個catch捕獲(前提是:1.then沒有第二個處理錯誤的函數(shù) 2.最近的catch沒有值穿透),在then/catch里throw new Error(xxx)等價于return Promise.reject(xxx)
    Promise.resolve()
    .then(() => {
    // return new Error('error!!!') // 會打印1?。。?!
    return Promise.reject(new Error('error!!!'))
    })
    .then((res) => {
    console.log(1, res)
    })
    .catch((e) => {
    console.error(2, e)
    })
  7. then返回的值不能是 promise 本身,否則會造成死循環(huán),原因:then里返回的promise會等待他狀態(tài)改變(或者說執(zhí)行完)才會進入到下一個then
    var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
    var a = promise.then(() => {
    return a
    })
    a.then(console.log)
    .catch(console.error)
  8. then方法的第二個參數(shù) vs catch方法, then里第二個處理錯誤的回調(diào)函數(shù)不會捕獲這個then第一個處理成功時的回調(diào)函數(shù)拋出的error,then/catch里沒有return值等價于return Promise.resolve(undefined)
    var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('haha')
    }, 1000)
    })
    promise
    .then(function success(res) {
    throw new Error('error')
    }, function fail1(e) {
    console.error(1, e)
    })
    .catch(function fail2(e) {
    console.error(2, e)
    })

三. Generator

1.Generator 函數(shù)是一個狀態(tài)機,封裝了多個內(nèi)部狀態(tài);執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,返回的遍歷器對象,可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)。
2.特點
  1. function關(guān)鍵字與函數(shù)名之間有一個星號
  2. 函數(shù)體內(nèi)部使用yield語句,定義不同的內(nèi)部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)。
3.yield* 語句

由于Generator函數(shù)返回的遍歷器對象,只有調(diào)用next方法才會遍歷下一個內(nèi)部狀態(tài),所以其實提供了一種可以暫停執(zhí)行的函數(shù)。yield語句就是暫停標志。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
(1)遇到y(tǒng)ield語句,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值。
(2)下一次調(diào)用next方法時,再繼續(xù)往下執(zhí)行,直到遇到下一個yield語句。
(3)如果沒有再遇到新的yield語句,就一直運行到函數(shù)結(jié)束,直到return語句為止,并將return語句后面的表達式的值,作為返回的對象的value屬性值。
(4)如果該函數(shù)沒有return語句,則返回的對象的value屬性值為undefined。
(5)如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個 Generator 函數(shù),默認情況下是沒有效果的
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"

4.next()方法

hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

  1. yield句本身沒有返回值,或者說總是返回undefined。
  2. 每次調(diào)用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內(nèi)部狀態(tài)的值,是yield語句后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結(jié)束。
  3. next方法可以帶一個參數(shù),該參數(shù)就會被當作上一個yield語句的返回值。
    function* f() {
    for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
    }
    }
    var g = f();
    g.next()
    // { value: 0, done: false }
    g.next()
    // { value: 1, done: false }
    g.next(true)
    // { value: 0, done: false }
5.for...of循環(huán)

for...of循環(huán)可以自動遍歷Generator函數(shù)時生成的Iterator對象,且此時不再需要調(diào)用next方法
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5

6.Generator方法

Generator.prototype.throw()
Generator.prototype.return()

  1. throw()可以在函數(shù)體外拋出錯誤,然后在Generator函數(shù)體內(nèi)捕獲
    var g = function* () {
    try {
    yield;
    } catch (e) {
    console.log(e);
    }
    };
    var i = g();
    i.next();
    i.throw(new Error('出錯了!'));
    // Error: 出錯了!(…)
  2. return()返回給定的值,并且終結(jié)遍歷Generator函數(shù)
    function* gen() {
    yield 1;
    yield 2;
    yield 3;
    }
    var g = gen();
    g.next() // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next() // { value: undefined, done: true }
    如果return方法調(diào)用時,不提供參數(shù),則返回值的value屬性為undefined
    function* gen() {
    yield 1;
    yield 2;
    yield 3;
    }
    var g = gen();
    g.next() // { value: 1, done: false }
    g.return() // { value: undefined, done: true }
7. co 模塊
  1. co 模塊是著名程序員 TJ Holowaychuk 于2013年6月發(fā)布的一個小工具,用于 Generator 函數(shù)的自動執(zhí)行
  2. 用法:
    Generator 函數(shù)只要傳入co函數(shù),就會自動執(zhí)行;co函數(shù)返回一個Promise對象,因此可以用then方法添加回調(diào)函數(shù)
    var co = require('co');
    var gen = function* () {
    var f1 = yield readFile('/etc/fstab');
    var f2 = yield readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
    };
    co(gen).then(function (){
    console.log('Generator 函數(shù)執(zhí)行完成');
    });
  3. 原理:
    自動執(zhí)行機制:當異步操作有了結(jié)果,能夠自動交回執(zhí)行權(quán)。
    (1)回調(diào)函數(shù)。將異步操作包裝成 Thunk 函數(shù),在回調(diào)函數(shù)里面交回執(zhí)行權(quán)。
    (2)Promise 對象。將異步操作包裝成 Promise 對象,用then方法交回執(zhí)行權(quán)。

四. async (需要node@8以上)

  1. async 函數(shù)Generator 函數(shù)的語法糖:將 Generator 函數(shù)的星號(*)替換成async,將yield替換成await,僅此而已。
    var fs = require('fs');
    var readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
    if (error) reject(error);
    resolve(data);
    });
    });
    };
    var gen = function* () {
    var f1 = yield readFile('/etc/fstab');
    var f2 = yield readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
    };
    寫成async函數(shù),就是下面這樣。
    var asyncReadFile = async function () {
    var f1 = await readFile('/etc/fstab');
    var f2 = await readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
    };
    2.用法:
    async function getStockPriceByName(name) {
    var symbol = await getStockSymbol(name);
    var stockPrice = await getStockPrice(symbol);
    return stockPrice;
    }
    getStockPriceByName('goog').then(function (result) {
    console.log(result);
    });
    (1)async函數(shù)返回一個 Promise 對象,可以使用then方法添加回調(diào)函數(shù)
    (2)async函數(shù)內(nèi)部return語句返回的值,會成為then方法回調(diào)函數(shù)的參數(shù)。
  2. await 命令
    async function f() {
    return await 123;
    }
    f().then(v => console.log(v)) // 123
    (1)wait 只能在 async 函數(shù)中使用
    (2)await命令后面是一個 Promise 對象。如果不是,會被轉(zhuǎn)成一個立即resolve的 Promise 對象。
    (3)只要一個await語句后面的 Promise 變?yōu)閞eject,那么整個async函數(shù)都會中斷執(zhí)行。
    async function f() {
    await Promise.reject('出錯了');
    await Promise.resolve('hello world'); // 不會執(zhí)行
    }
  3. for await...of
    for await...of循環(huán)用于遍歷異步的 Iterator 接口
    async function f() {
    for await (const x of createAsyncIterable(['a', 'b'])) {
    console.log(x);
    }
    }
    // a
    // b
  4. Generator + co vs async + await
    (1)內(nèi)置執(zhí)行器。
    Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,所以才有了co模塊,而async函數(shù)自帶執(zhí)行器。也就是說,async函數(shù)的執(zhí)行,與普通函數(shù)一模一樣,只要一行。
    (2)更好的語義。
    async和await,比起星號和yield,語義更清楚了。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達式需要等待結(jié)果。
    (3)更廣的適用性。
    co模塊約定,yield命令后面只能是 Thunk 函數(shù)或 Promise 對象,而async函數(shù)的await命令后面,可以是Promise 對象和原始類型的值(數(shù)值、字符串和布爾值,但這時等同于同步操作)。
    (4)返回值是 Promise。
    async函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、Promise的含義 Promise在JavaScript語言中早有實現(xiàn),ES6將其寫進了語言標準,統(tǒng)一了用法...
    Alex灌湯貓閱讀 887評論 0 2
  • Promise 對象 Promise 的含義 Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函...
    neromous閱讀 8,831評論 1 56
  • 特點 Promise能將回調(diào)分離出來,在異步操作執(zhí)行之后,用鏈式方法執(zhí)行回調(diào),雖然es5用封裝函數(shù)也能實現(xiàn),但是如...
    一二三kkxx閱讀 715評論 0 1
  • 7月18日,7月的最后一個周末,雖然天空下著雨,卻阻擋不了愛學習的小伙們學習的腳步,安徽牛商會的各個企業(yè)在...
    合肥徽馬科技閱讀 468評論 0 0
  • 近來因著十分勞累,又犯了眼疾,或曰干眼癥,具體表現(xiàn)便是眼睛酸痛,總動不動便流下淚來,有如賈誼那樣,所不同者,是賈長...
    競走的蝸牛閱讀 336評論 0 2

友情鏈接更多精彩內(nèi)容