
大家好,我是小丞同學,本文將會帶你理解和感受 Generator 函式的異步應用
引言
我們先引出一個非常常見的場景:對服務器端回傳的資料進行操作
與服務器端互動的程序是一個異步操作
如果按照正常的代碼撰寫的話,你可能會寫出這樣的代碼
我也不知道打的什么,大概意思就是異步請求結果回傳賦值給 data 然后輸出,
let data = ajax("http://127.0.0.1",ab) //隨便寫的
console.log(data)
雖然整個思路看起來沒什么毛病,對吧,但是它就是不行的,獲取資料是異步的,也就是說請求資料的時候,輸出已經執行了,這時候必然是 undefined
那為什么它要這么做呢?
JavaScript 是一門單執行緒的語言,如果沒有了異步執行,你想想會怎么樣
就像逛街一樣,你非要跟著前面的人走,它走了你才走,它停下了去買點東西,后面的人全部都停下來等它回來,那這會怎么辦,很顯然,路堵了!換到 JS 運行機制上來也是一樣的,會阻塞代碼運行,因此出現了“異步”的概念,接下來我們先了解一下異步的概念,以及傳統方法是如何實作異步操作的
什么是同步、異步
同步:任務會按順序依次執行,當遇到大量耗時任務,后面的任務就會被延遲,這種延遲稱為阻塞,阻塞會造成頁面卡頓
異步:不會等待耗時任務,遇到異步任務就開啟后立即執行下一個任務,耗時任務的后續邏輯通常通過回呼函式來定義執行,代碼執行順序混亂
實作異步編程
在 ES6 誕生之前,實作異步編程的方法有以下幾種,
- 回呼函式
- 事件監聽
- 發布/訂閱
- Promise 物件
下面來先來回顧以下傳統方法是如何實作異步編程的
Callback
回呼函式可以理解為一件想要去做的事情,由呼叫者定義好函式,交給執行者在某個時機去執行,把需要執行的操作放在函式里,將函式傳入給執行者執行
主要體現在,把任務的第二段寫在一個函式里面,等到重新執行這個任務的時候,直接呼叫
那有人就會問了,第二段是指什么,我們再舉一個例子,讀取檔案進行列印,這個操作肯定是異步的吧,那它怎么分兩段呢?
按照邏輯來分,第一段是讀取檔案,第二段是列印檔案,可以理解為第一段是請求資料,第二段是列印資料
阮老師的代碼實體
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
在第一階段執行結束后,會將結果回傳給后面的函式作為引數,傳入第二段
回呼函式的使用場景:
-
事件回呼
-
定時器的回呼
-
Ajax 請求
Promise
采用回呼函式的方法,本身是沒有問題的,但是問題出現在多個回呼函式的嵌套
想一想,我執行完執行你,你執行完執行他,他執行完又執行她…
是不是需要層層嵌套,那這樣套娃式的操作顯然不利于閱讀
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
同時你也可以這樣去思考一下,如果有其中一個代碼需要修改,那它的上層回呼和下層回呼都要修改,這也叫做強耦合
耦合,藕斷絲連,關聯性很強的意思
這種場景也叫做“回呼地獄”
而 Promise 物件的誕生就是為了解決這個問題,它采用了以一種全新的寫法,鏈式呼叫
Promise 可以用來表示一個異步任務執行的狀態,有三種狀態
- Pending:開始是等待狀態
- Fulfilled:成功的狀態,會觸發 onFulfilled
- Rejected:失敗的狀態,會觸發 onRejected
它的寫法如下
const promise = new Promise(function(resolve, reject) {
// 同步代碼
// resolve執行表示異步任務成功
// reject執行表示異步任務失敗
resolve(100)
// reject(new Error('reject')) // 失敗
})
promise.then(function() {
// 成功的回呼
}, function () {
// 失敗的回呼
})
Promise 物件呼叫 then 方法后會回傳一個新的 Promise 物件,這個新的 Promise 物件可以繼續呼叫 then 實作鏈式呼叫
后面的 then 方法是為上一個 then 回傳的 Promise 物件注冊回呼
前一個 then 方法中回呼函式的回傳值會作為后面 then 方法回呼的引數
鏈式呼叫的目的是為了解決回呼函式嵌套的問題
關于 Promise 的更多細節這里就不多說了,下一篇寫吧~
壞了,壞了,環環嵌套,我陷入回呼地獄了,努力更文
Promise 成功的解決了回呼地獄的問題,它又不是異步編程的終極方案,那它又帶來了什么問題呢?
- 無法取消 Promise
- 當處于 pending 狀態時是,無法得知進展
- 錯誤不能被
catch
但是這些都不是 Promise 的最大問題,它最大的問題是代碼冗余,當執行邏輯變得復雜時,代碼的語意會變得很不清楚,全是 then
其實看過上一篇文章的讀者們,看到這里應該對 Generator 實作異步編程有了一定的眉目,這里的 then 方法的作用,似乎 next 方法也能實作,啟動,運行,傳參,接下來我們來細說一下
Generator
Generator 函式可以暫停執行和恢復執行, 這是它能封裝異步任務的根本原因,
除此之外,它還有兩個特征,使它可以作為異步編程的完美解決方案,
- 函式體內外的資料傳遞
- 錯誤處理機制
資料傳遞
在學習它是如何實作異步編程的之前,我們先回顧一下 Generator 函式的執行方法
// 宣告Generator函式
function* gen(x){
let y = yield x + 2
return y
}
// 遍歷器物件
let g = gen()
// 第一次呼叫next方法
g.next() // { value: 3, done: false }
// 第二次呼叫 傳遞引數
g.next(2) // { value: 2, done: true }
首先執行 gen 函式,獲得遍歷器物件,此時函式并不會執行,當呼叫遍歷器物件的 next 方法時,執行到第一個 yield 陳述句,以此類推
也就是說只有呼叫 next 方法,才會往下執行
同時在上面的代碼中,我們可以通過 value 來獲取回傳的值,通過給 next 方法傳遞引數來實作資料交換
錯誤處理機制
Generator 函式內部可以部署錯誤處理代碼,捕獲函式體外拋出的錯誤
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出錯了');
或許會有人不理解為什么內部的 catch 可以捕獲外部的錯誤?
原因是我們通過 g.throw 來拋錯誤,其實是將錯誤拋入了生成器,畢竟我們是在 p 上來呼叫 throw 方法
實作異步編程
在我的上一篇文章詳細的介紹了生成器的執行機制,以及
yield執行特點,可以先閱讀一下
我們主意利用 yield 暫停生成器函式執行的特點,來使用生成器函式去實作異步編程,我們來看一個例子
Generator + Promise
function * main () {
const user = yield ajax('/api/usrs.json')
console.log(user)
}
const g = main()
const result = g.next()
result.value.then(data => {
g.next(data)
})
首先我們定義一個生成器函式 main ,然后在這個函式內部使用 yield 去回傳一個 ajax 的呼叫,也就是回傳了一個 Promise 物件,
然后去接收 yield 陳述句的回傳值,也就是第二個 next 方法的引數,
我們可以在外界去呼叫生成器函式得到它的迭代器物件,然后呼叫這個物件的 next 方法,這樣 main 函式就會執行到第一個 yield 的位置,也就是會執行到 ajax 的呼叫,這里 next 方法回傳物件的 value 值就是 ajax 回傳的 Promise 物件
因此我們可以通過 then 方法去指定這個 Promise 的回呼,在這個 Promise 回呼中我們就可以拿到這個 Promise 的執行結果 data,這時候我們就可以通過再呼叫一次 next 方法,把我們得到的 data 資料傳遞出去,這樣 main 函式就可以繼續執行了,而 data 就會被當作 yield 運算式的回傳值賦值給 user 使用了
異步迭代生成器
如果上面的 generator + promise 能夠理解的話,這個就更簡單了,就是單純的使用 generator 實作的異步編程
function foo(x, y) {
ajax("1.2.34.2", function(err,data) {
if(err) {
it.throw(err)
}else {
it.next(data)
}
})
}
function *main() {
let text = yield foo(11, 31)
console.log( text )
}
const it = main()
it.next()
在上面的代碼中就是一個簡單的例子,雖然看起來要比回呼函式實作的方法要多很多,但是你會發現代碼邏輯要好非常多
這里面最關鍵的代碼
let text = yield foo(11,31)
console.log( text )
這個在上一 part 我們已經解釋過了
在 yield foo(11, 31) 中,首先呼叫 foo(11, 31) 沒有回傳值,發送請求獲取資料,請求成功,呼叫 it.next(data) ,這樣就將 data 作為上一個 yield 的回傳值,這樣就將異步代碼同步化了
async await
在 Generator 中還有很多的內容,工具,并發,委托等等讓生成器變得十分強大,但是這樣也讓手寫一個執行器函式越來越麻煩,所以在 ES7 中又新增了 async await 這對關鍵字,它使用起來會更加的方便,
async 函式就是生成器函式的一個語法糖,
在語法上跟 Generator 函式非常類似,只要把生成器函式修改為 async 關鍵字修飾的函式,把 yield 修改為 await 就可以了,并且可以直接在外面呼叫這個函式,執行這個函式的話,內部這個執行程序會跟 Generator 函式會是完全一樣的
相比于 Generator 函式 async 函式最大的好處就是不需要去配合一些工具去使用,類似于 Co、runner 之類的
原因在于它是語言層面的標準異步編程,同時 async 函式可以回傳一個 Promise 物件,這樣也有利于控制代碼,
需要注意的是,await 只能出現在 async 函式體中
//將生成器函式改為 async 修飾的函式
async function main() {
try {
// 將 yield 換成 await
const a = await ajax('xxxx')
console.log(a)
const b = await ajax('xxx')
console.log(b)
const c = await ajax('xx')
console.log(c)
} catch (e) {
console.log(e)
}
}
// 回傳一個Promise物件
const promise = main()
從上面的代碼我們也可以知道,我們并不需要像 Generator 一樣通過 next 來控制執行
async await 是 Generator 和 Promise 的組合,解決了先前方法留下的問題,這應該是目前處理異步的最優方案了
總結
本文寫了異步編程的4個階段,這是一個不斷進步的程序,一步步的解決前面方法所帶來的問題,
- 回呼函式:導致了兩個問題
- 缺乏順序性:回呼地獄,造成代碼難以維護,閱讀性差等問題
- 缺乏可信任性:控制反轉,導致代碼可能會執行錯誤
- promise:解決了可信任性的問題,但是代碼過于冗余
- Generator:解決了順序性的問題但是需要手動控制
next,同時搭配工具使用代碼會十分的復雜 - async await:結合了
generator + promise,無需手動呼叫,完美解決
參考文獻
- 《JavaScript》異步編程
- 《Generator》函式的異步應用
- 《JavaScript高級程式設計(第四版)》
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/294235.html
標籤:其他
上一篇:講講ref參考
