JavaScript引擎实现async/await的方法实例

目录
  • 前言
  • 生成器 VS 协程
  • async/await
    • async
    • await
  • 小结
  • 总结

前言

我们都知道Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程,使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰

JavaScript 引擎是如何实现 async/await 的。如果上来直接介绍 async/await 的使用方式的话,那么你可能会有点懵,所以我们就从其最底层的技术点一步步往上讲解,从而带你彻底弄清楚 async 和 await 到底是怎么工作的。

首先介绍生成器(Generator)是如何工作的,接着讲解 Generator 的底层实现机制——协程(Coroutine);又因为 async/await 使用了 Generator 和 Promise 两种技术,所以紧接着我们就通过 Generator 和 Promise 来分析 async/await 到底是如何以同步的方式来编写异步代码的。

生成器 VS 协程

生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。

function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 2'

    console.log("开始执行第二段")
    yield 'generator 2'

    console.log("开始执行第三段")
    yield 'generator 2'

    console.log("执行结束")
    return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

执行上面这段代码,观察输出结果,你会发现函数 genDemo 并不是一次执行完的,全局代码和 genDemo 函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。下面我们就来看看生成器函数的具体使用方式:

  • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
  • 外部函数可以通过 next 方法恢复函数的执行。

关于函数的暂停和恢复,相信你一定很好奇这其中的原理,那么接下来我们就来简单介绍下 JavaScript 引擎 V8 是如何实现一个函数的暂停和恢复的,这也会有助于你理解后面要介绍的 async/await。

要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

为了让你更好地理解协程是怎么执行的,我结合上面那段代码的执行过程,画出了下面的“协程执行流程图”,你可以对照着代码来分析:

从图中可以看出来协程的四点规则:

  • 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
  • 要让 gen 协程执行,需要通过调用 gen.next。
  • 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
  • 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

不过,对于上面这段代码,你可能又有这样疑问:父协程有自己的调用栈,gen 协程时也有自己的调用栈,当 gen 协程通过 yield 把控制权交给父协程时,V8 是如何切换到父协程的调用栈?当父协程通过 gen.next 恢复 gen 协程时,又是如何切换 gen 协程的调用栈?

要搞清楚上面的问题,你需要关注以下两点内容。

第一点:gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。

第二点:当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。

为了直观理解父协程和 gen 协程是如何切换调用栈的

到这里相信你已经弄清楚了协程是怎么工作的,其实在 JavaScript 中,生成器就是协程的一种实现方式,这样相信你也就理解什么是生成器了。那么接下来,我们使用生成器和 Promise 来改造开头的那段 Promise 代码。改造后的代码如下所示:

//foo函数
function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}

//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
    return gen.next().value
}
getGenPromise(gen).then((response) => {
    console.log('response1')
    console.log(response)
    return getGenPromise(gen)
}).then((response) => {
    console.log('response2')
    console.log(response)
})

从图中可以看到,foo 函数是一个生成器函数,在 foo 函数里面实现了用同步代码形式来实现异步操作;但是在 foo 函数外部,我们还需要写一段执行 foo 函数的代码,如上述代码的后半部分所示,那下面我们就来分析下这段代码是如何工作的。

  • 首先执行的是let gen = foo(),创建了 gen 协程。然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
  • gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
  • 父协程恢复执行后,调用 response1.then 方法等待请求结果。
  • 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。

以上就是协程和 Promise 相互配合执行的一个大致流程。不过通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器(可参考著名的 co 框架),如下面这种方式:

function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}
co(foo());

通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。

async/await

虽然生成器已经能很好地满足我们的需求了,但是程序员的追求是无止境的,这不又在 ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。其实 async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。要搞清楚 async 和 await 的工作原理,我们就得对 async 和 await 分开分析。

async

我们先来看看 async 到底是什么?根据 MDN 定义,async 是一个通过异步执行隐式返回 Promise 作为结果的函数。

这里我们先来看看是如何隐式返回 Promise 的,你可以参考下面的代码:

async function foo() {
    return 2
}
console.log(foo())  // Promise {<resolved>: 2}

执行这段代码,我们可以看到调用 async 声明的 foo 函数返回了一个 Promise 对象,状态是 resolved,返回结果如下所示:

Promise {<resolved>: 2}

await

我们知道了 async 函数返回的是一个 Promise 对象,那下面我们再结合文中这段代码来看看 await 到底是什么。

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)

