Promise:一些有趣的用法

在2016年初识JS的时候就接触过Promise,那时候连异步和回调的概念还很模糊,但看到then这个词的时候我已经大概猜到了它的用处,可见Promise的设计非常好地顺应了我们的直觉,它的存在让JS的异步操作变得非常容易处理,本文将介绍一些比较trick的Promise用法

任务挂起: 把resolve传出来

想象这样一个场景,应用中有若干个业务逻辑需要等到用户登录成功后再执行。这很简单,只要在这些逻辑之前加一个await login(),这个写法的缺点也显而易见:await login() 会重复地在很多地方调用。于是不难想到第二种写法:

const afterLoginCallbacks = []
export async function login(){  
    await doLogin()  
    afterLoginCallBacks.forEach(callback => callback())
    afterLoginCallBacks.length = 0
}

export function setAfterLoginCallback(fn){
    afterLoginCallbacks.push(fn)
}

// somewhere1
setAfterLoginCallback(task1)
// somewhere2
setAfterLoginCallback(task2)
复制代码

没问题,这个方法实现了一个任务队列,把任务挂起,等登录后一起执行。我们也可以选择用promise来做同样的事:

const loginResolves = []
export async function login(){
    await doLogin()
    loginResolves.forEach(resolve => resolve())
    loginResolves.length = []
}
export function afterLogin(fn){
    return new Promise((resolve, reject) => {
        loginResolves.push(resolve)
    }).then(fn)
}

// somewhere1
afterLogin(task1)
// somewhere2
afterLogin(task2).then(...)
复制代码

这样,我们在各处调用afterLgoin方法的时候回创建一个Promise实例,并把它的resolve方法挂起,等login成功后调用这些resolve方法,使所有Promise实例resolve。因为afterLogin返回的是Promise,所以后面还可以继续接then。
这个例子是为了展示,我们可以把promise的resolve给存到其他地方,然后在我们需要的时候让promise resolve。这使得我们在处理离散的事件的时候有了更加优雅的解决方案。
现在想象一下这个场景:制作一个即时聊天应用,要求发送消息后在消息气泡旁边显示转圈的loading动画,在消息发送成功后让这个动画消失。一般来说即时聊天都会使用websocket,发送者通过socket push消息后,服务器会把消息转发给接收者,然后给发送者推送一条回执消息,告诉他消息已经发送成功。很自然地,前端的代码可以写成

function sendMsg(msg, msgId, onSuccess){
    socket.push({ msg, msgId })
    const onMessageListener = ({msgId: reciptMsgId}) => {
        if(reciptMsgId === msgId){
            onSuccess()
            socket.removeListener(onMessageListener)
        }
    }
    socket.onmessage(onMessageListener)
}

showLoading()
sendMsg('xxx', 'yyy', hideLoading)
复制代码

这个场景是否也可以改造成promise呢?
与发送xhr请求不同,socket是基于事件的,发送事件和回执事件是离散的,我们只能通过一个msgId来将两者联系起来,而且不得不用发布-订阅的模式来编码,这有的时候会让人非常难受,好在我们有Promise。借助上面提到的技巧,我们可以得到一个Promise的实现:

const resovleMap = {}
// 全局的socket事件监听
socket.onmessage(function(msg){
    const targetResolve = resolveMap[msg.msgId]
    if(targetResolve){
        targetResolve(msg)
    }
})
function doSendMsg(msg, msgId){
    return new Promise((resolve, reject) => {
        socket.push({msg, msgId})
        resolveMap[msgId] = resolve
    })
}

showLoading()
await sendMsg(msg, msgId)
hideLoading()
复制代码

在触发push事件的时候创建Promise实例,在接到回执事件的时候resolve该promise,使得发送消息的方法被包装成Promise,更符合我们的编码习惯

更多玩法

既然可以把resolve存到外部变量中,我们也可以把它作为返回值return出去,或是作为参数传给回调函数,衍生出更多有趣的玩法
比如想让回调函数的定义者决定Promise是否resolve:

function fn(callback){
    return new Promise((resolve, reject) => {
        callback(resolve, reject)
    }).then((arg) => {
        // do something
    })
}

fn(function(done){
    // do something
    done(arg)
})
复制代码

在做表单组件校验的时候可能可以用到这个技巧

注意事项

作为一个曾经踩过很多坑的人,给出几点建议:

  1. tricky的技巧不要滥用,一般都会有常规的解决方案,只有当技巧可以很好地提高代码的开发体验和可读性的时候,你可能才需要使用它们
  2. 使用Promise时,要确保你对微任务有很好的理解,确保你能准确地预测程序的行为,否则可能会出问题
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享