Promise.withResolvers:别再为手动存resolvers而写一堆临时变量

写JS这么多年,我经常需要手里面保留promise的resolve和reject引用,用于在某个异步回调发生时触发状态。比如一个懒加载的图片、一个请求互斥锁、一个等待用户确认的弹窗。以前的标准做法是:先声明一个空变量,再new Promise,然后把这个变量的值赋给resolve。代码倒也能跑,但总觉得像在给JS打补丁。

好在 ES2024 带来了Promise.withResolvers,现在拿到promise和它的控制函数只需要一行调用。今天这篇文章我就结合几个真实的开发场景,聊聊这个API怎么用,以及它如何让我的代码变得不再别扭。

一、过去我们怎么手动保存 resolve

举一个最常见的等待对话框回执的例子。页面里有一个异步操作,需要等待用户点击“确认”或“取消”后才继续执行。以前我会这样写:

function waitForUserConfirm() {
    let resolveFn;
    const promise = new Promise((resolve) => {
        // 把resolve存在外部变量上
        resolveFn = resolve;
    });
    // 这里拿到resolveFn,但没法立即用,因为赋值是在执行器里异步发生的
    // 实际上这里resolveFn可能还是undefined,得用queueMicrotask或者setTimeout处理
    // 这就是问题所在
}

通常为了解决这个延迟赋值,我们不得不这样写:

let resolveFn;
const promise = new Promise((resolve) => {
    resolveFn = resolve;
});
// 此时resolveFn已经可以被同步使用,因为执行器是同步执行的
// 这样写虽然能行,但包裹一层new Promise只是为了拿到resolve,总感觉绕。

但如果你有多个promise需要同时管理,比如一个并发池,需要动态添加任务,那手动保存的变量就会越来越难维护。

二、Promise.withResolvers 做了什么

一句话:它直接返回一个对象,包含promiseresolvereject三个属性。不需要你再手动new Promise并且把resolve存到外部变量上。

const { promise, resolve, reject } = Promise.withResolvers();

就这么简单。同时,promiseresolve已经建立联系,调用resolve可以改变promise状态。你可以立即保存resolve引用,甚至把resolve传递给其他函数。

三、实战案例1:把定时器封装成可取消的等待

以前我们写一个“延迟一段时间并且支持外部取消”的函数,需要手动用new Promise把clearTimeout包起来。现在有了withResolvers,清楚多了。

function wait(ms, signal) {
    const { promise, resolve, reject } = Promise.withResolvers();
    const timer = setTimeout(resolve, ms);

    if (signal) {
        if (signal.aborted) reject(new DOMException('取消', 'AbortError'));
        else signal.addEventListener('abort', () => {
            clearTimeout(timer);
            reject(new DOMException('取消', 'AbortError'));
        }, { once: true });
    }

    return promise;
}

注意这里我没有用setTimeout(() => resolve(), ms),而是直接把resolve作为setTimeout的回调。因为setTimeout会忽略传入函数的this,而且resolve不接收参数,所以没问题。

这个代码在ES6里也能写,但withResolvers让整个结构更加扁平。你会发现你不需要再内部定义let _resolve之类的东西了。

四、实战案例2:将Node的fs.readFile转Promise

Node里有些API还是回调风格。做一个转换器,以前是这样:

const fs = require('fs');

function readFilePromise(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf8', (err, data) => {
            if (err) reject(err);
            else resolve(data);
        });
    });
}

用withResolvers也能写:

const fs = require('fs');

function readFilePromise(path) {
    const { promise, resolve, reject } = Promise.withResolvers();
    fs.readFile(path, 'utf8', (err, data) => {
        if (err) reject(err);
        else resolve(data);
    });
    return promise;
}

虽然看起来差别不大,但是在需要把resolve或reject传给其他函数时,优势就出来了。

五、实战案例3:懒启动的异步任务(懒初始化)

有时候我们想执行一个异步任务,但又不想立刻开始,而是等某个条件满足时才真正启动。这个异步任务本身有自己的resolve/reject,我们可以在外部触发启动,同时还能等待它完成。

举个例子:实现一个可重用的“异步锁”。当一个异步操作正在进行时,其他调用方需要等待。等第一个完成后,锁释放,等待的promise才resolve。

class AsyncLock {
    constructor() {
        this._locked = false;
        this._waiters = [];
    }

    acquire() {
        if (!this._locked) {
            this._locked = true;
            return Promise.resolve(true);
        }
        const { promise, resolve } = Promise.withResolvers();
        this._waiters.push(resolve);
        return promise;
    }

