值得多聊聊的 Promise 模式,以及它能解决什么问题
urwz0772
7年前
<p>Promise 模式是解决 Callback hell 的一个很好的方式, 值得我们再多聊一聊。</p> <h2>什么是 Callback Hell</h2> <p>所谓 Callback Hell,就是我们熟悉的闭包或者说是回调函数,连续多级嵌套,导致代码结构的混乱。 比如这样:</p> <pre> <code class="language-javascript">step1(function(value1){ step2(value1, function(value2){ step3(value2, function(value3){ step4(value3, function(value4){ // Do something with value4 }); }); }); }); </code></pre> <p>这些 callback 的嵌套层级过多的话,会导致代码结构上很容易出错,不易阅读和维护。 甚至有一个网站专门介绍它: <a href="/misc/goto?guid=4958964046517860617" rel="nofollow,noindex">http://callbackhell.com/</a> 感兴趣的同学可以了解一下。</p> <h2>Promise 模式</h2> <p>基于 callback 的这个问题,业界也提出了很多解决方案,其中一个就是 Promise 模式。 关于这个模式,之前的文章中也给大家介绍过。这次更多跟大家要聊的,是一个基于 Promise 模式的实现库 - Q。 它提供了更多完善的接口。</p> <p>比如:</p> <pre> <code class="language-javascript">Q.fcall(function(){ return 2; }).then(function(val){ console.log(val); //... }).then(function(){ //... }).done(); </code></pre> <p>Q.fcall 是整个 Promise 链条的入口,是我们调用的第一个函数。 然后一系列的 then 调用,会传入接下来的回调函数。 前一个的返回值可以作为后一个调用的参数。 这样就可以把前面多层嵌套的 callback 结构变成线性的 Promise 结构。</p> <p>Promise 除了改变调用的语法结构之外,还有什么其他的收益呢。答案是肯定的。 假如我们要写一个单元测试,用于测试数据库访问接口。 我们需要预先插入一系列测试数据, 但数据库 API 的调用需要异步 callback, 如果没有 Promise, 我们可能就会写出这样的代码:</p> <pre> <code class="language-javascript">dao.add({"title" : "t1" }, function(success){ dao.add({"title" : "t2" }, function(success){ dao.add({"title" : "t3" }, function(success){ done(); } } }); </code></pre> <p>这还是指插入 3 条测试数据,就已经如此复杂,如果我们要插入几十条数据,这样的语法结构基本无法满足需求。 有人说了,我们可不可以这样并行的写呢:</p> <pre> <code class="language-javascript">dao.add({"title" : "t1" }, function(success){ }); dao.add({"title" : "t2" }, function(success){ }); dao.add({"title" : "t3" }, function(success){ }); </code></pre> <p>答案是不行, 因为这个方法是异步执行的, 我们需要等待所有的添加操作都完成,才能进行下一步操作,也就是必须等待 callback 调用,在 callback 中执行下一步操作。 所以这个写法会导致我们程序的逻辑错误。 再来看看使用 Promise 怎么解决这个问题:</p> <pre> <code class="language-javascript">var testData = []; //准备测试数据 for(var i = 0;i < 50;i++) { testData.push(function(){ var deferred = Q.defer(); var info = mockImageInfo(); dao.add(info, function(){ deferred.resolve(""); }); return deferred.promise; }); } testData.reduce(Q.when, Q("")).then(function(){ //数据插入完成后,进行操作 }).done(); </code></pre> <p>上面的代码一眼看上去是不是有些不好理解。这里给大家讲解一下, 首先我们开始的 for 循环会创建 50 个函数,每个函数都会返回一个 Promise 实例 - return deferred.promise 。 每个函数做的事情就是调用异步方法添加测试数据。</p> <p>Q 这个库提供了对异步函数的 Promise 支持, 首先调用 Q.defer() , 然后在异步函数的 callback 方法中调用 deferred.resolve 用于标示这个 Promise 执行成功。 真个函数的逻辑是这样, 我们先调用 dao.add 方法异步添加数据,然后立即调用 return deferred.promise 返回 Promise 实例。 因为这个时候 Promise 具体状态还没有确认,要等到异步方法执行完毕, 调用 deferred.resolve 确认状态后,这时整个 Promise 就算执行完成了。</p> <p>我们创建好这 50 个函数后, 怎么保证他们顺序执行呢? 接下来调用 testData.reduce 方法, 这个方法是 JS 对 Array 类型提供的内建方法,它的作用就是遍历整个数组, 对每个元素执行一个操作函数, 我们这里是 Q.when, 用于处理数组中所有 Promise 函数的执行。</p> <p>reduce 方法调用完成后,会保证所有 50 个测试数据都插入到数据库中, 然后调用 then 方法,执行下一步的相关操作。 这时所有 50 个异步调用都确保执行完毕。 想一下如果用我们之前的嵌套是写法插入这 50 个数据,那简直就是噩梦了。 Promise 这种方式我们可以插入任意数量的数据,不会影响代码结构。</p> <p>Q 的 Github 地址: <a href="/misc/goto?guid=4958534319681535099" rel="nofollow,noindex">https://github.com/kriskowal/q</a></p> <h2>总结</h2> <p>这次文章中和大家重新熟悉了一下 Promise 模式以及它解决的问题, 还给大家介绍了一个实现库 - Q。 另外呢也结合我实际开发过程中给大家列举了一个应用场景。 也是我在给一个工程写单元测试的时候,遇到的一个实际问题。相信通过这个实际的问题,能帮助大家更好的理解 Promise 的应用场景。</p> <p>如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~</p> <p> </p> <p>来自:http://www.swiftcafe.io/2017/05/24/promise-q/</p> <p> </p>