我在第一话中介绍了异步的概念、事件循环、以及JS编程中可能的3种异步状况(用户交互、I/O、定时器)。在编写异步操做代码时,最直接、也是每一个JSer最早接触的写法必定是回调函数(callback),好比下面这位段代码:html
ajax('www.someurl.com', function(res) { doSomething(); ... });
Ajax请求是一种I/O操做,每每须要较长时间来完成,为了避免阻塞单线程的JS程序,故设计为异步操做。此处,将一个匿名函数做为参数传给ajax,意思是“这个匿名函数先放你那儿,但暂不执行,须在收到response以后,再回过头来调用这个函数”,所以这个匿名函数也被称为“回调”。这样的写法相信每一个JSer都再熟悉不过了,但仔细想一想,这种写法可能有什么问题?程序员
问题就出在“控制反转”。ajax
匿名函数的代码,完彻底全是我写的。可是,这段代码什么时候被调用、调用几回、调用时传入什么参数……等等,我却没法掌握;而原本是被我所调用的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) { ... } ... } } } );
这样的流程是极其脆弱的,并且包含大量重复却没法复用的代码,体验很是糟心。oop
面对愈来愈复杂的业务场景,简单的回调已经愈来愈力不从心,更好的解决方案在哪儿呢?性能
也许咱们能够尝试换一种模式:不是把回调的控制权交出去,而是让异步操做在返回时触发一个事件,通知主线程异步操做的结果,随后主线程根据预先的设定执行事件相应的回调,这就是“事件订阅模式”。在这种模式下,原本要被反转的回调控制权又被反转回来了,所以称为“反控制反转”。伪代码以下:学习
on('ajax_return', function(val) { doSomething(); });
ajax(url, function(res) { emitEvent('ajax_return', res); });
on()是假想的用于注册事件回调的函数,emitEvent()是假想的用于触发事件的函数。
这种模式解决了控制反转的问题,并且用ES5也能轻松实现。可是,它尚未很好地解决异步流程的问题——总不能为每个异步操做都单独注册一个事件吧?不管如何,事件订阅模式给咱们提供了十分有益的启示,接下来上场的主角正是以这种模式为基础设计的。
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·中卷》第二部分:异步和性能