用 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 示例应用 前端依赖库 Q(Promise 工具库)中的 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/catch - 3-spawn.js:
Q.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 风格)Generator,Q 并不支持这种旧式 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 任何特性的纯 Generator:
function* 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() 返回的 value |
|---|---|---|---|
buffer.next(1) | 1(首次传入被忽略) | — | undefined |
buffer.next("a") | "a" | 1 | 1 |
buffer.next(2) | 2 | "a" | "a" |
buffer.next() | undefined | 2 | 2 |
buffer.next() | undefined | undefined | undefined |
也就是说:第一次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.js:
Q.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.js)
1-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/catch(2-error-propagation.js)
2-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(...)产出被拒绝的 Promise;Q(...).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.js)
3-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与10;Q.spawn立即执行 Generator:先等x(1 秒后输出5),再等y(再 1 秒后输出10),最后输出result 15;- 两段等待是严格串行的(总耗时约 2 秒),这正是"阻塞式"语义的体现——若希望并行,应先把两个任务都启动,再
yield各自的 Promise; - 由于
Q.spawn内部追加了.done(),任何未捕获的拒绝都会以未捕获异常形式抛出,适合顶层脚本、交互式 REPL 或浏览器入口使用。
示例四:用同步语法做流程控制(4-flow-control.js)
4-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/catch:
yield work遇到Q.reject(new Error("Oh dear"))时,拒绝被注入为异常,被catch (e)捕获,输出Caught: Oh dear;console.log("Never end up here")永远不会执行。这进一步确认了"拒绝 == 异常注入"的等价关系。
浏览器动画示例(0.html)
0.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 id="box">),脚本让它在约 2 秒内(100 次 × 20ms)从左向右平滑移动 100px,结束后弹出Done!。要点:
yield Q.delay(20)每次让出 20ms,Q.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 库本身使用jasminum+promises-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),仅供参考