前端的異步解決方案之Promise和Await/Async


Promise

Promise 對象是一個返回值的代理,這個返回值在promise對象創建時未必已知。它允許你為異步操作的成功返回值或失敗信息指定處理方法。 這使得異步方法可以像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值。 

我們來看一下官方定義,Promise實際上就是一個特殊的Javascript對象,反映了”異步操作的最終值”。”Promise”直譯過來有預期的意思,因此,它也代表了某種承諾,即無論你異步操作成功與否,這個對象最終都會返回一個值給你。
先寫一個簡單的demo來直觀感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise = new Promise((resolve, reject) => {
$.ajax('https://github.com/users', (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
});

promise.then((value) => {
console.log(value);
},(err) => {
console.log(err);
});
//也可以采取下面這種寫法
promise.then(value => console.log(value)).catch(err => console.log(err));

 

上面的例子,會在Ajax請求成功后調用resolve回調函數來處理結果,如果請求失敗則調用reject回調函數來處理錯誤。Promise對象內部包含三種狀態,分別為pending,fulfilled和rejected。這三種狀態可以類比於我們平常在ajax數據請求過程的pending,success,error。一開始請求發出后,狀態是Pending,表示正在等待處理完畢,這個狀態是中間狀態而且是單向不可逆的。成功獲得值后狀態就變為fulfilled,然后將成功獲取到的值存儲起來,后續可以通過調用then方法傳入的回調函數來進一步處理。而如果失敗了的話,狀態變為rejected,錯誤可以選擇拋出(throw)或者調用reject方法來處理。

請求的幾個狀態:

  1. pending( 中間狀態)—> fulfilled , rejected
  2. fulfilled(最終態)—> 返回value 不可變
  3. rejected(最終態) —> 返回reason 不可變

如圖所示:

promisespromises

promise

一個promise內部可以返回另一個promise,這樣就可以進行層級調用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const getAllUsers = new Promise((resolve, reject) => {
$.ajax('https://github.com/users', (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
});

const getUserProfile = function(username) {
return new Promise((resolve, reject) => {
$.ajax('https://github.com/users' + username, (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
};

getAllUsers.then((users) => {
//獲取第一個用戶的信息
return getUserProfile(users[0]);
}).then((profile) => {
console.log(profile)
}).catch(err => console.log(err));

Promise實現原理

目前,有多種Promise的實現方式,我選擇了https://github.com/then/promise的源碼進行閱讀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Promise(fn) {
var state = null; //用以保存處理狀態,true為fulfilled狀態,false為rejected狀態
var value = null; //用以保存處理結果值
var deferreds = [];
var self = this;
this.then = function(onFulfilled, onRejected) {
return new self.constructor(
function(resolve, reject) {...}
);
}; //返回一個延遲處理函數,調用這個方法,就能觸發用戶傳入的處理函數,分別對應處理promise的fulfilled狀態和rejected狀態

function handle(deferred) {...} //延遲隊列處理

function resolve(newValue) {...} //更新value值,並把state更新為true,代表結果正常

function reject(newValue) {...} //更新vlaue值,並把state更新為false,代表結果錯誤,這個value值就是錯誤原因方便后面調用處理

function finale() {...} //清空異步隊列

doResolve(fn, resolve, reject); //調用resolve和reject兩個回調函數處理結果
}

 

通過閱讀promise的源碼,我們可以很清楚地看到,在構建一個promise對象的時候,是利用函數式編程的特性,如惰性求值和部分求值等來進行將異步處理的。而處理多線程並發的機制就是利用setTimeout(fn,0)這個技巧。

構造Promise

Promise構造函數的初始函數需要有兩個參數,resolve和reject,分別對應fulfilled和rejected兩個狀態的處理。

1
2
3
4
5
6
7
8
var promise = new Promise((resolve, reject) => {
try {
var value = doSomething();
resolve(value);
} catch(err) {
reject(err);
}
});

Promise的常用方法

1.Promise.all(iterator):

​ 返回一個新的promise對象,其中所有promise的對象成功觸發的時候,該對象才會觸發成功,若有任何一個發成錯誤,就會觸發改對象的失敗方法。成功觸發的返回值是所有promise對象返回值組成的數組。直接看例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//設置三個任務
const tasks = {
task1() {
return new Promise(...); //return 1
},

task2() {
return new Promise(...); // return 2
},

task3() {
return new Promise(...); // return 3
}
};

//列表中的所有任務會並發執行,當所有任務執行狀態都為fulfilled后,執行then方法
Promise.all([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
//最終結果為:[1,2,3]

2.Promise.race(iterable): 返回一個新的promise對象,其回調函數迭代遍歷每個值,分別處理。同樣都是傳入一組promise對象進行處理,同Promise.all不同的是,只要其中有一個promise的狀態變為fulfilledrejected,就會調用后續的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//設置三個任務
const tasks = {
task1() {
return new Promise(...); //return 1
},

task2() {
return new Promise(...); // return 2
},

task3() {
return new Promise(...); // return 3
}
};

//列表中的所有任務會並發執行,只要有一個promise對象出現結果,就會執行then方法
Promise.race([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
//假設任務1最開始返回結果,則控制台打印結果為`1`

3.Promise.reject(reason): 返回一個新的promise對象,用reason值直接將狀態變為rejected

1
2
3
4
5
const promise2 = new Promise((resolve, reject) => {
reject('Failed');
});

const promise2 = Promise.reject('Failed');

上面兩種寫法是等價的。

4.Promise.resolve(value): 返回一個新的promise對象,這個promise對象是被resolved的。

與reject類似,下面這兩種寫法也是等價的。

1
2
3
4
5
const promise2 = new Promise((resolve, reject) => {
resolve('Success');
});

const promise2 = Promise.resolve('Success');

5.then 利用這個方法訪問值或者錯誤原因。其回調函數就是用來處理異步處理返回值的。

6.catch 利用這個方法捕獲錯誤,並處理。

Generator & Iterator 迭代器和生成器

雖然Promise解決了回調地獄(callback hell)的問題,但是仍然需要在使用的時候考慮到非同步的情況,而有沒有什么辦法能讓異步處理的代碼寫起來更簡單呢?在介紹解決方案之前,我們先來介紹一下ES6中有的迭代器和生成器。
迭代器(Iterator),顧名思義,它的作用就是用來迭代遍歷集合對象。
在ES6語法中迭代器是一個有next方法的對象,可以利用Symbol.iterator的標志返回一個迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const getNum = {
[Symbol.iterator]() {
let arr = [1,2,3];
let i = 0;
return {
next() {
return i < arr.length ? {value: arr[i++]} : {done: true};
}
}
}
}

//利用for...of語法遍歷迭代器
for(const num of getNum) {
console.log(num);
}

而生成器(Generator)可以看做一個特殊的迭代器,你可以不用糾結迭代器的定義形式,使用更加友好地方式實現代碼邏輯。
先來看一段簡單的代碼:

1
2
3
4
5
6
7
8
9
10
11
function* getNum() {
yield 1;
yield 2;
yield 3;
}
//調用生成器,生成一個可迭代的對象
const gen = getNum();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: true}

 

生成器函數的定義需要使用function*的形式,這也是它和普通函數定義的區別。yield是一個類似return的關鍵字,當代碼執行到這里的時候,會暫停當前函數的執行,並保存當前的堆棧信息,返回yield后面跟着表達式的值,這個值就是上面代碼所看到的value所對應的值。而done這個屬性表示是否還有更多的元素。當donetrue的時候,就表明這個迭代過程結束了。需要注意的是這個next方法其實傳入參數,這個參數表示上一個yield語句的返回值,如果你給next方法傳入了參數,就會將上一次yield語句的值設置為對應值。

利用generator的異步處理

先來看一下下面這段代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getFirstName() {
setTimeout(() => {
gen.next('hello');
},2000);
}

function getLastName() {
setTimeout(() => {
gen.next('world');
},1000);
}

function* say() {
let firstName = yield getFirstName();
let lastName = yield getLastName();
console.log(firstName + lastName);
}

var gen = say();

gen.next(); // {value: undefined, done: false}
//helloworld

 

我們可以發現,當第一次調用gen.next()后,程序執行到第一個yield語句就中斷了,而在getFirstName里顯式地將上一個yield語句的返回值改為hello,觸發了第二yield語句的執行。以此類推,最終就打印出我們想要的結果了。

spawn函數

我們可以考慮把上面的代碼改寫一下,在這里將Promise和Generator結合起來,將異步操作用Promise對象封裝好,然后,resolve出去,而創建一個spawn函數,這個函數的作用是自動觸發generatornext方法。來看一下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function getFirstName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello');
}, 2000);
});
}

function getLastName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('world');
}, 1000);
});
}

