2.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>
。
为什么要对明明可以以同步方式进行调用的函数,非要使用异步的调用方式呢?
2.3.1. 同步调用和异步调用同时存在导致的混乱
其实在Promise之外也存在这个问题,这里我们以一般的使用情况来考虑此问题。
这个问题的本质是接收回调函数的函数,会根据具体的执行情况,可以选择是以同步还是异步的方式对回调函数进行调用。
下面我们以 onReady(fn)
为例进行说明,这个函数会接收一个回调函数进行处理。
mixed-onready.js
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
mixed-onready.js会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。
如果在调用onReady之前DOM已经载入的话
对回调函数进行同步调用
如果在调用onReady之前DOM还没有载入的话
通过注册 DOMContentLoaded
事件监听器来对回调函数进行异步调用
因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的log消息顺序也会不同。
为了解决这个问题,我们可以选择统一使用异步调用的方式。
async-onready.js
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
关于这个问题,在 Effective JavaScript 的 第67项 不要对异步回调函数进行同步调用 中也有详细介绍。
绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。
对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。
如果想在将来某时刻调用异步回调函数的话,可以使用
setTimeout
等异步API。
Effective JavaScript
— David Herman
前面我们看到的 promise.then
也属于此类,为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise在规范上规定 Promise只能使用异步调用方式 。
最后,如果将上面的 onReady
函数用Promise重写的话,代码如下面所示。
onready-as-promise.js
function onReadyPromise() {
return new Promise(function (resolve, reject) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
由于Promise保证了每次调用都是以异步方式进行的,所以我们在实际编码中不需要调用 setTimeout
来自己实现异步调用。