17

[NodeJS] async 和 await 的本质

 4 years ago
source link: http://www.cnblogs.com/blowing00/p/12469552.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

​    绝大多数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?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK