1. 程式人生 > 前端設計 >非同步程式設計方案

非同步程式設計方案

前言

我們知道Javascript語言的執行環境是"單執行緒"。也就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務。

但是如果有一個任務執行的時間過長,那麼後面的任務都必須等待他執行完畢才可以繼續執行程式,就會造成堵塞,影響使用者體驗,導致整個頁面卡在某個地方。

為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步和非同步。本文主要介紹非同步程式設計幾種辦法,並通過比較,得到最佳非同步程式設計的解決方案!

事件佇列

瞭解非同步任務前,我們先補充一下事件佇列的概念。 事件佇列分為巨集任務以及微任務

  • 巨集任務(佇列):macroTask,計時器結束的回撥、事件回撥、http回撥等等絕大部分非同步函式進入巨集佇列
  • 微任務(佇列):microTask,Promise.then,MutationObserver

微任務的優先順序比巨集任務高

如果執行的巨集佇列的任務有微佇列任務的話 會新增到微佇列中然後立馬執行它 微佇列沒有任務才能繼續執行巨集任務

所以可以得到的結論就是,JS主執行緒(同步)-->微任務-->巨集任務按這個順序執行。

至於為什麼要區分巨集任務和微任務而不把兩者放在一起可能是為了優先順序吧,微任務要儘快完成,巨集任務按時完成這樣,個人理解X.X

非同步執行機制

  • 所有同步任務都在主執行緒上執行,形成一個執行棧。
  • 主執行緒之外,還存在一個"任務佇列"。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
  • 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
  • 主執行緒不斷重複上面的第三步。

同步和非同步

瞭解了事件佇列就對同步和非同步特別清晰了,我用自己理解的話來說把QAQ

  • 同步: 主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務
  • 非同步: 不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有等主執行緒任務執行完畢,"任務佇列"開始通知主執行緒,請求執行任務,該任務才會進入主執行緒執行
  • 同步為主線任務,非同步為支線任務,主線你必須一步一步往下走,而支線你可以走走停停,我瞎說的?理解下即可

回撥函式(Callback)

  • 優點: 程式碼簡單明瞭
  • 缺點: 回撥地獄,程式碼不易維護,不能try-catch,不能直接 return
ajax('XXX1',() => {
    // callback 函式體
    ajax('XXX2',() => {
        // callback 函式體
        ajax('XXX3',() => {
            // callback 函式體
        })
    })
})
複製程式碼

程式碼層層巢狀,看著就刺激,回撥多了後可讀性很差,流程會很混亂。

這裡補充下同步回撥和非同步回撥

  • 同步回撥:
    • 理解: 立即執行,完全執行完了才結束,不會放入回撥佇列中
    • 例子: 陣列遍歷相關的回撥函式 / Promise的excutor函式
  • 非同步回撥:
    • 理解: 不會立即執行,會放入回撥佇列中將來執行
    • 例子: 定時器回撥 / ajax回撥 / Promise的成功|失敗的回撥
// 同步回撥,不會放入回撥佇列,而是  立即執行
const arr = [1,2,3]
arr.forEach(item => console.log(item))

// 非同步回撥,會放入回撥佇列,所有同步執行完後才可能執行,放入佇列等待同步玩在執行
setTimeout(() => {
 console.log('timout 回撥')
},0)
複製程式碼

事件監聽

任務的執行不取決於程式碼的順序,而取決於某個事件是否發生

  • 優點: 好理解,能繫結多事件,每個事件可以指定回撥函式
  • 缺點: 整個程式都要變成事件驅動型,執行流程會變得很不清晰

實現原理也是利用定時器的原理去把func放入事件佇列裡,等全部執行完畢之後,才會執行事件佇列裡的方法

f1.on('done',fn);
//當func執行時,觸發監聽,執行了fn,進行程式碼的改寫

function func(){
    setTimeout(function () {
        // f1的邏輯程式碼
        func.trigger('done');
    },1000);
}
複製程式碼

釋出/訂閱

Vue中也用了這種模式,訂閱者可以訂閱某個人物,某個任務完成時,會對訂閱者進行通知,從而知道什麼時候自己可以開始執行。

