资讯专栏INFORMATION COLUMN

什么是 Promise

KnewOne / 1266人阅读

摘要:引言错误理解精心组织起来的异步代码还不如使用一团乱麻的回调函数。但是决议后,可以一直保留着这个结果,通过形式添加的回调函数,甚至在异步操作完成之后才添加的回调函数,都会被执行调用。

引言
错误理解精心组织起来的异步代码还不如使用一团乱麻的回调函数。

在处理异步的问题上,回调基本上能够胜任,不过这都是建立在一切正常运转的基础上。

然而事与愿违,回调受到控制反转的影响,把控制权交给了第三方,这种控制转移导致了一系列的信任问题(回调调用过早、回调调用过晚、回调不被调用、回调调用次数过少或过多等问题)。同时,基于回调的异步表达又是无序性的,回调地狱的使用,让我们正确理解代码的难度加大。

函数的确可以规避以上的问题,但是,毋庸置疑,这会再次加大代码的理解难度。
与其交给不信任的第三方,倒不如转交给一个位于我们和第三方间的可信任的中介机制,这里就是我们要说的 Promise

回调的转变

如何把回调交给 Promise, 其实很简单。
使用 Promise 后我们就无需再关心大部分的信任问题和无序性。因为 Promise 机制已经为我们处理好了,我们不需要写些特定逻辑来解决一些信任问题和并发带来的竞态问题,只要我们按照 Promise 规范正确执行即可。现在,以 setTimeout 代表异步操作来进行 Promise 改造。

// callback async
const callback_async = (x = Date.now(), callback) => {
    // do something now
    console.log("callback_async:初始时间戳", x)
    setTimeout(() => {
        // do something in the future
        let interval = Date.now() - x
        callback && callback(`callback_async:在${interval}毫秒后异步完成`)
    }, 1000)
}
callback_async(undefined, res => {
    console.log("callback_async:", res)
})

Promise 中我们依然能够看到回调的身影,只是回调作为参数传递的位置发生了变化。我们不再把回调交给第三方,而是让 Promise 从第三方获取某些数据,然后回调作为参数传递进去。

const promise_async = (x = Date.now()) => {
    return new Promise(resolve => {
        // do something now
        console.log("promise_async:初始时间戳", x)
        setTimeout(() => {
            // do something in the future
            let interval = Date.now() - x
            resolve(`promise_async:在${interval}毫秒后异步完成`)
        }, 1000)
    })
}
promise_async(undefined).then(res => {
    console.log(res)
})

不同之前的把回调直接传给第三方的做法,这次是靠着 Promise 这个中间机制来替异步任务管理着回调。

错误的处理

使用 Promise 后,怎么就会好了很多呢?首先说说在错误的处理上。
JavaScript 代码在执行的过程中若遇到错误就不会执行下去的。作为传入第三方的回调(同步回调或异步回调),如果在此之前就已经报错了,回调压根不会执行。在这种情况下,能通过回调捕获错误,也是很有意义的。我们很自然地想到了 try...catch , 不过在异步回调中,回调函数的执行栈与原函数分离开,导致外部是无法抓住异常。不过没关系,我们就多捕捉一遍。

在此,我们就用“error-first风格”模拟一下。

// callback async
const callback_async = (x = Date.now(), callback) => {
    try {
        console.log("callback_async:初始时间戳", x)
        // do something now
        // throw "callback-outer: error"
        setTimeout(() => {
            try {
                // do something in the future
                // throw "callback-inner: error"
                let interval = Date.now() - x
                callback && callback(null, `callback_async:在${interval}毫秒后异步完成`)
            } catch (error) {
                callback(error)
            }
        }, 1000)
    } catch (error) {
        callback(error)
    }
}
callback_async(undefined, (error, res) => {
    error?console.log("asyncError:", error):console.log("async:", res)
})

依次解开注释 throw ... ,我们就可以成功地捕获到错误或异常。但同时也发现,对于一个不断嵌套的异步回调,就回调地狱那样,我们会为每一个异步回调做 try...catch 的错误处理,这会使原有的代码更加混乱。

