深入探究JS中的異步編程和事件循環(huán)機(jī)制
異步編程
js是單線程事件循環(huán)模型,同步操作與異步操作時(shí)代碼所依賴的核心機(jī)制。異步行為是為了優(yōu)化因計(jì)算量大而時(shí)間長(zhǎng)的操作。
同步:當(dāng)一個(gè)進(jìn)程在執(zhí)行某個(gè)請(qǐng)求時(shí),如果這個(gè)請(qǐng)求需要等待一段時(shí)間才能返回,那么這個(gè)進(jìn)程會(huì)一直等下去,直到請(qǐng)求返回。
異步:當(dāng)一個(gè)進(jìn)程在執(zhí)行某個(gè)請(qǐng)求時(shí),如果這個(gè)請(qǐng)求需要等待一段時(shí)間才能返回,那么這個(gè)進(jìn)程會(huì)直接向下執(zhí)行,不需要等待,當(dāng)消息返回時(shí)再通知進(jìn)程處理。
Promise
Promise的出現(xiàn)主要時(shí)解決之間異步編程使用回調(diào)函數(shù)的回調(diào)地獄的問題。
Promise的狀態(tài)
Promise有三種狀態(tài):
- pending:待定
- fulfilled:成功
- rejected:失敗
兩種狀態(tài)變化:
- pending -> fulfilled:在promise中調(diào)用resolve()函數(shù)會(huì)產(chǎn)生此狀態(tài)轉(zhuǎn)變
- pending ->rejected:在promise中調(diào)用reject()函數(shù)會(huì)產(chǎn)生此狀態(tài)轉(zhuǎn)變
promise的基本流程
Promise構(gòu)造函數(shù)
用于創(chuàng)建Promise對(duì)象,需要傳入一個(gè)執(zhí)行器函數(shù),函數(shù)中需要帶兩個(gè)參數(shù):resolved,rejected;用于修改Promise的狀態(tài)
let p = new Promise((resolved,rejected)=>{ console.log(1); resolved(); }) // 1 console.log(p); //Promise?{<fulfilled>: undefined}
Promise.prototype.then()
用于定義成功或失敗狀態(tài)的回調(diào)函數(shù),并返回一個(gè)新的Promise。
let p = new Promise((resolved,rejected)=>{ setTimeout(()=>{ resolved("ok"); },1000); }); p.then(res=>{ console.log(res); },err=>{ console.log(err); }) // ok let p = new Promise((resolved,rejected)=>{ setTimeout(()=>{ rejected("err"); },1000); }); p.then(res=>{ console.log(res); },err=>{ console.log(err); }) // err
Promise.prototypr.catch()
用于定義失敗的回調(diào),then方法的語法糖,相當(dāng)于.then(undefined,onRejected);
let p = new Promise((resolved,rejected)=>{ setTimeout(()=>{ rejected("err"); },1000); }); p.then(res=>{ console.log(res); }).catch(err=>{ console.log(err); }) // err
Promise.resolve()
返回一個(gè)成功或者失敗的Promise;
let p = Promise.resolve("ok"); //Promise?{<fulfilled>: ok} let p = Promise.resolve(Promise.reject('err')); //Promise?{<rejected>: 'err'}
Promise.all()
返回一個(gè)新的Promise,只有所有的Promise都成功才成功,否則有一個(gè)失敗就返回失敗的promise
let p = Promise.all([ Promise.resolve(1), Promise.resolve(2), Promise.resolve() ]) // Promise?{<fulfilled>: [1,2,undefined]} let p = Promise.all([ Promise.reject('err'), Promise.resolve(1), Promise.resolve(2) ]) // Promise?{<rejected>: 'err'}
Promise.race()
返回一個(gè)新的Promise,第一個(gè)完成的Promise的結(jié)果狀態(tài)就是最終的結(jié)果狀態(tài)。
let p = Promise.race([ Promise.resolve(1), Promise.resolve(2), Promise.resolve() ]) // Promise?{<fulfilled>: 1} let p = Promise.all([ Promise.reject('err'), Promise.resolve(1), Promise.resolve(2) ]) // Promise?{<rejected>: 'err'}
Promise原理實(shí)現(xiàn)
function Promise(excutor){ const self = this; self.status = "pending"; self.data = undefined; self.callbacks = [] // 用來保存所有待調(diào)用的回調(diào)函數(shù) function resolve(value){ if(self.status !== "pending"){ return; } // 改變狀態(tài) self.status = "fulfilled"; self.data = value; // 異步調(diào)用回調(diào)函數(shù) if(self.callbacks.length > 0){ setTimeout(()=>{ self.callbacks.forEach((obj)=>{ obj.onResolved(); }) }) } } function reject(reason){ if(self.status !== "pending"){ return; } // 改變狀態(tài) self.status = "rejected"; self.data = value; // 異步調(diào)用回調(diào)函數(shù) if(self.callbacks.length > 0){ setTimeout(()=>{ self.callbacks.forEach((obj)=>{ obj.onRejected(); }) }) } } // 立即同步調(diào)用執(zhí)行器函數(shù) try{ excutor(resolve,reject); }catch(error){ reject(error); } } Promise.prototype.then = function(onResolved,onRejected){ const self = this; onResolved = typeof onResolved === 'function'?onResolved:value=>value; onRejected = typeof onResolved === 'function'?onRejected:reason=>{throw reason}; // 返回promise對(duì)象 return new Promise((resolve,reject)=>{ // 執(zhí)行回調(diào)函數(shù) function handle(callback){ try{ const x = callback(self.data); // 如果返回對(duì)象為promise if(x instanceof Promise){ x.then(resolve,reject); }else{ resolve(x); } }catch(err){ reject(err); } } // 定義回調(diào)之前,狀態(tài)已經(jīng)改變 if(self.status == 'fulfilled'){ setTimeout(()=>{ handle(onResolved); }) }else if(self.status == 'rejected'){ setTimeout(()=>{ handle(onRejected); }) }else{ // 定義回調(diào)之前狀態(tài)還未改變 self.callbacks.push({ onResolved(){ handle(onResolved); }, onRejected(){ handle(onRejected); } }) } }) } Promise.prototype.catch = function(onRejected){ return this.then(undefined,onRejected) } Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ if(value instanceof Promise){ value.then(resolve,reject); }else{ resolve(value); } }) } Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason); }) } Promise.all = function(promises){ return new Promise((resolve,reject)=>{ let count = 0; let plen = promises.length; let values = new Array(plen); for(let i = 0;i < plen;i++){ Promise.resolve(promises[i]).then(value=>{ count++; value[i] = value; if(count == plen) resolve(values); },err=>{ reject(err); }) } }) } Promise.race = function(promises){ return new Promise((resolve,reject)=>{ for(let i = 0;i < promises.length;i++){ Promise.resolve(promises[i]).then(value=>{ resolve(value); },err=>{ reject(err); }) } }) }
異步函數(shù)async/await
ES8提出使用async和await解決異步結(jié)構(gòu)組織代碼的問題
async
async關(guān)鍵字用于聲明異步函數(shù),使用async關(guān)鍵字可以讓函數(shù)具有異步特征,但總體上其代碼仍然時(shí)同步求值。
async function fn1(){ console.log(1); } fn1(); console.log(2); // 1 // 2
async的返回值是一個(gè)Promise對(duì)象,如果函數(shù)中返回一個(gè)值value,則async則返回Promise.resolve(value);
await
因?yàn)楫惒胶瘮?shù)主要針對(duì)不會(huì)馬上完成的任務(wù),所以自然需要一種暫停和恢復(fù)執(zhí)行的能力。
await關(guān)鍵字必須在異步函數(shù)中使用。
async/await中真正起作用的是await。async關(guān)鍵字,無論從哪個(gè)方面來看,都是一個(gè)標(biāo)識(shí)符。異步函數(shù)中不包含await關(guān)鍵字,其執(zhí)行基本上跟普通函數(shù)沒有區(qū)別。
async function fn(){ console.log(1); let p = await Promise.resolve('ok'); console.log(p); console.log(2); } fn(); console.log(3); // 1 // 3 // ok // 2
js事件循環(huán)機(jī)制
js運(yùn)行機(jī)制
因?yàn)閖s時(shí)單線程的,在執(zhí)行代碼時(shí),將不同函數(shù)的執(zhí)行上下文壓入棧中來保證代碼的有序執(zhí)行,在執(zhí)行同步代碼的時(shí)候,如果遇到異步代碼,js并不會(huì)等待其返回結(jié)果,而是將異步代碼交給對(duì)應(yīng)的異步代碼處理線程處理,繼續(xù)執(zhí)行棧中的任務(wù)。
當(dāng)異步事件執(zhí)行完畢之后,再將異步事件對(duì)應(yīng)的對(duì)調(diào)加入異步執(zhí)行隊(duì)列,當(dāng)主棧中的函數(shù)執(zhí)行完畢后,會(huì)從異步執(zhí)行隊(duì)列中選擇任務(wù)來執(zhí)行。
宏任務(wù)和微任務(wù)
在任務(wù)隊(duì)列中異步回調(diào)分為2中:微任務(wù)和宏任務(wù)。也就是說任務(wù)隊(duì)列可以分為兩種:微任務(wù)隊(duì)列和宏任務(wù)隊(duì)列。
宏任務(wù):全局代碼,settimeout/setinterval,UI渲染,
微任務(wù):promise.then,catch,finally,process.nextTick
宏任務(wù)和微任務(wù)的執(zhí)行順序
代碼從開始執(zhí)行調(diào)用全局執(zhí)行棧,script標(biāo)簽內(nèi)的代碼做為宏任務(wù)執(zhí)行。
執(zhí)行過程中同步代碼立即執(zhí)行,異步代碼由異步處理線程處理然后放入任務(wù)隊(duì)列中。
執(zhí)行過程中同步代碼立即執(zhí)行,異步代碼放入任務(wù)隊(duì)列中。
先看任務(wù)隊(duì)列中的微任務(wù)隊(duì)列是否存在微任務(wù)
有微任務(wù):執(zhí)行微任務(wù)隊(duì)列中的所有微任務(wù)
微任務(wù)執(zhí)行過程中所產(chǎn)生的微任務(wù)放到微任務(wù)隊(duì)列中,繼續(xù)執(zhí)行。
如果沒微任務(wù),查看是否具有宏任務(wù),有的話執(zhí)行,沒有的話事件輪詢結(jié)束。
執(zhí)行過程中所產(chǎn)生的微任務(wù)放到微任務(wù)隊(duì)列中。
完成單個(gè)宏任務(wù)之后,執(zhí)行微任務(wù)隊(duì)列中的任務(wù)。
常見面試題
異步編程的實(shí)現(xiàn)方式有哪些
- 回調(diào)函數(shù):使用回調(diào)函數(shù)的方式的缺點(diǎn)是,是個(gè)回調(diào)函數(shù)嵌套的時(shí)候,會(huì)照成回調(diào)函數(shù)地獄,上下兩層的回調(diào)函數(shù)間的代碼耦合度太高,不利于代碼維護(hù)。
- promise:使用promise的方式可以嵌套的函數(shù)調(diào)用做為鏈?zhǔn)秸{(diào)用,但是使用這種方法,有時(shí)會(huì)找出多個(gè)then的鏈?zhǔn)秸{(diào)用,可能造成代碼的語義不夠明確。
- generator:配合yield實(shí)現(xiàn),可以在函數(shù)內(nèi)實(shí)現(xiàn)中斷。
- async/await:async函數(shù)是generator和promise實(shí)現(xiàn)的一個(gè)自動(dòng)執(zhí)行的語法糖,他內(nèi)部自帶執(zhí)行器,當(dāng)函數(shù)內(nèi)部執(zhí)行到await語句的時(shí)候,如果語句返回promise對(duì)象,那么函數(shù)將會(huì)等待promise對(duì)象的狀態(tài)變?yōu)閞esolve后再基于向下執(zhí)行。因此可以將異步邏輯轉(zhuǎn)化為同步的順序來寫,并且這個(gè)函數(shù)可以自動(dòng)執(zhí)行。
如何改變promise的狀態(tài)
- 調(diào)用resolve(value),pending ->fulfilled
- 調(diào)用reject(reason),pending ->rejected
- 拋出異常:如果當(dāng)前時(shí)pending就會(huì)變成rejected
改變promise狀態(tài)和指定回調(diào)函數(shù)誰先誰后
都有可能,正常情況下是先指定回調(diào)再改變狀態(tài),但也可以先改變狀態(tài)再指定回調(diào)。
什么時(shí)候能得到數(shù)據(jù)?
- 先指定回調(diào),當(dāng)狀態(tài)發(fā)生改變時(shí),回調(diào)函數(shù)就會(huì)調(diào)用,得到數(shù)據(jù)。
- 先改變狀態(tài),當(dāng)指定回調(diào)函數(shù)時(shí),回調(diào)函數(shù)就會(huì)調(diào)用,得到數(shù)據(jù)。
promise中改變狀態(tài)和指定回調(diào)函數(shù)時(shí)做了什么事情
在promise內(nèi)部定義了一個(gè)回調(diào)隊(duì)列。
在狀態(tài)改變時(shí),也就是調(diào)用onresolved或者onrejected函數(shù)時(shí),會(huì)執(zhí)行回調(diào)隊(duì)列中的函數(shù)。
當(dāng)指定回調(diào)函數(shù)時(shí),首先會(huì)判斷當(dāng)前的狀態(tài)是否是pending,如果是則將回調(diào)函數(shù)加入回調(diào)隊(duì)列,否則直接執(zhí)行根據(jù)狀態(tài)執(zhí)行onresolved或者onrejected函數(shù)。
這就是為什么無論是先改變狀態(tài)還是先指定回調(diào)函數(shù),都能得到最終的結(jié)果的原因。
promise.then() 返回的新promise的結(jié)果由什么決定
如果拋出異常,新的promise變?yōu)閞eject,reason為拋出的異常
如果返回的時(shí)非promise的任意值,新promise變?yōu)閒ulfilled,value為返回的值
如果返回promise的時(shí)另一個(gè)promise對(duì)象,此peomise的結(jié)果就會(huì)成為新promise的結(jié)果。
promise異常穿透
- 當(dāng)使用promise的then鏈?zhǔn)秸{(diào)用時(shí),可以在最后指定失敗的回調(diào)
- 前面任何操作處理異常都會(huì)傳到最后的回調(diào)中處理
中斷promise鏈
當(dāng)使用promise的then鏈?zhǔn)秸{(diào)用,在中間中斷,不在調(diào)用后面的回調(diào)函數(shù)。
辦法:在回調(diào)函數(shù)中返回一個(gè)pending狀態(tài)的promise對(duì)象
async/await對(duì)比promise的優(yōu)勢(shì)
- 代碼讀起來更像同步,Promise雖然擺脫了回調(diào)地獄,但是then方法的鏈?zhǔn)秸{(diào)用也會(huì)帶來額外的閱讀負(fù)擔(dān)
- Promise傳遞中間值非常麻煩,而async、await幾乎是同步的寫法
Promist.catch后面的.then還會(huì)執(zhí)行嗎
.then回執(zhí)行,因?yàn)閏atch方法返回的還是一個(gè)promise對(duì)象,依然支持鏈?zhǔn)秸{(diào)用。
Promise中,resolve后面的語句是否還會(huì)執(zhí)行?
會(huì)被執(zhí)行。如果不需要執(zhí)行,需要在 resolve 語句前加上 return。
JS為什么是單線程語言
JavaScript的誕生就是為了處理瀏覽器網(wǎng)頁的交互(DOM操作的處理、UI動(dòng)畫等), 設(shè)計(jì)成單線程的原因就是不想讓瀏覽器變得太復(fù)雜,因?yàn)槎嗑€程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果(兩個(gè)線程修改了同一個(gè)DOM節(jié)點(diǎn)就會(huì)產(chǎn)生不必要的麻煩),這對(duì)于一種網(wǎng)頁腳本語言來說這就太復(fù)雜了。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
JS是怎么實(shí)現(xiàn)異步異常運(yùn)行的
JavaScript是單線程的,但它所運(yùn)行的宿主環(huán)境—瀏覽器是多線程,瀏覽器提供了各種線程供Event Loop調(diào)度來協(xié)調(diào)JS單線程運(yùn)行時(shí)不會(huì)阻塞
談一談setInterval的問題
setTimeout的作用是每隔一段指定事件執(zhí)行一個(gè)函數(shù),但是這個(gè)執(zhí)行不是真的到了時(shí)間立即執(zhí)行,它真正的作用是每隔一段時(shí)間將事件加入事件隊(duì)列中去,只有當(dāng)當(dāng)前的執(zhí)行棧為空的時(shí)候,才能去從事件隊(duì)列中取出事件執(zhí)行。
所以可能會(huì)出現(xiàn)這樣的情況,就是當(dāng)前執(zhí)行棧執(zhí)行的時(shí)間很長(zhǎng),導(dǎo)致事件隊(duì)列里邊積累多個(gè)定時(shí)器加入的事件,當(dāng)執(zhí)行棧結(jié)束的時(shí)候,這些事件會(huì)依次執(zhí)行,因此就不能到間隔一段時(shí)間執(zhí)行的效果。
正對(duì)這種現(xiàn)象,我們可以使用setTimeout遞歸調(diào)用來模擬實(shí)現(xiàn)setInterval,這樣就能確保了只有一個(gè)事件結(jié)束,我們才會(huì)觸發(fā)下一個(gè)定時(shí)器事件。
function mySetInterval(fn,delay){ var timer = { flag = true; }; function interval(){ if(timer.flag){ fn(); setTimeout(interval,delay); } } setTimeout(intetval,delay); return timer; } function myClearInterval(timer){ timer.flag = false; }
以上就是深入探究JS中的異步編程和事件循環(huán)機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于JS異步編程和事件循環(huán)機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談TypeScript 用 Webpack/ts-node 運(yùn)行的配置記錄
這篇文章主要介紹了淺談TypeScript 用 Webpack/ts-node 運(yùn)行的配置記錄,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10使用JavaScript實(shí)現(xiàn)表格編輯器(實(shí)例講解)
下面小編就為大家?guī)硪黄褂肑avaScript實(shí)現(xiàn)表格編輯器(實(shí)例講解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08使用JavaScript實(shí)現(xiàn)node.js中的path.join方法
Node.JS中的 path.join 非常方便,能直接按相對(duì)或絕對(duì)合并路徑,有時(shí)侯前端也需要這種方法,如何實(shí)現(xiàn)呢?感興趣的朋友跟隨腳本之家小編一起看看吧2018-08-08JS實(shí)現(xiàn)分頁瀏覽橫向圖片(類輪播)實(shí)例代碼
這篇文章主要介紹了JS實(shí)現(xiàn)分頁瀏覽橫向圖片(類輪播)實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-11-11javascript實(shí)現(xiàn)在網(wǎng)頁中運(yùn)行本地程序的方法
這篇文章主要介紹了javascript實(shí)現(xiàn)在網(wǎng)頁中運(yùn)行本地程序的方法,實(shí)例分析了JavaScript基于ActiveXObject運(yùn)行本地程序的技巧,需要的朋友可以參考下2016-02-02