本週再來翻譯一些技術文章,本次預計翻譯三篇文章以下:javascript
05.[譯]Web網頁內容是如何影響電池功耗的(How Web Content Can Affect Power Usage)html
06.[譯]在現代JavaScript中編寫異步任務(https://web.dev/off-main-thread/)java
我翻譯的技術文章都放在一個github倉庫中,若是以爲有用請點擊star收藏。我爲何要建立這個git倉庫?目的是經過翻譯國外的web相關的技術文章來學習和跟進web發展的新思想和新技術。git倉庫地址:https://github.com/yzsunlei/javascript-article-translatenode
在本文中,咱們將探討過去圍繞異步執行的JavaScript的演變以及它如何改變咱們編寫和讀取代碼的方式。咱們將從Web開發的開始,一直到現代異步模式示例。
JavaScript做爲編程語言具備兩個主要特徵,這兩個特徵對於理解咱們的代碼是如何工做的都很重要。首先是它的同步特性,這意味着代碼將幾乎在您閱讀時逐行運行,其次,它是單線程的,任什麼時候候都只執行一個命令。git
隨着語言的發展,新的模塊出如今場景中以容許異步執行。開發人員在解決更復雜的算法和數據流時嘗試了不一樣的方法,從而致使圍繞它們的新接口和模式的出現。github
如引言中所述,JavaScript一般會逐行運行您編寫的代碼。即便在最初的幾年中,該語言也有例外,儘管它們不多,您可能已經知道它們:HTTP請求,DOM事件和時間間隔。web
const button = document.querySelector('button'); // observe for user interaction button.addEventListener('click', function(e) { console.log('user click just happened!'); })
若是添加事件偵聽器(例如,單擊元素並觸發用戶交互),則JavaScript引擎會將事件偵聽器回調的任務放入隊列,但將繼續執行其當前堆棧中的內容。完成那裏的調用以後,它如今將運行偵聽器的回調。算法
此行爲相似於網絡請求和計時器發生的狀況,它們是Web開發人員訪問異步執行的第一個模塊。編程
儘管這些是JavaScript中常見的同步執行例外的,但相當重要的是要了解該語言仍然是單線程的,而且儘管它能夠將Task排隊,異步運行它們而後返回主線程,但它只能一次執行一段代碼。
咱們的工具手冊,其中Alla Kholmatova探索瞭如何建立有效且可維護的設計系統來設計出色的數字產品。認識Design Systems,瞭解常見的陷阱,陷阱和Alla多年來汲取的經驗教訓。
例如,讓咱們發送一個網絡請求。
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
放入隊列(代碼在主線程中繼續執行)。
注意:解釋JavaScript引擎如何將任務排隊和處理執行線程是一個很複雜的主題,可能值得一讀。不過,我仍是建議您查看「事件循環究竟是什麼?」菲利普·羅伯茨(Phillip Roberts)提供的幫助,以幫助您更好地理解。
在上述每種狀況下,咱們都在響應外部事件。達到必定的時間間隔,用戶操做或服務器響應。咱們自己沒法建立異步任務,咱們始終觀察到發生的事件超出了咱們的範圍。
這就是爲何將這種模板式的代碼稱爲「觀察者模式」,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開闢了一個在不一樣環境中甚至在網絡以外編寫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規範)的新功能的構建基礎打開了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,則將調用該函數(若是存在)。
注意:若是您須要更深刻地瞭解Promises的工做方式,我建議Jake Archibald 在Google的Web開發博客上寫的「JavaScript Promises:Introduction」一文。
如今咱們可使用這些新方法並避免回調鏈。
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的函數。
可是,Promises在全部狀況下都能提供幫助嗎?
讓咱們從新想象一下用Promises編寫的樣式預處理任務。
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,可是Promises某種程度上未能提供與操做串聯直接相關的清晰代碼縮進。
這其實是在調用then以後的第一個語句上實現的readFile。這些行以後發生的事情是須要建立一個新的做用域,咱們能夠在該做用域中首先建立目錄,而後將結果寫入文件中。這就致使了縮進節奏的中斷,乍看之下很難肯定指令序列。
解決此問題的一種方法是預先處理該問題的自定義方法,並容許該方法正確鏈接,可是咱們將向彷佛已經具備實現任務所需功能的代碼引入更多的複雜性。
注意:這是一個示例程序,咱們能夠控制某些方法,它們都遵循行業慣例,但並不是老是如此。經過更復雜的串聯或引入具備不一樣類型的庫,咱們能夠輕鬆破壞代碼風格。
使人高興的是,JavaScript社區再次從其餘語言語法中學到了東西,並添加了一種表示法,能夠在不少狀況下幫助異步任務串聯而不是像同步代碼那樣使人愉悅或直截了當。
A Promise在執行時被定義爲一個未解析的值,建立a的實例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))
在async方法內部,咱們可使用await保留字來肯定a的分辨率,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。
每次async方法找到一條await語句時,它將中止執行,直處處理中的值或Promise被解析爲止。
儘管異步執行,但使用async/await表示法會有明顯的後果,代碼看起來好像是async,這是咱們開發人員更習慣查看和推理的。
錯誤處理呢?爲此,咱們使用在該語言中已經存在很長時間的語句,try和catch。
const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less'); async function processLess() { try { 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') } catch(e) { console.error(e) } } processLess()
放心,在該過程當中引起的任何錯誤將由該catch語句內的代碼處理。咱們在中心位置負責錯誤處理,可是如今咱們有了一個易於閱讀和遵循的代碼。
具備返回值的後續操做不須要存儲在mkdir不會破壞代碼節奏的變量中;也無需在之後的步驟中建立新的做用域來訪問result的值。
能夠確定地說,Promises是該語言中引入的一個基本模塊,對於在JavaScript中啓用async/await
表示法是必需的,您能夠在現代瀏覽器和最新版本的Node.js中使用它。
注意:最近在JSConf中,Node的建立者和第一貢獻者Ryan Dahl很遺憾沒有堅持Promises的早期開發,主要是由於Node的目標是建立事件驅動的服務器和文件管理,而Observer模式更適合於此。
將Promises引入Web開發世界的目的是改變咱們在代碼中排隊操做的方式,並改變了咱們對代碼執行進行推理的方式以及咱們編寫庫和包的方式。
可是擺脫回調鏈很難解決,我認爲then在多年習慣於觀察者模式和主要提供商採用的方法以後,不得不經過一種方法並不能幫助咱們擺脫思路。像Node.js這樣的社區。
正如諾蘭·勞森(Nolan Lawson)在其有關Promise串聯中錯誤使用的出色文章中所說,舊的回調習慣會死掉!稍後,他解釋瞭如何避免這些陷阱。
我認爲Promises是中間步驟,它容許以天然的方式生成異步任務,但並無幫助咱們進一步改進更好的代碼模式,有時您實際上須要更適應和改進的語言語法。
當咱們嘗試使用JavaScript解決更復雜的難題時,咱們看到了對更成熟語言的需求,並嘗試了之前未曾在網絡上看到過的架構和模式。
咱們仍然不知道ECMAScript規範的表現如何,由於咱們一直將JavaScript治理擴展到網絡以外,並嘗試解決更復雜的難題。
如今很難說咱們須要從語言中真正地將這些難題轉變成更簡單的程序所須要的東西,可是我對Web和JavaScript自己如何推進事物,試圖適應挑戰和新環境感到滿意。與十年前開始在瀏覽器中編寫代碼相比,如今我以爲JavaScript是一個更加異步的友好的地方。
原文連接:https://www.smashingmagazine.com/2019/10/asynchronous-tasks-modern-javascript/