你不知道的JS讀書筆記(中)——異步

基礎(chǔ):
瀏覽器 -- 多進(jìn)程,每個tab頁獨(dú)立一個瀏覽器渲染進(jìn)程(瀏覽器內(nèi)核)

每個瀏覽器渲染進(jìn)程是多線程的,主要包括:
GUI渲染線程

JS引擎線程

  • 也稱為JS內(nèi)核,負(fù)責(zé)處理Javascript腳本程序。(例如V8引擎)
  • JS引擎線程負(fù)責(zé)解析Javascript腳本,運(yùn)行代碼。
  • JS引擎一直等待著事件循環(huán)隊列中任務(wù)的到來,然后加以處理,一個Tab頁(renderer進(jìn)程)中無論什么時候都只有一個JS線程在運(yùn)行JS程序

注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時間過長,這樣就會造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。

事件觸發(fā)線程

  • 歸屬于瀏覽器而不是JS引擎,用來控制事件循環(huán)(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)
  • 當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊、AJAX異步請求等),會將對應(yīng)任務(wù)添加到事件線程中
  • 當(dāng)對應(yīng)的事件符合觸發(fā)條件被觸發(fā)時,該線程會把事件添加到事件循環(huán)隊列的隊尾,等待JS引擎的處理

注意,由于JS的單線程關(guān)系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當(dāng)JS引擎空閑時才會去執(zhí)行)

定時器觸發(fā)線程

  • setInterval與setTimeout所在線程
  • 瀏覽器定時計數(shù)器并不是由JavaScript引擎計數(shù)的,(因?yàn)镴avaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會影響記計時的準(zhǔn)確)
  • 因此通過單獨(dú)線程來計時并觸發(fā)定時(計時完畢后,添加到事件循環(huán)隊列中,等待JS引擎空閑后執(zhí)行)

注意,W3C在HTML標(biāo)準(zhǔn)中規(guī)定,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms。

異步HTTP請求線程

  • 在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求
  • 將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個回調(diào)再放入事件循環(huán)隊列中。再由JavaScript引擎執(zhí)行。

正文:

異步

分塊程序、事件循環(huán)、并行

程序中現(xiàn)在運(yùn)行的部分和將來運(yùn)行的部分之間的關(guān)系就是異步編程的核心

ajax請求的異步:發(fā)出請求時,將來才能獲得請求返回結(jié)果
—— 應(yīng)該避免同步ajax請求,瀏覽器UI會被鎖定并阻塞所有用戶交互

異步基于事件循環(huán)機(jī)制 —— JavaScript引擎只是一個按執(zhí)行代碼片段的環(huán)境,而這個需求是由其運(yùn)行環(huán)境決定的:當(dāng)遇到需要等待某些條件(網(wǎng)絡(luò)數(shù)據(jù)、定時器計時到期等),條件滿足后,運(yùn)行環(huán)境才會把回調(diào)函數(shù)插入到事件循環(huán)隊列中(參考基礎(chǔ)資料 事件觸發(fā)線程、定時器線程)

書中提到:ES6精確指定了事件循環(huán)的工作細(xì)節(jié),在技術(shù)上將其納入了javascript引擎的勢力范圍,不是只由宿主環(huán)境管理,怎么理解?? 和前文基礎(chǔ)部分對于瀏覽器渲染進(jìn)程包含的多個線程之間關(guān)系有一定出入么?

異步并行意義完全不同??!
并行 —— 同步執(zhí)行 ,對于多線程編程,內(nèi)存的共享提升了復(fù)雜度
異步 —— 交替調(diào)度

Javascript是單線程執(zhí)行,從不跨線程共享數(shù)據(jù)

var a = 1,b=2

function foo(){
  a = a + 1;
  b = b*2
}

function bar(){
  a = a * 2;
  b = b + 1;
}

// ajax(...)是某個庫中提供的函數(shù)
ajax( 'http://some.url.1', foo );
ajax( 'http://some.url.2', bar );

由于js的單線程執(zhí)行特性,foo() bar()函數(shù)內(nèi)部的代碼具有原子性
雖然foo() bar()存在競態(tài)導(dǎo)致 a,b的最終值并不確定
但這種不確定性是在函數(shù)執(zhí)行順序上的(兩個ajax返回的順序)

1.4 并發(fā)(讓人有點(diǎn)懵)

旨在說明 “并發(fā)” 的幾種情況,在js中看似并發(fā)實(shí)際是由單線程事件循環(huán)機(jī)制實(shí)現(xiàn)的
均以兩個ajax請求的回調(diào)函數(shù)內(nèi)執(zhí)行不同代碼為例:

  • 非交互: 兩個并發(fā)任務(wù)間彼此獨(dú)立不相關(guān),這時不確定性是完全可以接受的
  • 交互: 并發(fā)任務(wù)間彼此通過作用域或者DOM間接交互,比如都向數(shù)組中插入一條數(shù)據(jù),這時數(shù)組中條目的順序就和并發(fā)任務(wù)執(zhí)行順序有關(guān)了,在需要保證正確交互順序的場景需要加入?yún)f(xié)調(diào)代碼
  • 協(xié)作: 并發(fā)協(xié)作,將一個會長期執(zhí)行的任務(wù)分割為多個步驟或多批任務(wù)執(zhí)行,以便占領(lǐng)事件循環(huán)隊列太久時間,如setTimeout(...0)進(jìn)行異步調(diào)度
任務(wù)隊列

ES6引入,任務(wù)隊列(job queue) —— 掛在事件循環(huán)隊列的每個tick之后的一個隊列, 在事件循環(huán)的每個tick中,可能出現(xiàn)的異步動作不會添加一個完整的新事件到事件循環(huán)隊列中,而是會在當(dāng)前tick的任務(wù)隊列末尾添加一個項(xiàng)目(一個任務(wù))

理論上說,任務(wù)隊列可能導(dǎo)致無限任務(wù)循環(huán)


image.png

回調(diào)

回調(diào)函數(shù)是javascript異步的基本單元

這章主要討論回調(diào)這種自javascript誕生以來就存在的異步方式存在什么問題(思維上的(大腦搞不定)、寫法上的(嵌套、硬編碼)、信任問題(控制反轉(zhuǎn))等),以引出下一章對新的異步方式Promise的討論

  • 給異步方法async傳入continuation,當(dāng)異步方法async結(jié)束時主動調(diào)用continuation執(zhí)行
  • 事實(shí)上async并不關(guān)心其結(jié)束對其他模塊有什么影響,但使用回調(diào)的方式,async在結(jié)束時必須主動調(diào)用所有傳入的continuation
  • 如果有數(shù)個異步任務(wù)需要鏈?zhǔn)綀?zhí)行,代碼寫法上很容易形成callback hell,但這不是重點(diǎn),即使把嵌套寫法解開寫成多個函數(shù)調(diào)用的方式,也同樣不易于閱讀;而且還有另一個問題,當(dāng)鏈?zhǔn)交卣{(diào)中一個斷掉時,如何處理錯誤情況?需要硬編碼在各個函數(shù)中進(jìn)行處理,代碼復(fù)雜度大大增加
  • 另外還有一個信任問題,有些情況下異步任務(wù)是第三方提供,使用回調(diào)其實(shí)就是將代碼控制器交給了第三方處理——控制反轉(zhuǎn)
調(diào)用回調(diào)過早
調(diào)用回調(diào)過晚(或不被調(diào)用)
調(diào)用回調(diào)次數(shù)過少或過多
未能傳遞所需的環(huán)境和參數(shù)
吞掉可能出現(xiàn)的錯誤和異常

針對回調(diào)的問題可以用一些特定邏輯來解決:
比如回調(diào)傳入兩種:正確、錯誤 或 error-first模式
針對回調(diào)過早問題:強(qiáng)制回調(diào)封裝

但這些解決方案并不通用、且需要每次重復(fù)編寫、難以復(fù)用

幾個問題:

  • 為什么setTimeout(...)定時器可能精度不高?

setTimeout(...)只是保證了回調(diào)函數(shù)不會在指定時間間隔之前執(zhí)行,時間間隔之后插入到事件循環(huán)隊列中,但此時隊列中可能有多個項(xiàng)目

  • 任務(wù)隊列和事件循環(huán)隊列是不同的概念,怎么區(qū)分?
    個人理解:

事件循環(huán) -> 基于事件循環(huán)隊列,其維護(hù)者不是js引擎,當(dāng)js引擎執(zhí)行到需要等待某個條件完成的代碼時(ajax,setTimeout等),會交給當(dāng)前運(yùn)行環(huán)境執(zhí)行并提供一個回調(diào)函數(shù)(運(yùn)行環(huán)境可能使用其他線程),js引擎繼續(xù)執(zhí)行接下來的代碼;條件滿足后,運(yùn)行環(huán)境將回調(diào)函數(shù)插入到事件循環(huán)隊列的末尾,js引擎會從事件循環(huán)隊列中獲取代碼執(zhí)行

任務(wù)隊列 -> 首先明確任務(wù)隊列的位置:掛在事件循環(huán)隊列的每個tick之后的一個隊列,當(dāng)出現(xiàn)一個新的任務(wù)時,總是掛到當(dāng)前事件循環(huán)tick結(jié)尾處!搞清楚以下代碼的執(zhí)行順序就能初步了解任務(wù)隊列與事件循環(huán)隊列的關(guān)系了:

console.log('A');
setTimeout( function(){
  console.log('B');
},0)
var p = new Promise((resolve, reject)=>{
  console.log('C');
  return Promise.resolve(console.log('D'));  // 嵌套promise
})


輸出順序: A C D B

另外,任務(wù)隊列是由js引擎自己控制的0 0 @TODO 了解具體實(shí)現(xiàn)方式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容