ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用 Q.async 与 Generator 把 Promise 写成同步代码:async-generators 示例深度解析

用 Q.async 与 Generator 把 Promise 写成同步代码:async-generators 示例深度解析 用 Q.async 与 Generator 把 Promise 写成同步代码async-generators 示例深度解析【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples导读本文聚焦于 WWI 示例应用 前端依赖库 QPromise 工具库中的 async-generators 示例目录。该目录以最小可运行的示例演示了如何用 ES6 Generator 函数结合Q.async装饰器让yield等价于其他语言中的await/defer语法从而把基于 Promise 的异步回调链改写成近似同步的阻塞式代码。读完本文你将掌握 Generator 与 Promise 的协作原理、Q.async/Q.spawn的底层实现机制以及如何用try/catch、for/if等同步语法编排异步流程。背景这份示例出现在哪里async-generators是 Q 库自带的官方示例之一位于 samples/databases/wide-world-importers/wwi-app/wwwroot/lib/q/。Q 是 WWI 前端基于 Bootstrap 3 与 jQuery 的经典 Web 应用引入的 Promise 库package.json 显示其版本为2.0.2遵循 MIT 许可核心文件为 q.js。该示例目录共包含 6 个文件README.md概念说明与基础用法0.html浏览器端 DOM 动画示例1-return.js返回值与返回值 Promise 化2-error-propagation.js异常传播与try/catch3-spawn.jsQ.spawn顶层入口4-flow-control.js用for/if/try做流程控制历史背景与兼容性警告README 开头即给出郑重警告本文档描述的行为很可能很快被标准化和实现的进展所淘汰请谨慎对待。这并非夸大——Generator 在 2013 年尚处于 ES6 标准草案阶段而现代 JavaScript 早已原生支持async/await其语法与本文的Q.async装饰器思路一脉相承。从概念史的角度这份示例正是async/await语法的前身。当时的环境约束README 原文事实Generator 函数当时正处于 ES6 标准轨道上截至 2013 年 7 月只有前沿 V8完全支持它们尚未进入发布的 Chromium预计会在 Chromium 29 中出现即便在那时也必须在chrome://flags中开启Experimental JavaScript features实验特性才能使用Firefox 使用的 SpiderMonkey 实现了旧式Python 风格GeneratorQ 并不支持这种旧式 Generator。对应的源码注释也印证了这一点q.js 中写道ES6 Generator 属于 V8 3.19 版本在--harmony-generators运行时标志下支持的特性Q 不兼容 SpiderMonkey 的旧式 Python 风格 Generator同时强调该代码在旧引擎中不会产生语法错误得益于对yield的惰性引用会随着语言演进持续受益。示例 0.html 的 HTML 注释同样注明适用于支持 ES6 Generator 的浏览器例如带--harmony标志的 Chromium 29。建议读者把本文当作异步编程演化史 Q 库 API 实战来阅读一方面理解async/await的思想源头另一方面掌握 Q 库的实际用法。今天的 Node.js / 现代浏览器中Generator 语法已被原生async/await取代但两者在控制流语义上完全同构。Generator 基础yield 既是产出也是接收无限计数器yield 产出值README 给出的第一个示例是不依赖 Q 任何特性的纯 Generatorfunction* count() { var i 0; while (true) { yield i; } } var counter count(); count.next().value 0; count.next().value 1; count.next().value 2;注意原文此处的变量名略有瑕疵创建的是counter调用却写成count.next()。若直接运行count.next()会因count是函数而非对象而报错。正确的调用对象是counter即counter.next().value 0; counter.next().value 1; counter.next().value 2;这是 Generator 最核心的机制函数体内yield后的表达式作为next()的返回值value每次next()调用推进一次执行直到done为true。这里的while (true)意味着它是一个永不结束的惰性序列。双通道通信yield 接收值yield不是单向输出它还能作为表达式接收next()传入的参数var buffer (function* () { var x; while (true) { x yield x; } }()); buffer.next(1).value undefined; buffer.next(a).value 1; buffer.value(2).value a; buffer.next().value 2; buffer.next().value undefined; buffer.next().value undefined;同样原文第 45 行存在笔误buffer.value(2)应为buffer.next(2)。该示例展示了一个回显缓冲协议调用传入参数上一次 yield 表达式求值结果本次 next() 返回的 valuebuffer.next(1)1首次传入被忽略—undefinedbuffer.next(a)a11buffer.next(2)2aabuffer.next()undefined22buffer.next()undefinedundefinedundefined也就是说第一次next(arg)的参数被忽略因为没有挂起的yield等待接收此后每次next(arg)的参数会变成上一次yield表达式的求值结果而next()返回的value则是本次执行到下一个yield时产出的值。这正是 Q.async 实现等待 Promise所依赖的底层机制——yield表达式既可以被注入Promise 的兑现值又可以产出下一个要等待的 Promise。Q.async把 Generator 变成异步编排器核心用法README 中的核心示例展示了Q.async的用法var eventualAdd Q.async(function* (oneP, twoP) { var one yield oneP; var two yield twoP; return one two; }); eventualAdd(eventualOne, eventualTwo).then(function (three) { three 3; });其中oneP、twoP是 Promise或任意可Q()包装的值。Q.async(makeGenerator)接收一个 Generator 工厂函数返回一个普通函数eventualAdd。调用eventualAdd(eventualOne, eventualTwo)时内部创建 Generator 实例调用generator.next()启动执行当yield oneP执行时若oneP是 Promise执行会暂停Q.async内部把oneP交给Q(...)包装并挂上回调待oneP兑现后把兑现值通过generator.next(value)注入yield表达式使one 兑现值继续执行到下一个yield最终 Generator 返回one two该返回值被Q(...)包装成 Promise即eventualAdd(...)的返回值。于是yield语义等价于 Go 与 C# 5 中的await/defer异步等待被折叠成同步赋值。源码级实现Q.async与Q.spawn的实现位于 q.jsQ.async Q_async; function Q_async(makeGenerator) { return function spawn() { function continuer(verb, arg) { var iteration; try { iteration generatorverb; } catch (exception) { return Q_reject(exception); } if (iteration.done) { return Q(iteration.value); } else { return Q(iteration.value).then(callback, errback); } } var generator makeGenerator.apply(this, arguments); var callback continuer.bind(continuer, next); var errback continuer.bind(continuer, throw); return callback(); }; } Q.spawn Q_spawn; function Q_spawn(makeGenerator) { Q_async(makeGenerator)().done(); }这段代码是整个示例的灵魂值得逐行拆解continuer(verb, arg)以verb区分两种操作——next正常推进arg为兑现值与throw注入异常arg为异常对象二者分别对应 Generator 的generator.next(arg)与generator.throw(arg)。try/catch包裹Generator 方法调用抛出的同步异常包括throw注入后未被捕获的异常被捕获并转为Q_reject(exception)即 Promise 拒绝。iteration.done分支Generator 已结束done true把返回值iteration.value用Q(...)包装成已兑现 Promise 返回未结束则把本次产出的值可能是 Promise交给Q(iteration.value).then(callback, errback)兑现后回调callback即continuer(next, value)继续推进拒绝则走errback即continuer(throw, reason)。Q.spawn对Q_async(makeGenerator)的结果立即调用并追加.done()终止 Promise 链使未处理的错误直接抛出而非被吞掉——这是顶层运行 Generator的标准姿势示例 3-spawn.js 与 0.html 均采用此模式。从源码结构看Q.async采用的正是生成器驱动generator trampoline模式整个异步推进由continuer回调链驱动与后来 V8 原生async/await的转译思路高度一致。示例一返回值与 Promise 化1-return.js1-return.js 演示了yield 一个普通值与Generator 返回值被 Promise 化use strict; var Q require(../../q); var generator Q.async(function* () { var ten yield 10; console.log(ten, 10); var twenty yield ten 10; console.log(twenty, 20); var thirty yield twenty 10; console.log(thirty, 30); return thirty 10; }); generator().then(function (forty) { console.log(forty, 40); }, function (reason) { console.log(reason, reason); });关键观察点yield后面跟的不是 Promise而是普通数字。Q(...)对非 Promise 值会立即返回已兑现的 Promise因此ten、twenty、thirty依次得到10、20、30——yield 对普通值与 Promise 值一视同仁。Generator 的return thirty 10即40被Q(iteration.value)包装成 Promise通过.then(forty ...)接收。控制台依次输出10 10、20 20、30 30、40 40。若中途任何 Promise 被拒绝控制流会跳到第二个回调reason这正是Generator 内同步化的 Promise 链的错误汇聚点。示例二异常传播与 try/catch2-error-propagation.js2-error-propagation.js 验证了 README 中异常会在每个后续 yield 中传播的行为use strict; var Q require(../../q); var generator Q.async(function* () { try { var ten yield Q.reject(new Error(Rejected!)); console.log(Should not get here 1); } catch (exception) { console.log(Should get here 1); console.log(exception.message, should be, Rejected!); throw new Error(Threw!); } }); generator().then(function () { console.log(Should not get here 2); }, function (reason) { console.log(Should get here 2); console.log(reason.message, should be, Threw!); });运行结果依次为Should get here 1 Rejected! should be Rejected! Should get here 2 Threw! should be Threw!机制还原与 q.js 逐行对应yield Q.reject(...)产出被拒绝的 PromiseQ(...).then(callback, errback)走errback即continuer(throw, rejectionReason)内部调用generator.throw(rejectionReason)把拒绝原因作为异常抛到yield表达式所在位置被try { ... } catch (exception)捕获输出第一组日志catch块内throw new Error(Threw!)再次抛出被continuer的try/catch捕获转为Q_reject最终 Promise 被拒绝触发.then的第二个回调输出第二组日志。由此可以得出 Q 的异常模型Generator 内抛出的异常、yield 的 Promise 被拒绝二者最终统一为返回 Promise 的拒绝try/catch可以捕获异步错误抛出的异常继续沿 Promise 链传播——这正是 README 源码注释q.js描述的语义如果 Generator 中抛出错误它会穿过后续每一个 yield 传播直到被捕获或完全逸出 Generator 函数并转化为装饰器返回 Promise 的拒绝。示例三Q.spawn 顶层运行3-spawn.js3-spawn.js 展示Q.spawn的典型场景——串行等待两个延时任务use strict; var Q require(../../q); function foo() { return Q.delay(5, 1000); } function bar() { return Q.delay(10, 1000); } Q.spawn(function* () { var x yield foo(); console.log(x); var y yield bar(); console.log(y); console.log(result, x y); });Q.delay(value, ms)在指定毫秒后兑现为value。这里foo、bar各自模拟 1000ms 的异步任务分别产出5与10Q.spawn立即执行 Generator先等x1 秒后输出5再等y再 1 秒后输出10最后输出result 15两段等待是严格串行的总耗时约 2 秒这正是阻塞式语义的体现——若希望并行应先把两个任务都启动再yield各自的 Promise由于Q.spawn内部追加了.done()任何未捕获的拒绝都会以未捕获异常形式抛出适合顶层脚本、交互式 REPL 或浏览器入口使用。示例四用同步语法做流程控制4-flow-control.js4-flow-control.js 是综合示例证明在Q.async内部可以自由使用for、if、try/catch等同步控制结构use strict; var Q require(../../q); // 用 if、while、for 等对 Promise 做阻塞式处理 var filter Q.async(function* (promises, test) { var results []; for (var i 0; i promises.length; i) { var val yield promises[i]; if (test(val)) { results.push(val); } } return results; }); var promises [ Q.delay(a, 500), Q.delay(d, 1000), Q(l) ]; filter(promises, function (letter) { return f letter; }).done(function (all) { console.log(all); // [ a, d ] }); // 用 try/catch 处理被拒绝的 Promise var logRejections Q.async(function* (work) { try { yield work; console.log(Never end up here); } catch (e) { console.log(Caught:, e.message); } }); var rejection Q.reject(new Error(Oh dear)); logRejections(rejection); // Caught: Oh dear该示例揭示了两个核心能力Promise 数组的串行过滤for循环逐个yield数组中的 Promise把每个兑现值注入val再用if (test(val))同步判断。a与d满足f letter按字符码比较f的码点大于a、d小于l因此输出[a, d]。这展示了对异步集合处理的优雅写法——同样的逻辑若用裸回调或.then链会嵌套得难以阅读。异步 try/catchyield work遇到Q.reject(new Error(Oh dear))时拒绝被注入为异常被catch (e)捕获输出Caught: Oh dearconsole.log(Never end up here)永远不会执行。这进一步确认了拒绝 异常注入的等价关系。浏览器动画示例0.html0.html 把上述能力落到真实 UI 场景——用 Generator 驱动 DOM 动画并展示了Q.delay的定时语义script src../../q.js/script script (function () { use strict; var deferredAnimate Q.async(function* (element) { for (var i 0; i 100; i) { element.style.marginLeft i px; yield Q.delay(20); } }); Q.spawn(function* () { yield deferredAnimate(document.getElementById(box)); alert(Done!); }); }()); /script页面中有一个 20×20 的红色方块div idbox脚本让它在约 2 秒内100 次 × 20ms从左向右平滑移动 100px结束后弹出Done!。要点yield Q.delay(20)每次让出 20msQ.delay兑现值为undefined后动画继续下一步——这是以同步循环实现帧动画的经典手法外层Q.spawn等待整个动画 Generator 完成后再alert形成两个 Generator 之间的嵌套编排源码注释确认其动画示例取自 ECMAScript 官方 wiki 的 deferred_functions 提案页作者为 Peter Hallam、Tom van Cutsem、Mark S. Miller、Dave Herman、Andy Wingo——这一提案正是后来async/await的早期雏形之一。阅读与运行指引运行这些示例的两种方式Node.js现代环境各.js示例均通过require(../../q)引用 Q 源码在示例目录下执行node 1-return.js、node 2-error-propagation.js、node 3-spawn.js、node 4-flow-control.js即可看到控制台输出4-flow-control.js需等约 1 秒的Q.delay完成。浏览器历史环境直接打开 0.html按文件注释所述需要支持 ES6 Generator 的浏览器如带--harmony标志的 Chromium 29。Q 库本身使用jasminumpromises-aplus-tests等工具测试见 package.json 的scripts.test核心实现集中在 q.js如需深入研究 Promise 与 Generator 的交互可继续阅读该文件第 576–611 行的Q_async/Q_spawn实现及其周边 Promise 基础设施。小结async-generators示例虽诞生于 Generator 尚未标准化的 2013 年却完整展示了Generator Promise这一异步编程范式的全部要素yield的双向通信、Q.async的生成器驱动实现、异常的统一拒绝化、以及用同步语法编写异步流程的能力。理解这份示例等于理解了现代async/await的原理原型Q.async之于 Generator正如 V8 转译器之于async/await。在今天阅读它价值不在照抄使用语法早已演进而在于从源码层面厘清await的实质——暂停、注入兑现值、错误即异常。【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表