前言
在本文中,我們將探討過去異步執(zhí)行的 JavaScript 的演變,以及它是怎樣改變我們編寫代碼的方式的。我們將從最早的 Web 開發(fā)開始,一直到現代異步模式。
作為編程語言, JavaScript 有兩個主要特征,這兩個特征對于理解我們的代碼如何工作非常重要。首先是它的同步特性,這意味著代碼將逐行運行,其次是單線程,任何時候都僅執(zhí)行一個命令。
隨著語言的發(fā)展,允許異步執(zhí)行的新工件出現在場景中。開發(fā)人員在解決更復雜的算法和數據流時嘗試了不同的方法,從而導致新的接口和模式出現。
同步執(zhí)行和觀察者模式
如簡介中所述,JavaScript 通常會逐行運行你編寫的代碼。即使在最初的幾年中,該語言也有這種規(guī)則的例外,盡管很少,你可能已經知道了它們:HTTP 請求,DOM 事件和time interval。
如果我們通過添加事件偵聽器去響應用戶對元素的單擊,則無論語言解釋器在運行什么,它都會停止,然后運行在偵聽器回調中編寫的代碼,之后再返回正常的流程。
與 interval 或網絡請求相同,addEventListener,setTimeout 和 XMLHttpRequest 是 Web 開發(fā)人員訪問異步執(zhí)行的第一批工件。
盡管這些是 JavaScript 中同步執(zhí)行的例外情況,但重要的是你要了解該語言仍然是單線程的。我們可以打破這種同步性,但是解釋器仍然每次運行一行代碼。
例如檢查一個網絡請求。
1
2
3
4
5
6
7
8
9
10
11
|
var request = new XMLHttpRequest(); request.open( 'GET' , '//some.api.at/server' , true ); // observe for server response request.onreadystatechange = function () { if (request.readyState === 4 && xhr.status === 200) { console.log(request.responseText); } } 11request.send(); |
不管發(fā)生什么情況,當服務器恢復運行時,分配給 onreadystatechange 的方法都會在取回程序的代碼序列之前被調用。
對用戶交互做出反應時,也會發(fā)生類似的情況。
1
2
3
4
5
6
|
const button = document.querySelector( 'button' ); // observe for user interaction button.addEventListener( 'click' , function (e) { console.log( 'user click just happened!' ); }) |
你可能會注意到,我們正在連接一個外部事件并傳遞一個回調,告訴代碼當事件發(fā)生時應該怎么做。十多年前,“什么是回調?”是一個非常受期待的面試問題,因為在很多代碼庫中到處都有這種模式。
在上述每種情況下,我們都在響應外部事件。不管是達到一定的時間間隔、用戶操作還是服務器響應。我們本身無法創(chuàng)建異步任務,我們總是 觀察 發(fā)生在我們力所能及范圍之外的事件。
這就是為什么這種方式的代碼被稱為觀察者模式的原因,在這種情況下,它最好由 addEventListener 接口來表示。很快,暴露這種模式的事件發(fā)送器庫或框架開始蓬勃發(fā)展。
NODE.JS 和事件發(fā)送器
Node.js 是一個很好的例子,它的官網把自己描述為“異步事件驅動的 JavaScript 運行時”,所以事件發(fā)送器和回調是一等公民。它甚至已經實現了一個 EventEmitter 構造函數。
1
2
3
4
5
6
7
8
|
const EventEmitter = require( 'events' ); const emitter = new EventEmitter(); // respond to events emitter.on( 'greeting' , (message) => console.log(message)); // send events emitter.emit( 'greeting' , 'Hi there!' ); |
這不僅是通用的異步執(zhí)行方法,而且是其生態(tài)系統的核心模式和慣例。Node.js 開辟了一個在不同環(huán)境中甚至在 web 之外編寫 JavaScript 的新時代。當然異步的情況也是可能的,例如創(chuàng)建新目錄或寫文件。
1
2
3
4
5
6
7
8
9
10
11
|
const { mkdir, writeFile } = require( 'fs' ); const styles = 'body { background: #ffdead; }' ; mkdir( './assets/' , (error) => { if (!error) { writeFile( 'assets/main.css' , styles, 'utf-8' , (error) => { if (!error) console.log( 'stylesheet created' ); }) } }) |
你可能會注意到,回調函數將第一個參數接作為 error ,如果得到了預期的響應數據,則將其作為第二個參數。這就是所謂的錯誤優(yōu)先回調模式,它成為作者和貢獻者為包和庫所做的約定。
Promise 和沒完沒了的回調鏈
隨著 Web 開發(fā)面臨的更復雜的問題,出現了對更好的異步工件的需求。如果我們查看最后一個代碼段,則會看到重復的回調鏈,隨著任務數量的增加,回調鏈的擴展效果不佳。
例如,我們僅添加兩個步驟,即文件讀取和樣式預處理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const { mkdir, writeFile, readFile } = require( 'fs' ); const less = require( 'less' ) readFile( './main.less' , 'utf-8' , (error, data) => { if (error) throw error less.render(data, (lessError, output) => { if (lessError) throw lessError mkdir( './assets/' , (dirError) => { if (dirError) throw dirError writeFile( 'assets/main.css' , output.css, 'utf-8' , (writeError) => { if (writeError) throw writeError console.log( 'stylesheet created' ); }) }) }) 16}) |
我們可以看到,由于多個回調鏈和重復的錯誤處理,編寫程序變得越來越復雜,代碼變得更加難以理解。
Promise、包裝和鏈模式
當 Promises 最初被宣布為 JavaScript 語言的新成員時,并沒有引起太多關注,它們并不是一個新概念,因為其他語言在幾十年前就已經實現了類似的實現。事實上自從它出現以來,他們就改變了我從事的大多數項目的語義和結構。
Promises不僅為開發(fā)人員引入了用于編寫異步代碼的內置解決方案,,而且還開辟了Web 開發(fā)的新階段,成為 Web 規(guī)范后來的新功能(如 fetch)的構建基礎。
從回調方法遷移到基于 promise 的方法在項目(例如庫和瀏覽器)中變得越來越普遍,甚至 Node.js 也開始緩慢地遷移到它上面。
例如,包裝 Node 的 readFile 方法:
1
2
3
4
5
6
7
8
9
10
|
const { readFile } = require( 'fs' ); const asyncReadFile = (path, options) => { return new Promise((resolve, reject) => { readFile(path, options, (error, data) => { if (error) reject(error); else resolve(data); }) }); } |
在這里,我們通過在 Promise 構造函數內部執(zhí)行來隱藏回調,方法成功后調用 resolve,定義錯誤對象時調用reject。
當一個方法返回一個 Promise 對象時,我們可以通過將一個函數傳遞給 then 來遵循其成功的解析,它的參數是 Promise 被解析的值,在這里是 data。
如果在方法運行期間拋出錯誤,則將調用 catch 函數(如果存在)。
注意:如果你需要更深入地了解 Promise 的工作原理,建議你看 Jake Archibald 在 Google 的 web 開發(fā)博客上寫的文章“ JavaScript Promises:簡介”。
現在我們可以使用這些新方法并避免回調鏈。
1
2
3
|
asyncRead( './main.less' , 'utf-8' ) .then(data => console.log( 'file content' , data)) . catch (error => console.error( 'something went wrong' , error)) |
它具有創(chuàng)建異步任務的原生方法,并以清晰的接口跟蹤其可能的結果,這擺脫了觀察者模式?;?Promise 的代碼似乎可以解決可讀性差且容易出錯的代碼。
在更好的語法突出顯示和更清晰的錯誤提示信息對編碼過程中提供的幫助下,對于開發(fā)人員來說,編寫更容易理解的代碼變得更具可預測性,并且執(zhí)行的情況更好,更容易發(fā)現可能的陷阱。
Promises 的采用在社區(qū)中非常普遍,以至于 Node.js 迅速發(fā)布其 I/O 方法的內置版本以返回 Promise 對象,例如從 fs.promises 中導入文件操作。
它甚至提供了一個 promisify 工具來包裝遵循錯誤優(yōu)先回調模式的函數,并將其轉換為基于 Promise 的函數。
但是 Promise 在所有情況下都能提供幫助嗎?
讓我們重新評估一下用 Promise 編寫的樣式預處理任務。
1
2
3
4
5
6
7
8
9
10
|
const { mkdir, writeFile, readFile } = require( 'fs' ).promises; const less = require( 'less' ) readFile( './main.less' , 'utf-8' ) .then(less.render) .then(result => mkdir( './assets' ) .then(writeFile( 'assets/main.css' , result.css, 'utf-8' )) ) . catch (error => console.error(error)) |
代碼中的冗余明顯減少了,尤其是在錯誤處理方面,因為我們現在依賴于 catch,但是 Promise 在某種程度上沒能提供直接與動作串聯相關的清晰代碼縮進。
實際上,這是在調用 readFile 之后的第一個 then 語句中實現的。這些代碼行之后發(fā)生的事情是需要創(chuàng)建一個新的作用域,我們可以在該作用域中先創(chuàng)建目錄,然后將結果寫入文件中。這會導致縮進節(jié)奏的中斷,乍一看就不容易確定指令序列。
注意:請注意,這是一個示例程序,我們可以控制某些方法,它們都遵循行業(yè)慣例,但并非總是如此。通過更復雜的串聯或引入不同的庫,我們的代碼風格可以輕松被打破。
令人高興的是,JavaScript 社區(qū)再次從其他語言的語法中學到了東西,并增加了一種表示方法,可以在大多數情況下幫助異步任務串聯,而不是像同步代碼那樣能夠令人輕松的閱讀。
Async 與 Await
Promise 被定義為執(zhí)行時的未解決的值,創(chuàng)建 Promise 實例是對此工件的“顯式”調用。
1
2
3
4
5
6
7
8
9
10
|
const { mkdir, writeFile, readFile } = require( 'fs' ).promises; const less = require( 'less' ) readFile( './main.less' , 'utf-8' ) .then(less.render) .then(result => mkdir( './assets' ) .then(writeFile( 'assets/main.css' , result.css, 'utf-8' )) ) . catch (error => console.error(error)) |
在異步方法內部,我們可以用 await 保留字來確定 Promise 的解決方案,然后再繼續(xù)執(zhí)行。
讓我們用這種語法重新編寫代碼段。
1
2
3
4
5
6
7
8
9
10
11
|
const { mkdir, writeFile, readFile } = require( 'fs' ).promises; const less = require( 'less' ) async function processLess() { const content = await readFile( './main.less' , 'utf-8' ) const result = await less.render(content) await mkdir( './assets' ) await writeFile( 'assets/main.css' , result.css, 'utf-8' ) } 11processLess() |
注意:請注意,我們需要將所有代碼移至某個方法中,因為我們無法在 異步函數的作用域之外使用 await 。
每當異步方法找到一個 await 語句時,它將停止執(zhí)行,直到 promise 被解決為止。
盡管是異步執(zhí)行,但用 async/await 表示會使代碼看起來好像是同步的,這是容易被開發(fā)人員閱讀和理解的東西。
那么錯誤處理呢?我們可以用在語言中存在了很久的try 和 catch。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const { mkdir, writeFile, readFile } = require( 'fs' ).promises; const less = require( 'less' ) async function processLess() { const content = await readFile( './main.less' , 'utf-8' ) const result = await less.render(content) await mkdir( './assets' ) await writeFile( 'assets/main.css' , result.css, 'utf-8' ) } try { processLess() } catch (e) { console.error(e) } |
我們大可放心,在過程中拋出的任何錯誤都會由 catch 語句中的代碼處理?,F在我們有了一個易于閱讀和規(guī)范的代碼。
對返回值進行的后續(xù)操作無需存儲在不會破壞代碼節(jié)奏的 mkdir 之類的變量中;也無需在以后的步驟中創(chuàng)建新的作用域來訪問 result 的值。
可以肯定地說,Promise 是該語言中引入的基本工件,對于在 JavaScript 中啟用 async/await 表示法是必需的,你可以在現代瀏覽器和最新版本的 Node.js 中使用它。
注意:最近在 JSConf 中,Node 的創(chuàng)建者和第一貢獻者 Ryan Dahl, 對在其早期開發(fā)中沒有遵守Promises 表示遺憾,主要是因為 Node 的目標是創(chuàng)建事件驅動服務器和文件管理,而 Observer 模式更適合這樣。
結論
將 Promise 引入 Web 開發(fā)的目的是改變我們在代碼中順序操作的方式,并改變了我們理解代碼的方式以及編寫庫和包的方式。
但是擺脫回調鏈更難解決,我認為在多年來習慣于觀察者模式和采用的方法之后,必須將方法傳遞給 then 并不能幫助我們擺脫原有的思路,例如 Node.js。
正如 Nolan Lawson 在他的出色文章“關于 Promise 級聯的錯誤使用“【https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html】 中所述,舊的回調習慣是死硬且頑固的!在文中他解釋了如何避免這些陷阱。
我認為 Promise 是中間步驟,它允許以自然的方式生成異步任務,但并沒有幫助我們進一步改進更好的代碼模式,有時你需要更適應改進的語言語法。
當嘗試使用JavaScript解決更復雜的難題時,我們看到了對更成熟語言的需求,并且我們嘗試了以前不曾在網上看到的體系結構和模式。
我們仍然不知道 ECMAScript 規(guī)范在幾年后的樣子,因為我們一直在將 JavaScript 治理擴展到 web 之外,并嘗試解決更復雜的難題。
現在很難說我們需要從語言中真正地將這些難題轉變成更簡單的程序,但是我對 Web 和 JavaScript 本身如何推動技術,試圖適應挑戰(zhàn)和新環(huán)境感到滿意。與十年前剛剛開始在瀏覽器中編寫代碼時相比,我覺得現在 JavaScript 是“異步友好”的。
原文:https://www.smashingmagazine.com/2019/10/asynchronous-tasks-modern-javascript/
到此這篇關于如何在現代JavaScript中編寫異步任務的文章就介紹到這了,更多相關JavaScript編寫異步任務內容請搜索服務器之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://mp.weixin.qq.com/s/j8e9Hl7e86RYUEd39YAVQA