JS非同步原理以及解決方案
JS是單執行緒的
JavaScript最大的特點就是他是單執行緒的,即同一時間只能做一件事。
為什麼js被設計成單執行緒的語言呢?
js作為一門指令碼語言,主要的用途是完成使用者互動以及DOM操作,而假如js同時擁有多個執行緒,其中一個執行緒在修改DOM節點,而另外一個執行緒也在修改這個DOM節點,那麼這時候就發生了衝突。單執行緒的特點,避免了js的複雜性,也是js的核心特徵。
補充 程序與執行緒
程序:是CPU資源分配的最小單位,即能夠擁有資源和獨立執行的最小單位
執行緒:CPU排程的最小單位,執行緒建立在程序的基礎上的一次程式執行單位,一個程序可以有多個執行緒。
瀏覽器是多程序的
複製程式碼
同步與非同步
程式的執行模式分為同步執行和非同步執行。
同步執行
即順序的執行程式碼,從上到下,依次執行語句,如下:
let a = 1
console.log(a)
let b = 2
console.log(b)
輸出結果為:1 2
複製程式碼
非同步執行
不會等待任務結束才開始下一個任務,對於耗時操作,在任務開啟後立即執行下一個任務,耗時任務的後續邏輯會通過回撥函式的方式定義,如下:
console.log(‘start’)
setTimeout(() => {
console.log('hello')
},100)
console.log('end')
輸出結果為:start end hello
複製程式碼
在這裡setTimeout開啟了一個非同步任務,在同步程式碼執行完畢後,100ms後執行了setTimeout的回撥函式。
執行過程
單執行緒意味著,所有的任務需要排隊執行,前一個結束,才會執行下一個,如果任務耗時很長,後一個任務就不得不一直等著。 而這種排隊等待並不是因為計算量大,而是因為一些IO裝置很慢(如ajax讀取資料),不得不等到結果出來,在往下執行,所以js語言設計者意識到,這時的主執行緒可以掛起等待中的任務,先執行排在後面的任務,等到掛起的任務有結果了,在把它執行下去,因而js中的任務也分成了兩類:同步任務和非同步任務。
任務執行機制
- 1、所有同步任務都在主執行緒上執行,形成一個執行棧。
- 2、主執行緒之外,還存在一個“任務佇列”,只要非同步任務有了結果,就在“任務佇列”裡註冊一個事件。
- 3、當執行棧中所有的任務都執行完畢(執行棧清空),系統會讀取“任務佇列”中的事件,對應事件的非同步任務,進入結束等待狀態,然後進入執行棧,開始執行。
- 4、主執行緒不斷的重複第三步。
這個主執行緒迴圈讀取事件的執行機制,也被稱作事件迴圈。
瀏覽器中js程式碼的執行過程
- js程式碼執行時,建立記憶體堆和執行棧
- 順次將js指令碼中的程式碼壓入執行棧,執行後彈出
- 當遇到非同步任務時,呼叫相應的webAPI,並開啟對應的執行緒去執行非同步任務
- 當非同步任務執行完畢,則在任務佇列中註冊事件
- 執行棧清空時,讀取任務佇列中的事件,並將回撥函式壓入執行棧執行,重複讀取執行,直到任務佇列中沒有等待的任務。
巨集任務 微任務
巨集任務 參與了事件迴圈的任務,需要排隊的執行的任務,如任務佇列中的任務。 建立巨集任務的操作:I/O、setTimeout、setInterval、setImmediate(node), requestAnimationFram(瀏覽器)
微任務 不參與事件迴圈,能夠跟在巨集任務後面執行的任務,不需要重新排隊。 建立微任務的操作:process.nextTick(node)、mutationObserver(瀏覽器)、Promise.then catch finally
巨集任務在程式執行過程中建立,微任務可以在巨集任務中建立也可以在微任務中建立,微任務會被單獨註冊到微任務佇列中,在當前巨集任務執行完畢後,會立即執行當前微任務列表中的微任務。
非同步解決方案
//當前有一個ajax呼叫封裝方法
function sendRequest(url,callback) {
let xhr = new XMLHttpRequest()
xhr.open('GET',url)
xhr.onload = callback
}
複製程式碼
回撥函式
sendRequest('/api/user',(response) => {
//處理response
})
複製程式碼
問題:容易引發回到地獄,使得程式碼難以維護,如下
sendRequest('/api/user',(response1) => {
sendRequest('/api/user',(response2) => {
sendRequest('/api/user',(response3) => {
//處理response
})
})
})
複製程式碼
pormise
處理多個非同步
let p1 = new Promise((resolve,reject) => {
sendRequest('/api/user',(response) => {
//resolve or reject
})
})
let p2 = new Promise((resolve,(response) => {
//resolve or reject
})
})
Promise.all([p1,p2]).then(([res1,res2]) => {
//處理response
})
複製程式碼
promise的then catch finally 都是微任務
promise特性
- promise有三種狀態,pending,fulfilled,rejected,成功狀態和失敗狀態不可互相轉變
- promise.then會返回一個新的promise,then的鏈式呼叫中,後面的then方法就是在為上一個then方法返回的promise註冊回撥,前面的then方法中回撥的返回值,會作為後面then方法回撥的引數,如果回撥中返回了一個promise,則後面的then方法的回撥會等待它的結果。
- then的第二個引數,是捕獲promise異常的回撥,它不能捕獲then中第一個回撥函式中的錯誤,catch方法可以捕獲promise的異常,也可以捕獲then中回撥函式的異常。
- 在promise的then和catch方法中,引數期望是函式,當傳入的引數不是函式的時候會發生值透傳,並將該值傳給下一個then或catch方法
generator
function *gen () {
let res = yield sendRequest('/api/user',(response) => response)
//處理res
}
let runGen = gen()
runGen.next()
複製程式碼
生成器函式特性
- 函式名前有一個*
- 通過呼叫函式生成一個控制器,如runGen
- 呼叫next()方法開始執行函式
- 遇到yield 函式執行暫停
- 再次遇到next()繼續執行函式
async/await
ES7中的新特性
async callUser() {
const result = await sendMessage('/api/user',(response) => response)
//處理 result
}
複製程式碼
特性
- async定義一個非同步函式,返回一個隱式的promise
- await必須在async定義的函式中使用
- await指令會暫停函式的執行,並等待Promise執行,然後繼續執行非同步函式,返回結果
- async/await是genterator的語法糖
——————————————————————個人雜記—————————————————————