一、Promise對象的含義、特點
含義: 異步編程的一種解決方案,傳統解決方案通過回調函數和時間,而
Promise
相當於一個容器,裏面保存着未來纔會結束的事件(通常是一個異步操作)的結果,從語法上說,Promise
是一個對象,可以獲取異步操作的消息,提供統一的API,各種異步操作都可以以同樣的方法進行處理
特點:(1)對象的狀態不受外界影響,有三種狀態:pending
(進行中)、fulfilled
(已成功)、rejected
(已失敗),只有異步操作的結果可以改變當前的狀態,其他任何操作都無法改變這個狀態。
(2)狀態一旦改變就不會在變了,任何時候都可以得到這個結果。Promise
對象的狀態改變只有兩種可能,從pending
到fulfilled
,從pending
到rejected
,只要狀態發生改變,就會一直保持這個結果,就算再對對象Promise
添加回調函數,也會立即得到這個結果,與事件Event
不同,事件的特帶你是如果錯過了它,再去監聽,是得不到結果的
(3)Promise
一旦新建就會立即執行,無法途中取消,如果設置回調函數,Promise
內部拋出錯誤,不會反應到外部,不會影響整體函數的執行,處於pending狀態時,無法得知當前進展哪一個階段
基本用法
ES6 規定,Promise
對象是一個構造函數,用來生成Promise
實例。
let promise=new Promise(function (resolve,reject){
setTimeout(resolve,100,1);
});
Promise
構造函數接受一個函數作爲參數,該函數的兩個參數也是一個函數,分別是resolved
和rejected
,resolved
將Promise
對象的狀態從未完成變爲成功,成功時的回調函數,會將異步操作的結果,作爲參數傳遞出去,rejected
將Promise
對象的狀態由未完成變爲失敗,失敗時調用的函數,也會將異步操作報出的錯誤,作爲參數傳遞出去
注:
setTimeout(resolve,100,1);
,第三個參數時第一個參數函數的的形參
promise.then(function (res){
console.log(res);//1
},function (arr){
console.log(arr);
}).catch(function (err){
console.log(err);
})
then
方法接受兩個回調函數作爲參數,第一個回調函數是Promise
對象的狀態變爲resolved
時調用,第二個回調函數是Promise
對象變爲rejected
時調用,這個函數是可選的,不一定要提供,這兩個函數都接受Promise對象傳出的值作爲參數,當Promise
對象的狀態發生改變時就會觸發then
方法綁定的回調函數
then()
方法的作用是Promise
實例添加解決(fulfillment)和拒絕(rejection)狀態的回調函數then()方法會返回一個新的Promise實例,所以then()
方法後面可以繼續跟另一個then()
方法進行鏈式調用
//promise執行的先後順序
let promise = new Promise(function (resolve, reject) {
console.log("start");
setTimeout(resolve, 1000, "shuju");
});
console.log("外部");
promise.then(function (res) {
console.log(res);
//對於res的操作需要在此代碼塊寫,因爲外部獲取不到
});
console.log("wan");
圖中的數據是其他代碼的結果,忽略掉,從結果看就是,Promise一旦新建就立即執行,輸入start,與此同時輸入同步結果外部、完,當setTimeout(resolve, 1000, "shuju");
執行完後,即Promise狀態改變後,resolve函數的結果作爲形參傳入then()
,並輸出結果!
異步加載圖片
let promise = new Promise(function (resolve, reject) {
let img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(new Error("圖片加載失敗" + url));
}
});
promise.then(function (res) {
console.log(res);
}).catch(function (err) {
console.log(err);
})
Promise對象實現的 Ajax 操作
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯了', error);
});
多個Promise連用
const promise1=new Promise(function(resolve,reject){
setTimeout(resolve,3000,"a");
})
const promise2=new Promise(function(resolve,reject){
setTimeout(resolve,1000,promise1);
})
promise2.then(function(res){
console.log(res);
})
promise1
和promise2
都是 Promise
的實例,但是promise2
的resolve
方法將promise1
作爲參數,即一個異步操作的結果是返回另一個異步操作,promise1
的狀態就會傳遞給promise2
,promise2
的回調函數就會等待promise1
執行完,狀態改變後再執行
再來一個例子
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代碼中,p1
是一個 Promise
,3 秒之後變爲rejected
。p2
的狀態在 1 秒之後改變,resolve
方法返回的是p1
。由於p2
返回的是另一個 Promise
,導致p2
自己的狀態無效了,由p1
的狀態決定p2
的狀態。所以,後面的then
語句都變成針對後者(p1
)。又過了 2 秒,p1
變爲rejected
,導致觸發catch
方法指定的回調函數。
多個then()鏈式調用
var p2 = new Promise(function (resolve) {
resolve(100);
});
p2.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("finally: " + value);
});
調用then()
方法之後都會新建一個promise
對象,這裏的return
語句相當於Promise
語句中的resolve()
或者rejected()
,他們的結果,作爲then
方法函數的參數傳入
多個Promise合併
Promise.all()
Promise.all()
方法用於將多個Promise
實例,包裝成一個新的Promise
實例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.all()
方法接受一個數組作爲參數,p1
、p2
、p3
都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve
方法,將參數轉爲 Promise 實例,再進一步處理。另外,Promise.all()
方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。
p的狀態由p1
、p2
、p3
決定,分成兩種情況。
(1)只有p1
、p2
、p3
的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p1
、p2
、p3
的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1
、p2
、p3
之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject
的實例的返回值,會傳遞給p的回調函數。
如果作爲參數的 Promise 實例,自己定義了catch
方法,那麼它一旦被rejected
,並不會觸發Promise.all()
的catch
方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了
上面代碼中,p1
會resolved
,p2
首先會rejected
,但是p2
有自己的catch方法,該方法返回的是一個新的 Promise 實例,p2指向的實際上是這個實例。該實例執行完catch
方法後,也會變成resolved
,導致Promise.all()
方法參數裏面的兩個實例都會resolved
,因此會調用then
方法指定的回調函數,而不會調用catch方法指定的回調函數。
如果p2
沒有自己的catch
方法,就會調用Promise.all()
的catch
方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了
Promise.race()
Promise.race()
方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
上面代碼中,只要p1
、p2
、p3
之中有一個實例率先改變狀態,p
的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。
二、Generator 函數的語法
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同
Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象。Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態
Generator 函數有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用
yield
表達式,定義不同的內部狀態,星號*的位置以下幾個都是合法的
//*的位置
//建議使用這一個
function* sum(x){
yield x+(yield x*2);
yield x+3;
}
function * sum(x){
yield x+(yield x*2);
yield x+3;
}
function *sum(x){
yield x+(yield x*2);
yield x+3;
}
調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是遍歷器對象。
function* c(){
yield 'hello';
yield 'world';
return 'over';
}
console.log(c);/*ƒ* c(){
yield 'hello';
yield 'world';
return 'over';
}*/
console.log(c());
總結:調用 Generator 函數,返回一個遍歷器對象,代表 Generator 函數的內部指針。以後,每次調用遍歷器對象的
next
方法,就會返回一個有着value
和done
兩個屬性的對象。value
屬性表示當前的內部狀態的值,是yield
表達式後面那個表達式的值;done
屬性是一個布爾值,表示是否遍歷結束。yield
表達式是暫停執行的標記,而next
方法可以恢復執行
function* c(){
yield 'hello';
yield 'world';
return 'over';
}
console.log(c);/*ƒ* c(){
yield 'hello';
yield 'world';
return 'over';
}*/
console.log(c());
var d=c();
console.log(d.next());//{value: "hello", done: false}
console.log(d.next());//{value: "world", done: false}
console.log(d.next());//{value: "over", done: true}
//遍歷結束的時候,即done爲true時,以後的next() value值都爲undefined
console.log(d.next());//{value: undefined, done: true}
console.log(d.next());//{value: undefined, done: true}
遍歷器對象的next方法的運行邏輯如下
(1)遇到
yield
表達式,就暫停執行後面的操作,並將緊跟在yield
後面的那個表達式的值,作爲返回的對象的value
屬性值。(2)下一次調用
next
方法時,再繼續往下執行,直到遇到下一個yield表達式。(3)如果沒有再遇到新的
yield
表達式,就一直運行到函數結束,直到return
語句爲止,並將return
語句後面的表達式的值,作爲返回的對象的value
屬性值。(4)如果該函數沒有
return
語句,則返回的對象的value
屬性值爲undefined
yield
表達式後面的表達式,只有當調用next
方法、內部指針指向該語句時纔會執行,因此等於爲 JavaScript 提供了手動的“惰性求值”的語法功能
function *sum1(){
yield 12+34;
}
console.log(sum1().next());//46
yield
表達式與return
語句既有相似之處,也有區別
相似之處在於,都能返回緊跟在語句後面的那個表達式的值。
區別在於每次遇到yield
,函數暫停執行,下一次再從該位置繼續向後執行,而return
語句不具備位置記憶的功能。一個函數裏面,只能執行一次(或者說一個)return
語句,但是可以執行多次(或者說多個)yield
表達式。正常函數只能返回一個值,因爲只能執行一次return
;
複雜一點的例子
function * sum(x){
yield x+(yield x*2);
}
var sums=sum(3);
//第一種
console.log(sums.next());//6 false
console.log(sums.next());//NAN false
console.log(sums.next());//undefined true
//第二種
console.log(sums.next());//6 false
console.log(sums.next(90));//93 false
console.log(sums.next());//undefined true
世紀大難題!!!被自己寫的函數難住了!!!
這裏需要注意的是
yield
表達式如果用在另一個表達式之中,必須放在圓括號裏面。
yield
表達式用作函數參數或放在賦值表達式的右邊,可以不加括號。
yield
表達式只能用在 Generator 函數裏面,用在其他地方都會報錯。
for…of循環
for...of
循環可以自動遍歷 Generator 函數運行時生成的Iterator
對象,且此時不再需要調用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
一旦next
方法的返回對象的done
屬性爲true
,for...of
循環就會中止,且不包含該返回對象,所以上面代碼的return
語句返回的6,不包括在for...of
循環之中。
yield*表達式
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
//yield*後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for...of循環。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同於
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
三、async函數
async
表示函數裏有異步操作,await
表示緊跟在後面的表達式需要等待結果。
async
函數的await
命令後面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成立即resolved
的 Promise 對象)。
async
函數的返回值是 Promise 對象
async
函數完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await
命令就是內部then
命令的語法糖。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
例子:下面代碼指定 50 毫秒以後,輸出hello world
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
async 函數有多種使用形式。
// 函數聲明
async function foo() {}
// 函數表達式
const foo = async function () {};
// 對象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭頭函數
const foo = async () => {};
返回Promise
對象
async
函數返回一個 Promise 對象。
async
函數內部return
語句返回的值,會成爲then
方法回調函數的參數。
async
函數返回的 Promise 對象,必須等到內部所有await
命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return
語句或者拋出錯誤,也就是說,只有async
函數內部的異步操作執行完,纔會執行then
方法指定的回調函數。
await
命令後面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值
await
命令後面是一個thenable
對象(即定義then
方法的對象),那麼await
會將其等同於 Promise 對象。
await
命令後面的 Promise 對象如果變爲reject
狀態,則reject
的參數會被catch
方法的回調函數接收到
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
任何一個
await
語句後面的 Promise 對象變爲reject
狀態,那麼整個async
函數都會中斷執行
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
最好把
await
命令放在try...catch
代碼塊中
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一種寫法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
多個
await
命令後面的異步操作,如果不存在繼發關係,最好讓它們同時觸發。
let foo = await getFoo();
let bar = await getBar();
上面代碼中,getFoo
和getBar
是兩個獨立的異步操作(即互不依賴),被寫成繼發關係。這樣比較耗時,因爲只有getFoo
完成以後,纔會執行getBar
,完全可以讓它們同時觸發。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面兩種寫法,getFoo
和getBar
都是同時觸發,這樣就會縮短程序的執行時間。
await
命令只能用在async
函數之中,如果用在普通函數,就會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代碼會報錯,因爲await
用在普通函數之中了。但是,如果將forEach
方法的參數改成async
函數,也有問題。
unction dbFuc(db) { //這裏不需要 async
let docs = [{}, {}, {}];
// 可能得到錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代碼可能不會正常工作,原因是這時三個db.post
操作將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for
循環。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果確實希望多個請求併發執行,可以使用Promise.all
方法。當三個請求都會resolved
時,下面兩種寫法效果相同。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的寫法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}