(1)JavaScript的异步执行
- “同步模式”就是后一个任务等待前一个任务结束,然后再执行。
- “异步模式”则每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立刻执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据返回了,再由系统通知执行第二段代码。
以下总结了”异步模式”编程的几种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的 JavaScript 程序
回调函数
假定有两个函数f1和f2,后者必须等到前者执行完成,才能执行。这时,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback) {
// f1 的代码
// f1 执行完成后,调用回调函数
callback();
}
f1(f2);
事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
// 当f1发生done事件,就执行f2。
f1.on('done', f2);
发布/订阅
“事件”完全可以理解成”信号”,如果存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式“(publish-subscribe pattern),又称”观察者模式“(observer pattern)。
// 首先,f2向”信号中心”jQuery订阅”done”信号。
Query.subscribe("done", f2);
// 然后,f1进行如下改写:
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
// 完成执行后,也可以取消订阅(unsubscribe)。
jQuery.unsubscribe("done", f2);
(2) Promise对象的基本用法
ES6原生提供了Promise对象。所谓Promise对象,就是代表了未来某个将要发生的事件(通常是一个异步操作)。它的好处在于,有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象还提供了一整套完整的接口,使得可以更加容易地控制异步操作。
ES6的Promise对象是一个构造函数,用来生成Promise实例。下面是Promise对象的基本用法。
var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(value) {
// failure
});
上面代码表示,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve方法和reject方法。如果异步操作成功,则用resolve方法将Promise对象的状态变为“成功”(即从pending变为resolved);如果异步操作失败,则用reject方法将状态变为“失败”(即从pending变为rejected)。
**注意,Promise将作为参数的函数封装成对象,并且给这个对象附带then函数用于设置resolve方法和reject方法的函数。**promise实例生成以后,可以用then方法分别指定resolve方法和reject方法的回调函数。
下面是一个使用Promise对象的简单例子。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
timeout(100).then(() => {
console.log('done');
});
上面代码的timeout方法返回一个Promise实例对象,表示一段时间以后改变自身状态,从而触发then方法绑定的回调函数。
下面是一个用Promise对象实现的Ajax操作的例子。
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
上面代码中,resolve方法和reject方法调用时,都带有参数。它们的参数会被传递给回调函数。reject方法的参数通常是Error对象的实例,而resolve方法的参数除了正常的值以外,还可能是另一个Promise实例,比如像下面这样。
var p1 = new Promise(function(resolve, reject){
// ... some code
});
var p2 = new Promise(function(resolve, reject){
// ... some code
resolve(p1);
})
上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,这时p1的状态就会传递给p2。如果调用的时候,p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是fulfilled或者rejected,那么p2的回调函数将会立刻执行。
Promise.prototype.then方法:链式操作
Promise.prototype.then方法返回的是一个新的Promise对象,因此可以采用链式写法。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// proceed
});
上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// 对comments进行处理
});
这种设计使得嵌套的异步操作,可以被很容易得改写,从回调函数的“横向发展”改为“向下发展”。
Promise.prototype.catch方法:捕捉错误
Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON("/posts.json").then(function(posts) {
// some code
}).catch(function(error) {
// 处理前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前两个回调函数的错误
});
Promise.all方法,Promise.race方法
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.all([p1,p2,p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例。(Promise.all方法的参数不一定是数组,但是必须具有iterator接口,且返回的每个成员都是Promise实例。)
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
下面是一个具体的例子。
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function(id){
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function(posts) {
// ...
}).catch(function(reason){
// ...
});
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.race([p1,p2,p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。
如果Promise.all方法和Promise.race方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。
Promise.resolve方法,Promise.reject方法
1. Promise.resolve
有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。
1.1 new Promise的快捷方式
静态方法Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。比如 Promise.resolve(42); 可以认为是以下代码的语法糖。
new Promise(function(resolve){
resolve(42);
});
在这段代码中的 resolve(42); 会让这个promise对象立即进入确定(即resolved)状态,并将 42 传递给后面then里所指定的 onFulfilled 函数。
方法 Promise.resolve(value); 的返回值也是一个promise对象,所以我们可以像下面那样接着对其返回值进行 .then 调用。
1.2. Thenable
Promise.resolve 方法另一个作用就是将 thenable 对象转换为promise对象。*ES6 Promises里提到了Thenable这个概念,简单来说它就是一个非常类似promise的东西。就像我们有时称具有 .length 方法的非数组对象为Array like一样,thenable指的是一个具有 .then 方法的对象。这种将thenable对象转换为promise对象的机制要求thenable对象所拥有的 then 方法应该和Promise所拥有的 then 方法具有同样的功能和处理过程*,在将thenable对象转换为promise对象的时候,还会巧妙的利用thenable对象原来具有的 then 方法。
到底什么样的对象能算是thenable的呢,最简单的例子就是 jQuery.ajax(),它的返回值就是thenable的。因为jQuery.ajax() 的返回值是 jqXHR Object 对象,这个对象具有 .then 方法。
$.ajax('/json/comment.json'); // => 拥有 `.then` 方法的对象
// 这个thenable的对象可以使用 Promise.resolve 来转换为一个promise对象。
// 将thenable对象转换promise对象
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象
promise.then(function(value){
console.log(value);
});
Promise.resolve 只使用了共通的方法 then ,提供了在不同的类库之间进行promise对象互相转换的功能。
简单总结一下 Promise.resolve 方法的话,可以认为它的作用就是将传递给它的参数填充(Fulfilled)到promise对象后并返回这个promise对象。
2. Promise.reject
Promise.reject(error)是和 Promise.resolve(value) 类似的静态方法,是 new Promise() 方法的快捷方式。比如 Promise.reject(new Error(“出错了”)) 就是下面代码的语法糖形式。
new Promise(function(resolve,reject){
reject(new Error("出错了"));
});
这段代码的功能是调用该promise对象通过then指定的 onRejected 函数,并将错误(Error)对象传递给这个 onRejected 函数。
Promise.reject(new Error("BOOM!")).catch(function(error){
console.error(error);
});
3.Promise执行顺序
在使用Promise.resolve(value) 等方法的时候,如果promise对象立刻就能进入resolve状态的话,那么你是不是觉得 .then 里面指定的方法就是同步调用的呢?实际上, .then 中指定的方法调用是异步进行的。
var promise = new Promise(function (resolve){
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function(value){
console.log(value); // 3
});
console.log("outer promise"); // 2
// 执行上面的代码会输出下面的log,从这些log我们清楚地知道了上面代码的执行顺序。
inner promise // 1
outer promise // 2
42 // 3
由于JavaScript代码会按照文件的从上到下的顺序执行,所以最开始 <1> 会执行,然后是 resolve(42); 被执行。这时候 promise 对象的已经变为确定状态,FulFilled被设置为了 42 。
下面的代码 promise.then 注册了 <3> 这个回调函数,这是本专栏的焦点问题。由于 promise.then 执行的时候promise对象已经是确定状态,从程序上说对回调函数进行同步调用也是行得通的。
但是即使在调用 promise.then 注册回调函数的时候promise对象已经是确定的状态,Promise也会以异步的方式调用该回调函数,这是在Promise设计上的规定方针。因此 <2> 会最先被调用,最后才会调用回调函数 <3> 。
Promise.then
这其中,我想大家已经认识了 .then().catch() 这种链式方法的写法了,其实在Promise里可以将任意个方法连在一起作为一个方法链(method chain)。
promise可以写成方法链的形式
aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
console.log(error);
});
如果把在 then 中注册的每个回调函数称为task的话,那么我们就可以通过Promise方法链方式来编写能以taskA → task B 这种流程进行处理的逻辑了。
1. promise chain
promise chain - 即方法链越短越好。 在这个例子里我们是为了方便说明才选择了较长的方法链。我们先来看看下面这样的promise chain。
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
在 上述代码 中,我们没有为 then 方法指定第二个参数(onRejected),也可以像下面这样来理解。
+ then 注册onFulfilled时的回调函数
+ catch 注册onRejected时的回调函数
在下面的情况下就会调用 onRejected 方法。
+ 发生异常的时候
+ 返回了一个Rejected状态的promise对象
Promise中的处理习惯上都会采用 try-catch 的风格,当发生异常的时候,会被 catch 捕获并被由在此函数注册的回调函数进行错误处理。另一种异常处理策略是通过 返回一个Rejected状态的promise对象 来实现的,这种方法不通过使用 throw 就能在promise chain中对 onRejected 进行调用。
下面我们再来看一个具体的关于 Task A → onRejected 的例子。
function taskA() {
console.log("Task A");
throw new Error("throw Error @ Task A")
}
function taskB() {
console.log("Task B");// 不会被调用
}
function onRejected(error) {
console.log(error);// => "throw Error @ Task A"
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
在本例中我们在taskA中使用了 throw 方法故意制造了一个异常。但在实际中想主动进行onRejected调用的时候,应该返回一个Rejected状态的promise对象。关于这种两种方法的异同,请参考 使用reject而不是throw 中的讲解。
2. promise chain 中如何传递参数
答案非常简单,那就是在 Task A 中 return 的返回值,会在 Task B 执行时传给它。我们还是先来看一个具体例子吧。
promise-then-passing-value.js
function doubleUp(value) {
return value * 2;
}
function increment(value) {
return value + 1;
}
function output(value) {
console.log(value);// => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出现异常的时候会被调用
console.error(error);
});
这段代码的入口函数是 Promise.resolve(1); ,整体的promise chain执行流程如下所示。
+ Promise.resolve(1); 传递 1 给 increment 函数
+ 函数 increment 对接收的参数进行 +1 操作并返回(通过return)
+ 这时参数变为2,并再次传给 doubleUp 函数
+ 最后在函数 output 中打印结果
每个方法中 return 的值不仅只局限于字符串或者数值类型,也可以是对象或者promise对象等复杂类型。
return的值会由 Promise.resolve(return的返回值); 进行相应的包装处理,因此不管回调函数中会返回一个什么样的值,最终 then 的结果都是返回一个新创建的promise对象。
3.每次调用then都会返回一个新创建的promise对象
从代码上乍一看, aPromise.then(…).catch(…) 像是针对最初的 aPromise 对象进行了一连串的方法链调用。然而实际上不管是 then 还是 catch 方法调用,都返回了一个新的promise对象。下面我们就来看看如何确认这两个方法返回的到底是不是新的promise对象。
var aPromise = new Promise(function (resolve) {
resolve(100);
});
var thenPromise = aPromise.then(function (value) {
console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true
如果我们知道了 then 方法每次都会创建并返回一个新的promise对象的话,那么我们就应该不难理解下面代码中对 then 的使用方式上的差别了。
// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log("1: " + value); // => 100
})
// vs
// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("2: " + value); // => 100 * 2 * 2
});
- 第1种写法中并没有使用promise的方法链方式,这在Promise中是应该极力避免的写法。这种写法中的 then 调用几乎是在同时开始执行的,而且传给每个 then 方法的 value 值都是 100 。
- 第2中写法则采用了方法链的方式将多个 then 方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then 方法的 value 的值都是前一个promise对象通过 return 返回的值。
下面是一个由方法1中的 then 用法导致的比较容易出现的很有代表性的反模式的例子。
✘ then 的错误使用方法
function badAsyncCall() {
var promise = Promise.resolve();
promise.then(function() {
// 任意处理
return newVar;
});
return promise;
}
这种写法有很多问题,首先在 promise.then 中产生的异常不会被外部捕获,此外,也不能得到 then 的返回值,即使其有返回值。
由于每次 promise.then 调用都会返回一个新创建的promise对象,因此需要像上述方式2那样,采用promise chain的方式将调用进行链式化,修改后的代码如下所示。
then 返回返回新创建的promise对象
function anAsyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意处理
return newVar;
});
}