hac*_*ape 5 javascript closures garbage-collection v8
我正在尝试将长生不老药的演员模型语言原语移植到 JS 中。我想出了一个解决方案(在 JS 中)来模拟receiveelixir 关键字,使用“接收器”函数和生成器。
这是一个简化的实现和演示,向您展示这个想法。
应用程序接口:
type ActorRef: { send(msg: any): void }
type Receiver = (msg: any) => Receiver
/**
* `spawn` takes a `initializer` and returns an `actorRef`.
* `initializer` is a factory function that should return a `receiver` function.
* `receiver` is called to handle `msg` sent through `actorRef.send(msg)`
*/
function spawn(initializer: () => Receiver): ActorRef
Run Code Online (Sandbox Code Playgroud)
演示:
function* coroutine(ref) {
let result
while (true) {
const msg = yield result
result = ref.receive(msg)
}
}
function spawn(initializer) {
const ref = {}
const receiver = initializer()
ref.receive = receiver
const gen = coroutine(ref)
gen.next()
function send(msg) {
const ret = gen.next(msg)
const nextReceiver = ret.value
ref.receive = nextReceiver
}
return { send }
}
function loop(state) {
console.log('current state', state)
return function receiver(msg) {
if (msg.type === 'ADD') {
return loop(state + msg.value)
} else {
console.log('unhandled msg', msg)
return loop(state)
}
}
}
function main() {
const actor = spawn(() => loop(42))
actor.send({ type: 'ADD', value: 1 })
actor.send({ type: 'BLAH', value: 1 })
actor.send({ type: 'ADD', value: 1 })
return actor
}
window.actor = main()Run Code Online (Sandbox Code Playgroud)
以上模型有效。但是我有点担心这种方法的性能影响,我不清楚它创建的所有闭包上下文的内存影响。
function loop(state) {
console.log('current state', state) // <--- `state` in a closure context <?? <??????
return function receiver(msg) { // ---> `receiver` closure reference ??? ?
if (msg.type === 'ADD') { ?
return loop(state + msg.value) // ---> create another context that link to this one???
} else {
console.log('unhandled msg', msg)
return loop(state)
}
}
}
Run Code Online (Sandbox Code Playgroud)
loop是返回“接收器”的“初始化程序”。为了保持内部状态,我将它(state变量)保存在“接收器”函数的闭包上下文中。
当接收到一条消息时,当前接收者可以修改内部状态,并将其传递给loop并递归创建一个新的接收者来替换当前的接收者。
显然,新的接收者也有一个新的闭包上下文来保持新的状态。在我看来,这个过程可能会创建一个深层的链接上下文对象链来阻止 GC?
我知道,通过关闭引用了上下文对象可能会在某些情况挂钩。如果它们是链接的,那么在最里面的闭包被释放之前,它们显然不会被释放。根据这篇文章V8 优化在这方面非常保守,图片看起来不漂亮。
如果有人能回答这些问题,我将不胜感激:
loop示例是否创建了深度链接的上下文对象?receiver创建receiver机制是否会在其他情况下最终创建深度链接的上下文对象?@TJCrowder 的后续问题。
闭包是词法的,所以它们的嵌套遵循源代码的嵌套。
说得好,这很明显,但我错过了
只是想确认我的理解是正确的,举一个不必要的复杂例子(请耐心等待)。
这两个在逻辑上是等价的:
// global context here
function loop_simple(state) {
return msg => {
return loop_simple(state + msg.value)
}
}
// Notations:
// `c` for context, `s` for state, `r` for receiver.
function loop_trouble(s0) { // c0 : { s0 }
// return r0
return msg => { // c1 : { s1, gibberish } -> c0
const s1 = s0 + msg.value
const gibberish = "foobar"
// return r1
return msg => { // c2 : { s2 } -> c1 -> c0
const s2 = s1 + msg.value
// return r2
return msg => {
console.log(gibberish)
// c3 is not created, since there's no closure
const s3 = s2 + msg.value
return loop_trouble(s3)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
然而,内存影响是完全不同的。
loop_trouble,c0是创造控股s0;返回r0 -> c0。r0,c1被创建, 持有s1, gibberish, 返回r1 -> c1。r1,c2被创建,持有s2,返回r2 -> c2我相信在上面的情况下,当r2(最里面的箭头函数)用作“当前接收器”时,它实际上不仅仅是r2 -> c2,但是r2 -> c2 -> c1 -> c0,所有三个上下文对象都被保留(如果我在这里已经错了,请纠正我)。
问题:哪种情况是真的?
gibberish我特意放入了变量。gibberish。换句话说, 的依赖s1 = s0 + msg.value足以链接c1 -> c0。因此,作为“容器”的环境记录始终被保留,因为容器中包含的“内容”可能因引擎而异,对吗?
一个非常幼稚的未优化方法可能会盲目地将所有局部变量包括在“内容”中,加上arguments和this,因为规范没有说明任何关于优化的内容。
一个更聪明的方法可以是窥视嵌套函数并检查到底需要什么,然后决定将什么包含在内容中。这在我链接的文章中被称为“促销” ,但那条信息可以追溯到 2013 年,恐怕它可能已经过时了。
无论如何,您是否有关于此主题的更多最新信息要分享?我对 V8 如何实现这种策略特别感兴趣,因为我目前的工作严重依赖于电子运行时。
注意:此答案假设您使用的是strict mode。你的代码片段没有。我建议始终使用严格模式,通过使用 ECMAScript 模块(自动处于严格模式)或放在"use strict";代码文件的顶部。(我必须更多地考虑arguments.callee.caller and other such monstrosities if you wanted to use loose mode, and I haven\'t below.)
\n\n\n
\n- 循环示例是否创建了深度链接的上下文对象?
\n
不深,不。内部调用loop不会将这些调用创建的上下文链接到对其进行调用的上下文。重要的是函数是在哪里loop创建的,而不是从哪里调用它。如果我做:
const r1 = loop(1);\nconst r2 = r1({type: "ADD", value: 2});\nRun Code Online (Sandbox Code Playgroud)\nThat creates two functions, each of which closes over the context in which it was created. That context is the call to loop. That call context links to the context where loop is declared\xc2\xa0\xe2\x80\x94 global context in your snippet. The contexts for the two calls to loop don\'t link to each other.
\n\n\n
\n- What does the lifespan of context object look like in this example?
\n
Each of them is retained as long as the receiver function referring to it is retained (at least in specification terms). When the receiver function no longer has any references, it and the context are both eligible for GC. In my example above, r1 doesn\'t retain r2, and r2 doesn\'t retain r1.
\n\n\n
\n- If current example does not, can this receiver creates receiver mechanism ends up creating deeply linked context objects under other situation?
\n
It\'s hard to rule everything out, but I wouldn\'t think so. Closures are lexical, so the nesting of them follows the nesting of the source code.
\n\n\n\n
\n- If "yes" to question 3, can you please show an example to illustrate such situation?
\n
N/A
\nNote: In the above I\'ve used "context" the same way you did in the question, but it\'s probably worth noting that what\'s retained is the environment record, which is part of the execution context created by a call to a function. The execution context isn\'t retained by the closure, the environment record is. But the distinction is a very minor one, I mention it only because if you\'re delving into the spec, you\'ll see that distinction.
\nRe your Follow-Up 1:
\n\n\nc3 is not created, since there\'s no closure
\n
c3已创建,只是在调用结束后不会保留它,因为没有任何内容会关闭它。
\n\n问题:哪种情况属实?
\n
两者都不。无论是否存在变量或参数,所有三个上下文(c0、c1和)都会保留(至少在规范术语中)c2gibberishs0s1 variable, etc. A context doesn\'t have to have parameters or variables or any other bindings in order to exist. Consider:
// ge = global environment record\n\nfunction f1() {\n // Environment record for each call to f1: e1(n) -> ge\n return function f2() {\n // Environment record for each call to f2: e2(n) -> e1(n) -> ge\n return function f3() {\n // Environment record for each call to f3: e3(n) -> e2(n) -> e1(n) -> ge\n };\n };\n}\n\nconst f = f1()();\nRun Code Online (Sandbox Code Playgroud)\n即使e1(n)、e2(n)、 和e3(n)没有参数或变量,它们仍然存在(并且在上面它们至少有两个绑定,一个 forarguments和一个 for this,因为它们不是箭头函数)。在上面的代码中,只要继续引用所创建的函数,e1(n)和都会被保留。e2(n)ff3f1()()
至少,规范是这样定义的。理论上,这些环境记录可以被优化掉,但这只是 JavaScript 引擎实现的细节。V8 在某个阶段做了一些闭包优化,但放弃了大部分,因为(据我所知)它在执行时间上的成本比它在内存减少上所弥补的要多。但即使他们在优化时,我认为这是他们优化的环境记录的内容(删除未使用的绑定之类的东西),而不是它们是否继续存在。 请参阅下文,我发现 2018 年的一篇博文表明他们有时确实会完全忽略它们。
再跟进2:
\n\n\n所以环境记录作为一个“容器”总是被保留......
\n
在规格方面,是的;这不一定是引擎真正要做的事情。
\n\n\n...容器中包含的“内容”可能因引擎而异,对吧?
\n
是的,所有规范都规定了行为,而不是如何实现它。来自环境记录部分:
\n\n\n环境记录纯粹是规范机制,不需要对应于 ECMAScript 实现的任何特定制品。
\n
\n\n...但这条信息可以追溯到 2013 年,我担心它可能已经过时了。
\n
我想是的,是的,尤其是因为 V8完全改变了引擎从那时起
\n\n\n您是否有关于此主题的更多最新信息可以分享?
\n
不是真的,但我确实找到了2018 年的这篇 V8 博客文章,其中说它们在某些情况下会“省略”上下文分配。所以肯定会进行一些优化。
\n