你能學到什麼
- 如何使用
Generator
+Promise
實現非同步程式設計 - 非同步程式設計的原理解析
前言
結合 上一篇文章 ,我們來聊聊 Generator
基礎原理
說到非同步程式設計,你想到的是async
和 await
,但那也只是 Generator
的語法糖而已。dva 中有一個 Effect
的概念,它就是使用 Generator
來解決非同步請求的問題,我們也來聊一聊 Generator
+ Promise
如何非同步程式設計:
開始之前,我們需要了解一些基本的概念:
-
Generator
作為ES6
中使用協程的解決方案來處理非同步程式設計的具體實現,它的特點是:Generator
中可以使用yield
關鍵字配合例項gen
呼叫next()
方法,來將其內部的語句分割執行。 簡言之 :next()
被呼叫一次,則yield
語句被執行一句,隨著next()
呼叫,yield
語句被依次執行。 -
Promise
表示一個非同步操作的最終狀態(完成或失敗),以及其返回的值。參考Promise-MDN
所以,非同步程式設計使用 Generator
和 Promise
來實現的原理是什麼呢?
- 因為
Generator
本身yield
語句是分離執行的,所以我們利用這一點,在yield
語句中返回一個Promise
物件 - 首次呼叫
Generator
中的next()
後, 假設返回值叫result
,那麼此時result.value
就是我們定義在yield
語句中的Promise
物件
注意:在這一步,我們已經把原來的執行流程暫停,轉而執行 Promise
的內容,已經實現了控制非同步程式碼的執行,因為此時我們如果不繼續執行 next()
則 generator
中位於當前被執行的 yield
後面的內容,將不會繼續執行,這已經達到了我們需要的效果
-
接下來我們就是在執行完當前
Promise
之後,讓程式碼繼續往下執行,直到遇到下一個yield
語句:
這一步是最關鍵的 所以我們怎麼做呢:步驟1: 在當前的
Promise
的then()
方法中,繼續執行gen.next()
步驟2: 當
gen.next()
返回的結果result.done === true
時,我們拿到result.value
【也就是一個新的Promise
物件】再次執行並且在它的then()
方法中繼續上面的步驟1,直至result.done === false
的時候。這時候呼叫resolve()
使promise
狀態改變,因為所有的yield
語句已經被執行完。
- 步驟1 保證了我們可以走到下一個
yield
語句 - 步驟2 保證了下一個
yield
語句執行完不會中斷,直至Generator
中的最後一個yield
語句被執行完。 流程示意圖:
具體實現
co 是著名大神 TJ 實現的
Generator
的二次封裝庫,那麼我們就從co
庫中的一個demo開始,瞭解我們的整個非同步請求封裝實現:
co(function*() {
yield me.loginAction(me.form);
...
});
複製程式碼
在這裡我們引入了co
庫,並且用co
來包裹了一個generator
(生成器)物件。
接下來我們看下co
對於包裹起來的generator
做了什麼處理
function co(gen) {
// 1.獲取當前co函式的執行上下文環境,獲取到引數列表
var ctx = this;
var args = slice.call(arguments, 1);
// 2.返回一個Promise物件
return new Promise(function(resolve, reject) {
// 判斷並且使用ctx:context(上下文環境)和arg:arguments(引數列表)初始化generator並且複製給gen
// 注意:
// gen = gen.apply(ctx, args)之後
// 我們呼叫 gen.next() 時,返回的是一個指標,實際的值是一個物件
// 物件的形式:{done:[false | true], value: ''}
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 當返回值不為gen時或者gen.next的型別不為function【實際是判斷是否為generator】時
// 當前promise狀態被設定為resolve而結束
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// 否則執行onFulfilled()
onFulfilled();
});
}
複製程式碼
總結一下這裡發生了什麼
- 返回一個
promise
promise
中將被包裹的generator
例項化為一個指標,指向generator
中第一個yield
語句- 判斷
generator
例項化出來的指標是否存在:如果沒有yield
語句則指標不存在 判斷指標gen.next()
方法是否為function
:如果不為function
證明無法執行gen.next()
條件有一項不滿足就將promise
的狀態置為resolve
否則執行onFulfilled()
接下來我們看下 onFulfilled()
的實現
function onFulfilled(res) {
// 在執行onFulfilled時,定義了一個ret來儲存gen.next(res)執行後的指標物件
var ret;
try {
ret = gen.next(res);
// 在這裡,yield語句丟擲的值就是{value:me.loginAction(me.form), done:false}
} catch (e) {
return reject(e);
}
// 將ret物件傳入到我們定義在promise中的next方法中
next(ret);
return null;
}
複製程式碼
總結一下,onFulfilled
最主要的工作就是
- 執行
gen.next()
使程式碼執行到yield
語句 - 將執行後返回的結果傳入我們自定義的
next()
方法中
那麼我們再來看 next()
方法
function next(ret) {
// 進入next中首先判斷我們傳入的ret的done狀態:
// 情況1:ret.done = true 代表我們這個generator中所有yield語句都已經執行完。
// 那麼將ret.value傳入到resolve()中,promise的狀態變成解決,整個過程結束。
if (ret.done) return resolve(ret.value);
// 情況2:當前ret.done = false 代表generator還未將所有的yield語句執行完,那麼這時候
// 我們把當前上下文和ret.value傳入toPromise中,將其轉換為對應的Promise物件`value`
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 當value確實是一個promise物件的時候,return value.then(onFulfilled,onRejected)
// 我們重新進入到了generator中,執行下一條yield語句
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
複製程式碼
總結一下,next
主要工作
- 判斷上一次
yield
語句的執行結果 - 將
yield
的result
的value
值【其實就是我們要非同步執行的Promise
】 - 執行
value
的then
方法,重新進入到onFulfilled
方法中,而在onFulfilled
中,我們又將進入到當前方法,如此迴圈的呼叫,實現了generator
和Promise
的執行切換,從而實現了Promise
的內容按照我們所定義的順序執行。
有同學可能對這裡的 toPromise
方法有一些疑惑,我先把程式碼貼出來
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
複製程式碼
其實這個函式做的事情就是,根據不同的型別進行轉換,使得最後輸出的型別都是一個 Promise
。那具體的轉換細節,大家可以參考co庫的原始碼。
至此實現非同步操作的控制。
最後
小冊 你不知道的 Chrome 除錯技巧 已經開始預售啦。
歡迎關注公眾號 「前端惡霸」,掃碼關注,會有很多好東西等著你~