V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
wuoty
V2EX  ›  JavaScript

请教 JS 中有关 Promise 和回调函数的写法问题

  •  
  •   wuoty · 5 天前 · 2455 次点击

    在网上浏览的时候看到了下面的一种写法,可以通过新建一个 Promise 对象,在 Promise 对象中绑定回调函数,并获取回调结果,类似与下面这种:

    
        const img = document.getElementById("bg");
        
        const message = () => {
          return new Promise((resolve, reject) => {
            img.onload = () => {
              resolve("Image loaded successfully"); // 返回回调函数的结果
            };
          });
        };
    
        const AsyncTest = async () => {
          try {
            const msg = await message();
            console.log(msg);
          } catch (error) {
            console.log(error);
          }
        };
    
    

    上面的代码确实能够将回调函数的返回值作为 Promise 的调用结果返回。

    随后突发奇想,用下面这种方式分别异步调用两次上面代码中的 AsyncTest 的函数:

    
        // 连续两次调用 AsyncTest 函数
        AsyncTest().then(() => {
          console.log("Async function executed");
        });
        AsyncTest().then(() => {
          console.log("Async function executed");
        });
        
    

    结果发现虽然两次调用,但是只有一个调用有返回,个人的理解是:

    • 第一次调用 Promise ,给 onload 绑定回调函数,其中传入的 resolve 是第一个 Promise 的
    • 此时图片还没有加载完成
    • 第二次调用 Promise ,覆盖掉了 onload 的回调函数,其中的 resolve 变成了第二个 Promise 的
    • 图片加载完成,调用 onload 函数,其中调用的是第二个 Promise 的 resolve

    这样是否就意味着,第一次的 Promise 始终没有完成,保存在栈空间中,并在 EventLoop 中循环等待 resolve 的调用。 在图片加载完成后拍了堆快照,发现堆中确实留下了一个 Promise 的空间:

    请教一下,这是否意味着这块内存空间是否会始终被占用,以及利用 Promise 监听 onload 回调这种写法是否妥当?

    28 条回复    2024-06-15 00:05:29 +08:00
    xiaoming1992
        1
    xiaoming1992  
       5 天前 via Android
    用 addEventListener ,在 load 及 error 及 aborted 中 removeEventListener ,这样应该 OK
    ysc3839
        2
    ysc3839  
       5 天前
    感觉上会被 GC 掉,因为第一个 onload 被覆盖掉后就没有对象引用第一个 Promise 了。
    实际如何我说不清。
    Al0rid4l
        3
    Al0rid4l  
       5 天前
    https://www.zhihu.com/question/627670924/answer/3270699180

    另外不建议 Promise 监听事件, 因为事件可能返回多个值(多次触发), 而 Promise 只有一个值, 事件用 Observable(Rx)
    RedNax
        4
    RedNax  
       5 天前
    理论上会被 GC 回收掉。
    第一个 Promise
    new Promise((resolve, reject) => {
    img.onload = () => {
    resolve("Image loaded successfully"); // 返回回调函数的结果
    };
    });
    不被 GC 是依赖 resolve 和 reject 的引用

    然而因为有第二个 Promise ,img.onload 不再 hold 这个函数:
    () => {
    resolve("Image loaded successfully"); // 返回回调函数的结果
    };
    那么函数里面的 resolve 也相当于没有被引用了。
    这样 GC 就知道这个 Promise 是可以安全 GC 的。
    RedNax
        5
    RedNax  
       5 天前
    @Al0rid4l 大多数情况下一次性事件(比如 onload )也无所谓吧,用 RX 有点杀鸡用牛刀……
    ashong
        6
    ashong  
       5 天前 via iPhone   ❤️ 3
    第一次调用后 img 的 onload 已经完成,第二次的 promise 不会 resolve
    jchonc
        7
    jchonc  
       5 天前
    我也猜楼上是对的,如果 AsyncTest 把 Message 加成参数再试试?
    Kaciras
        8
    Kaciras  
       5 天前
    没有引用的 Promise 会被 GC ,你可以搞个循环创建,然后在内存工具里看到锯齿状的图。
    jazzg62
        9
    jazzg62  
       5 天前
    promise 在 onload 中 resolve 是没有问题的,只是你写的这个代码有点问题,img 的 onload 函数只会触发一次,所以,也只会有一次 resolve 。
    你需要使用其他手段来判断 img 是否加载完成,如果没有加载完成,才绑定 onload 函数。

    一个不能 resolve 的 promise 确实会占用内存,会在事件循环一直等待处理
    ```javascript
    const message = () => {
    return new Promise((resolve, reject) => {
    if (img.complete) {
    resolve("Image loaded successfully"); // 返回回调函数的结果
    return ;
    }
    img.onload = () => {
    resolve("Image loaded successfully"); // 返回回调函数的结果
    };
    });
    };
    ```
    cxe2v
        10
    cxe2v  
       5 天前   ❤️ 1
    @ashong 你搞错了,先绑定两次,再执行的 onload 回调,建议复习一下事件循环机制和异步原理
    xiangyuecn
        11
    xiangyuecn  
       5 天前
    没错,是你理解的这样。很简单就是 onload 被第二个给覆盖了

    new Promise() 是同步方法,没错 是同步🐶
    jguo
        12
    jguo  
       5 天前   ❤️ 1
    你说的这一大串跟 promise 没什么关系。element.onload=handler 这种写法早就不推荐了,应该用 addEventListener 。
    lolizeppelin
        13
    lolizeppelin  
       5 天前
    别折腾了 用 rx !
    wuoty
        14
    wuoty  
    OP
       5 天前
    @Kaciras
    @jazzg62
    @RedNax

    试了一下,确实是这样的:

    ```js
    for (let i = 0; i < 100000; i++) {
    AsyncTest().then(() => {
    console.log("Async function executed");
    });
    }
    ```

    用循环去调用函数,会在覆盖时自动触发次要垃圾回收

    !()[]
    wuoty
        15
    wuoty  
    OP
       5 天前
    @xiaoming1992
    @jguo

    是的,看到网上有人教了用 onload 的这种写法,所以想看看这种写法会导致一些什么潜在的问题
    monokuma88
        16
    monokuma88  
       5 天前
    一个不负责任的优化方案(以能用就行为原则):
    const message = () => {
    return new Promise((resolve, reject) => {
    if (img.complete) {
    resolve("Image loaded successfully");
    return;
    }
    const prevOnload = img.onload;
    img.onload = () => {
    if (prevOnload) {
    prevOnload();
    }
    resolve("Image loaded successfully"); // 返回回调函数的结果
    };
    });
    };
    ashong
        17
    ashong  
       5 天前
    @cxe2v 确实👍
    abc1310054026
        18
    abc1310054026  
       5 天前
    其实关键在于不要出现无法“resolve”的 Promise 对象。

    在你这种场景下就是尽量使用 addEventListener 而不是 onXXX 。因为 onXXX 容易不小心被覆盖。
    wuzzispacelake
        19
    wuzzispacelake  
       5 天前
    这就是单纯的 onload 只会执行一次而已,跟你的 Promise 也没什么关系,虽然这么写很奇葩就是了
    lmshl
        20
    lmshl  
       5 天前   ❤️ 1
    ```
    import { fromEvent } from 'rxjs';

    const clicks = fromEvent(img, 'onload');
    clicks.subscribe(x => { <你的业务逻辑> });
    ```
    不要自作聪明发明那些不可维护的代码,你能想得到的场景,早已有无数前辈替你趟过坑。
    等你哪天觉得 “RxJS 不过如此,我还有个更好的想法” 的时候,再发明也不迟(顺便发几篇论文)
    amlee
        21
    amlee  
       5 天前
    真别 rxjs 一直说了,原生 api 就有解决方案,就是 addeventlister 添加事件触发函数,防止重复绑定,建议读读这篇

    https://zh.javascript.info/introduction-browser-events#addeventlistener
    enjoyCoding
        22
    enjoyCoding  
       5 天前   ❤️ 1
    这种带有副作用的函数(绑了时间没有解绑)调一次可以, 调两次不行. 因为元素是一个 onload 时间只会在第一次触发, 甚至如果不立刻调用都不会触发. 正常的操作应该写成无副作用的函数, 怎么改成无副作用的函数 每次都新建一个插入 body, 绑定有 addEvent 用完了解绑, 删除元素
    Al0rid4l
        23
    Al0rid4l  
       4 天前
    @RedNax 理论上来讲一次性事件当然是可以, 不过个人觉得这个习惯不好, 比如写习惯了一下没注意结果什么事件都用 Promise, 或者同事看见了以为用 Promise 把事件转 awaitable 是正确操作, 把其他事件也跟着这么写, 都很容易埋坑
    jerry4718
        24
    jerry4718  
       4 天前 via Android
    都是说的什么啊,问题明明出在 onload 被覆盖上面啊
    jerry4718
        25
    jerry4718  
       4 天前 via Android
    抱歉,眼瞎了
    我觉得用 Promise 来监听 onload 合不合理要分场景来说
    如果 img 生命周期很短,比如说封装的工具函数,直接 create 一个 img ,在 onload 里面获取 blob 或者尺寸信息,这时候会不会被覆盖是可控的,个人认为也可以视作“小而美”的
    但是如果生命周期比较长,emmmm ,那不好说
    ,有可能写出来的逻辑都不能符合预期,副作用这东西,要多讨厌有多讨厌
    nexo
        26
    nexo  
       4 天前 via iPhone
    @lolizeppelin rx 是什么
    xiyuesaves
        27
    xiyuesaves  
       4 天前
    RedNax
        28
    RedNax  
       3 天前
    @Al0rid4l RxJs 固然方便,但传染性太强,等完全转 RxJs 了,坑就在复杂到爆的面条 RxJS 代码里了,反而更难调试。
    我也倒不是反对 RxJs ,不少场景下还是有用的,但大多数情况下我是觉得太重了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3157 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 11:23 · PVG 19:23 · LAX 04:23 · JFK 07:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.