天天看點

用圖表學習掌握 異步/同步知識

用圖表學習掌握 異步/同步知識

英文 | https://medium.com/frontend-canteen/you-can-master-async-await-with-7-diagrams-ac96a97abe92

翻譯 | 楊小愛

您可能已經閱讀了一些關于 異步/同步 的文章,甚至使用它們編寫了一些代碼。但是你真的掌握了異步/同步嗎?

在本文中,讓我們讨論以下主題:

  • 異步/同步的基本用法。
  • 然後我們了解異步的祖先,生成器函數。
  • 最後,讓我們自己實作 異步/同步。

我準備了 7 個圖表來解釋這些概念,希望它們能幫助您更輕松地了解這些主題。

異步/同步的基礎

一句話總結異步/同步的用法就是:以同步的方式進行異步操作。

比如有這樣一個場景:我們需要請求一個API,收到響應後,再請求另一個API。

然後我們可以這樣寫代碼:

function request(num) { // mock HTTP request
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(num * 2)
    }, 1000)
  })
}


request(1).then(res1 => {
  console.log(res1) // it will print `2` after 1 second


  request(2).then(res2 => {
    console.log(res2) // it will print `4` after anther 1 second
  })
})      

或者還有另外一種場景:我們需要請求一個API,收到響應後,再以之前的響應作為參數請求另一個API。

然後我們可以這樣寫代碼:

request(5).then(res1 => {
  console.log(res1) // it will print `10` after 1 second


  request(res1).then(res2 => {
    console.log(res2) // it will print `20` after anther 1 second
  })
})      

上面兩段代碼确實可以解決問題,但是如果嵌套層級太多,代碼就會不美觀、不可讀。

解決這個問題的方法是使用異步/同步。它允許我們以同步的方式執行異步操作。

用異步/同步重構以上兩段代碼後,它們看起來像這樣:

示例 1:

async function fn () {
  await request(1)
  await request(2)
}
fn()      

示例 2:

async fun
ction fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2)
}
fn()      

JavaScript 引擎會等待 await 關鍵字之後的表達式的結果傳回,然後再繼續執行下面的代碼。

以上代碼執行流程示意圖:

用圖表學習掌握 異步/同步知識

就像你在加油站加油一樣,隻有目前一輛車加滿油後,才能輪到下一輛車加油。在async函數中,await指定異步操作隻能在隊列中一個一個執行,進而達到以同步方式執行異步操作的效果。

注意:await 關鍵字隻能用在 async 函數中,否則會報錯。

那我們要知道await後面不能跟普通函數,否則就達不到排隊的效果。

下面的代碼是一個不正确的例子。

function request(num) {
  setTimeout(() => {
    console.log(num * 2)
  }, 1000)
}


async function fn() {
  await request(1) // 2
  await request(2) // 4
  // print `2` and `4` at the same time
}
fn()      

生成器函數

async/await 本身的用法很簡單,但它實際上是一種文法糖。async/await 是 ES2017 中引入的一種文法。如果你嘗試将async/await文法的代碼編譯到ES2015版本,你會發現它們會被編譯成generate函數,是以這裡我們先了解generate函數。

生成器函數是使用 function* 文法編寫的。調用時,生成器函數最初不會執行它們的代碼。相反,它們傳回一種特殊類型的疊代器,稱為生成器。當調用生成器的 next 方法消耗了一個值時,生成器函數會一直執行,直到遇到 yield 關鍵字。

這是一個例子:

function* gen() {
  yield 1
  yield 2
  yield 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: undefined, done: true }      

上面代碼中,gen函數沒有傳回值,是以最後一次調用g.next()傳回的結果的value屬性是未定義的。

如果 generate 函數有傳回值,那麼最後一次調用 g.next() 傳回的結果的 value 屬性就是結果。

function* gen() {
  yield 1
  yield 2
  yield 3
  return 4
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: 4, done: true }      

如果我們用一張圖來表示上述函數的執行,它應該是這樣的:

用圖表學習掌握 異步/同步知識

yield a function

