天天看點

遮罩層沒有消失 - 我們來說說 async、promise 和 yield 之間的那些事

遮罩層沒有消失

我們請求資料時,通常會先開啟一個 loading,資料回來後,做一些處理,然後再将 loading 關閉。

但有時也會出現 loading 沒有關閉的情況。就像這樣:

async function request() {
    console.log('開啟遮罩')
    let json = await requestUserList() // {1}
    // 處理資料...
    console.log('關閉遮罩');
}

request()
           
// 模拟 ajax 請求使用者清單
// code 等于 2000 則為成功,否則是失敗
function requestUserList(json = { code: 2001, msg: '參數不正确', data: {} }) {
    if (Object.is(json.code, 2000)) {
        Promise.resolve(json)
    } else {
        Promise.reject(`請求失敗:${json.msg}`)
    }
}
           

由于請求失敗,進入 reject,于是 await(行 {1}) 後面的代碼将不在執行。

async 的本質

async

的本質是生成器。

Tip: 如果沒有接觸過生成器,可以看筆者的另一篇文章 - 疊代器 (Iterator) 和 生成器 (Generator)

接下來我将做一個實驗,首先用

async

寫一個小功能,再用生成器實作相同的功能。請看示例:

// 公共代碼

let seed = 0
// 模拟ajax。一秒後傳回資料
function fetchData(isResolve = true) {
    return new Promise((resolve, reject) => {
        let method = isResolve ? resolve : reject
        setTimeout(() => method({
            code: 200,
            data: seed++
        }), 1000)
    })
}
           
// async 版本

async function fn1() {
    let d1 = await fetchData()
    console.log(d1)
    let d2 = await fetchData()
    console.log(d2)
}
fn1()

/*
輸出:
{ code: 200, data: 0 }
{ code: 200, data: 1 }
*/
           

用生成器實作相同功能:

// 生成器版本

function fn2() {
    // 生成器
    function* generator() {
        let d1 = yield fetchData()
        console.log(d1)
        let d2 = yield fetchData()
        console.log(d2)
    }
    // 任務執行器
    return run(generator)
}

// 任務執行器。
// 由于執行 yield 會導緻暫停目前函數,并等待下一次 next() 的調用,
// 是以我們可以建立一個函數,讓其自動幫我們依次調用 next()
function run(genF) {
    return new Promise((resolve, reject) => {
        let gen = genF()

        function step(nextF) {
            let next
            try {
                next = nextF()
            } catch (e) {
                return reject(e)
            }

            if (next.done) {
                return resolve(next.value)
            }
            Promise.resolve(next.value).then(
                v => step(() => gen.next(v)),
                e => step(() => gen.throw(e))
            )
        }
        step(() => gen.next())
    })
}

fn2()

/*
輸出:
{ code: 200, data: 0 }
{ code: 200, data: 1 }
*/
           

我們對比下核心功能:

// async 版本
async function fn1() {
    let d1 = await fetchData()
    console.log(d1)
    let d2 = await fetchData()
    console.log(d2)
}
           
// 生成器版本
function* generator() {
    let d1 = yield fetchData()
    console.log(d1)
    let d2 = yield fetchData()
    console.log(d2)
}
           

不同的地方就是:

  1. async 換成

    *

  2. await 換成了

    yield

async 内置執行器

async 通常有如下特性:

  • async

    傳回一個

    Promise

  • 隻要有一個

    await

    失敗,就不會在執行下面的代碼,直接進入 reject
  • await

    報錯,例如通路一個不存在的變量

    let d2 = await xx

    ,也會進入 reject

請看示例:

async function fn1() {
    let d1 = await fetchData()
    console.log('d1: ', d1);
    let d2 = await xx
    console.log('d2: ', d2);
}
fn1().catch(() => { console.log('error') })

// d1:  { code: 200, data: 0 }
// error
           

這些特性由 async 内置執行器提供。我們通過上面提到的

run()

方法來分析:

// 任務執行器
function run(genF) {
    // 傳回一個  promise
    return new Promise((resolve, reject) => {
        let gen = genF()

        function step(nextF) {
            let next
            try {
                next = nextF()
            } catch (e) { // 語句報錯,會被捕獲,并進入到 reject
                return reject(e)
            }

            // 處理疊代器結束的情況
            if (next.done) {
                return resolve(next.value)
            }
            // Promise.resolve(11) 等價于 new Promise(resolve => resolve(11))
            // 如果給 Promise.resolve() 方法傳入一個 Promise,那麼這個 Promise 會被直接傳回
            Promise.resolve(next.value).then(           // {1}
                v => step(() => gen.next(v)),
                // 生成器抛出錯誤,會讓 promise 進入 reject
                e => step(() => gen.throw(e))           // {2}
            )
        }
        step(() => gen.next())
    })
}
           

