前置知识

想要了解Async/Await关键字内部是怎么运行的?在什么时机运行?需要提前了解js异步编程,宏任务、微任务概念。

js异步编程

浏览器是多线程的,其包含如下多个线程:

  • js主引擎
  • ajax请求线程。
  • DOM处理线程。
  • 定时任务线程。
  • 其他线程。

我们通常所说javascript是单线程的指的是js主引擎,其负责js代码的解释,预处理和执行。js主引擎维护一个执行栈,会按照顺序依次执行放入执行栈中的代码。如果某段代码执行时间过长,那么剩余代码会等待。

console.log('excute start')
const date = new Date()
while (new Date() - date < 3000) {
    // 模拟阻塞执行栈3秒
    console.log('waiting')
}
// 3秒后才会执行
console.log('excute end')

但是,当我们在js代码中调用浏览器其他的api(如定时任务)时,js主引擎就会执行异步操作,执行栈中的其他任务会继续执行,不用等待异步操作完成。

console.log('excute start')
setTimeout(() => {
    // 3秒后立即执行
    console.log('async excute')
}, 3000);
// 立即执行,不用等待3秒
console.log('excute end')

那么js主引擎怎么知道异步操作执行完毕呢?除了执行栈,还存在一个消息队列。其他api执行完异步操作后,会将回调事件放入消息队列中。当执行栈中的任务全部执行完毕后,js主引擎会去查看消息队列中是否有任务,如果有任务,就会取出最开始的任务将其放入执行栈中,执行栈会立即执行该任务。

js代码执行过程就是执行栈依次执行代码,执行完成后去消息队列中取出任务放入执行栈继续执行,这个过程会不断的重复,也就构成Event Loop(事件循环)。

我们通常使用回调函数来作为异步任务执行完成后执行的事件,但是当回调函数嵌套过深就会形成回调地狱,严重影响代码的可读性。

ajax('ajax1', function () {
    ajax('ajax2', function () {
        ajax('ajax3', function () {
            ajax('ajax4', function () {
                // ....
            })
        })
    })
})

为了解决回调函数嵌套导致的回调地狱问题,ES2015提出了Promise方案,Promise通过链式调用让回调函数扁平化,减少了回调嵌套。

new Promise(resolve => {
    ajax('ajax1', function (data) {
        resolve(data)
    })
}).then(value => {
    return new Promise(resolve => {
        ajax('ajax2', function (data) {
            resolve(data)
        })
    })
}).then(value => {
    return new Promise(resolve => {
        ajax('ajax3', function (data) {
            resolve(data)
        })
    })
})

同时,ES中还包含Generator生成器,其配合执行器co函数也可以解决回调函数嵌套问题。

function* main() {
    const v1 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('promise 1')
        }, 1000);
    })
    console.log(v1)
    const v2 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('promise 2')
        }, 1000);
    })
    console.log(v2)
}
// 执行器
function co(gen) {
    let generator = gen()
    function handleResult(result) {
        if (result.done) return
        else {
            result.value.then((value) => {
                handleResult(generator.next(value))
            }, err => {
                console.log(err)
            })
        }
    }
    handleResult(generator.next())
}

// 执行
co(main)

为了更进一步简化异步编程语法,ES2017提出Async/Await语法糖,其本质就是包装了Generator生成器。

async function main() {
    let v1 = await new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 1')
        }, 1000);
    })
    console.log(v1)
    let v2 = await new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 2')
        }, 1000);
    })
    console.log(v2)
}
// 执行
main()

如果揭开语法糖,其相当于

function* main() {
    let v1 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 1')
        }, 1000);
    })
    console.log(v1)
    let v2 = yield new Promise(resolve => {
        setTimeout(() => {
            resolve('async excute 2')
        }, 1000);
    })
    console.log(v2)
}

宏任务vs微任务

宏任务和微任务可以理解为异步任务的进一步细分,只不过执行时机不同,当某个宏任务执行完毕之后,会查找微任务消息队列中是否存在微任务,如果有,那么就执行所有微任务,然后再执行下一个宏任务。
下面是盗用的一张执行逻辑图:
1053223-20180831162350437-143973108.png

常见的生成宏任务的有:setTimeout(同等延迟时间下,setTimeOut的优先级高于setImmediate),setInterval, setImmediate, I/O, UI rendering。
常见的生成微任务的有:Promise,queueMicrotask。

setTimeout(() => {
    console.log('timeout')
}, 0)
new Promise(resolve => {
    resolve('promise')
}).then(v => {
    console.log(v)
})

上述代码中,由于Promise生成的是微任务,所有其早于setTimeout生成的宏任务,因此先输出promise,再输出timeout。

宏任务和微任务的执行过程可以用下面的代码简要说明:

// 事件循环, 主线程
while (macroQueue.waitForMessage()) {
    // 1. 执行完调用栈上当前的宏任务(同步任务)
    // call stack
    // 2. 遍历微任务队列,把微任务队里上的所有任务都执行完毕(清空微任务队列)
    // 微任务又可以往微任务队列中添加微任务
    for (let i = 0; i < microQueue.length; i++) {
        // 获取并执行下一个微任务(先进先出)
        microQueue[i].processNextMessage()
    }
    
    // 3. 渲染(渲染线程)

    // 4. 从宏任务队列中取 一个 任务,进入下一个消息循环
    macroQueue.processNextMessage();
}

Async/Await

上面讲过,Async/Await是为解决异步回调提出的方案,其本质是对Generator生成器进行包装的语法糖。

  • 如果async方法中没有await关键字,那么其就可以认为是一个普通的方法。
async function main() {
    console.log('await')
}
console.log('next')
main()
// 输出
// await
// next
  • 如果await关键字后面是一个Promise对象,那么相当于在Promise对象中注入了then处理方法接受异步操作返回值,开启一个微任务,因此会先输出next,再输出await。
async function main() {
    const v =await new Promise(resolve => {
        resolve('await')
    })
    console.log(v)
}
main()
console.log('next')
// 输出
// next
// await
  • 如果await关键字后面的Promise对象中没有执行resolve方法,就会导致Promise一直处在pending状态,无法执行then方法,因此await后面的代码不会执行,因此下例中的console.log(v)不会执行。
async function main() {
    const v = await new Promise(resolve => { })
    console.log(v)
}
main()
console.log('next')
// 输出
// next
  • 如果await关键字后面是一个非Promise的普通数据,那么其相当于执行Promise.resolve()。
async function main() {
    const v = await 'await'
    console.log(v)
}
main()
console.log('next')
// 输出
// next
// await

// 相当于
async function main() {
    const v = await Promise.resolve('await')
    console.log(v)
}
  • 如果async函数返回非Promise对象,那么返回值会被封装成Promise对象。
async function fn() {
    return 'foo'
    // 如果没有return,相当于return undefined
}
fn().then(data => {
    console.log(data)
})

// 相当于
function fn1() {
    return new Promise(resolve => {
        resolve('foo')
    })
}

carry
58 声望7 粉丝

学无止境