一個例子讀懂 JS 異步編程: Callback / Promise / Generator / Async

JS異步編程實踐理解

回顧JS異步編程方法的發展,主要有如下幾種方式:node

  1. Callback
  2. Promise
  3. Generator
  4. Async

需求

顯示購物車商品列表的頁面,用戶能夠勾選想要刪除商品(單選或多選),點擊確認刪除按鈕後,將已勾選的商品清除購物車,頁面顯示剩餘商品。express

爲了便於本文內容闡述,假設後端沒有提供一個批量刪除商品的接口,因此對用戶選擇的商品列表,須要逐個調用刪除接口。編程

用一個定時器表明一次接口請求。那思路就是遍歷存放用戶已選擇商品的id數組,逐個發起刪除請求del,待所有刪除完成後,調用獲取購物車商品列表的接口getsegmentfault

實現

let ids = [1, 2, 3] // 假設已選擇三個商品
let len = ids.length
let count = 0

let start // 便於後面計算執行時間

1. callback

傳統常規的寫法,若是是多個繼行任務就會陷入回調地獄。好比此例中get做爲del的回調函數後端

let get = () => {
    setTimeout(() => {
        console.log(`get:${new Date() -start}ms`)
    }, 1000)
}

let del = (id, cb) => {
    setTimeout(() => {
        console.log(id)
        count++
        if (count === len) {
            cb()
        }
    }, 1000)
}

let confirmDel = () => {
    start = new Date()
    for (id of ids) {
        del(id, get)
    }
    console.log(`done:${new Date() -start}ms`)
}

confirmDel()

注意觀察和對比done的打印順序和get完成時間。
setTimeout是異步執行的,沒有阻塞主流程的執行,因此done最早打印。
三個del任務是並行的,加上一個回調執行時間,因此整個點擊刪除按鈕事件耗時2秒左右數組

done:1ms
1
2
3
get:2007ms

2. Promise

let getP = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`get:${new Date() -start}ms`)
            resolve()
        }, 1000)
    })
}

let delP = (id, cb) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(id)
            count++
            if (count === len) {
                cb()
            }
            resolve()
        }, 1000)
    })
}

let confirmDelP = () => {
    start = new Date()
    for (id of ids) {
        delP(id, getP)
    }
    console.log(`done:${new Date() -start}ms`)
}

confirmDelP()

單純經常使用Promise寫法,看上去結構跟回調寫法同樣,並且運行時間也同樣。promise

done:2ms
1
2
3
get:2007ms

可是,若是使用Promise.all方法,就能很好將併發任務(三個del)和繼發任務(get)區分開了,就是get不用嵌入回調中了。瀏覽器

3. Promise.all

Promise對象then / catch / all / race / finally,以及resolve / reject更多內容請參閱MDN併發

let delP_1 = (id) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(id)
            resolve()
        }, 1000)
    })
}

let getP_1 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`get:${new Date() -start}ms`)
            resolve()
        }, 1000)
    })
}

let confirmDelP_all = () => {
    start = new Date()
    let p_Arr = ids.map(id => delP_1(id))

    Promise.all(p_Arr)
        .then(() => {
            return getP_1()
        })
        .then(() => {
            console.log(`done:${new Date() -start}ms`)
        })
}
confirmDelP_all()

在這裏,代碼的語義就很直觀了,先併發三個刪除del,所有成功後執行getget成功後done
注意看done的打印順序app

1
2
3
get:2008ms
done:2010ms

4. Generator

Generator類型是一種特殊的函數,它擁有本身獨特的語法和方法屬性。好比函數名前加*,配合yield 返回異步回調結果, 經過next 傳入函數、next返回特殊的包含value和done屬性的對象等等,具體見MDN

Generator是一種惰性求值函數,執行一次next()纔開啓一次執行,到yield又中斷,等待下一次next()。因此本人更喜歡叫它步進函數,很是適合執行繼發任務