這裡有 2 處需要專門說一下:

Promise.resolve(next.value)

gen.throw(e)

。請接着看:

gen.throw(e)

運作

gen.throw(e)

會讓 Promise 進入 reject。

我們将這個現象提取出來做個實驗:建立一個 Promise,在内部的一個函數作用域内在定義一個生成器,并讓其抛出錯誤

let aPromise = new Promise((resolve, reject) => {
    (function () {
        function* fn1() {
            yield 1
            yield 2
        }

        let gen = fn1()
        // 抛出錯誤
        gen.throw()
    }())
})

aPromise.then(() => {
    console.log('then')
}).catch(() => {
    console.log('catch')
})

// catch
           

實驗表明:Promise 中的生成器,哪怕内嵌在函數作用域内,一旦生成器抛出錯誤,Promise 将進入 reject。

Promise.resolve(next.value)

Promise.resolve(value)

,隻接受一個參數并傳回一個 Promise,如果給 Promise.resolve() 方法傳入一個 Promise,那麼這個 Promise 會被直接傳回。

它總是傳回一個 promise 對象。這個特性很重要。而且

await 1

(等價于

await Promise.resolve(1)

) 就使用了此特性。

在不知道輸入是同步還是異步的時候,很有用。比如我要統計一個輸入花費時間:

// 可能會報錯
const recordTime = (makeRequest) => {
  const timeStart = Date.now();
  makeRequest().then(() => { // throws error for sync functions (.then is not a function)
    const timeEnd = Date.now();
    console.log('time take:', timeEnd - timeStart);
  })
}
           
// 正常運作
const recordTime = async (makeRequest) => {
  const timeStart = Date.now();
  await makeRequest(); // works for any sync or async function
  const timeEnd = Date.now();
  console.log('time take:', timeEnd - timeStart);
}
           

使用 promise 還是 async/await

7 Reasons Why JavaScript Async/Await Is Better Than Plain Promises 這篇文章講述了 7 個

async/await

Promise

更好的方面。大部分我是贊同的,比如:

  • 更簡潔
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })
           
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
           
  • 更具可讀性
// 條件 - 有很多條件時,我們會這麼寫:
const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

// VS

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data)
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}
           
// 中間值:請求依賴中間值,可能會這麼寫
const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return promise2(value1)
        .then(value2 => {
          // do something          
          return promise3(value1, value2)
        })
    })
}

// VS

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}
           

在前面我們已經知道

async/await

的本質就是

生成器

執行器

,而執行器(

run()

方法)是通過 Promise 實作。是以 async 也就是 Promise 的文法糖,并且是一個實作了固定功能的文法糖。

是以,就靈活程度來講,Promise 可能更好。

是以,這兩個東西就不是替換關系,而是有他們各自使用的場景。

以下是我的一些個人習慣:

  • 隻有一個異步請求,并且需要處理錯誤情況,傾向

    Promise

fetchData(false).then(() => {
    // do ...
}).catch(() => {
    console.log('error')
})
           
try {
    let p = await fetchData(false)
    // do ...
} catch (e) {
    console.log('error')
}
           
  • async/await

    可以使用

    try...catch

    處理同步和異步錯誤
async () => {
  try {
    let p1 = await fetchData()
    // 依賴于第一個請求的參數值
    let p2 = await fetchData(p1.role === 1 ? option1 : option2)
    // 其他複雜邏輯
  } catch (err) {
    // do something...
  }
}
           
  • 邏輯比較複雜,比如異步嵌套、條件、中間值等等,使用

    async

容易忽略的幾點

await 與并行

如果多個請求沒有依賴關系,就讓他們同時發出。

下面這段代碼是串行:

async function foo() {
    let a = await createPromise(1)
    let b = await createPromise(2)
}
           

可以通過下面兩種方法改為并行:

// 方式一
async function foo() {
    let p1 = createPromise(1)
    let p2 = createPromise(2)
    // 至此,兩個異步操作都已經發出
    await p1
    await p2
}

// 方式二
async function foo() {
    let [p1, p2] = await Promise.all([createPromise(1), createPromise(2)])
}
           

Tip:更多細節請看 async -> await 與并行。

await 與 forEach

請問這段代碼是并行還是串行?

new Array(3).fill(0).forEach(async () => { // {1}
    let data = await fetchData()
    console.log('data: ', data);
})
           

答案:并行。

一秒後,控制台同時輸出:

data:  { code: 200, data: 0 }
data:  { code: 200, data: 1 }
data:  { code: 200, data: 2 }
           

注:如果将

async

(行{1})删除,将會報錯(

SyntaxError: await is only valid in async function

)。

