做者:Jeremias Menichelli翻譯:瘋狂的技術宅javascript
原文:https://www.smashingmagazine....css
未經容許嚴禁轉載html
在本文中,咱們將探討過去異步執行的 JavaScript 的演變,以及它是怎樣改變咱們編寫代碼的方式的。咱們將從最先的 Web 開發開始,一直到現代異步模式。前端
做爲編程語言, JavaScript 有兩個主要特徵,這兩個特徵對於理解咱們的代碼如何工做很是重要。首先是它的同步特性,這意味着代碼將逐行運行,其次是單線程,任什麼時候候都僅執行一個命令。java
隨着語言的發展,容許異步執行的新工件出如今場景中。開發人員在解決更復雜的算法和數據流時嘗試了不一樣的方法,從而致使新的接口和模式出現。git
如簡介中所述,JavaScript 一般會逐行運行你編寫的代碼。即便在最初的幾年中,該語言也有這種規則的例外,儘管不多,你可能已經知道了它們:HTTP 請求,DOM 事件和time interval。程序員
若是咱們經過添加事件偵聽器去響應用戶對元素的單擊,則不管語言解釋器在運行什麼,它都會中止,而後運行在偵聽器回調中編寫的代碼,以後再返回正常的流程。github
與 interval 或網絡請求相同,addEventListener
,setTimeout
和 XMLHttpRequest
是 Web 開發人員訪問異步執行的第一批工件。web
儘管這些是 JavaScript 中同步執行的例外狀況,但重要的是你要了解該語言仍然是單線程的。咱們能夠打破這種同步性,可是解釋器仍然每次運行一行代碼。面試
例如檢查一個網絡請求。
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); } } request.send();
無論發生什麼狀況,當服務器恢復運行時,分配給 onreadystatechange
的方法都會在取回程序的代碼序列以前被調用。
對用戶交互作出反應時,也會發生相似的狀況。
const button = document.querySelector('button'); // observe for user interaction button.addEventListener('click', function(e) { console.log('user click just happened!'); })
你可能會注意到,咱們正在鏈接一個外部事件並傳遞一個回調,告訴代碼當事件發生時應該怎麼作。十多年前,「什麼是回調?」是一個很是受期待的面試問題,由於在不少代碼庫中處處都有這種模式。
在上述每種狀況下,咱們都在響應外部事件。無論是達到必定的時間間隔、用戶操做仍是服務器響應。咱們自己沒法建立異步任務,咱們老是 觀察 發生在咱們力所能及範圍以外的事件。
這就是爲何這種方式的代碼被稱爲觀察者模式的緣由,在這種狀況下,它最好由 addEventListener
接口來表示。很快,暴露這種模式的事件發送器庫或框架開始蓬勃發展。
Node.js 是一個很好的例子,它的官網把本身描述爲「異步事件驅動的 JavaScript 運行時」,因此事件發送器和回調是一等公民。它甚至已經實現了一個 EventEmitter
構造函數。
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!');
這不只是通用的異步執行方法,並且是其生態系統的核心模式和慣例。 Node.js 開闢了一個在不一樣環境中甚至在 web 以外編寫 JavaScript 的新時代。固然異步的狀況也是可能的,例如建立新目錄或寫文件。
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
,若是獲得了預期的響應數據,則將其做爲第二個參數。這就是所謂的錯誤優先回調模式,它成爲做者和貢獻者爲包和庫所作的約定。
隨着 Web 開發面臨的更復雜的問題,出現了對更好的異步工件的需求。若是咱們查看最後一個代碼段,則會看到重複的回調鏈,隨着任務數量的增長,回調鏈的擴展效果不佳。
例如,咱們僅添加兩個步驟,即文件讀取和樣式預處理。
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'); }) }) }) })
咱們能夠看到,因爲多個回調鏈和重複的錯誤處理,編寫程序變得愈來愈複雜,代碼變得更加難以理解。
當 Promises
最初被宣佈爲 JavaScript 語言的新成員時,並無引發太多關注,它們並非一個新概念,由於其餘語言在幾十年前就已經實現了相似的實現。事實上自從它出現以來,他們就改變了我從事的大多數項目的語義和結構。
Promises
不只爲開發人員引入了用於編寫異步代碼的內置解決方案,,並且還開闢了Web 開發的新階段,成爲 Web 規範後來的新功能(如 fetch
)的構建基礎。
從回調方法遷移到基於 promise 的方法在項目(例如庫和瀏覽器)中變得愈來愈廣泛,甚至 Node.js 也開始緩慢地遷移到它上面。
例如,包裝 Node 的 readFile
方法:
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 構造函數內部執行來隱藏回調,方法成功後調用 resolve
,定義錯誤對象時調用reject
。
當一個方法返回一個 Promise
對象時,咱們能夠經過將一個函數傳遞給 then
來遵循其成功的解析,它的參數是 Promise 被解析的值,在這裏是 data
。
若是在方法運行期間拋出錯誤,則將調用 catch
函數(若是存在)。
注意:若是你須要更深刻地瞭解 Promise 的工做原理,建議你看 Jake Archibald 在 Google 的 web 開發博客上寫的文章「 JavaScript Promises:簡介」。
如今咱們可使用這些新方法並避免回調鏈。
asyncRead('./main.less', 'utf-8') .then(data => console.log('file content', data)) .catch(error => console.error('something went wrong', error))
它具備建立異步任務的原生方法,並以清晰的接口跟蹤其可能的結果,這擺脫了觀察者模式。基於 Promise 的代碼彷佛能夠解決可讀性差且容易出錯的代碼。
在更好的語法突出顯示和更清晰的錯誤提示信息對編碼過程當中提供的幫助下,對於開發人員來講,編寫更容易理解的代碼變得更具可預測性,而且執行的狀況更好,更容易發現可能的陷阱。
Promises 的採用在社區中很是廣泛,以致於 Node.js 迅速發佈其 I/O 方法的內置版本以返回 Promise 對象,例如從 fs.promises 中導入文件操做。
它甚至提供了一個 promisify
工具來包裝遵循錯誤優先回調模式的函數,並將其轉換爲基於 Promise 的函數。
可是 Promise 在全部狀況下都能提供幫助嗎?
讓咱們從新評估一下用 Promise 編寫的樣式預處理任務。
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
語句中實現的。這些代碼行以後發生的事情是須要建立一個新的做用域,咱們能夠在該做用域中先建立目錄,而後將結果寫入文件中。這會致使縮進節奏的中斷,乍一看就不容易肯定指令序列。
注意:請注意,這是一個示例程序,咱們能夠控制某些方法,它們都遵循行業慣例,但並不是老是如此。經過更復雜的串聯或引入不一樣的庫,咱們的代碼風格能夠輕鬆被打破。
使人高興的是,JavaScript 社區再次從其餘語言的語法中學到了東西,並增長了一種表示方法,能夠在大多數狀況下幫助異步任務串聯,而不是像同步代碼那樣可以使人輕鬆的閱讀。
Promise
被定義爲執行時的未解決的值,建立 Promise
實例是對此工件的「顯式」調用。
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
的解決方案,而後再繼續執行。
讓咱們用這種語法從新編寫代碼段。
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') } processLess()
注意:請注意,咱們須要將全部代碼移至某個方法中,由於咱們沒法在 異步函數的做用域以外使用 await
。
每當異步方法找到一個 await
語句時,它將中止執行,直到 promise 被解決爲止。
儘管是異步執行,但用 async/await 表示會使代碼看起來好像是同步的,這是容易被開發人員閱讀和理解的東西。
那麼錯誤處理呢?咱們能夠用在語言中存在了好久的try
和 catch
。
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
語句中的代碼處理。如今咱們有了一個易於閱讀和規範的代碼。
對返回值進行的後續操做無需存儲在不會破壞代碼節奏的 mkdir
之類的變量中;也無需在之後的步驟中建立新的做用域來訪問 result
的值。
能夠確定地說,Promise 是該語言中引入的基本工件,對於在 JavaScript 中啓用 async/await 表示法是必需的,你能夠在現代瀏覽器和最新版本的 Node.js 中使用它。
注意:最近在 JSConf 中,Node 的建立者和第一貢獻者 Ryan Dahl, 對在其早期開發中沒有遵照Promises 表示遺憾,主要是由於 Node 的目標是建立事件驅動服務器和文件管理,而 Observer 模式更適合這樣。
將 Promise 引入 Web 開發的目的是改變咱們在代碼中順序操做的方式,並改變了咱們理解代碼的方式以及編寫庫和包的方式。
可是擺脫回調鏈更難解決,我認爲在多年來習慣於觀察者模式和採用的方法以後,必須將方法傳遞給 then
並不能幫助咱們擺脫原有的思路,例如 Node.js。
正如 Nolan Lawson 在他的出色文章「關於 Promise 級聯的錯誤使用「 中所述,舊的回調習慣是死硬且頑固的!在文中他解釋瞭如何避免這些陷阱。
我認爲 Promise 是中間步驟,它容許以天然的方式生成異步任務,但並無幫助咱們進一步改進更好的代碼模式,有時你須要更適應改進的語言語法。
當嘗試使用JavaScript解決更復雜的難題時,咱們看到了對更成熟語言的需求,而且咱們嘗試了之前未曾在網上看到的體系結構和模式。
咱們仍然不知道 ECMAScript 規範在幾年後的樣子,由於咱們一直在將 JavaScript 治理擴展到 web 以外,並嘗試解決更復雜的難題。
如今很難說咱們須要從語言中真正地將這些難題轉變成更簡單的程序,可是我對 Web 和 JavaScript 自己如何推進技術,試圖適應挑戰和新環境感到滿意。與十年前剛剛開始在瀏覽器中編寫代碼時相比,我以爲如今 JavaScript 是「異步友好」的。