假設如今每個接口請求都是繼發任務,就是說只有當上一個請求成功後,纔開始下一個請求。在實際的場景中,一般是當前請求須要使用上一個請求返回的結果數據。此時使用Generator函數是最好的方式。

let generator

let getG = () => {
    setTimeout(() => {
        console.log(`get:${new Date() -start}ms`)
        generator.next()
    }, 1000)
}

let delG = (id) => {
    setTimeout(() => {
        console.log(id)
        generator.next()
    }, 1000)
}

function *confimrDelG () {
    start = new Date()
    for (id of ids) {
        yield delG(id)
    }
    yield getG()
    console.log(`done:${new Date() -start}ms`)
}

generator = confimrDelG()
generator.next()
console.log('會被阻塞嗎?')

觀察打印的時間,四個異步任務4秒左右。
注意"阻塞「文字最早打印

會被阻塞嗎?
1
2
3
get:4009ms
done:4011ms

我理解Generator就是一個用來裝載異步繼發任務的容器,不阻塞容器外部流程,可是容器內部任務用yield設置斷點,用next步進執行,能夠經過next向下一步任務傳值,或者直接使用yield返回的上一任務結果。

5. async / await

async 函數

咱們先看MDN上關於async function怎麼說的:

When an async function is called, it returns a Promise. When the async function returns a value, the Promise will be resolved with the returned value. When the async function throws an exception or some value, the Promise will be rejected with the thrown value.

也就是說async函數會返回一個Promise對象。

  • 若是async函數中是return一個值,這個值就是Promise對象中resolve的值;
  • 若是async函數中是throw一個值,這個值就是Promise對象中reject的值。

例子顯示下,咱們先用Promise寫法

function imPromise(num) {

  return new Promise(function (resolve, reject) {
    if (num > 0) {
      resolve(num);
    } else {
      reject(num);
    }
  })
}

imPromise(1).then(function (v) {
  console.log(v); // 1
})

imPromise(0).catch(function (v) {
  console.log(v); // 0
})

再用Async寫法

async function imAsync(num) {
  if (num > 0) {
    return num // 這裏至關於resolve(num)
  } else {
    throw num // 這裏至關於reject(num)
  }
}

imAsync(1).then(function (v) {
  console.log(v); // 1
});

// 注意這裏是catch
imAsync(0).catch(function (v) {
  console.log(v); // 0
})

因此理解Asyncnew Promise的語法糖也是這個緣由。但要注意一點的是上面imPromise函數和imAsync函數調用返回的結果區別。

`new Promise`生成的是一個`pending`狀態的`Promise`對象,而`async`返回的是一個`resolved`或`rejected`狀態的`Promise`對象,就是一個已經終結狀態的`promise`對象。理解這點,對下面的`await`理解很重要。
let p = imPromise(1)
console.log(p) // Promise { pending }
let a = imAsync(1)
console.log(a) // Promise { resolved }

await

再來看看MDN對於await是怎麼說的:

An async function can contain an await expression, that pauses the execution of the async function and watis for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value.

await會暫停當前async函數的執行,等待後面的Promise的計算結果返回之後再繼續執行當前的async函數

  • await 等待什麼??

await等待一個Promise對象從pending狀態到resoled或rejected狀態的這段時間。

因此若是要實現中斷步進執行的效果,await後面接的必須是一個pedding狀態的promise對象,其它狀態的promise對象或非promise對象一律不等待。
這也是awaityield的區別(yield無論後面是什麼,執行完緊接着的表達式就中斷)。

async / await 解決了什麼問題

Promise解決callback嵌套致使回調地獄的問題,但實際上並不完全,仍是在then中使用了回調函數。而async / await使得異步回調在寫法上完成沒有,就像同步寫法同樣。
看個例子:

// callback
get((a) => {
    (a,b) => {
        (b,c) => {
            (c,d) => {
                (d,e) => {
                    console.log(e)
                }
            }
        }
    }
})
// promise
get()
    .then(a => p1(a))
    .then(b => p1(b))
    .then(c => p1(c))
    .then(d => p1(d))
    .then(e => {console.log(e)})
