什么是Promise
Promise是抽象异步处理对象以及对其进行各种操作的组件。
Promise真的很重要很重要,一定要好好掌握。
// Promise 实例:var promise = new Promise((resolve,reject) => { if(true) { resolve(100) }; if(false) { reject('error') };});//使用promise.then(value => { console.log(value); //100}).catch(error => { console.error(error);});复制代码
回调函数
在解释Promise之前,先来回顾一下什么是回调函数。
回调函数,也被称作高阶函数。
函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说函数A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。
注意:回调函数不是立即就执行。它是在包含的函数体中指定的地方“回头调用”。
网上有一个通俗易懂的例子帮助理解回调函数:
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
//回调函数举例1$("#btn").click(() => { alert("点击后才出现");});//回调函数举例2function runAsyncCallback(callback){ setTimeout(() => { console.log('执行完成'); callback('数据'); }, 2000);}runAsync(data=>{ console.log(data); //2秒后先输出:执行完成,再输出:数据});复制代码
认识了回调函数,接下来的内容会帮助我们理解为什么需要Promise。
为什么需要Promise
有非常多的应用场景我们不能立即知道应该如何继续往下执行。例如很重要的ajax请求
的场景。通俗来说,由于网速的不同,可能你得到返回值的时间也是不同的,这个时候我们就需要等待,结果出来了之后才知道怎么样继续下去,例如下方的回调函数
案例:
// 需求:当一个ajax结束后,得到的值,需要作为另外一个ajax的参数被使用(即该参数得从上一个ajax请求中获取)var url = 'XXXXXX';var result;var XHR = new XMLHttpRequest();XHR.open('GET', url, true);XHR.send();XHR.onreadystatechange = function() { if (XHR.readyState == 4 && XHR.status == 200) { result = XHR.response; console.log(result); // 伪代码 var url2 = 'XXXXXX' + result.someParams; var XHR2 = new XMLHttpRequest(); XHR2.open('GET', url2, true); XHR2.send(); XHR2.onreadystatechange = function() { ... } }}复制代码
当上述需求中出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,代码就会变成一场灾难。也就是我们常说的回调地狱
。
这时,我们可能会希望:
- 让代码变得更具有可读性和可维护性
- 将请求和数据处理明确的区分开
这时Promise
就要闪亮登场了,Promise中有一个强大的then方法,可以解决刚刚遇到的回调地狱问题,并且让代码更优雅。
下面我们就一起来学习一下Promise,看一看它的强大之处。
Promise 的API
1、constructor (构造函数属性)
Promise
本身也是一个构造函数
,需要通过这个构造函数创建一个新的promise
对象作为接口,使用new
来调用Promise
的构造器来进行实例化,所以这个实例化出来的新对象:具有constructor属性,并且指针指向他的构造函数Promise。
var promise = new Promise((resolve, reject) => { // 此处代码会立即执行 // 当调用栈内容处理结束后,再通过promise.then()方法调用resolve 或 reject返回的数据});复制代码
2、Instance Method (实例方法)
promise.then()
Promise对象中的promise.then(resolve,reject)
实例方法,可以接收构造函数中处理的状态变化,并分别对应执行。
promise.then(onFulfilled, onRejected)复制代码
then方法有2个参数(都是可选参数):
- resolve 成功时
onFulfilled
会被调用 - reject 失败时
onRejected
会被调用
promise.then
成功和失败时都可以使用,并且then方法的执行结果也会返回一个Promise对象
。
promise.catch()
另外在只想对异常进行处理时可以采用 promise.then(undefined, onRejected)
这种方式,只指定reject时的回调函数即可。 不过这种情况下 promise.catch(onRejected)
应该是个更好的选择。
promise.catch(onRejected)复制代码
注意:在IE8及以下版本,使用 promise.catch()
的代码,会出现 identifier not found 的语法错误。(因为 catch
是ECMAScript的 (Reserved Word)有关。在ECMAScript 3中保留字是不能作为对象的属性名使用的。)
解决办法:不单纯的使用 catch
,而是使用 then
来避免这个问题。
--------------------------------------------------
//then和catch方法 举例function asyncFunction(value) { var p = new Promise((resolve, reject) => { if(typeof(value) == 'number'){ resolve("数字"); }else { reject("我不是数字"); } }); return p;}// 写法1:同时使用then和catch方法asyncFunction('123').then(value => { console.log(value); }).catch(error => { console.log(error);});//执行结果:数字// 写法2:只使用 then方法,不使用catch 方法// asyncFunction('abc').then(value => { // console.log(value); // },(error) => { // console.log(error);// });//执行结果:我不是数字复制代码
3、Static Method (静态方法)
像 Promise
这样的全局对象还拥有一些静态方法。
Promise.all()
Promise.resolve()
……
Promise 的状态 (Fulfilled、Rejected、Pending)
Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用。
用new Promise
实例化的promise对象有以下三个状态。
-
"unresolved" -
Pending
| 既不是resolve也不是reject的状态。等待中,或者进行中,表示Promise刚创建,还没有得到结果时的状态 -
"has-resolution" -
Fulfilled
| resolve(成功)时。此时会调用onFulfilled
-
"has-rejection" -
Rejected
| reject(失败)时。此时会调用onRejected
关于上面这三种状态的读法,其中 左侧为在 规范中定义的术语, 而右侧则是在 中描述状态的术语。
promise对象的状态,从Pending转换为Fulfilled或Rejected之后, 这个promise对象的状态就不会再发生任何变化。
当promise的对象状态发生变化时,用.then
来定义只会被调用一次的函数。
Promise的使用
1、创建Promise对象
前面很多次强调,Promise本身就是一个构造函数,所以可以通过new创建新的Promise对象:
var p = new Promise((resolve, reject) => { //做一些异步操作 setTimeout(() => { console.log('执行完成'); resolve('我的数据'); }, 0); console.log("我先执行")});//先输出:我先执行//1秒之后输出: 执行完成复制代码
我们执行了一个异步操作
,也就是setTimeout,1秒后,输出“执行完成”,并且调用resolve方法。但是只是new了一个Promise对象,并没有调用它,我们传进去的函数就已经执行了。为了避免这个现象产生, 所以我们用Promise的时候一般是包在一个函数中,需要的时候去运行这个函数。
如果你对执行的先后顺序还不理解,请参见
异步任务
:指不进入主线程、而进入"任务队列"(task queue)的任务
,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。也可以理解为可以改变程序正常执行顺序的操作就可以看成是异步操作。例如setTimeout和setInterval函数
同步任务
:指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
2、封装Promise对象
function asyncFunction(num) { var p = new Promise((resolve, reject) => { //创建一个Promise的新对象p if (typeof num == 'number') { resolve(); } else { reject(); } }); p.then(function() { //第一个function是resolve对应的参数 console.log('数字'); }, function() { //第二个function是reject对应的参数 console.log('我不是数字'); }) return p; //此处返回对象p}//执行这个函数我们得到了一个Promise构造出来的对象p,所以p.__proto__ === Promise.prototype,即p的指针指向了构造函数Promise,因此asyncFunction()能够使用Promise的属性和方法//此种写法可以多次调用asyncFunction这个方法asyncFunction('hahha'); //我不是数字asyncFunction(1234); //数字复制代码
我们刚刚讲到,then方法的执行结果也会返回一个Promise对象
,得到一个结果。因此我们可以进行then的链式执行,接收上一个then返回回来的数据并继续执行,这也是解决回调地狱
的主要方式。
3、Promise的链式操作和数据传递
下面我们就来看看如何确认then和catch两个方法返回的到底是不是新的promise对象。
var aPromise = new Promise(resolve => { resolve(100);});var thenPromise = aPromise.then(value => { console.log(value);});var catchPromise = thenPromise.catch(error => { console.error(error);});console.log(aPromise !== thenPromise); // => trueconsole.log(thenPromise !== catchPromise);// => true复制代码
===
是严格相等比较运算符,我们可以看出这三个对象都是互不相同的,这也就证明了 then
和 catch
都返回了和调用者不同的promise对象。我们通过下面这个例子进一步来理解:
// 1: 对同一个promise对象同时调用 `then` 方法var aPromise = new Promise(resolve => { resolve(100);});aPromise.then(value => { return value * 2;});aPromise.then(value => { return value * 2;});aPromise.then(value => { console.log("1: " + value); // 1: 100})// vs// 2: 对 `then` 进行 promise chain 方式进行调用var bPromise = new Promise(resolve => { resolve(100);});bPromise.then(value => { return value * 2;}).then(value => { return value * 2;}).then(value => { console.log("2: " + value); // 2: 400});复制代码
第1种写法中并没有使用promise的方法链方式,这在Promise中是应该极力避免的写法。这种写法中的 then
调用几乎是在同时开始执行的,而且传给每个 then
方法的 value
值都是 100
。
第2中写法则采用了方法链的方式将多个 then
方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then
方法的 value
的值都是前一个promise对象通过 return
返回的值,实现了Promise的数据传递
4、通过Promise封装ajax 解决回调地狱问题
我们在开篇,通过一个ajax的例子,引出了回调地狱的概念,强调了通过回调函数方式解决 多级请求都依赖于上一级数据时 所引发的问题。下面我们通过刚刚学习过的Promise对上面的ajax数据依赖的案例进行重写:
var url = 'XXXXX';// 封装一个get请求的方法function getJSON(url) { return new Promise((resolve, reject) => { var XHR = new XMLHttpRequest(); XHR.open('GET', url, true); XHR.send(); XHR.onreadystatechange = function() { if (XHR.readyState == 4) { if (XHR.status == 200) { try { var response = JSON.parse(XHR.responseText); resolve(response); } catch (e) { reject(e); } } else { reject(new Error(XHR.statusText)); } } } })}getJSON(url) .then(resp => { console.log(resp); return url2 = 'http:xxx.yyy.com/zzz?ddd=' + resp; }) .then(resp => { console.log(resp); return url3 = 'http:xxx.yyy.com/zzz?ddd=' + resp; });复制代码
new Promise写法的快捷方式
1、Promise.resolve
new Promise(resolve => { resolve(100);});// 等价于Promise.resolve(100); //Promise.resolve(100); 可以认为是上述代码的语法糖。// 使用方法Promise.resolve(100).then(value => { console.log(value);});复制代码
--------------------------------------------------
另:`Promise.resolve` 方法另一个作用就是将 [thenable](http://liubin.org/promises-book/#Thenable) 对象转换为promise对象。[ES6 Promises](http://liubin.org/promises-book/#es6-promises)里提到了[Thenable](http://liubin.org/promises-book/#Thenable)这个概念,简单来说它就是一个非常类似promise的东西。就像我们有时称具有 `.length` 方法的非数组对象为Array like(类数组)一样,thenable指的是一个具有 `.then` 方法的对象。将thenable对象转换promise对象复制代码
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象promise.then(function(value){ console.log(value);});复制代码
--------------------------------------------------
2、Promise.reject
new Promise((resolve,reject) => { reject(new Error("出错了"));});// 等价于 Promise.reject(new Error("出错了")); // Promise.reject(new Error("出错了")) 就是上述代码的语法糖。// 使用方法Promise.reject(new Error("BOOM!")).catch(error => { console.error(error);});复制代码
Promise.all()
接收一个 promise对象的
数组作为参数
,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用.then
方法。也就是说:Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。
// `delay`毫秒后执行resolvefunction timerPromisefy(delay) { return new Promise(resolve => { setTimeout(() => { resolve(delay); }, delay); });}var startDate = Date.now();// 所有promise变为resolve后程序退出Promise.all([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128)]).then(values => { console.log(Date.now() - startDate + 'ms'); // 约128ms console.log(values); // [1,32,64,128]});复制代码
这说明timerPromisefy
会每隔1, 32, 64, 128 ms都会有一个promise发生 resolve
行为,返回一个promise对象,状态为FulFilled,其状态值为传给 timerPromisefy
的参数,并且all会把所有异步操作的结果放进一个数组中传给then。
从上述结果可以看出,传递给 的promise并不是一个个的顺序执行的,而是同时开始、并行执行
的。
Promise.race()
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,接收一个promise对象数组为参数。
Promise.all
在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理, 与之相对的是Promise.race
只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。
// `delay`毫秒后执行resolvefunction timerPromisefy(delay) { return new Promise(resolve => { setTimeout(() => { resolve(delay); }, delay); });}// 任何一个promise变为resolve或reject 的话程序就停止运行Promise.race([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128)]).then(function (value) { console.log(value); // => 1});复制代码
上面的代码创建了4个promise对象,这些promise对象会分别在1ms,32ms,64ms和128ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后, .then
注册的回调函数就会被调用,这时候确定状态的promise对象会调用 resolve(1)
因此传递给 value
的值也是1,控制台上会打印出1
来。
小练习
下面内容的输出结果应该是啥?
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)
参考
如若发现文中纰漏请留言,欢迎大家纠错,我们一起成长。