function* say() {
let firstName = yield getFirstName();
let lastName = yield getLastName();
console.log(firstName + lastName);
}

function spawn(generator) {
return new Promise((resolve, reject) => {
var onResult = (lastPromiseResult) => {
var {value, done} = generator.next(lastPromiseResult);
if(!done) {
value.then(onResult, reject);
}else {
resolve(value);
}
}
onResult();
});
}

spawn(say()).then((value) => {console.log(value)});

 

到這里,這個解決方案就很接近接下來要介紹的async/await的實現方式了。

Async/Await

這兩個關鍵字其實是一起使用的,async函數其實就相當於funciton *的作用,而await就相當與yield的作用。而在async/await機制中,自動包含了我們上述封裝出來的spawn自動執行函數。
利用這兩個新的關鍵字,可以讓代碼更加簡潔和明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function getFirstName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('hello');
resolve('hello');
}, 2000);
});
}

function getLastName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('world');
resolve('world');
}, 1000);
});
}

async function say() {
let firstName = await getFirstName();
let secondName = await getLastName();
return firstName + lastName;
}

console.log(say());

 

執行結果為,先等待2秒打印hello,再等待1秒打印world,最后打印’helloworld’,與預期的執行順序是一致的。

上面的代碼你需要注意的是,你必須顯式聲明await,否則你會得到一個promise對象而不是你想要獲得的值。

比起Generator函數,async/await的語義更好,代碼寫起來更加自然。將異步處理的邏輯放在語法層面去處理,寫的代碼也更加符合人的自然思考方式。

錯誤處理

對於async/await這種方法來說,錯誤處理也比較符合我們平常編寫同步代碼時候處理的邏輯,直接使用try..catch就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getUsers() {
return $.ajax('https://github.com/users');
}

async function getFirstUser() {
try {
let users = await getUsers();
return users[0].name;
} catch (err) {
return {
name: 'default user'
}
}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM