異步編程(Promise、Generator、Async與Await)

篇頭

我們工作或面試中,經(jīng)常遇到這些問題:如何更編寫更優(yōu)美的異步代碼?你了解promise, async/await嗎?知道他們的執(zhí)行順序嗎?那么接下來我們將從淺入深逐步吃透這些問題。

引子

眾所周知 Javascript 是采用的單線程的工作模式?那么為什么會用這種模式呢?
很重要的一點是因為我們頁面交互的合適是操作DOM,為避免多線程可能會產(chǎn)生的線程同步問題,因為采用的是單線程工作模式。

  • 優(yōu)點:程序執(zhí)行更安全、更簡單
  • 缺點:容易被耗時長的任務(wù)卡住,造成程序假死
    在這種單線程的工作模式下,我們?yōu)榻鉀Q以上問題就出現(xiàn)了:同步模式、異步模式

同步模式

大多數(shù)任務(wù)都會異常同步模式執(zhí)行,任務(wù)會一次進(jìn)入任務(wù)的調(diào)用棧依次執(zhí)行,執(zhí)行后推出調(diào)用棧,這里我們不過多展開。

異步模式

一般多是耗時長的任務(wù),開啟后就立即執(zhí)行下個任務(wù),而自己本身的任務(wù)是已回調(diào)函數(shù)的方式去處理后續(xù)邏輯。這是我們解決單線程模式缺點的關(guān)鍵。

Promise

Promise 對象代表一個異步操作,有三種狀態(tài):

  • pending: 初始狀態(tài),不是成功或失敗狀態(tài)。
  • fulfilled: 意味著操作成功完成。
  • rejected: 意味著操作失敗。
const promise = new Promise((resolve,reject) => {
   rej(new Error("promise rejected"))
})

promise.then(val => {
   console.log("resolve", val);
}).catch(err => {
   console.log("reject", err);
}) 
console.log("end")
// end
// eject Error: promise rejected

then方法的參數(shù)是函數(shù),如果不是函數(shù)可以直接無視

模擬Ajax

