免费下载案例集|20+数字化领先企业人才培养实践经验 了解详情
写点什么

JavaScript 异步编程的 Promise 模式

  • 2011-09-15
  • 本文字数:4796 字

    阅读完需:约 16 分钟

异步模式在 web 编程中变得越来越重要,对于 web 主流语言 Javascript 来说,这种模式实现起来不是很利索,为此,许多 Javascript 库(比如 jQuery 和 Dojo)添加了一种称为 promise 的抽象(有时也称之为 deferred)。通过这些库,开发人员能够在实际编程中使用 promise 模式。IE 官方博客最近发表了一篇文章,详细讲述了如何使用 XMLHttpRequest2 来实践 promise 模式。我们来了解一下相关的概念和应用。

考虑这样一个例子,某网页存在异步操作(通过 XMLHttpRequest2 或者 Web Workers )。随着 Web 2.0 技术的深入,浏览器端承受了越来越多的计算压力,所以“并发”具有积极的意义。对于开发人员来说,既要保持页面与用户的交互不受影响,又要协调页面与异步任务的关系,这种非线性执行的编程要求存在适应的困难。先抛开页面交互不谈,我们能够想到对于异步调用需要处理两种结果——成功操作和失败处理。在成功的调用后,我们可能需要把返回的结果用在另一个 Ajax 请求中,这就会出现“函数连环套”的情况(在笔者的另一篇文章《 NodeJS 的异步编程风格》中有详细的解释)。这种情况会造成编程的复杂性。看看下面的代码示例(基于 XMLHttpRequest2):

复制代码
function searchTwitter(term, onload, onerror) {
    var xhr, results, url;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            onload(results);
        }
    };
    xhr.onerror = function (e) {
        onerror(e);
    };
    xhr.send();
}
function handleError(error) {
    /* handle the error */
}
function concatResults() {
    /* order tweets by date */
}
function loadTweets() {
    var container = document.getElementById('container');
    searchTwitter('#IE10', function (data1) {
        searchTwitter('#IE9', function (data2) {
            /* Reshuffle due to date */
            var totalResults = concatResults(data1.results, data2.results);
            totalResults.forEach(function (tweet) {
                var el = document.createElement('li');
                el.innerText = tweet.text;
                container.appendChild(el);
            });
        }, handleError);
    }, handleError);
}

上面的代码其功能是获取 Twitter 中 hashtag 为 IE10 和 IE9 的内容并在页面中显示出来。这种嵌套的回调函数难以理解,开发人员需要仔细分析哪些代码用于应用的业务逻辑,而哪些代码处理异步函数调用的,代码结构支离破碎。错误处理也分解了,我们需要在各个地方检测错误的发生并作出相应的处理。

为了降低异步编程的复杂性,开发人员一直寻找简便的方法来处理异步操作。其中一种处理模式称为 promise,它代表了一种可能会长时间运行而且不一定必须完整的操作的结果。这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象。

考虑这样一个例子,页面代码需要访问第三方的 API,网络延迟可能会造成响应时间较长,在这种情况下,采用异步编程不会影响整个页面与用户的交互。promise 模式通常会实现一种称为 then 的方法,用来注册状态变化时对应的回调函数。比如下面的代码示例:

复制代码
searchTwitter(term).then(filterResults).then(displayResults);

promise 模式在任何时刻都处于以下三种状态之一:未完成(unfulfilled)、已完成(resolved)和拒绝(rejected)。以 CommonJS Promise/A 标准为例,promise 对象上的 then 方法负责添加针对已完成和拒绝状态下的处理函数。then 方法会返回另一个 promise 对象,以便于形成 promise 管道,这种返回 promise 对象的方式能够支持开发人员把异步操作串联起来,如 then(resolvedHandler, rejectedHandler); 。resolvedHandler 回调函数在 promise 对象进入完成状态时会触发,并传递结果;rejectedHandler 函数会在拒绝状态下调用。

有了 promise 模式,我们可以重新实现上面的 Twitter 示例。为了更好的理解实现方法,我们尝试着从零开始构建一个 promise 模式的框架。首先需要一些对象来存储 promise。

复制代码
var Promise = function () {
        /* initialize promise */
    };

接下来,定义 then 方法,接受两个参数用于处理完成和拒绝状态。

复制代码
Promise.prototype.then = function (onResolved, onRejected) {
    /* invoke handlers based upon state transition */
};

同时还需要两个方法来执行理从未完成到已完成和从未完成到拒绝的状态转变。

复制代码
Promise.prototype.resolve = function (value) {
    /* move from unfulfilled to resolved */
};
Promise.prototype.reject = function (error) {
    /* move from unfulfilled to rejected */
};

现在搭建了一个 promise 的架子,我们可以继续上面的示例,假设只获取 IE10 的内容。创建一个方法来发送 Ajax 请求并将其封装在 promise 中。这个 promise 对象分别在 xhr.onload 和 xhr.onerror 中指定了完成和拒绝状态的转变过程,请注意 searchTwitter 函数返回的正是 promise 对象。然后,在 loadTweets 中,使用 then 方法设置完成和拒绝状态对应的回调函数。

