从Iterator到Generator:手搓generator来理解Async/Await风靡前端
Author:[email protected] Date:
从《ECMAScript进化史(1):话说Web脚本语言王者JavaScript的加冕历史》看,JavaScript是一门很弱鸡的语言。所以,ECMAScript标准的演进目标之一就是不断提高语言的表达力和处理数据的能力,Iterator(迭代器)和后来的Generator(生成器)极大地增强了JavaScript处理集合数据的能力。
Iterator
同样先看MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Iterator
Iterator 对象是一个符合迭代器协议的对象,其提供了 next() 方法用以返回迭代器结果对象。所有内置迭代器都继承自 Iterator 类。Iterator 类提供了 @@iterator 方法,该方法返回迭代器对象本身,使迭代器也可迭代。它还提供了一些使用迭代器的辅助方法。
迭代器协议:
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。
要成为可迭代对象,该对象必须实现 @@iterator 方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性。
一些内置类型同时是内置的可迭代对象,原生具备 Iterator 接口的数据结构如下。
Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象
下面的例子是数组的Symbol.iterator属性。
let arr = ['a', 'b']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: undefined, done: true }
普通的对象(例如{a:1, b:2}这样的字面量对象)并不是可迭代对象,因为它们默认并不实现Symbol.iterator接口。自ES2017起,Object.entries()和Object.values()正式成为了标准,提供了一种直接遍历对象属性的便捷方式。
迭代器协议
只有实现了一个拥有以下语义(semantic)的 next() 方法,一个对象才能成为迭代器:
next()
无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象(见下文)。如果在使用迭代器内置的语言特征(例如 for...of)时,得到一个非对象返回值(例如 false 或 undefined),将会抛出 TypeError("iterator.next() returned a non-object value")。
所有迭代器协议的方法(next()、return() 和 throw())都应返回实现 IteratorResult 接口的对象。它必须有以下属性:
done(可选)
如果迭代器能够生成序列中的下一个值,则返回 false 布尔值。(这等价于没有指定 done 这个属性。)
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
value(可选)
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
return(value) (可选)
无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象,其 value 通常等价于传递的 value,并且 done 等于 true。调用这个方法表明迭代器的调用者不打算调用更多的 next(),并且可以进行清理工作。
throw(exception) (可选)
无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象,通常 done 等于 true。调用这个方法表明迭代器的调用者监测到错误的状况,并且 exception 通常是一个 Error 实例。
迭代器DEMO
让我们构建一个简单的迭代器作为示例,这个迭代器将模拟一个掷骰子的游戏。游戏规则是:
连续掷骰子直到总和达到或超过20。
如果某一次掷骰得到1,则游戏立即结束,并抛出错误表示失败。
如果成功达到或超过20而没有掷出1,则游戏成功结束。
function createDiceGameIterator() { let sum = 0; return { next() { if (sum >= 20) { return { value: sum, done: true }; } else { const roll = Math.floor(1 + Math.random() * 6); sum += roll; if (roll === 1) { throw new Error("Game over: Rolled a 1"); } return { value: roll, done: false }; } }, [Symbol.iterator]() { return this; }, }; } // 创建游戏迭代器 const game = createDiceGameIterator(); try { // 迭代游戏过程 for (let roll of game) { console.log(`Rolled: ${roll} | Total: ${game.next().value}`); } } catch (error) { console.error(error.message); }
迭代器协议:我们的createDiceGameIterator函数返回一个对象,这个对象有一个next方法和一个Symbol.iterator方法,使其符合迭代器协议和可迭代协议。这样可以直接在for...of循环中使用。
next方法:每次调用返回当前掷骰子的结果,并更新总和。如果总和达到20,迭代完成。
done属性:返回true表示迭代完成,这里当掷骰子的总和达到或超过20时发生。
throw:如果掷骰子得到1,通过抛出错误来立即终止迭代。注意这不是迭代器协议的一部分,而是这个示例特有的逻辑,用于处理特定情况。
其是,大部分情况只需next即可
// 创建一个可迭代对象来生成斐波那契数列 const fibonacciIterable = { // 实现 [Symbol.iterator] 方法,使对象可迭代 [Symbol.iterator]() { let a = 1, b = 1; return { // 迭代器协议要求提供 next 方法 next() { let returnValue = a; [a, b] = [b, a + b]; // next 方法需要返回一个包含 value 和 done 属性的对象 // 在这个例子中,我们将永远返回 {done: false},因为斐波那契数列是无限的 return { value: returnValue, done: false }; } }; } }; // 使用 for...of 循环来迭代斐波那契数列的前10个数 for (const num of fibonacciIterable) { console.log(num); if (num > 50) break; // 给循环一个终止条件以避免无限循环 }
在上面的代码中next函数的实现是迭代器的核心,但是每次都要手动实现,生成器的出现就是为了更简单的使用迭代器
Generator
Generator是一个对象,是由生成器函数 (generator function)返回的,并且它符合可迭代协议和迭代器协议。
generator function函数是在普通的函数名称前加一个*号,且函数内部使用yeild关键词定义函数断点,让函数从上到下分批次执行并返回值。
function* loggerator() { console.log('开始执行'); yield '暂停'; console.log('继续执行'); return '停止';// 如果没有 } let logger = loggerator(); console.log(logger.next()); // 开始执行 { value: '暂停', done: false } console.log(logger.next()); // 继续执行 ,{ value: '停止', done: true } console.log(logger.next());// { value: undefined,, done: true }
看下MDN解释:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*
function* 声明创建一个绑定到给定名称的新生成器函数。生成器函数可以退出,并在稍后重新进入,其上下文(变量绑定)会在重新进入时保存。
function* idMaker() { let index = 0; while (index<10) { yield index++; } } const gen = idMaker(); console.log(gen.next().value); // 0 console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 console.log(gen.next().value); // 3function* 声明创建一个 GeneratorFunction 对象。每次调用生成器函数时,它都会返回一个新的 Generator 对象,该对象符合迭代器协议。当迭代器的 next() 方法被调用时,生成器函数的主体会被执行,直到遇到第一个 yield 表达式,该表达式指定了迭代器要返回的值,或者用 yield* 委托给另一个生成器函数。next() 方法返回一个对象,其 value 属性包含了 yield 表达式的值,done 属性是布尔类型,表示生成器是否已经返回了最后一个值。如果 next() 方法带有参数,那么它会恢复生成器函数的执行,并用参数替换暂停执行的 yield 表达式。
看的不明觉厉,再来看前端科普大佬,阮老师的:https://www.ruanyifeng.com/blog/2015/04/generator.html
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
function* gen(x){ var y = yield x + 2; return y; }整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
var g = gen(1);
注意:function* 函数并不会执行此函数,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是迭代器对象(Iterator Object)。
Generator的底层实现原理
状态机:Generator函数本质上是一个状态机,封装了多个内部状态。执行Generator函数会返回一个遍历器对象,该对象本质上是一个封装了Generator内部状态的指针对象。
函数暂停与恢复:Generator函数的执行可以在yield表达式处暂停,并在稍后通过next方法恢复执行。这种暂停和恢复的能力是通过函数调用栈的操作实现的。当遇到yield时,引擎会保存当前函数的上下文(包括变量状态、指令位置等),并将控制权交还给调用者。当通过next方法恢复时,引擎会恢复之前保存的上下文,并从上次暂停的位置继续执行。
协程:Generator可以被看作是轻量级的协程(coroutines),它们允许多个入口点用于暂停和恢复代码执行。JavaScript引擎实现了协程的调度机制,使得Generator函数的执行可以在任意yield点暂停和恢复。
迭代器协议:Generator遵循迭代器协议,即它们的返回对象具有next方法。这个next方法返回一个对象,该对象包含两个属性:value(yield表达式产出的值)和done(一个布尔值,表示Generator是否已经产出了它的最后一个值)。
Generator 函数通过yield关键字暂停和恢复执行,但它本身并不是专门为异步编程设计的。
Generator函数本身并不直接参与JavaScript的事件循环机制:Generator函数的执行是同步的,它的yield和next操作并不会导致JavaScript引擎将任务排入宏任务(macrotask)或微任务(microtask)队列。相反,Generator函数的控制流是由外部代码显式管理的,通常是通过调用next方法来实现。
通过结合Promise和迭代器协议,可以使用Generator来管理异步流程。
V8引擎中Generator函数工作原理概述
编译阶段:
当V8遇到一个Generator函数时,它会将这个函数编译成一系列的字节码指令。这些字节码指令包括了处理yield表达式的特殊指令,用于暂停和恢复函数执行。
函数调用栈:
在JavaScript中,每当一个函数被调用时,一个新的帧(frame)就会被推入调用栈中。这个帧包含了函数的局部变量、参数和返回地址等信息。
对于Generator函数,当遇到yield表达式时,V8会保存当前函数帧的状态,包括局部变量、当前执行位置等信息,并将控制权返回给调用者。这个过程涉及到将函数帧的状态序列化到堆内存中,以便之后可以恢复。
暂停和恢复:
当外部代码通过迭代器对象调用next方法时,V8会检查是否有一个已经暂停的Generator函数帧。如果有,V8会从堆内存中反序列化该函数帧的状态,并将其推回调用栈中。
V8随后会从上次暂停的位置恢复执行字节码指令,直到遇到下一个yield表达式或函数结束。
状态管理:
V8内部会为每个Generator函数实例维护一个状态。这个状态表明Generator是正在执行(executing)、已经暂停(suspended)、已经完成(completed)还是出错(errored)。
当Generator函数执行完毕或抛出错误时,V8会更新这个状态,并处理任何必要的清理工作。
与事件循环的交互:
Generator函数的执行本身是同步的,但它们可以用于管理异步操作。例如,你可以在yield表达式中等待一个异步操作(如Promise)的完成。
在这种情况下,当异步操作完成时,它的回调函数(可能是微任务)会调用Generator的next方法,从而恢复Generator函数的执行。
Generator注意事项
next、return参数可以完成花来……
Generator next参数
next方法可以带一个参数,该参数会被当作上一个yield表达式的返回值。这提供了一种在Generator的不同阶段之间进行通信的方式。
yield除了返回一个值之外,还能用来接收外界的输入。next()方法中传的第一个参数会被yield接收。
通过Generator函数和next方法的参数在函数的内部各个阶段之间传递信息,从而实现复杂的控制流程。
next参数的示例解释:
function* loginFlow() { const username = yield '请输入用户名:'; const password = yield '请输入密码:'; if (username === 'admin' && password === '123456') { yield '登录成功'; } else { yield '用户名或密码错误'; } } const login = loginFlow(); // 初始化Generator函数,此时函数暂不执行 console.log(login.next().value); // 输出:请输入用户名: console.log(login.next('admin').value); // 将'admin'作为上一个yield的返回值,并输出:请输入密码: console.log(login.next('123456').value); // 将'123456'作为上一个yield的返回值,并输出:登录成功
Redux-Saga: 在这个用于管理应用状态的库中,使用Generator(生成器,它基于Iterator接口)来处理异步流和复杂的同步流程。
Generator return参数
return() 将会忽略生成器中的任何代码。它会根据传值设定 value,并将 done 设为 true。
任何在 return() 之后进行的 next() 调用都会返回 done 属性为 true 的对象。
function* generatorFunction() { yield 1; yield 2; yield 3; } // 创建一个Generator对象 const generator = generatorFunction(); // 获取第一个yield的值 console.log(generator.next()); // 输出:{ value: 1, done: false } // 终止Generator,那么返回值就 无了 console.log(generator.return()); // 输出:{ undefined,: 100, done: true } // 终止Generator,并指定返回值 console.log(generator.return(100)); // 输出:{ value: 100, done: true } // 再次调用next(),验证Generator已经完成 console.log(generator.next()); // 输出:{ value: undefined, done: true }
这个特性可以提前终止后续的yield,比如:Generator函数用来迭代数组元素,但基于某些条件,我们可能想提前结束遍历并返回一个特定的结果。yield delegator(yield委托)
带星号的 yield 可以代理执行另一个 generator。这样你就可以根据需要连续调用多个 generator。
function* generateZeroToOne() {// 生成0到1的数字 yield 0; yield 1; } function* generateTwoAndThree() {// 生成数字2和数字3 yield 2; yield 3; } function* generateNumbers() {// 通过yield*委托将上述两个Generator函数结合起来 yield* generateZeroToOne(); // 委托给generateZeroToOne yield* generateTwoAndThree(); // 委托给generateTwoAndThree } for (let value of generateNumbers()) {// 遍历generateNumbers产生的值 console.log(value); // 预期依次输出:0, 1, 2, 3 }
generator可以将一系列相关的生成逻辑模块化地组织在不同的Generator函数中,而且还可以通过yield*委托机制灵活地组合这些逻辑,实现更加复杂的数据生成和处理策略。
Generator ES5实现
function asyncToSyncAndRun(gen){ var g = gen(); //此时g为生成器对象 function next(data){ var result = g.next(data); //注意:前面说过 result的结构,result是一个对象,里面的value对应yield后表达式的返回值 //所以result.value是一个Promise对象 if (result.done) return result.value;//如果遍历结束,return //未遍历结束,就把下一个next执行放在现在的Promise对象的回调中去 result.value.then(function(data){ next(data); }); } next();//触发next方法~ } //自动执行 asyncToSyncAndRun(asyncFun)
Generator ES5为何被弃用
Generator函数在ES6中被引入,以提供一种更优雅的异步编程解决方案。它们通过yield
关键字允许函数执行的暂停和恢复,这在处理复杂的异步操作时非常有用。然而,尽管Generator函数的引入带来了新的编程范式,它们在实际项目中的使用仍然相对较少,原因主要包括:
学习曲线: Generator函数引入了一种新的编程概念,包括
yield
关键字、必须使用特殊的迭代器对象来控制函数执行等。这些新概念为JavaScript开发者带来了额外的学习负担,特别是对于那些不熟悉协程概念的人。异步编程解决方案的演进: 当Generator函数被引入时,它们被视为异步编程的一种改进,特别是与回调地狱相比。然而,随后引入的
async/await
语法提供了更加简洁和直观的异步编程方式。async/await
背后的机制仍然基于Promise,它更容易理解和使用,并且能够更好地与现有的JavaScript库和框架集成。因此,很多开发人员和项目转而采用了async/await
,而不是Generator函数。调试和错误处理: Generator函数的执行不是线性的,它们可以在任何
yield
表达式处暂停和恢复。这种非线性执行模型可能会给调试带来额外的复杂性。而且,错误处理机制(如异常处理)也需要更多的工作,与传统的同步代码或Promise-based异步代码相比,可能更容易引入bug。性能考虑: 虽然在很多场景下,Generator的性能完全可以满足需求,但它们引入了额外的抽象层,可能会比直接使用Promise或
async/await
有轻微的性能开销。兼容性: 当Generator首次引入时,不是所有JavaScript环境都支持它们(虽然通过转译器如Babel可以实现兼容)。随着时间的推移,环境支持得到了改善,但早期的兼容性问题可能对它们的采用有一定的影响。
个人认为:根本原因JavaScript 生来就是简单的脚本语言(出圈基因),而Generator 太复杂!
参考文章:
https://dennisgo.cn/Articles/JavaScript/Generator.html
深入解析 JavaScript 中的 Generator 生成器 https://zhuanlan.zhihu.com/p/636245402
手写generator核心原理,再也不怕面试官问我generator原理 https://juejin.cn/post/6859281096152973326
手写generator核心原理及源码简析 https://blog.csdn.net/qq_46193451/article/details/110064977
「一次写过瘾」手写Promise全家桶+Generator+async/await https://segmentfault.com/a/1190000038537123
Promise从手写到扩展 | Promise/Generator/async | [Promise系列二](一) https://developer.aliyun.com/article/977152
转载本站文章《从Iterator到Generator:手搓generator来理解Async/Await风靡前端》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2016_0202_503.html