1. 程式人生 > >[NodeJS] async 和 await 的本質

[NodeJS] async 和 await 的本質

​    絕大多數nodejs程式設計師都會使用 async 和 await 關鍵字,但是極少有人能真正弄明白 async 和 await 的原理。這篇文章將從零“構建”出 async 和 await 關鍵字,從而幫助理清 async 和 await 的本質。

    先用一句話概括:async 和 await 是內建了執行器的 generator 函式。

 

    什麼是 generator 函式?顧名思義,generator 函式就是一個生成器。生成的是一個可以多次通過 .next() 迭代的物件,例如,定義一個 generator 函式如下:

let g = function* () {
  yield 1
  yield 2
  return 3
}

    其中,yield 關鍵字定義每次迭代的返回值,最後一個返回值用 return。

    然後,就可以用它來生成一個可迭代的物件:

let iter = g()
​
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

    以上程式碼執行的結果是:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }
{ value: undefined, done: true }

    generator 函式也可以接收引數:

let g = function* (a, b) {
  yield a
  yield b
  return a + b
}
​
let iter = g(1, 2)
​
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

    執行結果:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }
{ value: undefined, done: true }

    

    接下來是一個關鍵點:yield 關鍵字的值和 .next() 的引數的關係:

let g = function* () {
  let ret = yield 1
  return ret
}
​
let iter = g()
​
console.log(iter.next())
console.log(iter.next(2))

    以上程式碼的執行結果是:

{ value: 1, done: false }
{ value: 2, done: true }

    可以看出,第二次呼叫 .next() 的時候,傳入了引數2,這個 2 被賦值給了 ret。也就是說,

  let ret = yield 1

這行程式碼其實是被拆成兩段執行的。第一次呼叫 .next() 的時候,g 裡面的程式碼開始執行,執行到了 yield 1 這裡,就暫停並返回了。這時列印 .next() 的返回值是 { value: 1, done: false }。然後,執行 .next(2) 的時候,又回到了 g 裡面的程式碼,從 let ret = 2 開始執行。

    理清楚這一執行過程非常重要。因為,這意味著:

    如果我在 g 裡面 yield 一個 Promise 出去,在外面等 Promise 執行完之後,再通過 .next() 的引數把結果傳進來,會怎樣呢?

let asyncSum = function(a, b) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(a + b)
    }, 1000)
  })
}
​
let g = function* () {
  let ret = yield asyncSum(1, 2)
  console.log(ret)
  return ret
}
​
let iter = g()
​
let p = iter.next().value
p.then(sum => {
  iter.next(sum)
})

    執行結果就是等待一秒之後打印出3:

// 這裡掛起了一秒鐘
3

    請細細品味上面程式碼裡面的 g 函式:

let g = function* () {
  let ret = yield asyncSum(1, 2)
  console.log(ret)
  return ret
}

    將其與下面程式碼進行對比:

let g = async function () {
  let ret = await asyncSum(1, 2)
  console.log(ret)
  return ret
}

    發現了吧?事實上 async 函式就是 generator 函式。

    讀者會問了,不對啊,我們呼叫 async 函式,都是直接呼叫,返回一個 Promise ,而不用像上面呼叫 g 那麼麻煩的。

    沒錯。上面呼叫 g 的程式碼:

let iter = g()
​
let p = iter.next().value
p.then(sum => {
  iter.next(sum)
})

    叫做 g 的執行器。我們可以把它封裝起來:

let executor = function() {
  return new Promise(resolve => {
    let iter = g()
​
    let p = iter.next().value
    p.then(sum => {
      let ret = iter.next(sum)
      resolve(ret.value)
    })
  })
}
​
executor().then(ret => {
  console.log(ret)
})

    執行結果:

// 掛起一秒鐘
3 // g 裡面的 console.log(ret)
3 // .then 裡面的 console.log(ret)

    實際上,node的執行引擎悄悄地幫我們做了上面的事情,當我們直接呼叫一個 async 函式時,其實是在呼叫它的執行器。

 

    原理講到這裡就完了。下面是擴充套件部分。

 

    上面的 executor 函式是僅僅針對這個例子裡面的 g 寫的。那我們是否可能寫一個通用的執行器函式,適用於任何 generator 函式呢?不管 generator 函式裡面有多少個 yield ,這個執行器是否都可以自動全部處理完?

    答案當然是肯定的,用到了遞迴,請看完整程式碼:

let asyncSum = function(a, b) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(a + b)
    }, 1000)
  })
}
​
let asyncMul = function(a, b) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(a * b)
    }, 1000)
  })
}
​
let g = function* (a, b) {
  let sum = yield asyncSum(1, 2)
  let ret = yield asyncMul(sum, 2)
  return ret
}
​
function executor(generator, ...args) {
  let iter = generator.apply(this, args)
  let n = iter.next()
  if (n.done) {
    return new Promise(resolve => resolve(n.value))
  } else {
    return new Promise(resolve => {
      n.value.then(ret => {
        _r(iter, ret, resolve)
      });
    });
  }
}
​
function _r(iter, ret, resolve) {
  let n = iter.next(ret)
  if (n.done) {
    resolve(n.value)
  } else {
    n.value.then(ret => {
      _r(iter, ret, resolve)
    })
  }
}
​
executor(g, 1, 2).then(ret => {
  console.log(ret)
})

    執行結果:

// 這裡掛起了兩秒鐘
6

    不過上面這個 executor 是個不完善的版本,因為沒有考慮錯誤的情況。其實早在 async 和 await 還沒有出現的 2013 年,著名程式設計師 TJ Holowaychuk 就寫了一個完善的 generator 執行器。專案地址:https://github.com/tj/co 。其名字叫 co。典型用法就是:

co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

    

    關於 async 和 await 的本質,到這裡就結束啦。文章最後請細心的讀者思考一個問題:為什麼 TJ Holowaychuk 的這個模組名字要叫做 co?

&n