复制代码
function searchTwitter(term) {
    var url, xhr, results, promise;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    promise = new Promise();
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            promise.resolve(results);
        }
    };
    xhr.onerror = function (e) {
        promise.reject(e);
    };
    xhr.send();
    return promise;
}
function loadTweets() {
    var container = document.getElementById('container');
    searchTwitter('#IE10').then(function (data) {
        data.results.forEach(function (tweet) {
            var el = document.createElement('li');
            el.innerText = tweet.text;
            container.appendChild(el);
        });
    }, handleError);
}

到目前为止,我们可以把 promise 模式应用于单个 Ajax 请求,似乎还体现不出 promise 的优势来。下面来看看多个 Ajax 请求的并发协作。此时,我们需要另一个方法 when 来存储准备调用的 promise 对象。一旦某个 promise 从未完成状态转化为完成或者拒绝状态,then 方法里对应的处理函数就会被调用。when 方法在需要等待所有操作都完成的时候至关重要。

复制代码
Promise.when = function () {
    /* handle promises arguments and queue each */
};

以刚才获取 IE10 和 IE9 两块内容的场景为例,我们可以这样来写代码:

复制代码
var container, promise1, promise2;
container = document.getElementById('container');
promise1 = searchTwitter('#IE10');
promise2 = searchTwitter('#IE9');
Promise.when(promise1, promise2).then(function (data1, data2) {
    /* Reshuffle due to date */
    var totalResults = concatResults(data1.results, data2.results);
    totalResults.forEach(function (tweet) {
        var el = document.createElement('li');
        el.innerText = tweet.text;
        container.appendChild(el);
    });
}, handleError);

分析上面的代码可知,when 函数会等待两个 promise 对象的状态发生变化再做具体的处理。在实际的 Promise 库中,when 函数有很多变种,比如 when.some()、when.all()、when.any() 等,读者从函数名字中大概能猜出几分意思来,详细的说明可以参考 CommonJS 的一个 promise 实现 when.js

除了 CommonJS,其他主流的 Javascript 框架如 jQuery、Dojo 等都存在自己的 promise 实现。开发人员应该好好利用这种模式来降低异步编程的复杂性。我们选取 Dojo 为例,看一看它的实现有什么异同。

Dojo 框架里实现 promise 模式的对象是 Deferred,该对象也有 then 函数用于处理完成和拒绝状态并支持串联,同时还有 resolve 和 reject,功能如之前所述。下面的代码完成了 Twitter 的场景:

复制代码
function searchTwitter(term) {
    var url, xhr, results, def;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    def = new dojo.Deferred();
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            def.resolve(results);
        }
    };
    xhr.onerror = function (e) {
        def.reject(e);
    };
    xhr.send();
    return def;
}
dojo.ready(function () {
    var container = dojo.byId('container');
    searchTwitter('#IE10').then(function (data) {
        data.results.forEach(function (tweet) {
            dojo.create('li', {
                innerHTML: tweet.text
            }, container);
        });
    });
});

不仅如此,类似 dojo.xhrGet 方法返回的即是 dojo.Deferred 对象,所以无须自己包装 promise 模式。

复制代码
var deferred = dojo.xhrGet({
    url: "search.json",
    handleAs: "json"
});
deferred.then(function (data) {
    /* handle results */
}, function (error) {
    /* handle error */
});

除此之外,Dojo 还引入了 dojo.DeferredList, 支持开发人员同时处理多个 dojo.Deferred 对象,这其实就是上面所提到的 when 方法的另一种表现形式。

复制代码
dojo.require("dojo.DeferredList");
dojo.ready(function () {
    var container, def1, def2, defs;
    container = dojo.byId('container');
    def1 = searchTwitter('#IE10');
    def2 = searchTwitter('#IE9');
    defs = new dojo.DeferredList([def1, def2]);
    defs.then(function (data) {
        // Handle exceptions
        if (!results[0][0] || !results[1][0]) {
            dojo.create("li", {
                innerHTML: 'an error occurred'
            }, container);
            return;
        }
        var totalResults = concatResults(data[0][1].results, data[1][1].results);
        totalResults.forEach(function (tweet) {
            dojo.create("li", {
                innerHTML: tweet.text
            }, container);
        });
    });
});

上面的代码比较清楚,不再详述。

说到这里,读者可能已经对 promise 模式有了一个比较完整的了解,异步编程会变得越来越重要,在这种情况下,我们需要找到办法来降低复杂度,promise 模式就是一个很好的例子,它的风格比较人性化,而且主流的 JS 框架提供了自己的实现。所以在编程实践中,开发人员应该尝试这种便捷的编程技巧。需要注意的是,promise 模式的使用需要恰当地设置 promise 对象,在对应的事件中调用状态转换函数,并且在最后返回 promise 对象。