//訂閱done事件
$('#app').on('done',function(data){
  console.log(data)
})
//釋出事件
$('#app').trigger('done,'haha')
複製程式碼

Promise

Promise本身並不是非同步的,它只是實現了對非同步回撥的統一封裝

  • 優點:
    • 一旦狀態改變,就不會再變,任何時候都可以得到這個結果 .then避免了層層巢狀的回撥函式,可讀性比callback回撥函式好,可以進行try-catch
  • 缺點:
    • 無法取消 Promise ,錯誤需要通過回撥函式來捕獲

Promise 物件有三種狀態,pending(進行中)、fulfilled(已成功)和rejected(已失敗)。

Promise 的狀態一旦改變之後,就不會在發生任何變化,將回調函式變成了鏈式呼叫。

let p = new Promise((resolve,reject) => {
    console.log('pending(進行中)')
})

let p = new Promise((resolve,reject) => {
  reject('rejected(已失敗') // 丟擲錯誤也會被後面失敗回撥捕獲到
})

let p = new Promise((resolve,reject) => {
  resolve('fulfilled(已成功)')
})
複製程式碼

使用.then獲取結果

let p = new Promise((resolve,reject) => {
    resolve('1')
}).then(res=>console.log(res))//1
複製程式碼

使用.catch錯誤捕獲

let p = new Promise((resolve,reject) => {
    throw new Error('Error')
}).catch(console.log(error))
複製程式碼

return

Promise.resolve(1)
    .then(res => {
        console.log(res)
        return 2 //包裝成 Promise.resolve(2)
    })
    .then(res => console.log(res))
//等同於
let p = new Promise((resolve,reject) => {
        resolve('1')
    }).then(res => {
        console.log(res)
        return 2 //包裝成 Promise.resolve(2)
    }).then(res => console.log(res))
複製程式碼

then鏈式

let p = new Promise((resolve,reject) => {
    resolve('1')
}).then(res=>{
    console.log(res)//1
    return 20
}).then(res=>{
    console.log(res)//20
    return 30
})

p.then(res=>{
    console.log(res)//30
    return 40
})

p.finally(res=>{
    console.log('無論如何都會執行')
    throw new Error('接招吧 catch')
}).catch(err=>console.log(err))
複製程式碼

生成器Generators/ yield

生成器一句話 交出函式的執行權

  • 優點: 可以控制函式的執行 (自然的同步 / 順序方式表達任務的一系列步驟)
  • 缺點: 呼叫太頻繁,不能一次性得到結果,async.await出來後基本不用了,只有在react-sage中有見過了-.-

在函式名上加上*

 function* CreateGenerator() {
    console.log('one')
    let resilt =yield 1
    console.log('two')
    resilt =yield 2
    console.log('three')
    resilt =yield 3
}
let generator = CreateGenerator();//交出執行權

console.log(generator.next())//one        { value: 1,done: false }
console.log(generator.next())//two        { value: 2,done: false }
console.log(generator.next())//three      { value: 3,done: true }
console.log(generator.next())//           { value: undefined,done: true }
複製程式碼
  • Generator 函式除了狀態機,還是一個遍歷器物件生成函式。
  • 可暫停函式,yield可暫停,next方法可啟動,每次返回的是yield後的表示式結果。
  • yield表示式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值。

解決回撥地獄

function *fetch() {
    yield ajax(url,() => {})
    yield ajax(url1,() => {})
    yield ajax(url2,() => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

複製程式碼

Async/Await

  • 優點: 程式碼清晰,避免了promise多次.then
  • 缺點:await 將非同步程式碼改造成同步程式碼,如果多個非同步操作沒有依賴性而使用 await 會導致效能上的降低。

生成器語法糖 async 函式就是將 Generator 函式的星號(*)替換成 async,將 yield 替換成await。

單獨使用async 可以發現預設返回的就是Promise物件

async function func(){}
console.log(func())//Promise { undefined }
複製程式碼

await

無法單獨使用 需要配合async

function fn2() {
    return Promise.resolve(123)
  }

  async function fn3() {
    const result = await fn2();//得到的值為 Promise成功/失敗的值  失敗用try catch捕獲即可
    console.log(result)
  }
  fn3()//123
複製程式碼

注意點

  • async函式返回的是 Promise 物件。
  • async函式執行的時候,一旦遇到await會先執行該語句,執行完後再接著執行函式體內await後面的語句。
  • await執行完才執行後面的程式碼意味著他們會被放到為佇列中

##Promise練習題

async function async1() {
    console.log('async1 start');
    await async2();//立即執行該語句
    console.log('async1 end');//await後面的程式碼放到微任務佇列
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
},0)
async1();
new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('script end');
複製程式碼
// 同步 'script start'   async1 start  async2  promise1  script end
// 巨集佇列  setTimeout
// 微佇列  async1 end   promise2
// 同-微-巨集
複製程式碼

over總結

  • JS 非同步程式設計進化史:callback -> promise -> generator -> async + await
  • 用async+await完事
  • 端午節安康 粽子太香啦~