你真的知道Promise和async函数的区别是什么吗

4,072 阅读8分钟

前言

今年秋招某节提前批面试被问到这么一个问题:说一说async 函数与Promise的区别是什么?

关于这道题身边的同学和当时的我一样张口就来:async函数以串行写法的形式彻底解决异步回调的问题;async函数的语义比Promise更明确等等...反正就是讲一些用法上的区别或者是浅显的理解。当时面试官一直不太满意。

后来,在babel平台async函数polyfill后发现:async函数是一种兼顾了基于Promise的实现和生成器(也就是ES6的新特性Generator)的同步写法。所以面试官是想听到这个,但是当时的我并不知道async函数的内部是这样的。

本文将从头梳理一遍如何一步一步从PromiseGenerator函数转化为async函数,让大家能够彻底明白async函数内部原理。

Promise

1. Promise特点

《深入理解JavaScript特性》这本书中对Promise的解释是这样的:保存着一个未来可用的值的代理。我对这句话的理解是Promise内部可以写异步操作事件,这个操作未来得到的值存在Promise里。也可以在Promise中编写同步代码,但Promise本身是严格按照异步方式执行的。

Promise对象有以下两个特点:(来自阮一峰的ES6教程

  1. 对象的状态不受外界影响。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。

2. Promise的用法

//Promise在语义上是一个构造函数,用来生成Promise实例
const p = new Promise((resolve, reject) => {
    //...
    if(...操作成功){
        resolve(value)
    }else{
        reject(error)
    }
})
p.then(value => {
    //对未来值的处理
}, error => {
    //失败的错误
})

在这篇文章Promise不是主角,更多有关Promise的用法在阮一峰的ES6教程

生成器函数

生成器函数也就是ES6常说的Generator函数。Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(迭代器对象)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

//生成器函数用法
functionabc(){
    yield 'a'
    yield 'b'
    yield 'c'
}
let g = abc()
console.log(g.next()) //{ value: 'a', done: false }
console.log(g.next()) //{ value: 'b, done: false }
console.log(g.next()) //{ value: 'c, done: false }
console.log(g.next()) //{ value: undefiend, done: true }

如果不调用next的话生成器函数会在yield处挂起,并且返回值是迭代器迭代器{value:..., done: true/false}。这也是async函数会在await处挂起的基础。

下面看一段代码加深对生成器函数yield的理解:

👇

functionab(){
    let a = yield 'a'
    console.log(`a:${a}`)
    let b = yield 'b'
    console.log(`b:${b}`)
}
let g = ab()
console.log(g.next())   // { value: 'a', done: false }
console.log(g.next(1))  // a: 1 { value: 'b', done: false }
console.log(g.next(2))  // b: 2 { value: undefined, done: true }

第一次调用:Generator 函数开始执行,直到遇到第一个yield表达式为止。g.next()返回一个包含yield后的值的对象{ value: 'a', done: false }。此时并不会发生赋值赋值,只会执行yield 'a'就不会执行了。

第二次调用:此时调用g.next(1)后函数会继续向下执行代码直到执行到第二个yield就挂起。这时候就会继续之前的赋值行为, g.next(1)里的1会作为上一个yield 'a'的返回值赋值给a,所以此时a=1

第三次调用:和上一次一样b接收到了g.next(2)里的返回值2。此时g.next()对象里的done: true表示生成器里的yield操作已经结束。

可以看出第一次调用g.next()时传递的参数没有意义,因为它没有可接收的上一个yield

async函数

async函数类似一种同步写法,上一个await后的异步操作执行完毕之后才能执行下一个await里的操作。

举个例子:如果我们要实现从a.txt中读取出b.txt的路径,然后再从b.txt中取出里面的内容。那么这两个读取操作肯定不能并行,必须串行先拿到a的结果然后才能从b中拿到结果。

let fs = require('fs').promises  //让fs里方法都Promise化,不用自己转
//先看async 函数怎么写
async function read(){
    let path = awit fs.readFile('a.txt''utf-8')
    let result = awit fs.readFile(path, 'utf-8')
    return result
}
let res = read() //res是一个Promise对象
res.then(data => {
    //data就是成功后最后的结果
    console.log(data)
})

可以看出async函数返回的是一个Promise对象,并且内部逻辑是串行的。形式看起来和普通函数没有区别。

1. 简单的async函数转换

然后看看不用async函数应该怎么写(将async函数拆分为Promise和生成器)

functionread(){
    let path = yield fs.readFile('a.txt''utf-8')
    let result = yield fs.readFile(path, 'utf-8')
    return result
}
const g = read()
let {value, done} = g.next() //拿到第一次yield后的结果解构,此时的value值为从a中拿到的b的路径

Promise.resolve(value).then(data=>//如果路径(未来的值)能拿到的话就进行下一步
    let {value, done} = g.next(value) //将路径传给上一个yield的返回值也就是赋值给path,然后将b的结果解构
    Promise.resolve(value).then(data => {
        let {value, done} = g.next(data) //将b的结果传给上一个yield的返回值也就是赋值给result
    })
})

这里用了两个Promise嵌套+Generator函数实现了async函数功能,但是嵌套就挺恶心的而且代码复用性极差。

2.通用性的async函数转换

实现一个通用的代码应该怎么考虑?其实我们要实现的功能就是让function* read()这个函数可以自己动🤔而不是使用者去主动调用next方法。所以实现一个spawn辅助函数去让生成器函数read自己动起来。

function asyncExample(){
    return spawn(传入generator函数)
}
function spawn(generator){
    //将所有代码包装在一个Promise中(因为async函数返回值为Promise)
    return new Promise((resolve, reject)=>{
    const g = generator()
        
        //运行第一步
        step(() => g.next())
        
        function step(nextFn){
            //拿到yeild返回值。runNext函数为了捕获错误让函数更完美,如果写成nextFn()也行
            const {value, done} = runNext(nextFn) 
            if(done){ //done: true就成功完成了所有操作,返回value
                resolve(value)
                return
            }else{ //未完成操作就继续递归g.next
                Promise.resolve(value).then(
                 val => step(() => g.next(val)),
                    err => step(() => g.throw(err))
                )
            }
        }
        
        //捕获错误的函数
        function runNext(nextFn){
            try{
                return nextFn()
            }catch(err){
                reject(err)
            }
        }
    })
}

👌,这就写完了整个函数。

对于生成器read函数就直接传入即可。

functionread(){
    let path = yield fs.readFile('a.txt''utf-8')
    let result = yield fs.readFile(path, 'utf-8')
    return result
}
function asyncExample(){
    return spawn(read)
}
//和async函数一样的表现形式
asyncExample().then(data => {
    //data成功后最后的结果
    console.log(data)
})