JS異步的執行原理和回調詳解
一、JS異步的執行原理
我們知道JavaScript是單線程的,而瀏覽器是多線程的。單線程執行任務需要一個個排隊進行,假如一個任務需要很長時間執行(像ajax需要較長時間),會直接導致無響應,後面的任務一直在等待執行。這時候就需要用到異步。
想瞭解異步,首先我們要知道瀏覽器有最基本的三個常駐線程: JS引擎線程,事件觸發線程,GUI渲染線程。
其中JS引擎線程和事件觸發線程共同構成瞭一種事件循環機制,而GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新保存在一個隊列中,當JS引擎空閑時,立即被執行。
我們從它的事件循環機制解析:
JS引擎線程中分為同步和異步任務:
1.同步任務全部通過主線程執行,形成執行棧。
2.當有異步任務時交給異步進程(WebAPIs):包含事件觸發線程或者定時器線程等處理,形成任務隊列。
3.當執行棧中的任務全部處理完成,主線程為空閑的時候,會從任務隊列中提取任務到執行棧中執行。
通俗來說,JavaScript除瞭主線程之外還存在一個任務隊列,任務隊列存放需要異步執行的內容,執行完主線程後,就會不斷循環掃描執行任務隊列的任務,直至隊列清空。
畫解:
如圖小明因為學習耗時長會,如果沒做完就會一直無法玩DNF遊戲瞭,就把學習放到瞭異步任務隊列中,等玩完遊戲(主線程)再學習(任務隊列)。期間母親添加學習事件(DOM事件),小明每完成一個學習任務就看看還有啥任務(循環掃描),直至最後做完.
下面再看一個例子(瀏覽器刷新不斷點擊按鈕):
let myData = null //ajax請求 function ajax() { //騰訊新冠實時數據接口,僅做學習 axios.get('https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list?modules=chinaDayList,chinaDayAddList,nowConfirmStatis,provinceCompare') .then(data => { console.log("ajax返回成功"); myData = data.data console.log(myData); }) .catch(error => { console.log("ajax返回失敗"); }) } console.log(myData); ajax() setTimeout(() => { console.log('定時器'); }, 2000); console.log(myData); const btn = document.querySelector('button') btn.onclick = () => { console.log("點擊瞭"); }
null
null
ajax返回成功
Object
點擊瞭
定時器
點擊瞭
可以看到,console在主線程中是同步執行的,先執行,而在主線程外的任務隊列,存放著異步執行的內容,這裡是setTimeout,ajax和DOM事件,按照任務隊列順序執行(循環掃描隊列)。
為什麼要循環掃描呢?
通過點擊事件可以看出,當用戶進行交互時(點擊事件,滾動事件,窗口大小變化事件等),會向事件循環中的任務隊列添加新事件,然後等待執行,所以需要循環掃描。
二、JS異步中的回調
既然異步都是放在最後的任務隊列執行,那麼我們很多邏輯就難以實現,這時候我們需要處理這種異步邏輯,最常用的方式是回調——回頭調用。
回調函數:簡單來說就是,函數A中傳入函數B作為參數時,函數B即為A函數執行的回調函數。回調有嵌套回調和鏈式回調兩種。
下面是回調的一個簡單用法:
let myData = null console.log(myData); setTimeout(() => { console.log('定時器'); }, 2000); const btn = document.querySelector('button') btn.onclick = () => { console.log("點擊瞭"); } let name = "張三" function hr(callback) { setTimeout(() => { console.log(`我是${name}`); callback(); }, 2001); } console.log(myData); function gj() { console.log(`${name}你好,我是李四,認識一下吧`); } hr(gj)
null
null
點擊瞭
定時器
我是張三
張三你好,我是李四,認識一下吧
點擊瞭
很明顯的看到,當我們函數需要用到數據的時候就用到瞭回調,這裡用到的是異步回調。
回調雖然是解決異步常用的方法,可是伴隨著JS日益復雜的需求。同步異步需要越來越多的回調實現邏輯。同異步的混雜和過多的回調嵌套和縮進使得代碼變得難以解讀和維護,形成“回調地獄”。
我們看一個例子:
const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };
大多數人光是看到上面的代碼就感受到瞭腦子凍結的滋味,如果一個項目裡擁有上百個這樣的代碼塊,過一段時間,我相信連編寫他的人都會頭疼。來到自己的項目就像是來到瞭地獄。
最主要的是,與此同時回調還存在信任問題,他把執行控制權交給瞭某個第三方(比如ajax)。為瞭解決信任問題,我們必須在程序寫各種邏輯來解決回調帶來的信任問題。
·調用過早
·調用過完
·調用次數過多過少,沒有把需要的參數成功傳給回調函數,
·可能出現的錯誤被吞。
可以發現寫特定邏輯來解決特定的信任問題,已經使得難度大於本身應用價值瞭,還會造成代碼冗雜,可讀性差等問題。
綜上:回調解決異步存在缺陷:
1)不符合人對任務處理的邏輯思維
2)回調帶來的信任問題。
面對回調日益明顯的弊端,ES6更新瞭Promise用來解決異步問題。下一篇寫ES6——Promise。
總結
到此這篇關於JS異步的執行原理和回調的文章就介紹到這瞭,更多相關JS異步執行原理回調內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JavaScript的單線程和異步詳細
- 前端Vue中常用rules校驗規則詳解
- Javascript事件的捕獲方式和冒泡方式詳解
- JS異步編程Promise對象詳解
- JavaScript異步隊列進行try catch時的問題解決