    release() {
        if (this._waiters.length > 0) {
            const resolve = this._waiters.shift();
            resolve(true);
        } else {
            this._locked = false;
        }
    }
}

这里withResolvers让我们不用再在类属性里手动存resolve,直接使用对象上的resolve。整个逻辑看起来非常舒服。

六、更高级的玩法:把回调API包装成“可中途取消”

我最近写一个文件上传的模块,用的是第三方的SDK,那个SDK只支持回调方式,而且提供取消方法。以往要用Promise封装上传,还得保存取消函数,非常麻烦。

function uploadFile(file, options) {
    const { promise, resolve, reject } = Promise.withResolvers();
    const task = sdk.upload(file, {
        onSuccess: resolve,
        onError: reject,
        onCancel: () => reject(new Error('用户取消上传')),
    });

    // 返回promise的同时,带上cancel方法
    return {
        promise,
        cancel: () => task.cancel()
    };
}

然后调用端:

const { promise, cancel } = uploadFile(file);
// 如果用户点击取消
cancel();
// 此时promise会reject,可以捕获到错误

注意,因为withResolvers把resolve和reject暴露在外部,所以cancel方法可以访问到reject,并且通过SDK的cancel触发onCancel。这个模式比之前用new Promise然后小心翼翼保存reject要简单得多。

七、结合AbortSignal实现更统一的取消机制

在浏览器里,fetch已经支持AbortSignal,但第三方库不一定。我们可以用withResolvers将任意异步操作与AbortSignal联动。

function runWithAbort(asyncCallback, signal) {
    const { promise, resolve, reject } = Promise.withResolvers();

    const abortHandler = () => {
        reject(new DOMException('任务已取消', 'AbortError'));
    };

    if (signal.aborted) {
        reject(new DOMException('任务已取消', 'AbortError'));
    } else {
        signal.addEventListener('abort', abortHandler, { once: true });
    }

    // 让asyncCallback可以往promise转发结果
    asyncCallback(resolve, reject);

    // promise settle后移除事件监听,避免内存泄漏
    promise.finally(() => {
        signal.removeEventListener('abort', abortHandler);
    });

    return promise;
}

这样你就能把任何基于回调的API包装成可取消的Promise,而无需在回调内部反复检查signal。

八、多个Promise.withResolvers组合使用

如果你想实现一个“等待多个外部事件”的Promise,比如等待两个条件都满足,可以这样:

const first = Promise.withResolvers();
const second = Promise.withResolvers();

// 当某个异步回调触发时调 first.resolve(...)
// 当另一个条件满足时调 second.resolve(...)

Promise.all([first.promise, second.promise]).then(([a, b]) => {
    console.log('两个条件都满足了', a, b);
});

这种写法在事件驱动里非常有用。比如在WebSocket中,需要同时等待连接打开和订阅成功,两个消息都返回后才继续。

以前你不得不用两个变量,还要初始化成null,再在某个地方赋值。现在直接解构出来,不用考虑顺序问题。

九、注意:别滥用

虽然withResolvers很爽,但它并不意味着你应该用它替代普通的Promise构造器。如果你的异步操作逻辑本身就在一个函数内部,可以直接写new Promise((resolve, reject) => {...}),那根本不需要withResolvers。只有当你需要把resolve或reject传递到当前作用域之外,或者与其他代码共享控制权时,它才是最佳选择。

还有一个约定:如果你用了withResolvers,确保promise的settle状态不会因为resolve被调用多次而出现问题。Promise本身已经处理了状态锁,所以放心。

十、浏览器兼容性

这个API已经在Chrome 119、Firefox 121、Safari 17.4中支持了,Node.js 22.0.0开始支持。所以现在完全可以放心使用。如果你要兼容更老的环境,可以自己写一个polyfill,但没必要了。

总结

Promise.withResolvers并没有引入新的语义,它只是把原本需要手动书写样板代码的模式简化了。但正是这种简化,让代码真的流畅了很多。就像你用解构赋值一样,没什么牛的技术含量,但就是爽。

我自己的老项目里,重构完这类的临时变量,代码行数少了差不多十分之一。而且读起来更接近“我们的目的是等待结果”,而不是“让我们折腾一下resolve的存储位置”。

如果你还没用过这个API,我建议你试试。它也许没法让你写出更炫酷的算法,但至少让你写异步代码时少点难受。

Promise.withResolvers:别再为手动存resolvers而写一堆临时变量
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript Promise.withResolvers:别再为手动存resolvers而写一堆临时变量 https://www.taomawang.com/web/javascript/2504.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务