JS异步编程 | Async / Await / Generator 实现原理解析

2023-06-06 0 569

async/await同时实现

在数个反弹倚赖的情景中,虽然Promise透过拉艾初始化替代了反弹冗余,但过多的拉艾初始化时效性依然欠佳,业务流程掌控也不方便快捷,ES7 明确提出的async 表达式,总算让 JS 对触发器操作方式有了毁灭者软件系统,简约迷人地化解了以内三个难题。

构想三个这种的情景,触发器各项任务a->b->c间存有倚赖亲密关系,假如他们透过then拉艾初始化来处置那些亲密关系,时效性并并非较好。

假如他们想掌控当中某一操作过程,比如说在这类前提下,b不往下继续执行到c,所以也并非很方便快捷掌控。

Promise.resolve(a) .then(b => { // do something }) .then(c => { // do something })

但假如透过async/await来同时实现那个情景,时效性和业务流程掌控单厢方便快捷许多。

async () => { const a = await Promise.resolve(a); const b = await Promise.resolve(b); const c = await Promise.resolve(c); }

所以他们要怎样同时实现三个async/await呢,具体来说他们要晓得,async/await事实上是对Generator(计算机程序)的PCB,是三个句法糖。

虽然Generator再次出现没多久就被async/await替代了,许多老师对Generator较为孤单,因而他们先来看一看Generator的用语:

ES6 新导入了 Generator 表达式,能透过 yield URL,把表达式的继续执行流挂起,透过next()方式能转换到下三个状况,为发生改变继续执行业务流程提供更多了可能将,进而为触发器程式设计提供软件系统。

function* myGenerator() { yield 1 yield 2 return 3next() //{value: “1”, done: false} gen.next() //{value: “2”, done: false} gen.next() //{value: “3”, done: true}

也能透过给next()传参, 让yield具有返回值

function* myGenerator() { console.log(yield 1) //test1 console.log(yield 2) //test2 console.log(yield 3) //test3 } constgen = myGenerator(); gen.next() gen.next(test1) gen.next(test2) gen.next(test3)

他们看到Generator的用语,应该️会感到很熟悉,*/yield和async/await看起来其实已经很相似了,它们都提供更多了暂停继续执行的功能,但二者又有三点不同:

async/await自带继续执行器,不需要手动初始化next()就能自动继续执行下一步async表达式返回值是Promise对象,而Generator返回的是计算机程序对象await能够返回Promise的resolve/reject的值

他们对async/await的同时实现,其实也就是对应以内三点PCBGenerator。

自动继续执行

他们先来看一下,对这样三个Generator,手动继续执行是怎样三个业务流程。

function* myGenerator() { yield Promise.resolve(1); yield Promise.resolve(2); yield Promise.resolve(3); } // 手动继续执行迭代器 const gen = myGenerator() gen.next().value.then(val => { console.log(val) gen.next().value.then(val => { console.log(val) gen.next().value.then(val => { console.log(val) }) }) })//输出1 2 3

他们也能透过给gen.next()传值的方式,让yield能返回resolve的值。

function* myGenerator() { console.log(yield Promise.resolve(1)) //1 console.log(yield Promise.resolve(2)) //2 console.log(yield Promise.resolve(3)) //3 } // 手动继续执行迭代器 const gen = myGenerator() gen.next().value.then(val => { // console.log(val) gen.next(val).value.then(val => { // console.log(val) gen.next(val).value.then(val => { // console.log(val) gen.next(val) }) }) })

显然,手动继续执行的写法看起来既笨拙又丑陋,他们希望计算机程序表达式能自动往下继续执行,且yield能返回resolve的值。

基于这三个需求,他们进行三个基本的PCB,这里async/await是URL,不能重写,他们用表达式来模拟:

function run(gen) { var g = gen() function _next(val) { //PCB三个方式, 递归继续执行g.next() var res = g.next(val) 对象,并返回resolve的值 if(res.done) return res.value //递归终止前提 res.value.then(val => { //Promise的then方式是同时实现自动迭代的前提_next(val)//等待Promise完成就自动继续执行下三个next,并传入resolve的值 }) } _next() //第一次继续执行 }

对他们之前的例子,他们就能这种继续执行:

function* myGenerator() { console.log(yield Promise.resolve(1)) //1 console.log(yield Promise.resolve(2)) //2 console.log(yield Promise.resolve(3)) //3 } run(myGenerator)

这种他们就初步同时实现了三个async/await。

上边的代码只有五六行,但并并非一下就能看明白的,他们之前用了四个例子来做铺垫,也是为了让读者更好地理解这段代码。

简单来说,他们PCB了三个run方式,run方式里他们把继续执行下一步的操作方式PCB成_next(),每次Promise.then()的时候都去继续执行_next(),同时实现自动迭代的效果。

在迭代的操作过程中,他们还把resolve的值传入gen.next(),使得yield得以返回Promise的resolve的值

这里插一句,是并非只有.then方式这种的形式才能完成他们自动继续执行的功能呢?答案是否定的,yield后边除了接Promise,还能接thunk表达式,thunk表达式并非三个新东西,所谓thunk表达式,就是单参的只接受反弹的表达式。

无论是Promise还是thunk表达式,其核心都是透过传入反弹的方式来同时实现Generator的自动继续执行。thunk表达式只作为三个拓展知识,理解有困难的老师也能跳过这里,并不影响后续理解。

返回Promise & 异常处置

虽然他们同时实现了Generator的自动继续执行以及让yield返回resolve的值,但上边的代码还存有着几点难题:

需要兼容基本类型:这段代码能自动继续执行的前提是yield后面跟Promise,为了兼容后面跟着基本类型值的情况,他们需要把yield跟的内容(gen().next.value)都用Promise.resolve()转化一遍缺少错误处置:上边代码里的Promise假如继续执行失败,就会导致后续继续执行直接中断,他们需要透过初始化Generator.prototype.throw(),把错误抛出来,才能被外层的try-catch捕获到返回值是Promise:async/await的返回值是三个Promise,他们这里也需要保持一致,给返回值包三个Promise

他们改造一下run方式:

function run(gen) { //把返回值包装成promise return new Promise((resolve, reject) => { var g = gen() function _next(val) { //错误处置 try { var res = g.next(val) } catch(err) {return reject(err); } if(res.done) { return resolve(res.value); } //res.value包装为promise,以兼容yield后面跟基本类型的情况 Promise.resolve(res.value).then( val => { _next(val); }, err => { //抛出错误g.throw(err) }); } _next(); }); }

然后他们能测试一下:

function* myGenerator() { try { console.log(yield Promise.resolve(1)) console.log(yield 2) //2 console.log(yield Promise.reject(error)) } catch (error) { console.log(error) } } const result = run(myGenerator) //result是三个Promise //输出 1 2 error

到这里,三个async/await的同时实现基本完成了。最后他们能看一下babel对async/await的转换结果,其实整体的思路是一样的,但写法稍有不同:

//相当于他们的run() function _asyncToGenerator(fn) { // return三个function,和async保持一致。他们的run直接继续执行了Generator,其实是不太规范的 return function() { var self = this var args = arguments return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); //相当于他们的_next() function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, next, value); }//处置异常 function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, throw, err); } _next(undefined); }); }; } function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch(error) { reject(error);return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

使用方式:

const foo = _asyncToGenerator(function* () { try { console.log(yield Promise.resolve(1)) //1 console.log(yield 2) //2 return 3 } catch (error) { console.log(error) } }) foo().then(res => { console.log(res) //3 })

有关async/await的同时实现,到这里就告一段落了。但直到结尾,他们也不晓得await到底是怎样暂停继续执行的,有关await暂停继续执行的秘密,他们还要到Generator的同时实现中去寻找答案。

Generator同时实现

他们从三个简单的Generator使用实例开始,一步步探究Generator的同时实现基本原理:

function*foo() { yield result1 yield result2 yield result3 } const gen = foo() console.log(gen.next().value) console.log(gen.next().value) console.log(gen.next().value)

他们能在babel官网上在线转化这段代码,看一看ES5环境下是如何同时实现Generator的:

“use strict”; var _marked = /*#__PURE__*/ regeneratorRuntime.mark(foo); function foo() { return regeneratorRuntime.wrap(function foo$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return result1; case 2: _context.next = 4; return result2; case 4: _context.next =6; return result3; case 6: case “end”: return _context.stop(); } } }, _marked); } vargen = foo();console.log(gen.next().value); console.log(gen.next().value); console.log(gen.next().value);

代码咋一看不长,但假如仔细观察会发现有三个不认识的东西 —— regeneratorRuntime.mark和regeneratorRuntime.wrap,这两者其实是 regenerator-runtime 模块里的三个方式。

regenerator-runtime 模块来自facebook的 regenerator 模块,完整代码在runtime.js,那个runtime有700多行…-_-||,因而他们不能全讲,不太重要的部分他们就简单地过一下,重点讲解暂停继续执行相关部分代码。

个人觉得啃源码的效果并非较好,建议读者拉到末尾先看结论和简略版同时实现,源码作为三个补充理解。

regeneratorRuntime.mark()

regeneratorRuntime.mark(foo)那个方式在第一行被初始化,他们先看一下runtime里mark()方式的定义。

//runtime.js里的定义稍有不同,多了一些判断,以下是编译后的代码 runtime.mark = function(genFun) { genFun.__proto__ = GeneratorFunctionPrototype; genFun.prototype =Object.create(Gp); return genFun; };

这里边

GeneratorFunctionPrototype和Gp他们都不认识,他们被定义在runtime里,不过没亲密关系,他们只要晓得mark()方式为计算机程序表达式(foo)绑定了一系列原型就能了,这里就简单地过了。

regeneratorRuntime.wrap()

从上面babel转化的代码他们能看到,继续执行foo(),其实就是继续执行wrap(),所以那个方式起到什么作用呢,他想包装三个什么东西呢,他们先来看一看wrap方式的定义:

//runtime.js里的定义稍有不同,多了一些判断,以下是编译后的代码 function wrap(innerFn, outerFn, self) { var generator = Object.create(outerFn.prototype); var context = new Context([]); generator._invoke = makeInvokeMethod(innerFn, self, context); returngenerator; }

wrap方式先是创建了三个generator,并继承outerFn.prototype;然后new了三个context对象;makeInvokeMethod方式接收innerFn(对应foo$)、context和this,并把返回值挂到generator._invoke上;最后return了generator。

其实wrap()相当于是给generator增加了三个_invoke方式。

这段代码肯定让人产生许多疑问,outerFn.prototype是什么,Context又是什么,makeInvokeMethod又做了哪些操作方式。下面他们就来一一解答:

outerFn.prototype其实就是genFun.prototype

那个他们结合一下上面的代码就能晓得

context能直接理解为这种三个全局对象,用于储存各种状况和上下文:

varContinueSentinel = {};var context = { done: false, method: “next”, next: 0, prev: 0, abrupt: function(type, arg) {var record = {}; record.type = type; record.arg = arg; return this.complete(record); }, complete: function(record, afterLoc) {if (record.type === “return”) { this.rval = this.arg = record.arg;this.method = “return”; this.next = “end”; } returnContinueSentinel; }, stop: function() {this.done = true; return this.rval; } };

makeInvokeMethod的定义如下,它return了三个invoke方式,invoke用于判断当前状况和继续执行下一步,其实就是他们初始化的next()

//以下是编译后的代码 function makeInvokeMethod(innerFn, context){ // 将状况置为start var state =“start”; return function invoke(method, arg) { // 已完成 if (state === “completed”) { return { value: undefined, done: true }; } context.method = method; context.arg = arg; // 继续执行中while (true) { state = “executing”; var record = { type: “normal”, arg: innerFn.call(self, creturn的值) }; if (record.type === “normal”) { // 判断是否已经继续执行完成 state = context.done ?“completed” : “yield”; // ContinueSentinel其实是三个空对象,record.arg === {}则跳过return进入下三个循环 // 什么时候record.arg会为空对象呢, 答案是没有后续yield语句或已经return的时候,也就是switch返回了空值的情况(跟着上面的switch走一下就晓得了)if (record.arg === ContinueSentinel) { continue; } // next()的返回值 return{ value: record.arg, done: context.done }; } } }; }

为什么generator._invoke事实上就是gen.next呢,因为在runtime对next()的定义中,next()其实就return了_invoke方式

// Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { [“next”, “throw”, “return”].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } defineIteratorMethods(Gp);

低配同时实现 & 调用业务流程分析

这么一遍源码下来,估计许多读者还是懵逼的,毕竟源码中纠集了许多概念和PCB,一时半会不好完全理解,让他们跳出源码,同时实现三个简单的Generator,然后再回过头看源码,会得到更清晰的认识。

// 计算机程序表达式根据yield语句将代码分割为switch-case块,后续透过转换_context.prev和_context.next来分别继续执行各个case function gen$(_context){ while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return result1; case 2: _context.next =4; return result2; case 4: _context.next = 6; return result3; case 6: case “end”: return_context.stop(); } } }// 低配版context var context = { next:0, prev: 0, done: false, stop: function stop () { this.done = true } } // 低配版invoke let gen = function() { return { next: function() { value = context.done ?undefined: gen$(context) done = context.done return{ value, done } } } }// 测试使用 var g = gen() g.next() // {value: “result1”, done: false} g.next() // {value: “result2”, done: false} g.next() // {value: “result3”, done: false} g.next() // {value: undefined, done: true}

这段代码并不难理解,他们分析一下初始化业务流程:

他们定义的function*计算机程序表达式被转化为以内代码转化后的代码分为三大块:gen$(_context)由yield分割计算机程序表达式代码而来context对象用于储存表达式继续执行上下文invoke()方式定义next(),用于继续执行gen$(_context)来跳到下一步当他们初始化g.next(),就相当于初始化invoke()方式,继续执行gen$(_context),进入switch语句,switch根据context的标识,继续执行对应的case块,return对应结果当计算机程序表达式运行到末尾(没有下三个yield或已经return),switch匹配不到对应代码块,就会return空值,这时g.next()返回{value: undefined, done: true}

从中他们能看出,Generator同时实现的核心在于上下文的保存,表达式并没有真的被挂起,每一次yield,其实都继续执行了一遍传入的计算机程序表达式,只是在那个操作过程中间用了三个context对象储存上下文,使得每次继续执行计算机程序表达式的时候,都能从上三个继续执行结果开始继续执行,看起来就像表达式被挂起了一样。

JS异步编程 | Async / Await / Generator 实现原理解析

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务