-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Promise
什么是Promise?
JavaScript Promise迷你书:Promise是抽象异步处理对象以及对其进行各种操作的组件。
MDN:所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
Promise是一个对象,与其他的javascript对象的用法没什么不一样;Promise在异步编程中,主要起到代理作用,充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。
Promise的思想,其实就是每一个异步任务立刻返回一个Promise对象,因为是立即返回的,所以可以采用同步操作的流程。
JavaScript的异步执行
javascript语言的执行环境是“单线程”的,也就是说javascript一次只能完成一个任务,如果有多个任务,就必须排队,前面一个任务完成了,下一个任务才能开始执行。
单线程模式实现起来比较简单,执行环境也相对单纯,但是如果有一个任务耗时很长,那么后面的任务就必须排队等候,这样整个程序的执行时间就会被拖长。这经常会造成浏览器无响应(假死),整个页面卡在某个地方,其他任务无法执行。
如果只有两三个可能的事件,单线程语言编写的面向事件的代码要比多线程代码简单很多,但是如果有很多事件,同时要求数据的状态能够从上一个事件传递到下一个事件,那么就会像下面代码那样,出现【回调地狱】:
operation1(function(err, result) {
operation2(function(err, result) {
operation3(function(err, result) {
operation4(function(err, result) {
operation5(function(err, result) {
// do something useful
})
})
})
})
})javascript语言本身运行起来并不慢,慢的是读写外部数据,例如等待ajax请求返回的数据。为了解决这个问题,javascript语言将任务的执行模式分为:同步(Synchronous)和异步(Asynchronous)。
- 同步模式:传统做法,下一个任务要等上一个任务执行结束,才能执行,程序的执行顺序与任务的排列顺序是一致、同步的。
- 异步模式:把每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,并不是立即执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据返回了
异步模式编程的几种常用方法是:回调函数、事件监听、发布/订阅
回调函数
如果函数f2需要等待f1的执行结果,并且f1是一个耗时很长的任务,那么可以考虑把f2写成f1的回调函数。
var f1 = function(callback) {
// do something here
...
callback.apply(this, para);
};
var f2 = function(parameter) {
// do someting in customer callback
};
// call the fn with callback as parameter
f1(f2);
// 使用setTimeout()或者setInterval()也可以实现回调函数的异步调用
function f1(callback) {
setTimeout(function () {
// f1的任务代码
// ...
callback();
}, 0);
}
// 执行代码
f1(f2);这样就把同步操作变成了异步操作,setTimeout(fn,0)将fn放到下一轮事件循环执行。f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
虽然回调函数简单、容易理解和部署,但是各部分耦合度高,程序结构混乱,回调函数嵌套的时候难以追踪流程,并且每一个任务只能指定一个回调函数。
事件监听
事件驱动模式——任务的执行不取决于代码的顺序,而取决于某个事件是否发生。例如:
f1.on('done',f2); // 当f1发生done事件,就执行f2
function f1(){
setTimeout(function (){
// f1的任务代码
f1.trrigger('done'); // 执行完成后,立即触发done事件,从而开始执行f2
},1000)
}事件监听可以绑定多个事件,每个事件可以指定多个回调函数,很好地解决了“耦合度高”的问题,但是这样子整个程序就会变成事件驱动,运行流程会变得很不清晰。
发布/订阅
发布/订阅模式,又称为观察者模式,如果把事件理解成“信号”,并且存在一个“信号中心”,某个任务执行完成后,就向信号中心publish一个信号,其他任务可以向信号中心订阅这个信号,从而知道什么时候自己可以开始执行。
// 首先f2向“信号中心”订阅done信号
jQuery.subscribe('done',f2);
// f1代码块
function f1(){
setTimeout(function(){
// f1的任务代码
jQuery.publish('done'); // f1执行完成后,向‘信号中心’发布done信号,从而引发f2的执行
},1000);
}
// f2执行完成后,可以对其进行取消订阅操作
jQuery.unsubscribe('done',f2);这种方法我们可以通过查看“消息中心”,了解存在多少信号、每个信号的订阅情况,从而监控程序的运行。
创建promise对象
- new Promsie(fn)返回一个promise对象
- 在fn中指定异步等处理
- 处理结果正常就调用resolve(处理结果值)
- 处理结果错误就调用reject(Erro对象)
Promise对象只有三种状态:
- 异步操作“未完成”(pending)
- 异步操作“已完成”(resolved/fulfilled)
- 异步操作“失败”(rejected)
这三种状态的途径只有两种:
- 异步操作从“未完成”到“已完成”
- 异步操作从“未完成”到“失败”
这种变化只能发生一次,所以一旦当前状态变成“已完成”或“失败”,就不会再有新的状态变化了。
所以Promise对象的最终结果为:
- 异步操作成功,Promise对象传回一个值,状态变为resolved
- 异步操作失败,Promise对象抛出一个错误,状态变为rejected
所以Promise的基本用法如下:
var promise = new Promise(function(resolve,reject){ // Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve方法和reject
if(/* 异步操作成功 */){ // 如果异步操作成功,则用resolve方法把Promise对象的状态变为“成功”(即从pending变为resolved)
resolve(value);
}else{ // 如果异步操作失败,则用reject方法将状态改为“失败”(即从pending变为rejected)
reject(error);
}
});
promise.then(function(value){ // promise实例生成后,可以用then方法和reject方法的回调函数
// success
},function(value){
// faile
});下面用Promise来通过异步处理方式来获取XMLHttpRequest(XHR)的数据
首先创建一个用Promise把XHR处理包装起来名为getURL的函数体
function getURL(URL){
return new Promise(function(resolve,reject){
var req = new XMLHttpRequest();
req.open('GET',URL,true);
req.onload = function(){
if(req.status === 200){ // getURL只有在通过XHR取得结果状态为200时才会调用resolve
resolve(req.responseText); // 在response的内容中加入参数(resolve方法的参数没有特别规定,基本上把要传给回调函数参数放进去就可以了,then方法就可以接收到这个参数)
}else{
reject(new Error(req.statusText)); // 发生错误时创建一个Error对象后再将具体的值传进去(传给reject的参数也没有特别规定,一般只要是Error对象就可以了)
// 传给reject的参数,一般包含了reject原因的Error对象,这里是因为状态值不为200所以被reject,所以reject中放入statusText(该参数能被then方法的第二个参数或catch方法中使用)
}
};
req.onerror = function(){ // XHR中onerror事件被触发的时候就是发生错误,调用reject
reject(new Error(req.statusText));
};
req.send();
});
}
// 运行实例
var URL = 'XXX'; // getURL('XXX'); 能够返回promise对象
getURL(URL).then(function onFulfilled(value){ // getURL函数中的 resolve(req.responseText) 会将promise对象变为resolve状态,同时使用其调用onFulfilled函数
console.log(vaue);
}).catch(function onRejected(error){ // 等同于 getURL().then(onFulfilled,onRejected)
console.log(error);
});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('/json/posts.json').then(function(json){
console.log('Contents:' + json);
},function(error){
console.error('出错了',error);
});Promise提供的各种方法
Promise.resolve
静态方法Promise.resolve(value)可以认为是new Promise()方法的快捷方式:
new Promise(function(resolve){
resolve(42); // resolve(42)会让这个promise对象立即进入resolved状态,并将42传递给后面then指定的onFulfilled函数
});
// 上述代码的语法糖为:
Promise.resolve(42);
// Promise.resolve(value);的返回值也是一个promise对象,所以能够像下面的代码那样对其返回值进行.then调用
Promise.resolve(42).then(function(value){
console.log(value);
});Promise.resolve方法的另一个作用就是将thenable对象转换为promise对象
thenable对象,是一个非常类似promise的东西,指的是一个具有.then方法的对象。最简单的thenable例子就是jQuery.ajax(),它的返回值就是thenable
$.ajax('/json/area.json'); // 因为jQuery.ajax()的返回值是jqXHR Object对象,拥有'.then'方法的对象将thenable对象转换为promise对象
var promise = Promise.resolve($.ajax('/json/area.json')); // promise对象
promise.then(function(value){
console.log(value);
});简单来说,Promise.resolve方法的作用就是将传递给它的参数填充到promise对象后并返回这个promise对象
Promise很多处理内部也是使用Promise.resolve方法把值转为promise对象后再进行处理。
resolve方法的参数除了正常的值以外,还可能是另一个Promise实例
// p1/p2都是Promise的实例,p2的resolve方法将p1作为参数,这时p1的状态就会传递给p2.
// 如果调用的时候,p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;
// 如果p1的状态已经是fulfilled或者rejected,那么p2的回调函数立马执行
var p1 = new Promise(function(resolve,reject){
// ....dosomething
});
var p2 = new Promise(function(resolve,reject){
// ...dosomething
resolve(p1);
})Promise.reject
Promise.reject(error)是和Promise.resolve(value)类似的静态方法,是 new Promise()方法的快捷方式。
new Promise(function(resolve,reject){
reject(new Error("出错了"));
});
// 语法糖
Promise.reject(new Error('出错了'));
Promise.reject(new Error('BOM!')).catch(function(error){
console.log(error);
});Promise.prototype.then方法:链式操作
因为Promise.prototype.then和 Promise.prototype.catch方法返回 promises对象, 所以它们可以被链式调用—— 一种被称为 composition 的操作。
aPromise.then(function taskA(value){
// taskA
}).then(function taskB(value){
// taskB
}).catch(function onRejected(error){
console.log(error);
});.then有四种写法,各有差异:
// then方法接一个回调函数finalHandler
// 写法一:finalHandler回调函数的参数是doSomethingElse函数运行的结果
doSomething().then(function(){
return doSomenthingElse();
}).then(finalHandler);
// finalHandler回调函数的参数是undefined
doSomething().then(function(){
doSomenthingElse();
return;
}).then(finalHandler);
// finalHandler回调函数的参数,是doSomethingElse函数返回的回调函数的运行结果
doSomething().then(doSomenthingElse()).then(finalHandler);
// doSomethingElse会接收到doSomething()返回的结果
doSomething().then(doSomenthingElse).then(finalHandler);Promise.prototype.catch方法:捕捉错误
实际上 Promise.prototype.catch 只是 promise.then(undefined, onRejected); 方法的一个别名而已。 也就是说,这个方法用来注册当promise对象状态变为Rejected时的回调函数。
var promise = Promise.reject(new Error('message'));
promise.catch(function(error){
console.error(error)
}); // Error: message
// 上面的代码在IE8下会出现语法错误:identifier not found(因为IE8及以下版本都是基于ECMAScript 3实现的,在ECMAScript 3中保留字是不能作为对象的属性名使用的,因此不能将 catch 作为属性来使用)
// 使用中括号标记法,可以将非合法标识符作为对象的属性名使用
var promise = Promise.reject(new Error('message'));
promise['catch'](function(error){
console.error(error);
}); // Error: message
// 或者用then避免这个问题
var promise = Promise.reject(new Error('message'));
promise.then(undefiend,function(error){
console.error(error);
}); // Error: messagePromise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,也就是说,错误总会被下一个catch语句捕获。
getJSON('/json/area.json').then(function(post){
return getJSON(post.commentURL);
}).then(function(comments){
// dosomething
}).catch(function(error){
// 处理前两个回调函数的错误
});Promise.all方法,Promise.race方法
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例
// Promise.all方法接受一个数组作为参数,p1/p2/p3都是Promise对象的实例
// Promise.all方法接受的参数不一定是数组,但必须具有iterator接口,且返回的每个成员都是Promise实例
var p = Promise.all([p1,p2,p3]);p的状态由p1、p2、p3决定,分成两种情况:
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
//生成一个Promise对象的数组
var promise = [2,3,5,7,11,13].map(function(id){
return getJSON("/post/" + id + ".json");
});
Promise.all(promise).then(function(posts){
// dosomething
}).catch(function(reason){
// dosomething
});Promise.race方法同样是将多个Promise实例包装成一个新的Promise实例
// 只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值
var p = Promise.all([p1,p2,p3]);Promise的应用
加载图片
var preloadImage = function(path){
return new Promise(function(resolve,reject){
var imasge = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};Ajax操作
// 传统写法
function search(term,onload,onerror){
var xhr,results,url;
url = 'XXX' + term;
xhr = new XMLHttpRequest();
xhr.open('GET',url,true);
xhr.onload = function(e){
if(this.status === 200){
results = JSON.pares(this.responseText);
onload(results);
}
};
xhr.onerror = unction(e){
onerror(e);
};
xhr.send();
};
search('hello World',console.log,console.error);
// 使用Promise对象
function search(term){
var url = 'XXX' + term;
var xhr = new XMLHttpRequest();
var result;
var p = new Promise(function(resolve,reject){
xhr.open('GET',url,true);
xhr.onload = function(e){
if(this.status === 200){
result = JOSN.parse(this.responseText);
resolve(result);
}
};
xhr.onerror = function(e){
reject(e);
};
xhr.send();
});
return p;
};
search('hello World').then(console.log,console.error);运用Ajax实现加载图片
function imgLoad(url){
return new Promise(function(resolve,reject){
var request = new XMLHttpRequest();
request.open('GET',url);
request.responseType = 'blod';
request.onload = function(){
if(request.status === 200){
resolve(request.response);
}else{
reject(new Error('图片加载失败:' + request.statusText));
}
};
request.onerror = function(){
reject(new Error('发生网络错误'));
};
request.send();
});
}