Promise 建立
Promise 对象用来传递异步操作消息,代表一个未来才会知道结果的事件,并且对不同事件提供统一的 API 以便进一步处理。Promise 具有以下特点:
- 由异步操作结果决定改状态,其他操作绝不影响该状态;
- 对象状态不受外界影响:Promise 代表的异步操作有三个状态:
- Pending: 进行中
- Resolved: 已完成(Fulfilled)
- Rejected: 已失败
- 一旦状态改变,就不会再变:Promise 的状态只有2种可能:
- 从 Pending 到 Resolved
- 从 Pending 到 Rejected
对于同一个 promise, 当以上状态发生一个(只能发生其一),就不会再改变了。之后任何时间你都能得到这个状态,且永不改变。
有了 Promise 就可以将层层的回调写为同步的样子,表示起来更清晰。不过需要注意以下几点:
- Promise 一旦建立就立即执行,并且无法中断或取消
- 如果没有设置回调函数,那么 Promise 中的产生的错误不会抛到外部
- Pending 状态时,我们无法知道其具体进度
Promise 的基本结构如下:
var promise = new Promise(function(resolve, reject){
if(/*异步操作成功*/){
resolve(value);
} else {
reject(error);
}
});
构造函数接受一个回调函数为参数,回调函数具有2个参数,也都是函数,resolve 在 Promise 状态变为 resolved 时调用,reject 在 Promise 状态变为 rejected 时调用。resolve 的接受一个参数——值或另一个 promise 对象; rejectj接受一个参数——错误。需要说明的是,这里的 resole 和 reject 函数已经由系统部署好了,我们可以不写。
promise 构建好以后我们就可以调用它的then()
方法,then(resolve(value){},reject(value){})
方法接受2个函数参数,resolve 在 Promise 状态变为 resolved 时调用,reject 在 Promise 状态变为 rejected 时调用。其中 reject 参数是可选的。和构造函数不同的是,then 方法的 reject 和 resolve 都使用 promise 传出的值作为其唯一的参数。
这里写一个简单的例子,理解一下:
function timeout(ms){
return new Promise((resolve, reject) => {
console.log("promise"); //"promise"
setTimeout(resolve, ms, 'done');
});
}
timeout(2000).then((value) => {
console.log(value); //2秒后得到 "done"
});
利用 Promise 异步加载图片:
function loadImageAsync(url){
return new Promise(function(resole, reject){
var image = new Image();
image.onload = function(){
resolve(image);
};
image.onerror = function(){
reject(new Error(`Could not load image at ${url}`));
};
image.src = url;
});
}
利用 Promise 实现 Ajax:
var id = document.getElementById("primary");
var getJSON = function(url){
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.response = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler(){
if(client.readyState !== 4) return;
if(this.status === 200){
resolve(client.response);
} else {
reject(new Error(this.statusText));
}
}
});
return promise;
}
getJSON('info.json').then(
json => id.innerHTML = "<pre>" + json + "</pre>",
err => id.innerHTML = err
);
如果 resolve 的参数是一个promise:
var p1 = new Promise(function(resolve, reject){
//...
});
var p2 = new Promise(function(resolve, reject){
//...
resolve(p1);
});
上面代码中 p1 的状态传给了 p2,也就是p1运行完成(状态为 resolve 或 reject)后 p2 的回调函数会立刻开始执行:
var p1 = new Promise(function(resolve, reject){
setTimeout(() => reject(new Error('failed')), 3000);
});
var p2 = new Promise(function(resolve, reject){
setTimeout(() => resolve(p1), 1000);
});
p2.then(result => console.log(result));
p2.catch(error => console.log(error));
p1 建立,进入 setTimeout 异步计时器。之后 p2 建立,进入 setTimeout 异步计时器。1s 后 p2 准备执行 resolve, 但是 resolve 的参数是 p1, 此时 p1 还是 Pending 状态,所以 p2 开始等待。又过了 2s, p1 的 reject 执行,变为 rejected 状态,随即 p2 也跟着变成 rejected 状态。
Promise 对象方法
- then() 方法
then(resolve(value){},reject(value){})
方法接受2个函数参数,resolve 在 Promise 状态变为 resolved 时调用,reject 在 Promise 状态变为 rejected 时调用。其中 reject 参数是可选的。和构造函数不同的是,then 方法的 reject 和 resolve 都使用 promise 传出的值作为其唯一的参数。
then()
方法返回一个新的 Promise 实例,注意,不是之前那个。因此可以用链式调用,不断添加"回调"函数。 then 的返回值成了下一个 then 中回调函数的参数:
var p = new Promise(function(resolve, reject){
resolve("from new Promise");
}).then(function (value){
console.log(value); //from new Promise 其次输出这个
return "from the first 'then'";
}).then(function(value){
console.log(value); //from the first 'then' 最后输出这个
return "from the second 'then'";
});
console.log(p); //Promise{...} 先输出这个
注意,如果 promise 的状态是 resolved 则执行 then参数中的第一个回调函数;如果 promise 的状态是 rejected 则执行 then参数中的第二个回调函数。这个状态是不断传递下来的,这一点和之前的例子类似。
- catch() 方法:
catch(reject) 方法是then(null, reject)
的别名,在发生错误的时候执行其参数函数:
new Promise(function(resolve, reject){
resolve("resolved");
}).then(function(val){
console.log(val); //resolved
throw new Error("man-made Error");
}).catch(function(err){
console.log(err.message); //man-made Error
});
错误会从最初的请求沿着回调函数,一直被传递下来。这一点和传统的错误冒泡类似,无论哪里有错误都可以被捕获到:
new Promise(function(resolve, reject){
reject(new Error("original Error"));
}).then(function(val){
console.log(val); //不执行
throw new Error("man-made Error");
}).catch(function(err){
console.log(err.message); //original Error
});
当然也可以在半路截住错误:
new Promise(function(resolve, reject){
reject(new Error("original Error"));
}).then(function(val){
console.log(val); //不执行
throw new Error("man-made Error");
}, function(err){
console.log(`Uncaught Error: ${err.message}`); //Uncaught Error: original Error
}).catch(function(err){
console.log(err.message); //不执行
});
这里需要注意以下几点:
- reject 和 throw 一样可以抛出错误。
- 在 Promise 状态变为 resolved 或 rejected 之后抛出的错误会被忽略。
- 建议总是使用 catch() 方法,而不要在 then() 方法中定义 reject 函数。
- 如果一个 promise 既没有 catch方法,也没有可以捕获到错误的 then 方法,那么这个错误就消失了。它不会到 promise 外面来。
- try...catch... 只能捕获同步代码的错误,不能捕获异步代码的错误(这个是 ES5 就有的)。
- catch() 方法可以继续抛出错误,就像 try...catch 中的 catch 一样可以抛出错误。
这里需要说明的是第4条:错误不会到 Promise 外面是 ES6 规范的说法。具体理解(浏览器环境):控制台依旧会报错,但是不影响 promise 语句之后续代码执行。此外,promise 语句内的异步语句(如事件,定时器等等)抛出的错误,不属于 promise 内部,发生错误会传播出去:
var p = new Promise(function(resolve, reject){
resolve("ok");
setTimeout(function(){throw new Error("setTimeout error")},0);
});
p.then(function(val){console.log(val);}); //ok
//Uncaught Error: setTimeout error
其次,就以上前两个注意事项举一例说明:
new Promise(function(resolve, reject){
resolve("resolved");
throw "original Error"; //被忽略
}).then(function(val){
console.log(val); //resolved
throw (new Error("man-made Error"));
}).catch(function(err){
console.log(err.message); //man-made Error
});
catch 方法的返回值还是一个新的 promise 对象,可以继续调用 then 等其他方法:
new Promise(function(resolve, reject){
reject(new Error("reject"));
}).catch(function(err){
console.log("1st catch"); //被跳过
return "continue";
}).then(function(val){
console.log(val); //continue
});
如果 catch之前没有错误,该 catch 会被跳过。这意味着,catch 不能捕获在其后面的语句中出现的错误:
new Promise(function(resolve, reject){
resolve("resolved");
}).catch(function(err){
console.log("1st catch"); //被跳过
}).then(function(val){
console.log(val); //resolved
throw (new Error());
}).catch(function(err){
console.log("2nd catch"); //2nd catch
});
- finally() 方法
finally() 接受一个回调函数(无参数)为参数,和 try...catch...finally 中的 finally 类似,不论 promise 是什么状态,该回调函数都一定会运行。可以用它关闭文件,或者关闭服务器等:
server.listen(0).then(function(){
//do sth.
}).finally(server.stop);
finally() 内部实现如下:
Promise.prototype.finally = function(callback){
return this.then(
value => {Promise.resolve(callback()).then(() => value)},
error => {Promise.resolve(callback()).then(() => {throw error})}
);
};
- done() 方法
done() 方法用在 promise 处理语句的末端,用来处理可能未捕获的错误,并抛向全局。如果其带有参数,可以等效为 done() 之前多了一个 then():
p.done(fun1, fun2);
//相当于
p.then(fun1,fun2).done();
done() 内部实现如下:
Promise.prototype.done = function(onResolve, onRejected){
this.then(onResolve, onRejected).catch(function(err){
setTimeout(() => {throw err}, 0);
});
};
Promise 静态方法
- Promise.all()
将多个 promise 对象合并成一个新的 promise 实例。其接受一个装仅有 promise 对象的可遍历结构为参数,如果不是 promise 对象,系统会调用Promise.resolve()
进行类型转换。
promise.all() 方法得到的新的 promise 对象状态由构成它的所有 promise 对象决定,具体分为2种情况:
- 当所有构成它的 promise 对象的状态都变成 resolved,这个新的对象状态才变为 resolved。此时构成它所有的 Promise 的返回值构成一个数组作为新的 promise 对象的回调函数参数;
- 当所有构成它的 promise 对象的状态有一个变成 rejected,这个新的对象状态就变为 rejected。此时第一个被 reject 的 Promise 的返回值作为新的 promise 对象的回调函数参数;
//伪代码, 由于没有正确的 url
var getJSON = function(url){
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.response = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler(){
if(client.readyState !== 4) return;
if(this.status === 200){
resolve(client.response);
} else {
reject(new Error(this.statusText));
}
}
});
return promise;
}
var pros = ['url1', 'url2', 'url3'].map(url => getJSON(url));
Promise.all(pros).then(function(){
console.log("all successful");
}, function(){
console.log("one rejected"); //one rejected, 由于没有正确的 url
});
- Promise.race()
将多个 promise 对象合并成一个新的 promise 实例。其接受一个装仅有 promise 对象的可遍历结构为参数,如果不是 promise 对象,系统会调用Promise.resolve()
进行类型转换。
和 promise.all() 不同的是 Promise.race() 方法得到的新的 promise 对象状态由构成它的 promise 对象中最先改变状态的那一个决定。
//伪代码, 由于没有正确的 url
var getJSON = function(url){
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.response = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler(){
if(client.readyState !== 4) return;
if(this.status === 200){
resolve(client.response);
} else {
reject(new Error(this.statusText));
}
}
});
return promise;
}
//如果5s不能获得数据就报错
var p = Promise.race([
getJSON("url"),
new Promise(function(resolve, reject){
setTimeout(() => reject(new Error("Timeout")), 5000);
})
]).then(res => console.log(res))
.catch(err => console.log(err)); //Error, 由于没有正确的 url
- Promise.resolve()
将现有对象转化为 promise 对象:
var p = Promise.resolve($.ajax('url')); //jQuery的 $.ajax 方法
//等同于:
var p = new Promise(function(resolve){
resolve($.ajax('url'));
});
如果传入 Promise.resolve() 的对象不具有 then 方法(ie. unthenable), 则返回一个状态为 resolved 的新 promise 对象。
Promise.resolve("hello").then(function(val){
console.log(val); //hello
});
如果你仅仅想得到一个 promise 对象,那利用 resolve() 方法是最简单的:
var promise = Promise.resolve();
- Promise.reject()
Promise.reject(reason)
, 返回一个状态为 rejected 的 promise 实例。参数 reason 会被传递被实例的回调函数。
Promise.reject(new Error("error occured")).catch(err => console.log(err.message)); //error occured
应用举例
- 加载图片:
var preloadImage = function(url){
return new Promise(function(resolve, reject){
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = url;
});
};
- 使用 Generator 管理流程,用 promise 进行异步操作
function getFoo(){
return new Promise(function(resolve){
resolve("foo");
});
}
function* gen(){
try{
var foo = yield getFoo();
console.log(foo);
} catch(e) {
console.log(e);
}
}
var it = gen();
(function go(result){
if(result.done) return result.value;
return result.value.then(function(value){
return go(it.next(value));
}, function(err){
return go(it.throw(error));
});
})(it.next()); //foo
- 异步中模拟 sleep 函数
const sleep = (time) => new Promise(function(resolve){
setTimeout(resolve, time);
});
(async () => {
for(var i = 0; i < 5; i++){
await sleep(1000);
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();