JavaScript 單執行緒之非同步程式設計

地靈 發表於 2022-05-17
JavaScript

Js 單執行緒之非同步程式設計

先了解一個概念,為什麼 JavaScript 採用單執行緒模式工作,最初設計這門語言的初衷是為了讓它執行在瀏覽器上面。它的目的是為了實現頁面的動態互動,而互動的核心是進行 Dom 操作,這也就決定了必須使用單執行緒模式,否則就會出現很複雜的執行緒同步問題。假如有兩個同步執行緒工作,其中一個執行緒進行了新增 demoA,另一個執行緒進行了刪除 demoA,此時瀏覽器就無法明確以哪個執行緒的工作為準,所以 JavaScript 就成了單執行緒模式工作,單執行緒工作的優點就是,多個任務,同步執行,簡單安全,同時缺點也很明顯,如果有一個任務耗時的時間非常非常久,那我們就得排隊等待,這就會導致整個頁面出現像卡死這種情況。
為了解決這種阻塞問題,JavaScript 將任務的執行模式 分成了兩種:
  • 同步模式(Synchronous)
  • 非同步模式(Asynchronous)

同步模式

指的是程式碼中的任務依次執行,後一個任務必須等待前一個任務結束才能開始執行,程式的執行順序和程式碼的編寫順序完全一致。舉個簡單栗子
console.log('start')

function part1 () {
    console.log('loading part1')
}

function part2 () {
    console.log('loading part2')
    part1()
}

part2()

console.log('end')

// 順序結果 start --- loading part2 --- loading part1 --- end
// 同時就會觸發概念裡面那個問題,如果有一個任務耗時特別長,就會出現頁面卡死情況,為了避免這種情況,也就有了另一種模式,非同步

非同步模式

指的是,不會去等待這個任務的結束才開始下一個任務,對於耗時操作,開啟之後,立即執行下一個任務,後續邏輯一般會通過回撥函式的方式定義。用程式碼來舉個例子
console.log('start')

setTimeout (function timer1 () {
    console.log('timer1 invoke')
}, 2000)

setTimeout (function timer2 () {
    console.log('timer2 invoke')
    
    setTimeout (function inner () {
        console.log('inner')
    }, 1000)
}, 1000)

console.log('end')

// 結果 start --- end --- timer2 invoke --- timer1 invoke --- inner
// 同步任務開始執行,碰到計時器丟到任務佇列中,同步任務執行完畢,沒有微任務,開始執行巨集任務,根據倒數計時時間,以及裡面是否還有其他對應操作,直到結束。
// js 執行引擎先去做完呼叫棧裡面所有的任務,通過事件迴圈從訊息佇列中再取出一個任務出來繼續執行,以此類推,同時還能隨時往訊息佇列中放入新的任務,排隊等待執行,以上就是基本的概念。

回撥函式

所有非同步程式設計方案的根本都是回撥函式,回撥函式可以理解為一件你想要做的事情,定義他的執行規則,然後交給執行者去執行的函式。舉個例子
function foo (callback) {
    setTimeout(function () {
        callback()
    }, 3000)
}

foo(function () {
    console.log('這就是一個回撥函式')
    console.log('呼叫者定義這個函式,執行者執行這個函式')
    console.log('其實就是呼叫者告訴執行者非同步任務結束後應該做什麼')
})
// 隨之而來,就出現了另一個問題,如果說函式A為回撥函式,但是他又接受函式B作為引數,函式B又有一個函式C,這就形成了層層巢狀,就會出現回撥地獄。為了解決這個問題,出現了 Promise

Promise

由 CommonJS 社群提出的 Promise,在ES2015 中被標準化,成為語言規範
所為的 Promise 就是一個物件,用來去表示一個非同步任務最終結束過後究竟是成功還是失敗,初始化由內部對外界作出一個承諾 pending 待定狀態,最終成功之後,會變為 Fulfilled 同時會有一個 onFulfilled 回撥,如果失敗,會變為 Rejected,也會有一個 onRejected 回撥,而且一旦由 pending 變為成功或者失敗,其狀態就不會更改了。
// 基本栗子
const promise = new Promise(function (resolve, reject) {
    resolve(200) // 成功
    
    // reject(new Error('rejected')) // 失敗
})
// 通過.then 呼叫
promise.then(function (value) {
    console.log('resolved', value)
}, function(error) {
    console.log('rejected, error')
})
註釋失敗程式碼那一行,結果就是 200成功
註釋成功程式碼那一行,結果就是 失敗
裡面的.then方法,是返回一個全新的 Promise 物件,它的目的是為了實現一個 Promise 鏈條,也就是承諾成功或失敗之後再返回一個新的承諾,也就是鏈式呼叫的方式來解決回撥地獄巢狀。總結以下幾點:
  • Promise 物件的 then 方法會返回一個全新的 Promise 物件
  • 後面的 then 方法就是在為上一個 then 返回的 Promise 註冊回撥
  • 前面 then 方法中回撥函式的返回值作為後面 then 方法回撥的引數
  • 如果回撥函式中返回的是 Promise,那後面 then 方法的回撥等待它的結束
對於異常處理 onRejected 會自動彈出異常,也還有常用的 .catch 來處理異常

Promise 靜態方法

建立一個成功的 Promise.resolve() 物件,反之還有一個 Promise.reject() 建立一個失敗的物件

Promise 並行執行

Promise.all([]) 接收一個陣列,每一個元素都是一個 Promise 物件,也就是非同步任務,當裡面所有的非同步任務結束後這個方法結束後會返回一個全新的 Promise 物件,這個 Promise 物件 會返回一個陣列的結果,裡面的每一項都對應著,每一個非同步任務的結果,但是隻要有一項失敗,那就會立即丟擲錯誤。
Promise.race([]) 方法同上,不同的是,只有一項成功,就會返回最新成功的那個結果。