// async / await
(async (a) => {
    const b = await A(a);
    const c = await A(b);
    const d = await A(c);
    const e = await A(d);
    console.log(e)

})()

async / await 實現繼發任務

咱們用async / await改寫上面Generator的例子

let delP_1 = (id) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(id)
            resolve()
        }, 1000)
    })
}

let getP_1 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`get:${new Date() -start}ms`)
            resolve()
        }, 1000)
    })
}

async function confimrDelAsync () {
    start = new Date()
    for (id of ids) {
        await delP_1(id)
    }
    await getP_1()
    console.log(`done:${new Date() -start}ms`)
}

confimrDelAsync()
console.log('被阻塞了嗎?')

打印結果基本跟generator同樣。但在語義上更明確。

被阻塞了嗎?
1
2
3
get:4014ms
done:4016ms

async / await 實現併發任務

let delP_1 = (id) => {
    setTimeout(() => {
        console.log(id)
    }, 1000)
}

let getP_1 = () => {
    setTimeout(() => {
        console.log(`get:${new Date() -start}ms`)
    }, 1000)
}

async function confimrDelAsync () {
    start = new Date()
    for (id of ids) {
        await delP_1(id)
    }
    await getP_1()
    console.log(`done:${new Date() -start}ms`)
}

confimrDelAsync()
console.log('被阻塞了嗎?')

不返回Promise對象,或者使promise對象處理resoled狀態,就能夠不執行等待。但這樣的寫法跟直接用同步方式寫同樣,因此並不推薦,顯得畫蛇添足。

done:4ms
1
2
3
get:1009ms

async / await 實現併發和繼發的混合任務

若是事件函數中併發任務和繼發任務都有,此時使用async / await纔是最好的解決方式。其中的併發任務用promise.all實現,由於它返回的正是await可用的pending狀態的Promise對象。

let delP_1 = (id) => {
    setTimeout(() => {
        console.log(id)
        resolve()
    }, 1000)
}

let getP_1 = () => {
    setTimeout(() => {
        console.log(`get:${new Date() -start}ms`)
        resolve()
    }, 1000)
}

async function confimrDelAsync_all () {
    start = new Date()

    let p_Arr = ids.map(id => delP_1(id))

    await Promise.all(p_Arr)
    await getP_1()
    console.log(`done:${new Date() -start}ms`)
}
confimrDelAsync_all()
console.log('被阻塞了嗎?')

觀察時間是繼發任務的一半。且不阻塞主流程。

被阻塞了嗎?
1
2
3
get:2009ms
done:2010ms

因此說asyncpromise的語法糖,可是函數返回的promise的狀態是不同的。說awaityield的語法糖,可是await只能接受pending狀態的promise對象
async能夠單獨使用,await不能單獨使用,只能在async函數體內使用

因此針對開頭的需求:

顯示購物車商品列表的頁面,用戶能夠勾選想要刪除商品(單選或多選),點擊確認刪除按鈕後,將已勾選的商品清除購物車,頁面顯示剩餘商品。

最好的解決方案是:
 `promise.all` 與 `async / await`結合

其次是:
 `promise.all`

在實際項目中還應該加上捕獲錯誤的代碼。
async / await中結合try...catch
promise中,由於錯誤具備冒泡以性質,因此在結尾加上.catch便可。

尾聲

文章只是本身的一個併發和繼發混合需求引起的知識總結。但JS編程還有不少內容,包括異步事件、事件循環(瀏覽器和nodejs區別)、異步任務錯誤的捕獲、promise/generator/async具體API細節等。還須要繼續學習。

參考連接

https://blog.csdn.net/ken_ding/article/details/81201248
https://segmentfault.com/a/1190000009070711?from=timeline&isappinstalled=0#articleHeader5 《Javascript ES6 函數式編程入門指南》 第10章 使用Generator

相關文章
相關標籤/搜索