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 · 165 天前 · 3359 次点击
    这是一个创建于 165 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在网上浏览的时候看到了下面的一种写法,可以通过新建一个 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  
       165 天前 via Android
    用 addEventListener ,在 load 及 error 及 aborted 中 removeEventListener ,这样应该 OK
    ysc3839
        2
    ysc3839  
       165 天前
    感觉上会被 GC 掉,因为第一个 onload 被覆盖掉后就没有对象引用第一个 Promise 了。
    实际如何我说不清。
    Al0rid4l
        3
    Al0rid4l  
       165 天前
    https://www.zhihu.com/question/627670924/answer/3270699180

    另外不建议 Promise 监听事件, 因为事件可能返回多个值(多次触发), 而 Promise 只有一个值, 事件用 Observable(Rx)
    RedNax
        4
    RedNax  
       165 天前
    理论上会被 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  
       165 天前
    @Al0rid4l 大多数情况下一次性事件(比如 onload )也无所谓吧,用 RX 有点杀鸡用牛刀……
    ashong
        6
    ashong  
       165 天前 via iPhone   ❤️ 3
    第一次调用后 img 的 onload 已经完成,第二次的 promise 不会 resolve
    jchonc
        7
    jchonc  
       165 天前
    我也猜楼上是对的,如果 AsyncTest 把 Message 加成参数再试试?
    Kaciras
        8
    Kaciras  
       165 天前
    没有引用的 Promise 会被 GC ,你可以搞个循环创建,然后在内存工具里看到锯齿状的图。
    jazzg62
        9
    jazzg62  
       165 天前
    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  
       165 天前   ❤️ 1
    @ashong 你搞错了,先绑定两次,再执行的 onload 回调,建议复习一下事件循环机制和异步原理
    xiangyuecn
        11
    xiangyuecn  
       165 天前
    没错,是你理解的这样。很简单就是 onload 被第二个给覆盖了

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

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

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

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

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

    是的,看到网上有人教了用 onload 的这种写法,所以想看看这种写法会导致一些什么潜在的问题
    monokuma88
        16
    monokuma88  
       165 天前
    一个不负责任的优化方案(以能用就行为原则):
    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  
       165 天前
    @cxe2v 确实👍
    abc1310054026
        18
    abc1310054026  
       165 天前
    其实关键在于不要出现无法“resolve”的 Promise 对象。

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

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

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