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

14 天前
 wuoty

在网上浏览的时候看到了下面的一种写法,可以通过新建一个 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 始终没有完成,保存在栈空间中,并在 EventLoop 中循环等待 resolve 的调用。 在图片加载完成后拍了堆快照,发现堆中确实留下了一个 Promise 的空间:

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

2595 次点击
所在节点    JavaScript
28 条回复
xiaoming1992
14 天前
用 addEventListener ,在 load 及 error 及 aborted 中 removeEventListener ,这样应该 OK
ysc3839
14 天前
感觉上会被 GC 掉,因为第一个 onload 被覆盖掉后就没有对象引用第一个 Promise 了。
实际如何我说不清。
Al0rid4l
14 天前
https://www.zhihu.com/question/627670924/answer/3270699180

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

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

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

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

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

!()[]
wuoty
13 天前
@xiaoming1992
@jguo

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

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

const clicks = fromEvent(img, 'onload');
clicks.subscribe(x => { <你的业务逻辑> });
```
不要自作聪明发明那些不可维护的代码,你能想得到的场景,早已有无数前辈替你趟过坑。
等你哪天觉得 “RxJS 不过如此,我还有个更好的想法” 的时候,再发明也不迟(顺便发几篇论文)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1049064

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX