【细读JS忍者秘籍】深入生成器函数的底层原理
深入生成器函数的底层原理
分析执行上下文
生成器函数本质上还是一个函数,所以它的执行离不开 执行上下文
function* generator() {
console.log("status1");
yield "hello";
console.log("status2");
yield "world";
}
let gen = generator();
let one = gen.next();
1. 我们试着分析在执行gen = generator()
这句代码前的执行上下文状态:
此时执行上下文栈(后面统称ECS) 中的栈顶是 全局执行上下文
该上下文中, 全局对象
(window) 上保存了 generator
函数对象。
gen , one 是 let
声明所以在 词法环境中未进行初始化
2. 当执行 gen = generator()
时 , 创建 generator
函数的执行上下文:
此时,ECS的栈顶是 生成器函数 generator
的执行上下文:
从这个时刻开始,就体现出 function *
与 function
的区别:
当 generator
的执行上下文生成好以后,
不会和常规的函数一样,开始执行代码。
而是会创建一个 指向 generator
执行上下文的 迭代器
并返回该对象
注意:
- 这里并没有执行生成器函数内部的代码
- 生成好执行上下文后,创建了一个指向该上下文环境的对象(被称作迭代器)然后返回
和正常的函数一样,生成器函数的上下文被弹出栈顶 (这也是不堵塞挂起的原因)
但由于上下文环境生成的对象没有失去引用,因此被保留了下来(和闭包一个原理)
执行next 方法
执行 next 方法时,也很特殊:
-
不会创建 next 方法的上下文,而是将迭代器中保存的生成器函数的上下文重新激活压入栈顶,并从上次挂起的
yeild
位置开始继续执行代码。 -
从这里可以看出,这就是 生成器函数不会堵塞执行的原因,因为只有执行
next
函数时,生成器函数的上下文才会被压入栈顶。每次执行到yeild
关键字就让next
返回结果,并弹出栈顶挂起,等待下次激活。
你是否还记得 next() 方法的参数?
我们可以在执行 next 方法时传入参数。
该参数很奇怪,会作为上一次yeild语句的返回值。
刚开始我会觉得这种设计很奇怪,但是现在看来这是一种很正常的逻辑:
我们执行 next 时,将生成器函数从挂起状态恢复到执行状态。
next方法的参数,为这次的恢复执行提供了一个信息,该信息保存在 上次挂起(这次恢复) 的语句,也就是作为上次yeild语句的返回值
我们通过yeild语句从生成器中得到返回值,再使用迭代器的 next 方法把值传回生成器,实现了双向通信。
普通函数每次调用都会创建一个新的执行上下文,而生成器函数不同,自始至终都是使用一个上下文,而这个上下文对象由于被迭代器引用,所以不会被当作垃圾从内存中回收掉。
普通函数的执行上下文只有调用时才新创建,相比之下,生成器函数的上下文会暂时挂起并在将来恢复。
总结:
-
执行生成器函数后,不执行其中的代码,而是返回一个保存了其上下文对象的迭代器。
-
每次调用迭代器的
next
方法,不会创建next
方法的上下文,而是将迭代器中保存的生成器的上下文重新激活压入栈顶。 -
next 方法的参数作为上次挂起的语句的返回值。
转载自:https://juejin.cn/post/7166524464094511118