“幸运”的是,Promise 已经为我们处理好了这个问题。对于错误或异常,我们只需要注册 rejectedcatch 的回调即可。不过 Promise 也存在着和上面相同的问题,无法捕获脱离上下文环境的错误或异常,我们只能收到手动 reject

const promise_async = (x = Date.now()) => {
    return new Promise((resolve, reject) => {
        // do something now
        // throw "promise-outer: error"
        console.log("promise_async:初始时间戳", x)
        setTimeout(() => {
            try {
                // do something in the future
                // throw "promise-inner: error"
                let interval = Date.now() - x
                resolve(`promise_async:在${interval}毫秒后异步完成`)
            } catch (error) {
                reject(error)
            }
        }, 1000)
    })
}
promise_async(undefined).catch(error => {
    console.log(error)
})

对于多个异步任务,Promise 仍然能够很好的处理错误,因为 Promise 使用的 this-then-that 的流程控制,默认处理函数只是把错误重新抛出,这使得错误可以继续沿着Promise链传播下去,直到显式的 rejectedcatch 捕获错误。

Promise化

Promise 带来的好处远远不止这些。一旦 Promise 决议, 它就永远保持这个状态,这个 Promise.then(...) 注册的回调就会被自动调用,且只会被调用一次。这也算解决了回调调用过少、过多及不被调用的问题。即使不能解决,但也可以在此基础上再做处理。你要是问为什么,我只能说人家就是干这个的,作为一个可信任的中间协商机制。

说到一旦决议就不能改变,这个很重要么,是的,真的很重要。
在基于回调模式的异步处理中,JavaScript 代码执行后会一直走下去,遇到回调就直接执行了。但是 Promise 决议后,可以一直保留着这个结果,通过 .then(..) 形式添加的回调函数,甚至在异步操作完成之后才添加的回调函数,都会被执行调用。这也是上一个 Promise 里的错误只能在 Promise 链的下一个回调里捕获的原因。

知道了 Promise 的好处,也知道了基于回调模式的异步处理方式,我们就可以尝试把“error-first风格”的回调 Promise 化。

// Promise Wrap
var promise_wrap = function(fn){
    return function() {
        let args = Array.from(arguments);
        return new Promise((resolve, reject) => {
            fn.apply(null, args.concat((error, value) => {
                error ? reject(error): resolve(value)
            }))
        })
    }
}

在这里我们可以看到,为了统一处理现在和将来,我们把它们都变成了将来,即所有的操作都成了异步,同步回调也变成了异步回调。

JavaScript 异常错误也是如此,在 Promise 创建过程中或查看决议结果过程中出现的异常错误,这个异常错误被捕捉都会变成异步行为。这样做减少了由函数顺序不确定性(竞态条件)带来的诸多问题。

保持扁平化

从回调模式跨到 Promise,总会不小心保留着原来的风格,比如嵌套。
Promise 链式编程最好保持扁平化,不然不就变成另一个回调地狱了?关键是还没有返回或终止 Promise 链。

// parallel Promise
var parallel_promise = (x = Date.now()) => {
    Promise.resolve().then(() => {
        new Promise(resolve => {
            setTimeout(() => {
                let interval = Date.now() - x;
                resolve(`parallel-inner:在${interval}毫秒后完成`)
            }, 3000)
        }).then(res => {
            console.log(res)
        })
    }).then(res => {
        let interval = Date.now() - x;
        console.log(`parallel-outer:在${interval}毫秒后完成; res: ${res}`)
    })
}
parallel_promise(undefined)

从上面的执行结果可以看出,parallel-outer 并非在 parallel-inner 后执行。这是没有正确将 Promise 相连接的结果。

实际上,这里就是两个独立竞争的 Promise(同时在执行异步任务而不是一个接着一个)。同时我们也会注意到外层 then(...) 注册回调中 resundefined,因为对于没有任何显式的决议,这个值就是 undefined

// serial Promise
var serial_promise = (x = Date.now()) => {
    Promise.resolve().then(() => {
        return new Promise(resolve => {
            setTimeout(() => {
                let interval = Date.now() - x;
                resolve(`serial-1:在${interval}毫秒后完成`)
            }, 3000)
        }).then(res => {
            console.log(res)
            return res
        })
    }).then(res => {
        let interval = Date.now() - x;
        console.log(`serial-2:在${interval}毫秒后完成; res: ${res}`)
    })
}
serial_promise(undefined)

