1. 程式人生 > 前端設計 >JS非同步原理以及解決方案

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的語法糖

——————————————————————個人雜記—————————————————————