在网上浏览的时候看到了下面的一种写法,可以通过新建一个 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 回调这种写法是否妥当?

用 addEventListener ,在 load 及 error 及 aborted 中 removeEventListener ,这样应该 OK

感觉上会被 GC 掉,因为第一个 onload 被覆盖掉后就没有对象引用第一个 Promise 了。实际如何我说不清。

www.zhihu.com/question/627670924/answer/3270699180另外不建议 Promise 监听事件, 因为事件可能返回多个值(多次触发), 而 Promise 只有一个值, 事件用 Observable(Rx)

理论上会被 GC 回收掉。第一个 Promisenew Promise((resolve, reject) => { img.onload = () => { resolve("Image loaded successfully"); // 返回回调函数的结果 }; });不被 GC 是依赖 resolve 和 reject 的引用然而因为有第二个 Promise ,img.onload 不再 hold 这个函数: () => { resolve("Image loaded successfully"); // 返回回调函数的结果 };那么函数里面的 resolve 也相当于没有被引用了。这样 GC 就知道这个 Promise 是可以安全 GC 的。

大多数情况下一次性事件(比如 onload )也无所谓吧,用 RX 有点杀鸡用牛刀……

第一次调用后 img 的 onload 已经完成,第二次的 promise 不会 resolve

我也猜楼上是对的,如果 AsyncTest 把 Message 加成参数再试试?

没有引用的 Promise 会被 GC ,你可以搞个循环创建,然后在内存工具里看到锯齿状的图。

promise 在 onload 中 resolve 是没有问题的,只是你写的这个代码有点问题,img 的 onload 函数只会触发一次,所以,也只会有一次 resolve 。你需要使用其他手段来判断 img 是否加载完成,如果没有加载完成,才绑定 onload 函数。一个不能 resolve 的 promise 确实会占用内存,会在事件循环一直等待处理javascriptconst message = () => { return new Promise((resolve, reject) => { if (img.complete) { resolve("Image loaded successfully"); // 返回回调函数的结果 return ; } img.onload = () => { resolve("Image loaded successfully"); // 返回回调函数的结果 }; });};

你搞错了,先绑定两次,再执行的 onload 回调,建议复习一下事件循环机制和异步原理

没错,是你理解的这样。很简单就是 onload 被第二个给覆盖了new Promise() 是同步方法,没错 是同步🐶

你说的这一大串跟 promise 没什么关系。element.onload=handler 这种写法早就不推荐了,应该用 addEventListener 。

别折腾了 用 rx !

试了一下,确实是这样的:jsfor (let i = 0; i < 100000; i++) { AsyncTest().then(() => { console.log("Async function executed"); });}用循环去调用函数,会在覆盖时自动触发次要垃圾回收!()[]

是的,看到网上有人教了用 onload 的这种写法,所以想看看这种写法会导致一些什么潜在的问题

一个不负责任的优化方案(以能用就行为原则):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"); // 返回回调函数的结果 }; });};

确实👍

其实关键在于不要出现无法“resolve”的 Promise 对象。在你这种场景下就是尽量使用 addEventListener 而不是 onXXX 。因为 onXXX 容易不小心被覆盖。

这就是单纯的 onload 只会执行一次而已,跟你的 Promise 也没什么关系,虽然这么写很奇葩就是了

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

真别 rxjs 一直说了,原生 api 就有解决方案,就是 addeventlister 添加事件触发函数,防止重复绑定,建议读读这篇 zh.javascript.info/introduction-browser-events#addeventlistener

这种带有副作用的函数(绑了时间没有解绑)调一次可以, 调两次不行. 因为元素是一个 onload 时间只会在第一次触发, 甚至如果不立刻调用都不会触发. 正常的操作应该写成无副作用的函数, 怎么改成无副作用的函数 每次都新建一个插入 body, 绑定有 addEvent 用完了解绑, 删除元素

理论上来讲一次性事件当然是可以, 不过个人觉得这个习惯不好, 比如写习惯了一下没注意结果什么事件都用 Promise, 或者同事看见了以为用 Promise 把事件转 awaitable 是正确操作, 把其他事件也跟着这么写, 都很容易埋坑

都是说的什么啊,问题明明出在 onload 被覆盖上面啊

抱歉,眼瞎了我觉得用 Promise 来监听 onload 合不合理要分场景来说如果 img 生命周期很短,比如说封装的工具函数,直接 create 一个 img ,在 onload 里面获取 blob 或者尺寸信息,这时候会不会被覆盖是可控的,个人认为也可以视作“小而美”的但是如果生命周期比较长,emmmm ,那不好说,有可能写出来的逻辑都不能符合预期,副作用这东西,要多讨厌有多讨厌

rx 是什么

#26 rxjs.dev/