一篇文章詳解JS的四種異步解決方案
前言
「異步編程」是前端工程師日常開發(fā)中經常會用到的技術,也是校招面試過程中??嫉囊粋€知識點。
通過掌握「異步編程」的四種方式,可以讓我們能夠更好地處理JavaScript中的異步操作,提高代碼的性能和用戶體驗。
因此,「今天就想和大家來聊聊JS異步編程的四種方式!」
同步&異步的概念
在講這四種異步方案之前,我們先來明確一下同步和異步的概念:
所謂「同步(synchronization)」,簡單來說,就是「順序執(zhí)行」,指的是同一時間只能做一件事情,只有目前正在執(zhí)行的事情做完之后,才能做下一件事情。
「同步操作的優(yōu)點」在于做任何事情都是依次執(zhí)行,井然有序,不會存在大家同時搶一個資源的問題。
「同步操作的缺點」在于「會阻塞后續(xù)代碼的執(zhí)行」。如果當前執(zhí)行的任務需要花費很長的時間,那么后面的程序就只能一直等待。
所謂「異步(Asynchronization)」,指的是當前代碼的執(zhí)行不影響后面代碼的執(zhí)行。當程序運行到異步的代碼時,會將該異步的代碼作為任務放進「任務隊列」,而不是推入主線程的調用棧。等主線程執(zhí)行完之后,再去任務隊列里執(zhí)行對應的任務即可。
因此,「異步操作的優(yōu)點就是:不會阻塞后續(xù)代碼的執(zhí)行?!?/strong>
js中異步的應用場景
開篇講了同步和異步的概念,那么在JS中異步的應用場景有哪些呢?
「定時任務」:setTimeout、setInterval
「網(wǎng)絡請求」:ajax請求、動態(tài)創(chuàng)建img標簽的加載
「事件監(jiān)聽器」:addEventListener
實現(xiàn)異步的四種方法
對于「setTimeout、setInterval、addEventListener」這種異步場景,不需要我們手動實現(xiàn)異步,直接調用即可。
但是對于「ajax請求」、「node.js中操作數(shù)據(jù)庫這種異步」,就需要我們自己來實現(xiàn)了~
1、 回調函數(shù)
在微任務隊列出現(xiàn)之前,JS實現(xiàn)異步的主要方式就是通過「回調函數(shù)」。
以一個簡易版的Ajax請求為例,代碼結構如下所示:
function ajax(obj){ let default = { url: '...', type:'GET', async:true, contentType: 'application/json', success:function(){} }; for (let key in obj) { defaultParam[key] = obj[key]; } let xhr; if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject('Microsoft.XMLHTTP'); } xhr.open(defaultParam.type, defaultParam.url+'?'+dataStr, defaultParam.async); xhr.send(); xhr.onreadystatechange = function (){ if (xhr.readyState === 4){ if(xhr.status === 200){ let result = JSON.parse(xhr.responseText); // 在此處調用回調函數(shù) defaultParam.success(result); } } } }
我們在業(yè)務代碼里可以這樣調用「ajax請求」:
ajax({ url:'#', type:GET, success:function(e){ // 回調函數(shù)里就是對請求結果的處理 } });
「ajax請求」中的success方法就是一個回調函數(shù),回調函數(shù)中執(zhí)行的是我們請求成功之后要做的進一步操作。
這樣就初步實現(xiàn)了異步,但是回調函數(shù)有一個非常嚴重的缺點,那就是「回調地獄」的問題。
大家可以試想一下,如果我們在回調函數(shù)里再發(fā)起一個ajax請求呢?那豈不是要在success函數(shù)里繼續(xù)寫一個ajax請求?那如果需要多級嵌套發(fā)起ajax請求呢?豈不是需要多級嵌套?
如果嵌套的層級很深的話,我們的代碼結構可能就會變成這樣:
因此,為了解決回調地獄的問題,提出了「promise」、「async/await」、「generator」的概念。
2、Promise
「Promise」作為典型的微任務之一,它的出現(xiàn)可以使JS達到異步執(zhí)行的效果。
一個「Promise函數(shù)」的結構如下列代碼如下:
const promise = new Promise((resolve, reject) => { resolve('a'); }); promise .then((arg) => { console.log(`執(zhí)行resolve,參數(shù)是${arg}`) }) .catch((arg) => { console.log(`執(zhí)行reject,參數(shù)是${arg}`) }) .finally(() => { console.log('結束promise') });
如果我們需要嵌套執(zhí)行異步代碼,相比于回調函數(shù)來說,「Promise」的執(zhí)行方式如下列代碼所示:
const promise = new Promise((resolve, reject) => { resolve(1); }); promise.then((value) => { console.log(value); return value * 2; }).then((value) => { console.log(value); return value * 2; }).then((value) => { console.log(value); }).catch((err) => { console.log(err); });
即通過then來實現(xiàn)多級嵌套(「鏈式調用」),這看起來是不是就比回調函數(shù)舒服多了~
每個「Promise」都會經歷的生命周期是:
進行中(pending) :此時代碼執(zhí)行尚未結束,所以也叫未處理的(unsettled)
已處理(settled) :異步代碼已執(zhí)行結束 已處理的代碼會進入兩種狀態(tài)中的一種:已拒絕(rejected):遇到錯誤,異步代碼執(zhí)行失敗 ,由reject()觸發(fā)
已完成(fulfilled):表明異步代碼執(zhí)行成功,由resolve()觸發(fā)
因此,「pending」,「fulfilled」, 「rejected」就是「Promise」中的三種狀態(tài)啦~
需要注意的是,在「Promise」中,要么包含resolve() 來表示 「Promise」 的狀態(tài)為fulfilled,要么包含 reject() 來表示「Promise」的狀態(tài)為rejected。
不然我們的「Promise」就會一直處于pending的狀態(tài),直至程序崩潰...
除此之外,「Promise」不僅很好的解決了鏈式調用的問題,它還有很多高頻的操作:
·Promise.all(promises) :接收一個包含多個Promise對象的數(shù)組,等待所有都完成時,返回存放它們結果的數(shù)組。如果任一被拒絕,則立即拋出錯誤,其他已完成的結果會被忽略
·Promise.allSettled(promises) : 接收一個包含多個Promise對象的數(shù)組,等待所有都已完成或者已拒絕時,返回存放它們結果對象的數(shù)組。每個結果對象的結構為{status:'fulfilled' // 或 'rejected', value // 或reason}
·Promise.race(promises) : 接收一個包含多個Promise對象的數(shù)組,等待第一個有結果(完成/拒絕)的Promise,并把其result/error作為結果返回
示例代碼如下所示:
function getPromises(){ return [ new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))), new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))), new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))), ]; } Promise.all(getPromises()).then(console.log); Promise.allSettled(getPromises()).then(console.log); Promise.race(getPromises()).then(console.log);
打印結果為:
3、Generator
「generator」是ES6提出的一種異步編程的方案。因為手動創(chuàng)建一個iterator十分麻煩,因此ES6推出了「generator」,用于更方便的創(chuàng)建iterator。
也就是說,「generator」就是一個返回值為iterator對象的函數(shù)。
在講「generator」之前,我們先來看看iterator是什么:
?iterator中文名叫「迭代器」。它為js中各種不同的數(shù)據(jù)結構(Object、Array、Set、Map)提供統(tǒng)一的訪問機制。
?
任何數(shù)據(jù)結構只要部署iterator接口,就可以完成遍歷操作。
因此iterator也是一種對象,不過相比于普通對象來說,它有著專為迭代而設計的接口。
我們通過一個例子來看看generator的特征:
function* createIterator() { yield 1; yield 2; yield 3; } // generators可以像正常函數(shù)一樣被調用,不同的是會返回一個 iterator let iterator = createIterator(); console.log(iterator.next().value); // 1 console.log(iterator.next().value); // 2 console.log(iterator.next().value); // 3
形式上,「generator」 函數(shù)是一個普通函數(shù),但是有兩個特征:
·function關鍵字與函數(shù)名之間有一個星號
·函數(shù)體內部使用yield語句,定義不同的內部狀態(tài)
在普通函數(shù)中,我們想要一個函數(shù)最終的執(zhí)行結果,一般都是return出來,或者以return作為結束函數(shù)的標準。運行函數(shù)時也不能被打斷,期間也不能從外部再傳入值到函數(shù)體內。
但在「generator」中,就打破了這幾點,所以「generator」和普通的函數(shù)完全不同。
當以function* 的方式聲明了一個「generator」生成器時,內部是可以有許多狀態(tài)的,以yield進行斷點間隔。期間我們執(zhí)行調用這個生成的「generator」,他會返回一個遍歷器對象,用這個對象上的方法,實現(xiàn)獲得一個yield后面輸出的結果。
function* generator() { yield 1 yield 2 }; let iterator = generator(); iterator.next() // {value: 1, done: false} iterator.next() // {value: 2, done: false} iterator.next() // {value: undefined, done: true}
4、 async/await
最后我們來講講「async/await」,終于講到這兒了?。?!
「async/await」是ES7提出的關于異步的終極解決方案。我看網(wǎng)上關于「async/await」是誰的語法糖這塊有兩個版本:
第一個版本說「async/await」是Generator的語法糖
第二個版本說「async/await」是Promise的語法糖
其實,這兩種說法都沒有錯。
「關于async/await是Generator的語法糖:」
所謂generator語法糖,表明的就是「aysnc/await」實現(xiàn)的就是generator實現(xiàn)的功能。但是「async/await」比generator要好用。因為generator執(zhí)行yield設下的斷點采用的方式就是不斷的調用iterator方法,這是個手動調用的過程。
而async配合await得到的就是斷點執(zhí)行后的結果。因此「async/await」比generator使用更普遍。
「關于async/await是Promise的語法糖:」
如果不使用「async/await」的話,Promise就需要通過鏈式調用來依次執(zhí)行then之后的代碼:
function counter(n){ return new Promise((resolve, reject) => { resolve(n + 1); }); } function adder(a, b){ return new Promise((resolve, reject) => { resolve(a + b); }); } function delay(a){ return new Promise((resolve, reject) => { setTimeout(() => resolve(a), 1000); }); } // 鏈式調用寫法 function callAll(){ counter(1) .then((val) => adder(val, 3)) .then((val) => delay(val)) .then(console.log); } callAll();//5
雖然相比于回調地獄來說,鏈式調用確實順眼多了。但是其呈現(xiàn)仍然略繁瑣了一些。
而「async/await的出現(xiàn),就使得我們可以通過同步代碼來達到異步的效果」:
async function callAll(){ const count = await counter(1); const sum = await adder(count, 3); console.log(await delay(sum)); } callAll();// 5
由此可見,「Promise搭配async/await的使用才是正解!」
總結
到此這篇關于JS四種異步解決方案的文章就介紹到這了,更多相關JS異步解決方案內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
js 復制功能 支持 for IE/FireFox/mozilla/ns
js 復制功能 支持 for IE/FireFox/mozilla/ns...2007-11-11解決bootstrap-select 動態(tài)加載數(shù)據(jù)不顯示的問題
今天小編就為大家分享一篇解決bootstrap-select 動態(tài)加載數(shù)據(jù)不顯示的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08