观察上面这段代码,你能判断出打印出来的内容是什么吗?这得先来分析 async 结合 await 到底会发生什么。在详细介绍之前,我们先站在协程的视角来看看这段代码的整体执行流程图:

结合上图,我们来一起分析下 async/await 的执行流程。

首先,执行console.log(0)这个语句,打印出来 0。

紧接着就是执行 foo 函数,由于 foo 函数是被 async 标记过的,所以当进入该函数的时候,JavaScript 引擎会保存当前的调用栈等信息,然后执行 foo 函数中的console.log(1)语句,并打印出 1。

接下来就执行到 foo 函数中的await 100这个语句了,这里是我们分析的重点,因为在执行await 100这个语句时,JavaScript 引擎在背后为我们默默做了太多的事情,那么下面我们就把这个语句拆开,来看看 JavaScript 到底都做了哪些事情。

当执行到await 100时,会默认创建一个 Promise 对象,代码如下所示

let promise_ = new Promise((resolve,reject){
  resolve(100)
})

在这个 promise_ 对象创建的过程中,我们可以看到在 executor 函数中调用了 resolve 函数,JavaScript 引擎会将该任务提交给微任务队列。

然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。

主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。接下来继续执行父协程的流程,这里我们执行console.log(3),并打印出来 3。

随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数,如下所示:

promise_.then((value)=>{
   //回调函数被激活后
  //将主线程控制权交给foo协程,并将vaule值传给协程
})

该回调函数被激活以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。

foo 协程激活之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句,执行完成之后,将控制权归还给父协程。

以上就是 await/async 的执行流程。正是因为 async 和 await 在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。

小结

Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。

使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。

另外,V8 引擎还为 async/await 做了大量的语法层面包装,所以了解隐藏在背后的代码有助于加深你对 async/await 的理解。async/await 无疑是异步编程领域非常大的一个革新,也是未来的一个主流的编程风格。

其实,除了 JavaScript,Python、Dart、C# 等语言也都引入了 async/await,使用它不仅能让代码更加整洁美观,而且还能确保该函数始终都能返回 Promise。

总结

