ES6+ Generator 函數應用
1. 前言
上一節我們注意學習了生成器的概念和基本用法,并通過兩個案例來說明。但是生成器更加廣泛和設計之初是為了解決異步而產生的。我們會通過一個開發中常見的問題入手來看 生成器函數到底是怎么來解決異步調用的問題,并且會實現一個簡版的 co 庫。
2. 案例
在開發過程中會遇到一個很常見的需求,我們想獲取一個值,但不能直接拿到,我們只能先請求一個接口如:api_1,獲取這個值的接口地址如:api_2。然后,請求 api_2 接口才能獲取這個值。這是一個典型的需要異步回調才能完成的功能。
在學習 Promise 的時候我們也針對這樣的情況,我們可以使用 Promise 來完成這樣的功能:
var promise = function (url) {
return new Promise((resolve, reject) => {
ajax(url, (data) => {
resolve(data) // 成功
}, (error) => {
reject(error) // 失敗
})
})
}
promise('api_1').then(res1 => {
promise(res1).then(res2 => {
console.log(res2)
})
})
從上面的代碼中可以看出,在這種情況下,使用 Promise 好像并沒有解決回調地獄的問題。那如何解決這種問題呢?我們想到了 Generator 函數具有暫停功能,那是不是我們可以讓請求 api_2 接口時暫停,等到 api_1 請求成功獲取到地址后再去請求呢?按照這個思路我們可以有下面的代碼:
const ajax = function(api) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (api === 'api_1') {
resolve('api_2');
}
if (api === 'api_2') {
resolve(100);
}
}, 0)
})
}
function * getValue() {
const api = yield ajax('api_1');
const value = yield ajax(api);
return value;
}
console.log(getValue()); // Object [Generator] {}
上面的代碼是我們模擬 ajax 請求,通過使用生成器函數寫出的代碼讓我們感覺有了同步的感覺,但是這樣去執行 getValue 函數是不會得到結果的。那么我們要怎樣去獲得結果呢?根據生成器函數的特點,可以這樣寫:
let it = getValue();
let { value } = it.next();
value.then((data) => {
let { value } = it.next(data);
value.then((data) => {
Console.log(data);
});
});
從上面的代碼中看出還是有嵌套,好像并沒有解決問題。但如果你細心,你會發現每個回調的邏輯基本都是一樣的。那么我們能不能對這樣的嵌套函數進行封裝呢?答案當然是可以的,有個庫就專門解決了這個痛點 —— co 庫,有興趣的可以去研究一下這個庫,代碼很少,下面我們就來封裝一個這樣的庫。
先讓我們看看 co 庫是怎么使用的:
co(getValue()).then(res => {
console.log(res);
})
從上面的代碼中可以看出,把函數傳入進去,并讓函數執行,然后在 then 的成功回調中可以獲取 getValue
函數返回的最終結果。這樣非常清晰地解決了上面我們需要手動執行的方法,下面我來分析一下具體的實現步驟:
- 從上面的用法可以看出,co 返回的是一個 Promise 實例,所以我們需要返回一個
new Promise()
實例; - 傳入的生成器函數執行后,我們可以調用 next () 函數拿到返回的值和是否執行完的狀態,判斷 done 如果是 true 時說明執行完了,可以執行 resolve;
- 當生成器函數沒有執行完時,這時我們就需要遞歸地去調用這個 next () 來執行下一步,因為傳入的值是一個 Promise 實例,要想獲取它的結果就需要鏈式調用 then 方法,然后拿到結果進行遞歸執行。
有了上面的步驟分析,不難得到下面的代碼:
function co(it) {
return new Promise((resolve, reject) => {
function next(data) {
let { value, done } = it.next(data);
if (done) {
resolve(value);
} else {
Promise.resolve(value).then(data => {
next(data);
}, reject)
}
}
next(undefined);
})
}
上面的代碼中需要注意的是,如果 yield 返回的不是一個 Promise 對象時,我們對 value 使用了 Promise.resolve()
進行了包裝,這樣就可以處理返回一個普通值時沒有 then 方法的問題。
3. 小結
本節主要講解了 Generator 函數在異步中的應用,解決了某些場景下還會產生回調地獄的問題,通過封裝 co 方法讓我們的代碼寫起來像是同步一樣,但是 Generator 函數還不是我們解決異步的終極方案,下一節我們將學習 async 函數,看它是怎么來解決異步的。