全面掌握現代非同步解決方案

語言: CN / TW / HK

theme: channing-cyan highlight: night-owl


小知識,大挑戰!本文正在參與“[程式設計師必備小知識](https://juejin.cn/post/7008476801634680869 "https://juejin.cn/post/7008476801634680869")”創作活動 > TIP 👉 **落霞與孤鶩齊飛,秋水共長天一色。唐·王勃《膝王閣序》**

前言

在我們日常專案開發中,我們在做業務開發的時候會涉及到複選框組的功能,所以封裝了這個複選框組的元件。 ## Promise 長久以來,我們一直期望著一種既能實現非同步、又可以確保我們的程式碼好寫又好看的解決方案出現。帶著這樣的目標,經過反覆的探索,我們終於迎來了 Promise。 用 Promise 實現非同步,我們這樣做 ```js const https = require('https'); function httpPromise(url){ return new Promise(function(resolve,reject){ https.get(url, (res) => { resolve(data); }).on("error", (err) => { reject(error); }); }) } httpPromise().then(function(data){ }).catch(function(error){ }) ``` 可以看出,Promise 會接收一個執行器,在這個執行器裡,我們需要把目標的非同步任務給”填進去“。 在 Promise 例項建立後,執行器裡的邏輯會立刻執行,在執行的過程中,根據非同步返回的結果,決定如何使用 resolve 或 reject 來改變 Promise例項的狀態。 Promise 例項有三種狀態: • pending 狀態,表示進行中。這是 Promise 例項建立後的一個初始態; • fulfilled 狀態,表示成功完成。這是我們在執行器中呼叫 resolve 後,達成的狀態; • rejected 狀態,表示操作失敗、被拒絕。這是我們在執行器中呼叫 reject後,達成的狀態。 在上面這個例子裡,當我們用 resolve 切換到了成功態後,Promise 的邏輯就會走到 then 中的傳入的方法裡去;用 reject 切換到失敗態後,Promise 的邏輯就會走到 catch 傳入的方法中去。 這樣的邏輯,本質上與回撥函式中的成功回撥和失敗回撥無異。但這種寫法毫無疑問大大地提高了程式碼的質量。最直接的例子就是當我們進行大量的非同步鏈式呼叫時,回撥地獄不復存在了。取而代之的,是層級簡單、賞心悅目的 Promise 呼叫鏈: ```js httpPromise(url1) .then(res => { console.log(res); return httpPromise(url2); }) .then(res => { console.log(res); return httpPromise(url3); }) .then(res => { console.log(res); return httpPromise(url4); }) .then(res => console.log(res));。 ``` ## Generator 除了 Promise, ES2015 還為我們提供了 Generator 這個好幫手~ 。\ 如果你對 Generator 是什麼、以及其語法特性暫時還沒有太多的瞭解,可以點選 [這裡](https://es6.ruanyifeng.com/#docs/generator)先進行預備知識的學習。 Generator 一個有利於非同步的特性是,它可以在執行中被中斷、然後等待一段時間再被我們喚醒。通過這個“中斷後喚醒”的機制,我們可以把 Generator看作是非同步任務的容器,利用 yield 關鍵字,實現對非同步任務的等待。 上面的例子完全可以寫成 ```js function* httpGenerator() { let res1 = yield httpPromise(url1) console.log(res); let res2 = yield httpPromise(url2) console.log(res); let res3 = yield httpPromise(url3) console.log(res); let res4 = yield httpPromise(url4) console.log(res); } ``` 當然啦,單純這麼改還不夠,我們還需要在呼叫層面再完善一下才能讓這個生成器如期執行起來。\ 但在完善之前,咱們就單純看這種寫法,是不是比 Promise 鏈式呼叫更好看、更清晰了?這時候你一眼看過去就知道這段邏輯在幹嘛,而不必再對所謂的“鏈”作分析 ```js function runGenerator(gen) { var it = gen(), ret; // 創造一個立即執行的遞迴函式 (function iterate(val){ ret = it.next(val); if (!ret.done) { // 如果能拿到一個 promise 例項 if ("then" in ret.value) { // 就在它的 then 方法裡遞迴呼叫 iterate ret.value.then( iterate ); } } })(); } runGenerator(httpGenerator) ``` 大家一起來看下 runGenerator 這個方法,當我們把 httpGenerator 傳進去後,會發生如下過程: 1. 為傳入的 Generator 建立它對應的迭代器 it。然後,我們第一次呼叫 iterate 函式,入參為空。 1. iterate 函式內部,呼叫 it 的 next 方法,生成器函式開始執行,執行到第一個 yield 關鍵字處的邏輯執行完後暫停。它會返回一個包含了 httpPromise(url1) 這個呼叫返回的 promise物件(我們下文稱 promise1)、以及一個 done: false 的標識,用來表示當前生成器函式內部的邏輯還沒執行完(大致如下): ```js { value: Promise { , ...// 省略一系列 promise 物件關聯資訊 }, done: false } ``` 1. 因為 done 為 false,所以我們會進一步判斷當前拿到的是否是一個 promise 物件(根據它有沒有 then 屬性)。判斷為真後,我們在 promise1 的 then 方法裡傳入 iterate 函式本身。 1. promise1 的 then 方法裡的 iterate 函式呼叫,拿到了 promise1 的返回結果(即針對 url1 的請求結果)作為入參。it.next 被第二次呼叫,生成器函式被“喚醒”了。注意,被“喚醒”後的生成器函式,按照流程走,它執行的第一個語句就是: ```js let res1 = yield httpPromise(url1) ``` 這一步會把 next(val) 中的 val 傳給 res1,而 val,恰恰就是 promise1 的返回結果。一切正如我們所預期~~ 而後,生成器函式會繼續執行到第二個 yield 關鍵字處,執行完後暫停。 此時 next 方法返回一個包含了 httpPromise(url2) 這個呼叫返回的 promise 物件(我們下文稱 promise2)、以及一個 done: false 的標識(用來表示當前生成器函式內部的邏輯還沒執行完)。因為 done 為 false,所以我們會進一步判斷當前拿到的是否是一個 promise 物件(根據它有沒有 then 屬 性)。判斷為真後,我們在 promise2 的 then 方法裡傳入 iterate 函式本身。 1. 迴圈上述過程過程,直到生成器內部邏輯執行完為止。\ 通過“自動執行”生成器函式對應迭代器的 next 方法,我們把非同步的寫法進一步優化了。它不再需要地獄般的回撥,甚至不再需要 Promise 長長的鏈式呼叫,而是可以像寫同步程式碼一樣簡單、清晰地實現非同步特性! 不過仔細想想,咱們這個 runGenerator 其實非常簡陋,它雖然體現了自動執行的思想,卻不具備通用性,無法相容更多場景——確實,要寫出一個完整週到的 runGenerator 函式,不是一件輕鬆的事情。但是有一個好用的 runGenerator,又確實是廣大開發者的強訴求。於是我們有了一個叫 co 的庫,專門來封裝自執行這一層的邏輯: ```js const co = require('co'); co(httpGenerator()); ``` 這裡的 co,大家就可以把它看作是一個加強版的 runGenerator。我們只需要在程式碼裡引入 co 庫,然後把寫好的 generator 傳進去,就可以輕鬆地實現 generator 非同步了。 ## Async/Await 就當大家正在紛紛感慨 co 真好使,generator + promise + co 的非同步方案真優雅時,更強的傢伙出現了。這玩意兒甚至甩開了 co、甩開了 generator,有了它,你什麼都不用操心,只需要寫幾個關鍵字,就能把非同步程式碼處理得像同步程式碼一樣優雅!這玩意兒就是 async/await。 它的用法非常簡單。首先,我們用 async 關鍵字宣告一個函式為“非同步函式”: ```js async function httpRequest() { } ``` 然後,我們就可以在這個函式內部使用 await 關鍵字了: ```js async function httpRequest() { let res1 = await httpPromise(url1) console.log(res1) } ``` 這個 await 關鍵字很絕,它的意思就是“我要非同步了,可能會花點時間,後面的語句都給我等著”。當我們給 httpPromise(url1) 這個非同步任務應用了 await 關鍵字後,整個函式會像被“yield”了一樣,暫停下來,直到非同步任務的結果返回後,它才會被“喚醒”,繼續執行後面的語句。 是不是覺得這個“暫停”、”喚醒“的操作,和 generator 非同步非常相似?事實上,async/await 本身就是 generator 非同步方案的語法糖。它的誕生主要就是為了這個單純而美好的目的——讓你寫得更爽,讓你寫出來的程式碼更美。 「歡迎在評論區討論」 #### 希望看完的朋友可以給個贊,鼓勵一下