【问题标题】:javascript asynchronous callback lifetimejavascript 异步回调生命周期
【发布时间】:2019-01-14 05:32:28
【问题描述】:

我是 javascript 新手,但有一些 c++ 背景,所以以下内容让我感到困惑:我从浏览器创建了一些 WebSocket 连接,如下所示:

function ready() {
    var connection = new WebSocket('ws://localhost:3000');

    connection.onmessage = function(msg) {        
        console.log(msg.data);
    };
}

document.addEventListener("DOMContentLoaded", ready);

connection 变量是本地变量,因此当ready() 退出时,对WebSocket 对象的引用超出范围。我的期望是连接应该随着它的回调而死,但不知何故我仍然能够得到connection.onmessage 回调。我的理解是它与javascript 范围无关,而是WebSocket 连接在内部组织的问题。是这样吗?你能解释一下这种行为吗?我相信 javascript 中的每个异步 api 也是如此,另一个例子:

function get(url) {
    var req = new XMLHttpRequest();
    req.open("GET", url);

    req.onload = function() {
        console.log(req.response);
    }

    req.send();
}

get("file.json");

因此,回调似乎与它们所放置的对象无关。提前致谢!

【问题讨论】:

    标签: javascript asynchronous websocket xmlhttprequest


    【解决方案1】:

    Javascript 是一种垃圾收集语言。这意味着 Javascript 中的对象或变量的内容在超出函数范围时不会自动释放。相反,对象会一直存在,直到没有更多的活动代码可以访问它为止。

    因此,在您的函数中,您在 connection 对象上设置了一个事件处理程序。只要某处的活动代码仍然引用该连接对象,那么 Javascript 就不会对该对象进行垃圾收集。

    在这种特殊情况下,connection 对象表示与另一个主机的实时 TCP 连接,并且该 TCP 连接背后的 Javascript 代码和本机代码仍然具有对连接对象的引用并且仍然可以在其上触发事件。 Javascript 引擎知道这段代码仍然有对该对象的引用,因此它不会对它进行垃圾收集。而且,只要有新数据包到达 webSocket 连接,它肯定会调用你的 onMessage 处理程序。

    使该连接对象符合垃圾回收条件的方法是关闭该连接。这将导致 webSocket 代码和它背后的本机代码清除它对连接对象的任何引用,然后它将有资格进行垃圾回收。

    在 Javascript 中,永远不要认为函数局部变量的生命周期与函数返回的时间严格相关。您很快就会发现(特别是由于 Javascript 中许多操作的异步特性),函数中的局部变量在函数返回后很长一段时间内仍保持活动状态,这非常非常有用。

    我使用的心智模型(可能是也可能不是它的实现方式)有助于我理解这一切,永远不要将函数中的局部变量视为分配在堆栈帧上。相反,将它们视为在堆上动态分配的。只要需要,它们就可以在该堆上存活很长时间,包括函数已经返回后的很长时间。当没有代码正在使用它们或可以再使用它们时,垃圾收集器将释放它们。

    由于异步回调(如您的示例中),这样的回调很容易导致局部变量在包含函数已经返回后很长时间保持活动状态。

    连接变量是本地的,所以当 ready() 退出时,对 WebSocket 对象的引用超出范围

    当包含函数返回时,函数范围内的东西不会自动释放。它们不像在 C/C++ 中那样分配在堆栈上。它们被垃圾回收,不再使用时将被释放。

    我的期望是连接应该随着它的回调而死,但不知何故我仍然能够得到 connection.onmessage 回调

    见上文。 Javascript 中的局部变量在它们的宿主函数返回后可以存活很长时间。

    我的理解是它与 javascript 范围无关,而是与 WebSocket 连接在内部如何组织有关。

    这不是特定于 webSocket 的。如上所述,这是因为 Javascript 垃圾收集。管理(仍然活动的)webSocket 连接的代码有一个对connection 对象的实时引用,因此可以防止它被垃圾收集。要终止该实时引用,您将关闭 webSocket 连接,然后管理 webSocket 连接的代码将清除它对连接对象的引用,然后它将有资格进行垃圾回收。

    【讨论】:

    • 感谢详细解释!我可能不清楚,但在我的示例中,让我感到困惑的是,连接对象和回调函数对象都是局部变量,没有从全局根对象引用它们,而只是相互交叉引用,所以我想他们都应该被GC'd。我错过了下面实际上有一个实时 TCP 连接。因此,对于您描述的心理模型,我想我不应该真正关心在涉及 javascript 异步 API 时在类似情况下访问已删除的对象。再次感谢!
    • @TerentyRezman - 您不能在 Javascript 中引用已删除的对象,因为如果您有对它的引用,它将不会被垃圾收集。而且,底层操作系统级别对象(如套接字)上的事件处理程序也会导致在该对象处于活动状态时不会进行垃圾收集,并且仍然可以生成您注册的事件。
    【解决方案2】:

    JavaScript 中的事件监听器更像订阅。在您取消订阅之前,您将继续收到信息。如果你想在收到一条消息后关闭它,你可以设置你的onload函数获取信息后立即关闭连接:

    function ready() {
        var connection = new WebSocket('ws://localhost:3000');
    
        connection.onmessage = function(msg) {        
            console.log(msg.data);
            // this closes the websocket connection
            connection.close();
        };
    }
    
    document.addEventListener("DOMContentLoaded", ready);
    

    更新

    一般来说,您关心的是闭包在 JavaScript 中的工作方式。我无法直接链接到它,但如果您转到here 并向上滚动 3 个代码块,则有一个关于回调和闭包的示例。它更多地与迭代有关,但有一些相关的信息。整篇文章绝对值得一读,以更好地了解 JavaScript 中作用域的一些细微差别。

    【讨论】:

    • 困扰我的是连接和回调在作为局部变量超出范围后仍然有效,而不是我想终止它)我的意思是对它们的引用存在的地方,所以它们不是GC'ed
    • onload 是一个回调函数,只要有活动连接,它就会保持活动状态。离开范围不会关闭连接,因为您没有设置任何关闭它的条件。
    • 所以你说 javascript WebSocket 对象只是表示 websocket 与 javascript 环境的连接,但连接存在于内部某处,并且回调不绑定到 javascript WebSocket 对象而是绑定到该内部连接,所以当 javascript WebSocket 对象死了,连接和回调仍然存在,对吧?
    • @TerentyRezman 这与 Websockets 无关;这是一般回调的行为。 XHR、Websocket 和许多其他 API 提供了类似的方法来允许您的程序响应事件。当侦听器发生事件时,所有侦听器都会按照它们附加的顺序运行。这与删除所有其他侦听器的 onmessageonload 不同。执行回调时,它必须具有侦听器的范围才能进行处理。因此,connection 在连接关闭并且所有引用都消失之前不会被 GC。
    • JavaScript 中的 GC 并不是我们可以主动控制和在需要时进行操作的东西。但是有一些规则可以控制何时可以执行。对于给定的范围,如果没有引用范围内的内容,那么它可以被垃圾收集。但是,如果有未解决的项目,如 Promise 或回调(如事件侦听器),则范围仍然可用。更具体地说,websocket 连接从未关闭,因此它在内存中,并且它对onmessage 的回调存在于ready 函数内部。所以ready的内容不能被GC'd。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-06
    • 1970-01-01
    • 1970-01-01
    • 2020-05-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多