溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么在現(xiàn)代JavaScript中編寫異步任務(wù)

發(fā)布時(shí)間:2021-02-01 09:28:29 來源:億速云 閱讀:152 作者:小新 欄目:開發(fā)技術(shù)

小編給大家分享一下怎么在現(xiàn)代JavaScript中編寫異步任務(wù),相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

在本文中,我們將探討過去異步執(zhí)行的 JavaScript 的演變,以及它是怎樣改變我們編寫代碼的方式的。我們將從最早的 Web 開發(fā)開始,一直到現(xiàn)代異步模式。

作為編程語言, JavaScript 有兩個(gè)主要特征,這兩個(gè)特征對(duì)于理解我們的代碼如何工作非常重要。首先是它的同步特性,這意味著代碼將逐行運(yùn)行,其次是單線程,任何時(shí)候都僅執(zhí)行一個(gè)命令。

隨著語言的發(fā)展,允許異步執(zhí)行的新工件出現(xiàn)在場(chǎng)景中。開發(fā)人員在解決更復(fù)雜的算法和數(shù)據(jù)流時(shí)嘗試了不同的方法,從而導(dǎo)致新的接口和模式出現(xiàn)。

同步執(zhí)行和觀察者模式

如簡(jiǎn)介中所述,JavaScript 通常會(huì)逐行運(yùn)行你編寫的代碼。即使在最初的幾年中,該語言也有這種規(guī)則的例外,盡管很少,你可能已經(jīng)知道了它們:HTTP 請(qǐng)求,DOM 事件和time interval。

如果我們通過添加事件偵聽器去響應(yīng)用戶對(duì)元素的單擊,則無論語言解釋器在運(yùn)行什么,它都會(huì)停止,然后運(yùn)行在偵聽器回調(diào)中編寫的代碼,之后再返回正常的流程。

與 interval 或網(wǎng)絡(luò)請(qǐng)求相同,addEventListener,setTimeout 和 XMLHttpRequest 是 Web 開發(fā)人員訪問異步執(zhí)行的第一批工件。

盡管這些是 JavaScript 中同步執(zhí)行的例外情況,但重要的是你要了解該語言仍然是單線程的。我們可以打破這種同步性,但是解釋器仍然每次運(yùn)行一行代碼。

例如檢查一個(gè)網(wǎng)絡(luò)請(qǐng)求。

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ā)生什么情況,當(dāng)服務(wù)器恢復(fù)運(yùn)行時(shí),分配給 onreadystatechange 的方法都會(huì)在取回程序的代碼序列之前被調(diào)用。

對(duì)用戶交互做出反應(yīng)時(shí),也會(huì)發(fā)生類似的情況。

const button = document.querySelector('button');

// observe for user interaction
button.addEventListener('click', function(e) {
 console.log('user click just happened!');
})

你可能會(huì)注意到,我們正在連接一個(gè)外部事件并傳遞一個(gè)回調(diào),告訴代碼當(dāng)事件發(fā)生時(shí)應(yīng)該怎么做。十多年前,“什么是回調(diào)?”是一個(gè)非常受期待的面試問題,因?yàn)樵诤芏啻a庫中到處都有這種模式。

在上述每種情況下,我們都在響應(yīng)外部事件。不管是達(dá)到一定的時(shí)間間隔、用戶操作還是服務(wù)器響應(yīng)。我們本身無法創(chuàng)建異步任務(wù),我們總是 觀察 發(fā)生在我們力所能及范圍之外的事件。

這就是為什么這種方式的代碼被稱為觀察者模式的原因,在這種情況下,它最好由 addEventListener 接口來表示。很快,暴露這種模式的事件發(fā)送器庫或框架開始蓬勃發(fā)展。

NODE.JS 和事件發(fā)送器

Node.js 是一個(gè)很好的例子,它的官網(wǎng)把自己描述為“異步事件驅(qū)動(dòng)的 JavaScript 運(yùn)行時(shí)”,所以事件發(fā)送器和回調(diào)是一等公民。它甚至已經(jīng)實(shí)現(xiàn)了一個(gè) EventEmitter 構(gòu)造函數(shù)。

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)系統(tǒng)的核心模式和慣例。Node.js 開辟了一個(gè)在不同環(huán)境中甚至在 web 之外編寫 JavaScript 的新時(shí)代。當(dāng)然異步的情況也是可能的,例如創(chuàng)建新目錄或?qū)懳募?/p>

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');
 })
 }
})

你可能會(huì)注意到,回調(diào)函數(shù)將第一個(gè)參數(shù)接作為 error ,如果得到了預(yù)期的響應(yīng)數(shù)據(jù),則將其作為第二個(gè)參數(shù)。這就是所謂的錯(cuò)誤優(yōu)先回調(diào)模式,它成為作者和貢獻(xiàn)者為包和庫所做的約定。

