JS異步處理的進(jìn)化史深入講解
前言
javascript是一門(mén)單線程的語(yǔ)言,也就是說(shuō)一次只能完成一件任務(wù),如果有多個(gè)任務(wù),就需要排隊(duì)進(jìn)行處理。如果一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)也必須排隊(duì)等待,這樣大大的影響了整個(gè)程序的執(zhí)行。為了解決這個(gè)問(wèn)題,javascript語(yǔ)言將任務(wù)分為兩種模式:
- 同步:當(dāng)我們打開(kāi)網(wǎng)站,網(wǎng)頁(yè)的頁(yè)面骨架渲染和頁(yè)面元素渲染,就是一大推同步任務(wù)。
- 異步:我們?cè)跒g覽新聞時(shí),加載圖片或音樂(lè)之類(lèi)占用資源大且耗時(shí)久的任務(wù)就是異步任務(wù)。
本文主要針對(duì)近兩年javascript的發(fā)展,主要介紹異步處理的進(jìn)化史。目前,在javascript異步處理中,有以下幾種方式:
callback
回調(diào)函數(shù)是最早解決異步編程的方法。無(wú)論是常見(jiàn)的setTimeout還是ajax請(qǐng)求,都是采用回調(diào)的形式把事情在某一固定的時(shí)刻進(jìn)行執(zhí)行。
 //常見(jiàn)的:setTimeout  setTimeout(function callback(){   console.log('aa'); }, 1000); //ajax請(qǐng)求 ajax(url,function callback(){ console.log("ajax success",res); })