function ajax(url) {
    return new Promise((resolve,reject) => {
        let xhr = new XMLHttpRequest()
        xhr.open("get", url)
        xhr.responseType = "json"
        xhr.onload = function() {
            if (this.status === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
        xhr.send()
    })
}
ajax("package.json").then(res => {
    console.log(res)
})

由此我們可以看出promise的本質(zhì)就是已回調(diào)函數(shù)的方式去定義異步任務(wù)結(jié)束之后需要執(zhí)行的任務(wù)
但是好多剛開始學(xué)習(xí)用promise的同學(xué)經(jīng)常會陷入一個誤區(qū),一個異步任務(wù)在另一個異步任務(wù)回調(diào)結(jié)束后執(zhí)行仍然使用了嵌套,這就背離了我們使用promise的初衷。那么解決這個問題的方式就是利用promise鏈?zhǔn)秸{(diào)用

Promise的鏈?zhǔn)秸{(diào)用

const promise = new Promise((resolve,reject) => {
    resolve()
})

promise.then(() => {
    return 1;
}).then((res) => {
    console.log(res);
// 輸出1
}).then(() => {
    console.log("3");
}) 
  • Promisethen方法會返回一個全新的Promise對象
  • 后面的then都是在為上一個的then返回的promise添加狀態(tài)明確過后的回調(diào)
  • 前面的then方法中回調(diào)函數(shù)的返回值會作為后面then方法回調(diào)的參數(shù)
  • 如果回調(diào)中返回的是Promise, 那么后面的then方法的回調(diào)會等待它的結(jié)束

Promise的異常處理

常見的異常處理

//第一種
promise.then(resolve => {
    console.log("resolve", resolve);
}, reject => {
    console.log("reject", reject);
})
//第二種
promise.then(resolve => {
    console.log("resolve", resolve);
}).catch(err => {
    console.log("reject", err);
})

\color{red}{unhandledrejection} 是掛載在全局上,用來處理代碼中沒有被手動捕獲的異常,雖說提供了該方法,但還是建議全手動捕獲可能出現(xiàn)的異常

瀏覽器中的使用方式:

window.addEventListener("unhandledrejection", event => {
    const {reason, promise} = event;
    //reason => Promise 失敗的原因, 一般是個對象
    //promise => 出現(xiàn)異常的 Primose對象
    event.preventDefault()
},false)

node環(huán)境下的使用方式:

process.on("unhandledRejection", (reason, promise) => {
    //reason => Promise 失敗的原因, 一般是個對象
    //promise => 出現(xiàn)異常的 Primose對象
})

Promise的靜態(tài)方法

resolve

Promise.resolve("1")
    .then(val => {console.log(val)})

new Promise((resolve,reject) => {
    resolve("1")
})
//兩者等價
let p1 = new Promise((resolve,reject) => {
    resolve("1")
})
let p2 = Promise.resolve(p1)

console.log(p1 === p2)
// true

由此可見將一個Promise對象包裹在另一個Promise對象里面返回的還是最開始的Promise

初此之外還一個特殊的點,如果Promise的參數(shù)是一個包含著如下的一個then的對象,那么也可以當(dāng)做一個回調(diào)去執(zhí)行

Promise.resolve({
    then: (onFulfilled,onReject) => {
        onFulfilled("test 1")
    }
}).then(val => {
    console.log(val)
})
// test 1

reject

同理

Promise.reject("err").catch(err => console.log(err))

Promise 并行執(zhí)行

我們常見的場景是一個頁面加載時需要同時請求多個接口,而接口之間又不相互關(guān)聯(lián),假如依次串行請求明顯時間會很長,那么我們需要將其并行執(zhí)行。
那么如何知道并行的請求都已完成返回的時間點呢?那么需要我們利用到Promise.all()

Promise.all

Promise.all([promise1, promise2])
    .then(res => {
        //res 是一個返回值的數(shù)組
    })
    .catch(err => {
        //只要有一個promise 異常
    })

Promise.race

Promise.all([promise1, promise2])
    .then(res => {
        //等待第一個promise結(jié)束
    })

Promise的執(zhí)行時序 (宏任務(wù)、微任務(wù))

宏任務(wù)

大部分異步調(diào)用都是宏任務(wù)

微任務(wù)

常見微任務(wù): Promise & MutationObserver & process.nextTick(node環(huán)境下)

Generator 異步方案

Generator(生成器函數(shù))是ES2015被提出來的, 下面我們先回顧一下

function * foo() {
    console.log("start")

    try{
        let val = yield "foo"
        console.log(val)
    } catch(e) {
        console.log(e)
    } 

}

const generator = foo() //沒有立即執(zhí)行

const result1 = generator.next() //會執(zhí)行foo() 直到遇到關(guān)鍵字 yield為止
console.log(result1)
//start
//{value: "foo", done: false} 

const result2 = generator.next("bar") //剛才yield繼續(xù)往下執(zhí)行, next里的參數(shù)可以復(fù)制給 yield前邊的變量
console.log(result2)
// bar

generator.throw(new Error("generator error"))

那么接下來我們可以利用Generator中這一特性結(jié)合Promise使用

function * main() {
    const json = yield ajax("package.json") //那么我們這里就近似一種同步的書寫方式
    console.log(json)
}
const g = main();
const result = g.next();

result.value.then(data => {
    g.next(data)
})

為避免多個promise造成代碼的堆積,我們可以利用一下遞歸, 并且再做一下封裝

function * main() {
    const json1 = yield ajax("package.json")
    console.log(json1)
    const json2 = yield ajax("package.json")
    console.log(json2)
    const json3 = yield ajax("package.json")
    console.log(json3)
}
function co(generator) {
const g = main();

function loopHandle(result) {
    if (result.done) return
    result.value
        .then(data => loopHandle(g.next(data)), err => g.throw(err))
}

loopHandle(g.next())
}

co(main)

Aysnc/Await

aysnc/await是generator的語法糖

async function  main() {
    const json1 = await ajax("package.json")
    console.log(json1)
    const json2 = await ajax("package.json")
    console.log(json2)
    const json3 = await ajax("package.json")
    console.log(json3)
}

main()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容