所以说,

一个好的经验法则是总是返回或终止Promise链,并且一旦得到一个新的Promise,返回它。
小结

Promise 来表达异步和管理并发无疑是种进步,它在程序的顺序性和可信任性上提供了自己的解决方案。它不是回调的替代品,只是帮着异步任务管理回调的可信任的中间机制。

相对于直接粗暴的回调,Promise 并不会带来性能上的提升,但是它会让我们的程序更加健壮,也使得代码更加简洁,更加符合我们有序的思维方式。

当然,Promise 也有自己的局限性。在并发 Promise.race(...) 上,我们只要第一个决议即可。当出现第一个决议的 Promise 时,其它的 Promise 就没有必要进行下去了。然而,我们没把法终止。

在错误处理上,Promise 链中错误总是由下一个 Promise 捕获。如果错误发生在最后一个 Promise 呢?还有,对于嵌套的 Promise,内部 Promise 已经进行了错误处理,但是外部 Promise 却捕获不到,这样真的好么?

Promise 恢复了可信任性,但我们还想让异步流程的表达风格更贴近同步的形式,链式调用不说不好,只是我们带着同步操作的惯性。还好,ES6、ES7已经给出了方案。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102352.html

相关文章

  • Promise之你看得懂的Promise

    摘要:存放成功回调的函数存放失败回调的函数监听回调函数然后是需要多加一个状态判断,当中是异步操作时,需要在我们之前定义的回调函数数组中添加一个回调函数。参数函数返回的对象,函数的返回值,最外层的上的和。 本文由作者陈旭锋(任职网易考拉)授权网易云社区发布。 Promise源码详解学习知识要善于思考,思考,再思考。 —— 爱因斯坦 1.回调地狱曾几何时,我们的代码是这样的,为了拿到回调的结果,...

    wwolf 评论0 收藏0
  • [译] 深入理解 Promise 五部曲:5. LEGO

    摘要:一个就像一个乐高玩具。问题是不是你小时候玩儿的那个有趣,它们不是充满想象力的打气筒,也不是一种乐高玩具。这是对的并不是给开发者使用的,它们是给库作者使用的。不会超过这两种情况。第二个是根据第一个处理函数如何运行来自动变成状态成功或者失败。 原文地址:http://blog.getify.com/promis... 在 Part4:扩展问题 中,我讨论了如何扩展和抽象Promise是多么...

    LiveVideoStack 评论0 收藏0
  • Javascript基础之-Promise

    摘要:转载自是什么呢根据的定义是一个被用于延时计算的最终结果的占位符这个怎么理解呢比如说,我要去麦当劳买点吃的,下单以后人家会先给你一个订单号,等人家外卖做好了,会提示你,并用那个订单小票来换取你真正的食物,在这时候,那个订单小票就是你这顿饭的 转载自: http://www.lht.ren/article/3/ Promise是什么呢?根据ecma-262的定义: Promise是一个被用...

    Carson 评论0 收藏0
  • 在非阻塞IO下的nodejs下的同步并行 ES6的 promise 从入门深入(一)

    摘要:我们先介绍一下中的的一些调用再结合的应用逐步深入。这就是一些简单的的调用看起来不多,但是靠这个真得解决了许多必须同步并行的环境本身是一个对象在开始支持。存在两个回调函数根据个人的需求进行处理。 什么是promise?为什么要在nodejs中使用promise?使用promise到底有什么好处呢?实在太多了,一一说来不如直接上实战。我们先介绍一下nodejs中的promise的一些调用....

    luffyZh 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • 深入 Promise

    摘要:首先从这个构造函数说起,它是全局对象的属性的值,这也就是为什么浏览器环境下我们能直接调用它的原因,就像这些构造函数一样。的产生就是像正常使用构造函数那样构建一个,不过传给构造函数是内部自动创建的,作用是把记录到中。 showImg(https://segmentfault.com/img/bVbgYy2?w=1200&h=600); > new Promise((resolve, re...

    cfanr 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<