Promise 和沒完沒了的回調(diào)鏈

隨著 Web 開發(fā)面臨的更復(fù)雜的問題,出現(xiàn)了對(duì)更好的異步工件的需求。如果我們查看最后一個(gè)代碼段,則會(huì)看到重復(fù)的回調(diào)鏈,隨著任務(wù)數(shù)量的增加,回調(diào)鏈的擴(kuò)展效果不佳。

例如,我們僅添加兩個(gè)步驟,即文件讀取和樣式預(yù)處理。

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})

我們可以看到,由于多個(gè)回調(diào)鏈和重復(fù)的錯(cuò)誤處理,編寫程序變得越來越復(fù)雜,代碼變得更加難以理解。

Promise、包裝和鏈模式

當(dāng) Promises 最初被宣布為 JavaScript 語言的新成員時(shí),并沒有引起太多關(guān)注,它們并不是一個(gè)新概念,因?yàn)槠渌Z言在幾十年前就已經(jīng)實(shí)現(xiàn)了類似的實(shí)現(xiàn)。事實(shí)上自從它出現(xiàn)以來,他們就改變了我從事的大多數(shù)項(xiàng)目的語義和結(jié)構(gòu)。

Promises不僅為開發(fā)人員引入了用于編寫異步代碼的內(nèi)置解決方案,,而且還開辟了Web 開發(fā)的新階段,成為 Web 規(guī)范后來的新功能(如 fetch)的構(gòu)建基礎(chǔ)。

從回調(diào)方法遷移到基于 promise 的方法在項(xiàng)目(例如庫和瀏覽器)中變得越來越普遍,甚至 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 構(gòu)造函數(shù)內(nèi)部執(zhí)行來隱藏回調(diào),方法成功后調(diào)用 resolve,定義錯(cuò)誤對(duì)象時(shí)調(diào)用reject。

當(dāng)一個(gè)方法返回一個(gè)  Promise  對(duì)象時(shí),我們可以通過將一個(gè)函數(shù)傳遞給 then 來遵循其成功的解析,它的參數(shù)是 Promise  被解析的值,在這里是 data。

如果在方法運(yùn)行期間拋出錯(cuò)誤,則將調(diào)用 catch 函數(shù)(如果存在)。

注意:如果你需要更深入地了解 Promise 的工作原理,建議你看 Jake Archibald 在 Google 的 web 開發(fā)博客上寫的文章“ JavaScript Promises:簡(jiǎn)介”。

現(xiàn)在我們可以使用這些新方法并避免回調(diào)鏈。

asyncRead('./main.less', 'utf-8')
 .then(data => console.log('file content', data))
 .catch(error => console.error('something went wrong', error))

它具有創(chuàng)建異步任務(wù)的原生方法,并以清晰的接口跟蹤其可能的結(jié)果,這擺脫了觀察者模式。基于 Promise 的代碼似乎可以解決可讀性差且容易出錯(cuò)的代碼。

在更好的語法突出顯示和更清晰的錯(cuò)誤提示信息對(duì)編碼過程中提供的幫助下,對(duì)于開發(fā)人員來說,編寫更容易理解的代碼變得更具可預(yù)測(cè)性,并且執(zhí)行的情況更好,更容易發(fā)現(xiàn)可能的陷阱。

Promises 的采用在社區(qū)中非常普遍,以至于 Node.js 迅速發(fā)布其 I/O 方法的內(nèi)置版本以返回 Promise 對(duì)象,例如從 fs.promises 中導(dǎo)入文件操作。

它甚至提供了一個(gè) promisify 工具來包裝遵循錯(cuò)誤優(yōu)先回調(diào)模式的函數(shù),并將其轉(zhuǎn)換為基于 Promise 的函數(shù)。

但是 Promise 在所有情況下都能提供幫助嗎?

讓我們重新評(píng)估一下用 Promise 編寫的樣式預(yù)處理任務(wù)。

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))

代碼中的冗余明顯減少了,尤其是在錯(cuò)誤處理方面,因?yàn)槲覀儸F(xiàn)在依賴于 catch,但是 Promise 在某種程度上沒能提供直接與動(dòng)作串聯(lián)相關(guān)的清晰代碼縮進(jìn)。

實(shí)際上,這是在調(diào)用 readFile 之后的第一個(gè) then 語句中實(shí)現(xiàn)的。這些代碼行之后發(fā)生的事情是需要?jiǎng)?chuàng)建一個(gè)新的作用域,我們可以在該作用域中先創(chuàng)建目錄,然后將結(jié)果寫入文件中。這會(huì)導(dǎo)致縮進(jìn)節(jié)奏的中斷,乍一看就不容易確定指令序列。