如果yield後面跟着函數調用,那麼這裡程式執行完之後,會立即調用函數。并且函數的傳回值會放在 g.next() 的結果的 value 屬性中。

function fn(num) {
  console.log(num)
  return num
}
function* gen() {
  yield fn(1)
  yield fn(2)
  return 3
}
const g = gen()
console.log(g.next()) 
// 1
// { value: 1, done: false }
console.log(g.next())
// 2
//  { value: 2, done: false }
console.log(g.next()) 
// { value: 3, done: true }      
用圖表學習掌握 異步/同步知識

Promise 

同樣,Promise 對象也可以放在 yield 之後。那麼程式的執行流程和之前一樣。

function fn(num) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(num)
    }, 1000)
  })
}
function* gen() {
  yield fn(1)
  yield fn(2)
  return 3
}
const g = gen()
console.log(g.next()) // { value: Promise { <pending> }, done: false }
console.log(g.next()) // { value: Promise { <pending> }, done: false }
console.log(g.next()) // { value: 3, done: true }      

此代碼的執行流程示意圖:

用圖表學習掌握 異步/同步知識

但是,我們要的不是處于pending狀态的Promise對象,而是Promise完成後存儲在其中的值。那麼我們如何修改上面的代碼呢?

很簡單,我們隻需要調用 .then 方法:

const g = gen()
const next1 = g.next()
next1.value.then(res1 => {
  console.log(next1) // print { value: Promise { 1 }, done: false } after 1 second
  console.log(res1) // print `1` after 1 second


  const next2 = g.next()
  next2.value.then(res2 => {
    console.log(next2) // print { value: Promise { 2 }, done: false } after 2 seconds
    console.log(res2) // print `2` after 2 seconds
    console.log(g.next()) // print { value: 3, done: true } after 2 seconds
  })
})      

以上代碼執行流程示意圖:

用圖表學習掌握 異步/同步知識

在 next() 中傳遞一個參數

然後,在調用 next() 函數時,我們可以傳遞參數。

function* gen() {
  const num1 = yield 1
  console.log(num1)
  const num2 = yield 2
  console.log(num2)
  return 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next(11111))
// 11111
//  { value: 2, done: false }
console.log(g.next(22222)) 
// 22222
// { value: 3, done: true }      

這裡需要注意的是,第一次調用next()方法時,傳參是沒有作用的。

每次調用 g.next() 時,傳回的結果都與我們之前的情況沒有什麼不同。而num1會接受g.next(11111)的參數11111,num2會接受g.next(11111)的參數22222。

此代碼的執行流程示意圖:

用圖表學習掌握 異步/同步知識

Promise + Pass param

之前我們提到過Promise對象可以放在yield之後,我們也提到過可以在next函數中傳入參數。

如果我們将這兩個功能放在一起,它會變成這樣:

function fn(nums) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(nums * 2)
    }, 1000)
  })
}
function* gen() {
  const num1 = yield fn(1)
  const num2 = yield fn(num1)
  const num3 = yield fn(num2)
  return num3
}
const g = gen()
const next1 = g.next()
next1.value.then(res1 => {
  console.log(next1) // print { value: Promise { 2 }, done: false } after 1 second
  console.log(res1) // print `2` after 1 senond


  const next2 = g.next(res1) // pass privouse result
  next2.value.then(res2 => {
    console.log(next2) // print { value: Promise { 4 }, done: false } after 2 seconds
    console.log(res2) // print `4` after 2 senond


    const next3 = g.next(res2) // pass privouse result `res2`
    next3.value.then(res3 => {
      console.log(next3) // print { value: Promise { 8 }, done: false } after 3 seconds
      console.log(res3) // print `8` after 3 senond


       // pass privouse result `res3`
      console.log(g.next(res3)) // print { value: 8, done: true } after 3 seconds
    })
  })
})      
用圖表學習掌握 異步/同步知識

其實上面的寫法和async/await很像。

唯一的差別是:

  • gen函數執行後,傳回值不是Promise對象。但是 asyncFn 的傳回值是 Promise
  • gen函數需要執行特定的操作才相當于asyncFn的排隊效果
  • gen函數執行的操作是不完善的,它規定隻能處理三層嵌套

下面我們将解決這些問題并自己實作 async/await。

實作async/await

用圖表學習掌握 異步/同步知識

為了解決前面提到的問題,我們可以封裝一個高階函數。這個高階函數可以接受一個生成器函數,經過一系列的處理,傳回一個新的函數,工作起來就像一個真正的異步函數。

function generatorToAsync(generatorFn) {
  // do something
  return `a function works like a real async function`
}      

異步函數的傳回值應該是一個 Promise 對象,是以我們的 generatorToAsync 函數的模闆應該是這樣的:

function* gen() {


}
function generatorToAsync (generatorFn) {
  return function () {
    return new Promise((resolve, reject) => {


    })
  }
}


const asyncFn = generatorToAsync(gen)


console.log(asyncFn()) // an Promise object      

然後,我們可以将前面的代碼複制到 generatorToAsync 函數中:

function fn(nums) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(nums * 2)
    }, 1000)
  })
}
function* gen() {
  const num1 = yield fn(1)
  const num2 = yield fn(num1)
  const num3 = yield fn(num2)
  return num3
}
function generatorToAsync(generatorFn) {
  return function () {
    return new Promise((resolve, reject) => {
      const g = generatorFn()
      const next1 = g.next()
      next1.value.then(res1 => {


        const next2 = g.next(res1)
        next2.value.then(res2 => {


          const next3 = g.next(res2)
          next3.value.then(res3 => {


            resolve(g.next(res3).value)
          })
        })
      })
    })
  }
}


const asyncFn = generatorToAsync(gen)


asyncFn().then(res => console.log(res))      

但是,上面的代碼隻能處理三個yield,而在實際項目中,yield的個數是不确定的,可能是3、5或10。是以我們還需要調整代碼,讓我們的generatorToAsync函數可以處理任何 産量數:

function generatorToAsync(generatorFn) {
  return function() {
    const gen = generatorFn.apply(this, arguments) // there may be arguments of gen function


    // return a Promise object
    return new Promise((resolve, reject) => {


      function go(key, arg) {
        let res
        try {
          res = gen[key](arg) 
        } catch (error) {
          return reject(error)
        }


        // get `value` and `done`
        const { value, done } = res
        if (done) {
          // if `done` is true, meaning there isn't any yield left. Then we can resolve(value)
          return resolve(value)
        } else {
          // if `done` is false, meaning there are still some yield left.


          // `value` may be a normal value or a Promise object
          return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
        }
      }


      go("next")
    })
  }
}


const asyncFn = generatorToAsync(gen)


asyncFn().then(res => console.log(res))      

用法

異步/等待版本代碼:

async function asyncFn() {
  const num1 = await fn(1)
  console.log(num1) // 2
  const num2 = await fn(num1)
  console.log(num2) // 4
  const num3 = await fn(num2)
  console.log(num3) // 8
  return num3
}
const asyncRes = asyncFn()
console.log(asyncRes) // an Promise object
asyncRes.then(res => console.log(res)) // 8      

generatorToAsync 版本代碼:

function* gen() {
  const num1 = yield fn(1)
  console.log(num1) // 2
  const num2 = yield fn(num1)
  console.log(num2) // 4
  const num3 = yield fn(num2)
  console.log(num3) // 8
  return num3
}


const genToAsync = generatorToAsync(gen)
const asyncRes = genToAsync()
console.log(asyncRes) // an Promise object
asyncRes.then(res => console.log(res)) // 8      

結論

在本文中,我們首先了解了 async/await 的基本用法,然後詳細介紹了生成器函數的用法。async/await 本質上是生成器函數的文法糖。最後,我們使用生成器函數來實作 async/await。

希望今天的内容對你有用,謝謝你的閱讀。

最後,如果你覺得有幫助的話,請點贊我,關注我,并将它分享給你身邊做開發的朋友,也許能夠幫助到他,與你一起學習進步。

用圖表學習掌握 異步/同步知識