技术社区对异步编程的关注也在升温,国内社区也发出了自己的声音。资深技术专家老赵就发布了一套开源的异步开发辅助库 Jscex,它的设计很巧妙,抛弃了回调函数的编程方式,采用一种“线性编码、异步执行”的思想,感兴趣的读者可以查看这里

不仅仅是前端的 JS 库,如今火热的 NodeJS 平台也出现了许多第三方的 promise 模块,具体的清单可以访问这里

注:本文中的所有代码示例均来自于 IE 官方博客。

作者的微信公众号“老崔瞎编”,关注 IT 趋势,承载前沿、深入、有温度的内容。感兴趣的读者可以搜索 ID:laocuixiabian,或者扫描下方二维码加关注。

2011-09-15 01:4275442
用户头像

发布了 501 篇内容, 共 255.1 次阅读, 收获喜欢 59 次。

关注

评论

发布
暂无评论
发现更多内容

未来源码|Dart 3正式发布:100%健全的空值安全、迄今为止最大版本

MobTech袤博科技

操作系统国产化步入深水区,小程序技术助力生态搭建

没有用户名丶

小程序容器

打造面向未来的开发者服务新范式,龙蜥社区开发者服务平台 devFree MeetUp 硬核启动!欢迎报名

OpenAnolis小助手

Meetup 龙蜥社区 基础设施SIG devFree 开发者服务平台

led显示屏安装步骤和注意点

Dylan

调试 安装 LED显示屏

MoE 系列(四)|Go 扩展的异步模式

SOFAStack

Go 程序员 开发 网关 Envoy负载均衡

重磅!用友荣登全球5强

用友BIP

CST为什么要关闭 GPU 卡的 ECC 模式而开启 TCC 模式?操作使用【详解】

思茂信息

cst cst使用教程 电磁仿真 cst电磁仿真 cst仿真软件

年营收将破千亿?运营商云的底气在哪里?

ToB行业头条

TDenigne 签约路特斯科技,助力高性能跑车领域数据架构升级

TDengine

时序数据库 #TDengine

第二届全国博士后创新创业大赛报名开始啦!海内外博士、博士后

科兴未来News

博士后 双创比赛 博士

众筹互助软件架构搭建原理

Congge420

OpenMLDB v0.8.0 发布

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

全球护照NFC核验 | 羽山科技

羽山数据

nfc 护照 全球护照

技术驱动,数据赋能,华为云GaussDB给世界一个更优选择

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 5 月 PK 榜

flutter系列之:使用AnimationController来控制动画效果

程序那些事

flutter 大前端 程序那些事

IDD Swap算力LP挖矿部署流程(详细

Congge420

校园校区共享电单车怎么投?找谁投?

共享电单车厂家

共享电动车厂家 校园共享电动车 共享电动车投放 共享电单车合作

华为首席架构师推荐的《云原生架构下微服务最佳》

做梦都在改BUG

Java 架构 微服务 云原生

免费堡垒机有哪些?功能多吗?后续可以升级吗?

行云管家

安全运维 免费软件 免费 免费堡垒机

提高数据的安全性和可控性,数栈基于 Ranger 实现的 Spark SQL 权限控制实践之路

袋鼠云数栈

数据安全

【经验总结】你想知道的BGA焊接问题都在这里

华秋PCB

工具 电路 PCB PCB设计 焊接

数据挖掘实践(金融风控):金融风控之贷款违约预测挑战赛(上篇)[xgboots/lightgbm/Catboost等模型]--模型融合:stacking、blending

汀丶人工智能

数据挖掘 机器学习 深度学习 数据建模

数据挖掘实践(金融风控):金融风控之贷款违约预测挑战赛(下篇)[xgboots/lightgbm/Catboost等模型]--模型融合:stacking、blending

汀丶人工智能

人工智能 数据挖掘 机器学习 深度学习 数学建模

京东顶级架构师是如何应对几天后618狂欢节的,带你走进顶级大佬

做梦都在改BUG

Java 架构 系统设计 高并发 亿级流量

索信达助力,贵阳银行荣获“金融行业数字化转型最佳创新应用奖”

索信达控股

数字化转型 金融 银行

性能测试的时机

陈磊@Criss

TSBS 报告-TimescaleDB vs TDengine

TDengine

时序数据库 tsdb #TDengine

NGINX 与当下爆火的 ChatGPT 聊天,回答质量参差不齐

NGINX开源社区

nginx ChatGPT

百度工程师移动开发避坑指南——内存泄漏篇

百度Geek说

ios android 开发 企业号 5 月 PK 榜

ChatGPT人功智能开发方案详情

Congge420

基于数字孪生的智慧校园解决方案,数字孪生赋能创建安全、绿色、智能的数字校园|UINO优锘数字孪生解决方案

ThingJS数字孪生引擎

智慧校园 数字孪生 智慧校园解决方案 智慧校园管理系统 可视化引擎

JavaScript异步编程的Promise模式_JavaScript_崔康_InfoQ精选文章