注意:請(qǐng)注意,這是一個(gè)示例程序,我們可以控制某些方法,它們都遵循行業(yè)慣例,但并非總是如此。通過更復(fù)雜的串聯(lián)或引入不同的庫,我們的代碼風(fēng)格可以輕松被打破。

令人高興的是,JavaScript 社區(qū)再次從其他語言的語法中學(xué)到了東西,并增加了一種表示方法,可以在大多數(shù)情況下幫助異步任務(wù)串聯(lián),而不是像同步代碼那樣能夠令人輕松的閱讀。

Async 與 Await

Promise 被定義為執(zhí)行時(shí)的未解決的值,創(chuàng)建 Promise 實(shí)例是對(duì)此工件的“顯式”調(diào)用。

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))

在異步方法內(nèi)部,我們可以用 await 保留字來確定 Promise 的解決方案,然后再繼續(xù)執(zhí)行。

讓我們用這種語法重新編寫代碼段。

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()

注意:請(qǐng)注意,我們需要將所有代碼移至某個(gè)方法中,因?yàn)槲覀儫o法在 異步函數(shù)的作用域之外使用 await 。

每當(dāng)異步方法找到一個(gè) await 語句時(shí),它將停止執(zhí)行,直到 promise 被解決為止。

盡管是異步執(zhí)行,但用 async/await 表示會(huì)使代碼看起來好像是同步的,這是容易被開發(fā)人員閱讀和理解的東西。

那么錯(cuò)誤處理呢?我們可以用在語言中存在了很久的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)
}

我們大可放心,在過程中拋出的任何錯(cuò)誤都會(huì)由 catch 語句中的代碼處理。現(xiàn)在我們有了一個(gè)易于閱讀和規(guī)范的代碼。

對(duì)返回值進(jìn)行的后續(xù)操作無需存儲(chǔ)在不會(huì)破壞代碼節(jié)奏的 mkdir 之類的變量中;也無需在以后的步驟中創(chuàng)建新的作用域來訪問 result 的值。

可以肯定地說,Promise 是該語言中引入的基本工件,對(duì)于在 JavaScript 中啟用 async/await 表示法是必需的,你可以在現(xiàn)代瀏覽器和最新版本的 Node.js 中使用它。

注意:最近在 JSConf 中,Node 的創(chuàng)建者和第一貢獻(xiàn)者 Ryan Dahl, 對(duì)在其早期開發(fā)中沒有遵守Promises 表示遺憾,主要是因?yàn)?Node 的目標(biāo)是創(chuàng)建事件驅(qū)動(dòng)服務(wù)器和文件管理,而 Observer 模式更適合這樣。

結(jié)論

將 Promise 引入 Web 開發(fā)的目的是改變我們?cè)诖a中順序操作的方式,并改變了我們理解代碼的方式以及編寫庫和包的方式。

但是擺脫回調(diào)鏈更難解決,我認(rèn)為在多年來習(xí)慣于觀察者模式和采用的方法之后,必須將方法傳遞給 then 并不能幫助我們擺脫原有的思路,例如 Node.js。

正如 Nolan Lawson 在他的出色文章“關(guān)于 Promise 級(jí)聯(lián)的錯(cuò)誤使用“【https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html】 中所述,舊的回調(diào)習(xí)慣是死硬且頑固的!在文中他解釋了如何避免這些陷阱。

我認(rèn)為 Promise 是中間步驟,它允許以自然的方式生成異步任務(wù),但并沒有幫助我們進(jìn)一步改進(jìn)更好的代碼模式,有時(shí)你需要更適應(yīng)改進(jìn)的語言語法。

當(dāng)嘗試使用JavaScript解決更復(fù)雜的難題時(shí),我們看到了對(duì)更成熟語言的需求,并且我們嘗試了以前不曾在網(wǎng)上看到的體系結(jié)構(gòu)和模式。

我們?nèi)匀徊恢?ECMAScript 規(guī)范在幾年后的樣子,因?yàn)槲覀円恢痹趯?JavaScript 治理擴(kuò)展到 web 之外,并嘗試解決更復(fù)雜的難題。

現(xiàn)在很難說我們需要從語言中真正地將這些難題轉(zhuǎn)變成更簡(jiǎn)單的程序,但是我對(duì) Web 和 JavaScript 本身如何推動(dòng)技術(shù),試圖適應(yīng)挑戰(zhàn)和新環(huán)境感到滿意。與十年前剛剛開始在瀏覽器中編寫代碼時(shí)相比,我覺得現(xiàn)在 JavaScript 是“異步友好”的。

以上是“怎么在現(xiàn)代JavaScript中編寫異步任務(wù)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

js
AI