漫话JavaScript与异步·第二话——Promise:一诺千金
一、难以掌控的回调
我在第一话中介绍了异步的概念、事件循环、以及JS编程中可能的3种异步情况(用户交互、I/O、定时器)。在编写异步操作代码时,最直接、也是每个JSer最先接触的写法一定是回调函数(callback),比如下面这位段代码:
ajax('www.someurl.com', function(res) {
doSomething();
...
});
Ajax请求是一种I/O操作,往往需要较长时间来完成,为了不阻塞单线程的JS程序,故设计为异步操作。此处,将一个匿名函数作为参数传给ajax,意思是“这个匿名函数先放你那儿,但暂不执行,须在收到response之后,再回过头来调用这个函数”,因此这个匿名函数也被称为“回调”。这样的写法相信每个JSer都再熟悉不过了,但仔细想想,这种写法可能有什么问题?
问题就出在“控制反转”。
匿名函数的代码,完完全全是我写的。但是,这段代码何时被调用、调用几次、调用时传入什么参数……等等,我却无法掌握;而本来是被我所调用的ajax函数,竟堂而皇之地接管了我的代码,回调的控制权旁落到了写ajax函数的那家伙手里——控制被反转了。
很多情况下,“那家伙”是个非常可信的机构或公司(比如Google的Chrome团队)、或是比你我牛得多的天才程序员,因此可以放心地把回调交给他。但也有很多情况下,事情并非如此:假如你在开发一个电商网站的代码,把“刷一次信用卡”的回调传给一个第三方库,而那个库很不巧地在某种特殊情况下把这个回调调用了5次,那么,你的老板可能不得不做好准备,在电话中亲自安抚怒气冲冲的顾客。而且,即使换一个第三方合作伙伴,就能保证不再出类似的问题吗?
换句话说,我们无法100%信任接管回调的第三方(当然,那个“第三方”也可能是自己)。
另一个问题是,异步操作本质上是无法保证完成时间的,因此,当多个异步操作需要按先后顺序依次执行、并且后面的步骤依赖于前面步骤的返回结果时,如果用回调的写法,就只能把后一个的步骤硬编码在前一个步骤的回调中,整个操作流程形成一个嵌一个的回调金字塔,再加上异常处理和多分支等情况,口味更加酸爽:
ajax(url, function (res){
ajax(res.url, function(res) {
ajax(res.url, function(res) {
if (res.status == '1') {
ajax(res.url, function(res) {
...
}
}
else if (res.status == '2') {
ajax(url2, function(res) {
...
}
...
}
}
}
);
这样的流程是极其脆弱的,而且包含大量重复却无法复用的代码,体验非常糟心。
面对越来越复杂的业务场景,简单的回调已经越来越力不从心,更好的解决方案在哪儿呢?
二、事件订阅模式的启示
也许我们可以尝试换一种模式:不是把回调的控制权交出去,而是让异步操作在返回时触发一个事件,通知主线程异步操作的结果,随后主线程根据预先的设定执行事件相应的回调,这就是“事件订阅模式”。在这种模式下,本来要被反转的回调控制权又被反转回来了,因此称为“反控制反转”。伪代码如下:
on('ajax_return', function(val) {
doSomething();
});
ajax(url, function(res) {
emitEvent('ajax_return', res);
});
on()是假想的用于注册事件回调的函数,emitEvent()是假想的用于触发事件的函数。
这种模式解决了控制反转的问题,而且用ES5也能轻松实现。但是,它还没有很好地解决异步流程的问题——总不能为每一个异步操作都单独注册一个事件吧?无论如何,事件订阅模式给我们提供了十分有益的启示,接下来上场的主角正是以这种模式为基础设计的。
三、理解Promise的姿势
Promise是一种范式,专治异步操作的各种疑难杂症。本节不打算逐一介绍Promise的API,而是着重探求其设计思想,由此学习其正确的使用方法。
第一,Promise基于事件订阅模式。我们知道,Promise有三种状态:未决议、决议、拒绝。从未决议变化到决议或拒绝,就相当于触发了一个匿名事件,使得通过then方法注册的fulfilled或rejected回调被调用,实现了反控制反转。
第二,Promise“只能决议一次”的特性,使得“裸回调”和不可信的thenable对象都可以包装为可信的Promise对象。示例代码如下:
// 例1.将ajax函数的返回结果Promise化
let p1 = new Promise((resolve, reject) => {
ajax(url, function(res) {
if (res.error) reject(res.error);
resolve(res);
});
}); // 例2.将不规范的thenable对象Promise化
let obj = {
then: function(cb, errcb) {
cb(1);
cb(2); // 不合规范的用法!
errcb('evil laugh');
}
}; let p2 = new Promise((resolve, reject) => {
obj.then(resolve, reject);
});
// 或写成如下语法糖
let p2 = Promise.resolve(obj);
例1中,传给ajax的匿名函数不知道会被调用几次,然而由于Promise的特性,保证了只有第一次调用会使Promise的状态发生决议,之后的调用都被直接忽略。
例2中,obj对象有一个then方法,接受两个函数作为参数,所以它是一个thenable对象;但是其内部的代码却完全不符合Promise规范——"fulfilled"被调用了两次,"rejected"也在resolve时被调用,完全是乱来嘛!但是,只要把它包装成p2,那就没有问题了——resolve(1)顺利执行,resolve(2)和reject('evil laugh')被直接忽略。
第三,then方法注册的回调一定会被异步调用,比如:
console.log('A');
Promise.resolve('B').then(console.log);
console.log('C');
执行结果是 A C B。
这是为了将现在值(同步)和未来值(异步)归一化,避免出现Zalgo现象(指同一个操作既可能同步返回也可能异步返回,比如缓存命中则同步返回、未命中则异步返回)。
再看一段代码:
setTimeout(function(){console.log('A');}, 0);
setTimeout(function(){console.log('B');}, 0);
Promise.resolve('C').then(console.log);
Promise.resolve('D').then(console.log);
console.log('E');
执行结果为 E C D A B。
原因在于,Promise的then回调实现异步不是用setTimeout(.., 0),而是用一种叫做Job Queue(任务队列)的专门机制。传统的setTimeout(.., 0)把回调放在Event Loop的末尾,作为一个新的event老老实实排队;而Job Queue是Event Loop中每个event后面挂着的一个队列,往这个队列里插入回调,可以抢在下个event之前执行,相当于“插队”,因此Promise一旦决议,可以以最快的速度(在当前同步代码执行完之后,立刻)调用回调,没有别的异步能够抢在前面(除了另一个Promise)!
第四,then方法会返回一个新的Promise,以fulfilled回调为其resolve,以rejected回调为其reject,因此连续调用then方法可以构成一条Promise链。由于链上的Promise决议有先后顺序(别忘了,每一步都是异步的),因此可以用来控制异步操作的顺序。当然,一般情况下同步操作就不要强行异步化了,我见过p.then(res=>res.text).then(...)这样的代码,除了增加程序复杂度以外好像没什么用处。。。
从以上几点可以看出,Promise是一种非常强大的模式,对于异步操作中可能遇到的信任问题、硬编码流程问题等,都设计了相应的机制来加以克服,试着正确地了解它、使用它,你一定能体会到它的好处,从而爱不释手。但是,探寻更优雅的异步操作方法的任务,还没有结束……
推荐阅读:《你不知道的JavaScript·中卷》第二部分:异步和性能
漫话JavaScript与异步·第二话——Promise:一诺千金的更多相关文章
- 漫话JavaScript与异步·第一话——异步:何处惹尘埃
自JavaScript诞生之日起,频繁与异步打交道便是这门语言的使命,并为此衍生出了许多设计和理念.因此,深入理解异步的概念对于前端工程师来说极为重要. 什么是异步? 程序是分"块" ...
- 漫话JavaScript与异步·第三话——Generator:化异步为同步
一.Promise并非完美 我在上一话中介绍了Promise,这种模式增强了事件订阅机制,很好地解决了控制反转带来的信任问题.硬编码回调执行顺序造成的"回调金字塔"问题,无疑大大提 ...
- JavaScript及其异步实现续:Promise让一切更简单
在写这篇文章之前,我参考了以下文章.所以我文中的例子都是精准的,而且有循可依.下面抛出例子的链接: Understanding JQuery.Deferred and Promise Deferred ...
- JavaScript的异步编程之Promise
Promise 一种更优的异步编程统一 方法,如果直接使用传统的回调函数去完成复杂操作就会形成回调深渊 // 回调深渊 $.get('/url1'() => { $.get('/url2'() ...
- Javascript异步编程之三Promise: 像堆积木一样组织你的异步流程
这篇有点长,不过干货挺多,既分析promise的原理,也包含一些最佳实践,亮点在最后:) 还记得上一节讲回调函数的时候,第一件事就提到了异步函数不能用return返回值,其原因就是在return语句执 ...
- (翻译)异步编程之Promise(1):初见魅力
原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...
- js异步原理与 Promise
一.Javascript的异步原理 javascript 是单线程语言,所以同一时间只执行一个运算.但有些方法是不能瞬间完成或不可预知何时完成的(如网络请求.settimeout等),为了让它们不对后 ...
- [译]理解Javascript的异步等待
原文链接: https://ponyfoo.com/articles/understanding-javascript-async-await 作者: Nicolás Bevacqua 目前async ...
- 理解Javascript的异步等待
目前async / await特性并没有被添加到ES2016标准中,但不代表这些特性将来不会被加入到Javascript中.在我写这篇文章时,它已经到达第三版草案,并且正迅速的发展中.这些特性已经被I ...
随机推荐
- 我的python之路【第二篇】数据类型与方法
一.Python中有哪些数据类型 整型 在32位的系统中: 取值范围就是-(2^31) 到2^31-1 在64位系统中: 取值范围就是-(2^63) 到2^63-1 浮点型 布尔型 字符型 字符串 ...
- 使用Docker容器来源码编译etcd
背景 etcd是CoreOS公司开发的分布式键值对存储库.在Kubernetes中,我们需要使用etcd作为所有REST API对象的持久化存储. 不幸的是,在github的release中,Core ...
- python服务器环境搭建(3)——参数配置
前面我们已安装好了python服务器运行所需要的相关软件,而最重要最繁琐的就是参数配置,写这篇就踩了好多坑,花了好多时间,遇到了各种各样的问题.好了费话少说,直接进入本篇话题. PS:本人不是专业的运 ...
- Object-C定时器,封装GCD定时器的必要性!!! (二)
上一篇:Object-C定时器,封装GCD定时器的必要性!!! (一) 上一篇认识了Object-C中的几种定时器,这一篇将Dispatch定时器(GCD定时器)封装起来. p.p1 { margin ...
- XAF-通知模块概述 web+win
通知模块概述 1.支持 WinForms和ASP.NET程序. 2.支持调度模块或自定义业务对象. 3.功能:在指定的时间,弹出一个窗口,用户可以查看提醒.也可以取消或推迟. 如需演示项目的源码,可以 ...
- png、jpg、gif三种图片格式的区别
png.jpg.gif三种图片格式的区别 2014-06-17 为什么想整理这方面的类容,我觉得就像油画家要了解他的颜料和画布.雕塑家要了解他的石材一样,作为网页设计师也应该对图片格式的特性有一定 ...
- 概念学习 - JNDI, JDBC, ODBC, DataSource
layout: post title: 概念学习 - JNDI, JDBC, ODBC, DataSource --- 最近在学习Java Hibernate,对数据库资源访问这块好多概念模糊,所以在 ...
- c++中关于值对象与其指针以及const值对象与其指针的问题详细介绍
话不多说,先附上一段代码与运行截图 //1 const int a = 10; //const 值对象 int *ap = (int *)&a;//将const int*指针强制转化为int* ...
- IEnumerable<T>和IQueryable<T>
建议29.区别LINQ查询中的IEnumerable<T>和IQueryable<T> LINQ查询方法一共提供了两类扩展方法,在System.Linq命名空间下,有两个静态类 ...
- [Linux] PHP程序员玩转Linux系列-telnet轻松使用邮箱
1.PHP程序员玩转Linux系列-怎么安装使用CentOS 2.PHP程序员玩转Linux系列-lnmp环境的搭建 3.PHP程序员玩转Linux系列-搭建FTP代码开发环境 4.PHP程序员玩转L ...