回調(diào)函數(shù)的處理一般將函數(shù)callback作為參數(shù)傳進(jìn)函數(shù),在合適的時(shí)候被調(diào)用執(zhí)行?;卣{(diào)函數(shù)的優(yōu)點(diǎn)就是簡(jiǎn)單、容易理解和實(shí)現(xiàn),但有個(gè)致命的缺點(diǎn),容易出現(xiàn)回調(diào)地獄(Callback hell),即多個(gè)回調(diào)函數(shù)嵌套使用。造成代碼可讀性差、可維護(hù)性差且只能在回調(diào)中處理異常。
ajax(url, () => { //todo ajax(url1, () => { //todo ajax(url2, () => { //todo }) }) })
事件監(jiān)聽(tīng)
事件監(jiān)聽(tīng)采用的是事件驅(qū)動(dòng)的模式。事件的執(zhí)行不取決于代碼的順序,而是某個(gè)事件的發(fā)生。
假設(shè)有兩個(gè)函數(shù),為f1綁定一個(gè)事件(jQuery的寫(xiě)法),當(dāng)f1函數(shù)發(fā)生success事件時(shí),執(zhí)行函數(shù)f2:
f1.on('success',f2);
對(duì)f1進(jìn)行改寫(xiě):
function f1(){ ajax(url,() => { //todo f1.trigger('success');//觸發(fā)success事件,從而執(zhí)行f2函數(shù) }) }
事件監(jiān)聽(tīng)的方式較容易理解,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù),而且可以"去耦合",有利于實(shí)現(xiàn)模塊化。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型,運(yùn)行流程會(huì)變得很不清晰。閱讀代碼的時(shí)候,很難看出主流程。
發(fā)布訂閱
我們假定,存在一個(gè)"信號(hào)中心",某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心"發(fā)布"(publish)一個(gè)信號(hào),其他任務(wù)可以向信號(hào)中心"訂閱"(subscribe)這個(gè)信號(hào),從而知道什么時(shí)候自己可以開(kāi)始執(zhí)行。這就叫做 發(fā)布/訂閱模式(publish-subscribe pattern),又稱(chēng)**觀察者模式"(observer pattern) **。
//利用jquery的插件實(shí)現(xiàn) //首先,f2向消息中心訂閱success事件 jQuery.subscribe('success',f2); //對(duì)f1進(jìn)行改寫(xiě): function f1(){ ajax(url,() => { //todo jQuery.publish('success');//當(dāng)f1執(zhí)行完畢后,向消息中心jQuery發(fā)布success事件,從而執(zhí)行f2函數(shù) }) } //f2執(zhí)行完畢后,可以取消訂閱 jQuery.unsubscribe('success',f2)
該方法和事件監(jiān)聽(tīng)的性質(zhì)類(lèi)似,但我們可以通過(guò)消息中心來(lái)查閱一共有多少個(gè)信號(hào),每個(gè)信號(hào)有多少個(gè)訂閱者。
Promise
**Promise**是CommonJS工作組提出的一種規(guī)范,可以獲取異步操作的消息,也是異步處理中常用的一種解決方案。Promise的出現(xiàn)主要是用來(lái)解決回調(diào)地獄、支持多個(gè)并發(fā)的請(qǐng)求,獲取并發(fā)請(qǐng)求的數(shù)據(jù)并且解決異步的問(wèn)題。
let p = new Promise((resolve, reject) => { //做一些異步操作 setTimeout(()=>{ let num = parseInt(Math.random()*100); if(num > 50){ resolve("num > 50"); // 如果數(shù)字大于50就調(diào)用成功的函數(shù),并且將狀態(tài)變成Resolved }else{ reject("num <50");// 否則就調(diào)用失敗的函數(shù),將狀態(tài)變成Rejected } },10000) }); p.then((res) => { console.log(res); }).catch((err) =>{ console.log(err); })
Promise有三種狀態(tài):等待pending、成功fulfied、失敗rejected;狀態(tài)一旦改變,就不會(huì)再變化,在Promise對(duì)象創(chuàng)建后,會(huì)馬上執(zhí)行。等待狀態(tài)可以變?yōu)閒ulfied狀態(tài)并傳遞一個(gè)值給相應(yīng)的狀態(tài)處理方法,也可能變?yōu)槭顟B(tài)rejected并傳遞失敗信息。任一一種情況出現(xiàn)時(shí),Promise對(duì)象的 then 方法就會(huì)被調(diào)用(then方法包含兩個(gè)參數(shù):onfulfilled 和 onrejected,均為 Function。當(dāng)Promise狀態(tài)為fulfilled時(shí),調(diào)用 then 的 onfulfilled 方法,當(dāng)Promise狀態(tài)為rejected時(shí),調(diào)用 then 的 onrejected 方法)。
需要注意的是: Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 對(duì)象, 所以可以被鏈?zhǔn)秸{(diào)用,如下圖:
Promise的方法:
- Promise.all(iterable):誰(shuí)執(zhí)行得慢,以誰(shuí)為準(zhǔn)執(zhí)行回調(diào)。返回一個(gè)promise對(duì)象,只有當(dāng)iterable里面的所有promise對(duì)象成功后才會(huì)執(zhí)行。一旦iterable里面有promise對(duì)象執(zhí)行失敗就觸發(fā)該對(duì)象的失敗。對(duì)象在觸發(fā)成功后,會(huì)把一個(gè)包iterable里所有promise返回值的數(shù)組作為成功回調(diào)的返回值,順序跟iterable的順序保持一致;如果這個(gè)新的promise對(duì)象觸發(fā)了失敗狀態(tài),它會(huì)把iterable里第一個(gè)觸發(fā)失敗的promise對(duì)象的錯(cuò)誤信息作為它的失敗錯(cuò)誤信息。Promise.all方法常被用于處理多個(gè)promise對(duì)象的狀態(tài)集合。
- Promise.race(iterable): 誰(shuí)執(zhí)行得快,以誰(shuí)為準(zhǔn)執(zhí)行回調(diào)。iterable參數(shù)里的任意一個(gè)子promise被成功或失敗后,父promise馬上也會(huì)用子promise的成功返回值或失敗詳情作為參數(shù)調(diào)用父promise綁定的相應(yīng)句柄,并返回該promise對(duì)象。
- Promise.reject(err)與Promise.resolve(res)
Generators/yield
Generators是ES6提供的異步解決方案,其最大的特點(diǎn)就是可以控制函數(shù)的執(zhí)行??梢岳斫獬梢粋€(gè)內(nèi)部封裝了很多狀態(tài)的狀態(tài)機(jī),也是一個(gè)遍歷器對(duì)象生成函數(shù)。Generator 函數(shù)的特征:
- function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào);
- 函數(shù)體內(nèi)部使用yield表達(dá)式,定義不同的內(nèi)部狀態(tài);
- 通過(guò)yield暫停函數(shù),next啟動(dòng)函數(shù),每次返回的是yield表達(dá)式結(jié)果。next可以接受參數(shù),從而實(shí)現(xiàn)在函數(shù)運(yùn)行的不同階段,可以從外部向內(nèi)部注入不同的值。next返回一個(gè)包含value和done的對(duì)象,其中value表示迭代的值,后者表示迭代是否完成。
舉個(gè)例子:
function* createIterator(x) { let y = yield (x+1) let z = 2*(yield(y/3)) return (x+y+z) } // generators可以像正常函數(shù)一樣被調(diào)用,不同的是會(huì)返回一個(gè) iterator let iterator = createIterator(4); console.log(iterator.next()); // {value:5,done:false} console.log(iterator.next()); // {value:NaN,done:false} console.log(iterator.next()); // {value:NaN,done:true} let iterator1 = createIterator(4);//返回一個(gè)iterator //next傳參數(shù) console.log(iterator1.next()); // {value:5,done:false} console.log(iterator1.next(12)); // {value:4,done:false} console.log(iterator1.next(15)); // {value:46,done:true}
代碼分析:
- 當(dāng)不參數(shù)時(shí),next的value返回NaN;
- 當(dāng)傳參數(shù)時(shí),作為上一個(gè)yeild的值,在第一次使用next時(shí),傳參數(shù)無(wú)效,只有第二次開(kāi)始,才有效。
- 第一次執(zhí)行next時(shí),函數(shù)會(huì)被暫停在yeild(x+1),所以返回的是4+1=5;
- 第二次執(zhí)行next時(shí),傳入的12為上一次yeild表達(dá)式的值,所以y=12,返回的是12/3=4;
- 第三次執(zhí)行next時(shí),傳入的15為上一次yeild表達(dá)式的值,所以z=30,y=12;x=4,返回30+12+4=46
async/await
初入async/await
async/await在ES7提出,是目前在javascript異步處理的終極解決方案。
async 其本質(zhì)是 Generator 函數(shù)的語(yǔ)法糖。相較于Generator放入改進(jìn)如下:
- 內(nèi)置執(zhí)行器:Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,而async函數(shù)自帶執(zhí)行器。其調(diào)用方式與普通函數(shù)一模一樣,不需要調(diào)next方法;
- 更好的語(yǔ)義:async表示定義異步函數(shù),而await表示后面的表達(dá)式需要等待,相較于*和yeild更語(yǔ)義化;
- 更廣的適用性:co模塊約定,yield命令后面只能是Thunk函數(shù)或 Promise對(duì)象。而 async 函數(shù)的await命令后面則可以是Promise 或者 原始類(lèi)型的值;
- 返回Promise:async 函數(shù)返回值是Promise對(duì)象,比 Generator函數(shù)返回的 Iterator對(duì)象方便,可以直接使用 then() 方法進(jìn)行鏈?zhǔn)秸{(diào)用;
語(yǔ)法分析
async語(yǔ)法
用來(lái)定義異步函數(shù),自動(dòng)將函數(shù)轉(zhuǎn)換為promise對(duì)象,可以使用then來(lái)添加回調(diào),其內(nèi)部return的值作為then回調(diào)的參數(shù)。
async function f(){ return "hello async"; } f().then((res) => { //通過(guò)then來(lái)添加回調(diào)且內(nèi)部返回的res作為回調(diào)的參數(shù) console.log(res); // hello async })
在異步函數(shù)的內(nèi)部可以使用await,其返回的promise對(duì)象必須等到內(nèi)部所以await命令后的promise對(duì)象執(zhí)行完,才會(huì)發(fā)生狀態(tài)變化即執(zhí)行then回調(diào)。
const delay = function(timeout){ return new Promise(function(resolve){ return setTimeout(resolve, timeout); }); } async function f(){ await delay(1000); await delay(2000); return '完成'; } f().then(res => console.log(res));//需要等待3秒之后才會(huì)打?。和瓿?/pre>
await即表示異步等待,用來(lái)暫停異步函數(shù)的執(zhí)行,只能在異步函數(shù)和promise使用,且當(dāng)使用在promise前面,表示等待promise完成并返回結(jié)果。
async function f() { return await 1 //await后面不是Promise的話,也會(huì)被轉(zhuǎn)換為一個(gè)立即為resolve的promise }; f().then( res => console.log("處理成功",res))//打印出:處理成功 1 .catch(err => console.log("處理是被",err))////打印出:Promise{<resolved>:undefined}
錯(cuò)誤處理
如果await后面的異步出現(xiàn)錯(cuò)誤,等同于async返回的promise對(duì)象為reject,其錯(cuò)誤會(huì)被catch的回調(diào)函數(shù)接收到。需要注意的是,當(dāng) async 函數(shù)中只要一個(gè) await 出現(xiàn) reject 狀態(tài),則后面的 await 都不會(huì)被執(zhí)行。
let a; async function f(){ await Promise.reject("error") a = await 1 //該await并沒(méi)有執(zhí)行 } err().then(res => console.log(a))
怎么處理呢,可以把第一個(gè)await放在try/catch,遇到函數(shù)的時(shí)候,可以將錯(cuò)誤拋出并往下執(zhí)行。
async function f() { try{ await Promise.reject('error'); }catch(error){ console.log(error); } return await 1 } f().then(res => console.log('成功', res))//成功打印出1
如果有多個(gè)await處理,可以統(tǒng)一放在try/catch模塊中,而且async可以使得try/catch同時(shí)處理同步和異步錯(cuò)誤。
總結(jié)
通過(guò)以上六種javascript異步處理的常用方法,可以看出async/await可以說(shuō)是異步終極解決方案了,最后看一下async/await用得最多的場(chǎng)景:
如果一個(gè)業(yè)務(wù)需要很多個(gè)異步操作組成,并且每個(gè)步驟都依賴(lài)于上一步的執(zhí)行結(jié)果,這里采用不同的延時(shí)來(lái)體現(xiàn):
//首先定義一個(gè)延時(shí)函數(shù) function delay(time) { return new Promise(resolve => { setTimeout(() => resolve(time), time); }); } //采用promise鏈?zhǔn)秸{(diào)用實(shí)現(xiàn) delay(500).then(result => { return delay(result + 1000) }).then(result => { return delay(result + 2000) }).then(result => { console.log(result) //3500ms后打印出3500 }).catch(error => { console.log(error) }) //采用async實(shí)現(xiàn) async function f(){ const r1 = await delay(500) const r2 = await delay(r1+1000) const r3 = await delay(r2+2000) return r3 } f().then(res =>{ console.log(res) }).catch(err=>{ console.log(err) })
可以看出,采用promise實(shí)現(xiàn)采用了很多then進(jìn)行不停的鏈?zhǔn)秸{(diào)用,使得代碼變得冗長(zhǎng)和復(fù)雜且沒(méi)有語(yǔ)義化。而 async/await首先使用同步的方法來(lái)寫(xiě)異步,代碼非常清晰直觀,而且使代碼語(yǔ)義化,一眼就能看出代碼執(zhí)行的順序,最后 async 函數(shù)自帶執(zhí)行器,執(zhí)行的時(shí)候無(wú)需手動(dòng)加載。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
微信小程序?qū)崿F(xiàn)多個(gè)按鈕的顏色狀態(tài)轉(zhuǎn)換
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)多個(gè)按鈕的顏色狀態(tài)轉(zhuǎn)換,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02Mozilla 表達(dá)式 __noSuchMethod__
這是一個(gè)很特殊的方法,但是其存在的意義很大。不過(guò)很可惜只有firefox支持了。一個(gè)簡(jiǎn)單的例子解釋一下它的用處2009-04-04JS實(shí)現(xiàn)閃動(dòng)的title消息提醒效果
這篇文章主要介紹了JS實(shí)現(xiàn)閃動(dòng)的title消息提醒效果,考慮并兼容了大部份的瀏覽器,需要的朋友可以參考下2014-06-06Bootstrap每天必學(xué)之標(biāo)簽與徽章
Bootstrap每天必學(xué)之標(biāo)簽與徽章,對(duì)Bootstrap標(biāo)簽與徽章小編也了解的很少,希望通過(guò)這篇文章和大家更多的去學(xué)習(xí)Bootstrap標(biāo)簽與徽章,從中得到收獲。2015-11-11詳解JavaScript中怎么實(shí)現(xiàn)鏈表
鏈表是一系列節(jié)點(diǎn)串聯(lián)形成的數(shù)據(jù)結(jié)構(gòu),鏈表存儲(chǔ)有序的元素集合,鏈表中的元素在內(nèi)存中并不是連續(xù)放置的,本文給大家介紹了在JavaScript中怎么實(shí)現(xiàn)鏈表,需要的朋友可以參考下2023-12-12JavaScript提高網(wǎng)站性能優(yōu)化的建議(二)
這篇文章主要介紹了JavaScript提高網(wǎng)站性能優(yōu)化的建議(二)的相關(guān)資料,需要的朋友可以參考下2016-07-07鼠標(biāo)劃過(guò)實(shí)現(xiàn)延遲加載并隱藏層的js代碼
鼠標(biāo)劃過(guò)延遲加載隱藏層的效果,想必大家都有見(jiàn)到過(guò)吧,在本文將為大家詳細(xì)介紹下使用js是如何實(shí)現(xiàn)的,感興趣的朋友可以參考下2013-10-10