如果需要将上述代碼改為

串行

,可以這樣寫:

async function requestData() {
    for (let item of new Array(3).fill(0)) {
        let data = await fetchData()
        console.log('data: ', data);
    }
}
requestData()
           
// 每過一秒,将會輸出一條記錄
data:  { code: 200, data: 0 }
data:  { code: 200, data: 1 }
data:  { code: 200, data: 2 }
           

亦或這樣:

async function requestData() {
    let allP = []
    for (let item of new Array(3).fill(0)) {
        allP.push(await fetchData())
    }
    console.log('allP: ', allP);
}
requestData()
           
// 三秒後一次性輸出
allP:  [
  { code: 200, data: 0 },
  { code: 200, data: 1 },
  { code: 200, data: 2 }
]
           

async 與 return

請問下面這段代碼将輸出什麼:

// 一半會拒絕
async function halfRefused() {
    console.log('0');

    // 等待3秒
    await new Promise(r => setTimeout(r, 3000));
    console.log('1');

    const flag = Math.random() >= 0.5

    if (flag) {
        return 'peng';
    }
    throw Error('li');
}

async function fn() {
    try {
        halfRefused();
    }
    catch (e) {
        return 'caught';
    }
}

fn().then(v => console.log('resolve', v), v => console.log('reject', v))
console.log('20');
           

首先輸出:

0
20
resolve undefined
           

過三秒後輸出:

1

或

1
UnhandledPromiseRejectionWarning: Error: li
           

運作 fn() 函數:它不會等待,總是進入 resolve,并傳回 undefined。通常,這是錯誤的代碼。

增加 await
async function fn() {
    try {
        await halfRefused();
    }
    catch (e) {
        return 'caught';
    }
}
           

輸出:

0
20
// 等 3 秒
1
resolve undefined
           

0
20
// 等 3 秒
1
resolve caught
           

會等待,總是進入 resolve,填充 undefined 或 caught。

增加 return
async function fn() {
    try {
        return halfRefused();
    }
    catch (e) {
        return 'caught';
    }
}
           
0
20
// 等 3 秒
1
resolve peng
           
0
20
// 等 3 秒
1
reject Error: li
           

會等待,resolve "peng" 或 reject "li",catch 塊不會執行

增加 return await
async function fn() {
    try {
        return await halfRefused();
    }
    catch (e) {
        return 'caught';
    }
}
           

等同于:

async function fn() {
    try {
        return await halfRefused();
    }
    catch (e) {
        return 'caught';
    }
}
           
0
20
// 等 3 秒
1
resolve peng
           
0
20
// 等 3 秒
1
resolve caught
           

會等待,總會進入 resolve,catch 塊會執行。

錯誤堆棧

vue-cli + element-ui

這種項目(已配置

source-map

)中測試,發現下面兩種寫法報錯都隻能定位到行{2}。

async

并不能定位到出錯的那一行(行{1}):

const makeRequest = () => {
  return fetchData()
    .then(() => fetchData())
    .then(() => fetchData())
    .then(() => fetchData())
    .then(() => fetchData())
    .then(() => {
      throw new Error('oops')
    })
}

const makeRequest = async () => {
  await fetchData()
  await fetchData()
  await fetchData()
  await fetchData()
  await fetchData()
  throw new Error('oops')         // {1}
}

makeRequest()
  .catch(err => {
    // {2}
    console.log(err)
  })
           

調試

async

有時會比

Promise

更容易調試。請看示例:

const makeRequest = () => {
  return fetchData()
    .then(() => fetchData())
    .then(() => fetchData())
    // 不允許這麼寫
    .then(() => debugger fetchData())
    // 可以
    .then(() => {
      debugger
      fetchData()
    })
    .then(() => {
      throw new Error('oops')
    })
}
           
const makeRequest = async () => {
  await fetchData()
  await fetchData()
  await fetchData()
  await fetchData()
  // 最佳
  debugger
  await fetchData()
  throw new Error('oops')
}
           

yield 傳參注意點

直接看示例:

function* fn2() {
    let a = yield 1
    // 沒有傳參,a 的值也不會是 1
    console.log('a: ', a);
    let b = yield a + 2
    // 傳入 3,是以輸出 6
    yield b + 3
}

let gen = fn2()
console.log('gen.next(): ', gen.next())
console.log('gen.next(): ', gen.next())
console.log('gen.next(): ', gen.next(3))

// gen.next():  { value: 1, done: false }
// a:  undefined
// gen.next():  { value: NaN, done: false }
// gen.next():  { value: 6, done: false }
           

作者:彭加李

出處:https://www.cnblogs.com/pengjiali/p/15775855.html

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。