到此这篇关于JavaScript引擎实现async/await的文章就介绍到这了,更多相关js实现async/await内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入理解JavaScript的async/await

    async 和 await 在干什么 任意一个名称都是有意义的,先从字面意思来理解.async 是"异步"的简写,而 await 的意思是等待.所以应该很好理解 async 用于申明一个 function 是异步的,而 await 等待某个操作完成. 那么async/await到底是干嘛的呢?我们先来简单介绍一下. async/await 是一种编写异步代码的新方法.之前异步代码的方案是回调和 promise. async/await 是建立在 promise 的基础上.(对promi

  • JavaScript async/await原理及实例解析

    随着Node 7的发布,越来越多的人开始研究据说是异步编程终级解决方案的 async/await. 异步编程的最高境界,就是根本不用关心它是不是异步. async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案. async 和 await 起了什么作用 async 起什么作用 这个问题的关键在于,async 函数是怎么处理它的返回值的! 我们当然希望它能直接通过return语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了.所以,写段代码来试试,看它到底会返回

  • 在JS循环中使用async/await的方法

    async / await是ES7的重要特性之一,也是目前社区里公认的优秀异步解决方案.目前,async / await这个特性已经是stage 3的建议,可以看看TC39的进度,本篇文章将分享在JS循环中使用async/await的方法. 在开发maty.js时,遇到一个数组任务,数组项是内部异步执行的函数,期望是同步依次执行每项函数,每项函数执行完本身的异步任务后,继续下一项. 刚开始单纯使用map来循环执行,并且await每项函数.如下所示: starters.map(async (fn,

  • Js中async/await的执行顺序详解

    前言 虽然大家知道async/await,但是很多人对这个方法中内部怎么执行的还不是很了解,本文是我看了一遍技术博客理解 JavaScript 的 async/await(如果对async/await不熟悉可以先看下这篇文章)后拓展了一下,我理了一下await之后js的执行顺序,希望可以给别人解疑答惑,先简单介绍一下async/await. async/await 是一种编写异步代码的新方法.之前异步代码的方案是回调和 promise. async/await 是建立在 promise 的基础上

  • JS为什么说async/await是generator的语法糖详解

    关于async的介绍,在阮一峰的ES6入门教程中说到: async 函数是什么?一句话,它就是 Generator 函数的语法糖. 可是,为什么这么说呢? 首先,比如说有一个异步操作,使用 async/await 语法来以同步模拟异步操作. 使用 async/await 实现一个 sleep 的功能 function sleep(time) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1); },

  • JS中async/await实现异步调用的方法

    async/await多个函数关联调用 async/await使得异步代码看起来像同步代码 async函数会隐式地返回一个promise,而promise的reosolve值就是函数return的值 Async/Await不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码 async声明一个异步函数 await只能在async函数中使用,后面跟一个promise对象 所以在模拟异步调用函数时,函数体内返回promise as

  • JavaScript引擎实现async/await的方法实例

    目录 前言 生成器 VS 协程 async/await async await 小结 总结 前言 我们都知道Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程,使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读.基于这个原因,ES7 引入了 async/

  • 微信小程序中使用 async/await的方法实例分析

    本文实例讲述了微信小程序中使用 async await的方法.分享给大家供大家参考,具体如下: 微信小程序中有大量接口是异步调用,比如 wx.login().wx.request().wx.getUserInfo() 等,都是使用一个对象作为参数,并定义了 success().fail() 和 complete() 作为异步调用不同情况下的回调. 但是,以回调的方式来写程序,真的很伤,如果有一个过程需要依次干这些事情: wx.getStorage() 获取缓存数据,检查登录状态 wx.getSe

  • 详解JavaScript Promise和Async/Await

    概述 一般在开发中,查询网络API操作时往往是比较耗时的,这意味着可能需要一段时间的等待才能获得响应.因此,为了避免程序在请求时无响应的情况,异步编程就成为了开发人员的一项基本技能. 在JavaScript中处理异步操作时,通常我们经常会听到 "Promise "这个概念.但要理解它的工作原理及使用方法可能会比较抽象和难以理解. 四个示例 那么,在本文中我们将会通过实践的方式让你能更快速的理解它们的概念和用法,所以与许多传统干巴巴的教程都不同,我们将通过以下四个示例开始: 示例1:用生

  • React/Redux应用使用Async/Await的方法

    Async/Await是尚未正式公布的ES7标准新特性.简而言之,就是让你以同步方法的思维编写异步代码.对于前端,异步任务代码的编写经历了 callback 到现在流行的 Promise ,最终会进化为 Async/Await .虽然这个特性尚未正式发布,但是利用babel polyfill我们已经可以在应用中使用它了. 现在假设一个简单的React/Redux应用,我将引入 Async/Await 到其代码. Actions 此例子中有一个创建新文章的 Action ,传统方法是利用 Prom

  • javascript设置和获取cookie的方法实例详解

    本文实例讲述了javascript设置和获取cookie的方法.分享给大家供大家参考,具体如下: 1. 设置cookie function setCookie(cookieName,cookieValue,cookieExpires,cookiePath) { cookieValue = escape(cookieValue);//编码latin-1 if(cookieExpires=="") { var nowDate = new Date(); nowDate.setMonth(n

  • javascript设置文本框光标的方法实例小结

    本文实例总结了javascript设置文本框光标的方法.分享给大家供大家参考,具体如下: 对于text //得到光标位置 function getCaret(textbox) { var control = document.activeElement; textbox.focus(); var rang = document.selection.createRange(); rang.setEndPoint("StartToStart",textbox.createTextRange

  • JavaScript解析及序列化JSON的方法实例分析

    本文实例讲述了JavaScript解析及序列化JSON的方法.分享给大家供大家参考,具体如下: JSON 之所以这么流行,是因为 JSON 数据结构可以被解析为 JavaScript 对象.JSON 之前的 XML 数据结构要被解析,需要先解析成 DOM 文档,然后再从中提取出数据.相比之下,JSON 数据结构方便多咯O(∩_∩)O~ 所以 JSON 就成为 web 开发中,用于数据交换的事实标准. 1 JSON 对象 早期的 JSON 解析器是使用 JavaScript 的 eval() 函数

  • JavaScript函数的4种调用方法实例分析

    本文实例讲述了JavaScript函数的4种调用方法.分享给大家供大家参考,具体如下: JavaScript 函数有 4 种调用方式: 1. 作为一个函数调用 2. 函数作为方法调用 3. 使用构造函数调用函数 4. 作为函数方法调用函数 分述如下: 每种方式的不同方式在于 this 的初始化. 作为一个函数调用 function myFunction(a, b) { return a * b; } myFunction(10, 2); // myFunction(10, 2) 返回 20 以上

随机推荐