【深扒】深入理解 JavaScript 中的异步編程

小丞同學 2021-08-15 19:25:33 阅读数:471

本文一共[544]字,预计阅读时长:1分钟~
深入 入理 理解 javascript 异步

异步編程

大家好,我是小丞同學,本文將會帶你理解和感受 Generator 函數的异步應用

引言

我們先引出一個非常常見的場景:對服務器端返回的數據進行操作

與服務器端交互的過程是一個异步操作

如果按照正常的代碼編寫的話,你可能會寫出這樣的代碼

我也不知道打的什麼,大概意思就是异步請求結果返回賦值給 data 然後輸出,

let data = ajax("http://127.0.0.1",ab) //隨便寫的
console.log(data)

雖然整個思路看起來沒什麼毛病,對吧。但是它就是不行的,獲取數據是异步的,也就是說請求數據的時候,輸出已經執行了,這時候必然是 undefined

那為什麼它要這麼做呢?

JavaScript 是一門單線程的語言,如果沒有了异步執行,你想想會怎麼樣

就像逛街一樣,你非要跟著前面的人走,它走了你才走,它停下了去買點東西,後面的人全部都停下來等它回來,那這會怎麼辦,很顯然,路堵了!換到 JS 運行機制上來也是一樣的,會阻塞代碼運行。因此出現了“异步”的概念,接下來我們先了解一下异步的概念,以及傳統方法是如何實現异步操作的

什麼是同步、异步

同步:任務會按順序依次執行,當遇到大量耗時任務,後面的任務就會被延遲,這種延遲稱為阻塞,阻塞會造成頁面卡頓

异步:不會等待耗時任務,遇到异步任務就開啟後立即執行下一個任務,耗時任務的後續邏輯通常通過回調函數來定義執行,代碼執行順序混亂

實現异步編程

在 ES6 誕生之前,實現异步編程的方法有以下幾種。

  1. 回調函數
  2. 事件監聽
  3. 發布/訂閱
  4. Promise 對象

下面來先來回顧以下傳統方法是如何實現异步編程的

Callback

回調函數可以理解為一件想要去做的事情,由調用者定義好函數,交給執行者在某個時機去執行,把需要執行的操作放在函數裏,將函數傳入給執行者執行

主要體現在,把任務的第二段寫在一個函數裏面,等到重新執行這個任務的時候,直接調用

那有人就會問了,第二段是指什麼,我們再舉一個例子,讀取文件進行打印,這個操作肯定是异步的吧,那它怎麼分兩段呢?

按照邏輯來分,第一段是讀取文件,第二段是打印文件,可以理解為第一段是請求數據,第二段是打印數據

阮老師的代碼實例

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {

if (err) throw err;
console.log(data);
});

在第一階段執行結束後,會將結果返回給後面的函數作為參數,傳入第二段

回調函數的使用場景:

  1. 事件回調

  2. 定時器的回調

  3. 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 成功的解决了回調地獄的問題,它又不是异步編程的終極方案,那它又帶來了什麼問題呢?

  1. 無法取消 Promise
  2. 當處於 pending 狀態時是,無法得知進展
  3. 錯誤不能被 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 函數最大的好處就是不需要去配合一些工具去使用,類似於 Corunner 之類的

原因在於它是語言層面的標准异步編程,同時 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個階段,這是一個不斷進步的過程,一步步的解决前面方法所帶來的問題。

  1. 回調函數:導致了兩個問題
    • 缺乏順序性:回調地獄,造成代碼難以維護,閱讀性差等問題
    • 缺乏可信任性:控制反轉,導致代碼可能會執行錯誤
  2. promise:解决了可信任性的問題,但是代碼過於冗餘
  3. Generator:解决了順序性的問題但是需要手動控制 next,同時搭配工具使用代碼會十分的複雜
  4. async await:結合了 generator + promise,無需手動調用,完美解决

參考文獻

  1. 《JavaScript》异步編程
  2. 《Generator》函數的异步應用
  3. 《JavaScript高級程序設計(第四版)》
版权声明:本文为[小丞同學]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815192515869v.html