一. 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
});
- promise.catch可以捕獲promise返回的錯誤
var promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
}); - Promise.resolve 是將一個值包裹成 promise對象,狀態(tài)是fulfilled
Promise.resolve('haha')
等價于
new Promise(function (resolve, reject) {resolve('haha')}) - 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.擴展
- 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)
}) - 在Promise構(gòu)造函數(shù)里throw一個error,相當于 reject(error)
- 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)
}) - 構(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)
}) - 值穿透,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)
}) - 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)
}) - 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) - 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.特點
- function關(guān)鍵字與函數(shù)名之間有一個星號
- 函數(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 }
- yield句本身沒有返回值,或者說總是返回undefined。
- 每次調(diào)用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內(nèi)部狀態(tài)的值,是yield語句后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結(jié)束。
- 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()
- 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: 出錯了!(…) - 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 模塊
- co 模塊是著名程序員 TJ Holowaychuk 于2013年6月發(fā)布的一個小工具,用于 Generator 函數(shù)的自動執(zhí)行
- 用法:
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í)行完成');
}); - 原理:
自動執(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以上)
- 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ù)。 - 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í)行
} - 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 - 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方法指定下一步的操作。