1. Promise简介
promise是异步编程的一种解决方案,它出现的初衷是为了解决回调地狱的问题。
打个比方,我需要:
--(延迟1s)--> 输出1 --(延迟2s)--> 输出2 --(延迟3s)--> 输出3
通常写法:
setTimeout(()=> {console.log('1');setTimeout(()=> {console.log('2');setTimeout(()=> {console.log('3'); }, 3000)}, 2000)
}, 1000)
这样的多重的嵌套的回调被称为回调地狱,这样的代码可读性很差,不利于理解。
如果用promise的话画风一转
function delay(time, num) {return new Promise((res, rej)=> {setTimeout(()=> {console.log(num);res();}, time*1000)});
}
delay(1, 1).then(()=> {return delay(2, 2);
}).then(()=> {delay(3, 3);
})
使用了promise的链式调用,代码结构更清晰。
是不是很棒?那还不赶快get起来~
2. Promise的使用
调用方式如下:
new Promise((resolve, reject)=> {if('some option') {resolve('some value');} else {reject('some error');}
}).then(val=> {// ...
},error=> {// ...
}
)
Promise构造函数接收一个函数型参数fn,fn有两个参数,分别是:resolve、reject,Promise还有一个Promise.prototype.then方法,该方法接收两个参数,分别是成功的回调函数succ和失败的回调函数error。
在fn中调用resolve会触发then中的succ回调,调用reject会触发error回调。
2.1 参数传递
- 在fn内部调用resolve/reject传入的参数会作为相应参数传入相应的回调函数
new Promise((res, rej)=> {res('happy') }).then(val=> {console.log(val); // happy });new Promise((res, rej)=> {rej('error!'); }).then(val=> {}, err=> {console.log(err); // error! });
- 链式调用时若上一级没有传递值则默认为undefined
new Promise((res, rej)=> {res('a'); }).then(val=> {return 'b' }).then(val=> {console.log(val); // 'b' }).then((val)=> {console.log(val); // 'undefined' });
- 若上一级的then中传递的并非函数,则忽略该级
new Promise((res, rej)=> {res('a'); }).then(val=> {return 'b'; }).then(val=> {console.log(val); // 'b'return 'c'; }).then({ // 并非函数name: 'lan' }).then((val)=> {console.log(val); // 'c' });
2.2 参数传递例题
let doSomething = function() {return new Promise((resolve, reject) => {resolve('返回值');});
};let doSomethingElse = function() {return '新的值';
}doSomething().then(function () {return doSomethingElse();
}).then(resp => {console.warn(resp);console.warn('1 =========<');
});doSomething().then(function () {doSomethingElse();
}).then(resp => {console.warn(resp);console.warn('2 =========<');
});doSomething().then(doSomethingElse()).then(resp => {console.warn(resp);console.warn('3 =========<');
});doSomething().then(doSomethingElse).then(resp => {console.warn(resp);console.warn('4 =========<');
});
结合上面的讲解想一想会输出什么?(答案及解析)
3. Promise.prototype.then
当Promise中的状态(pending ---> resolved or rejected)发生变化时才会执行then方法。
- 调用then返回的依旧是一个Promise实例 ( 所以才可以链式调用... )
new Promise((res, rej)=> {res('a');
}).then(val=> {return 'b';
});// 等同于
new Promise((res, rej)=> {res('a');
}).then(val=> {return new Promise((res, rej)=> {res('b');});
});
- then中的回调总会异步执行
new Promise((res, rej)=> {console.log('a');res('');
}).then(()=> {console.log('b');
});
console.log('c');
// a c b
- 如果你不在Promise的参数函数中调用resolve或者reject那么then方法永远不会被触发
new Promise((res, rej)=> {console.log('a');
}).then(()=> {console.log('b');
});
console.log('c');
// a c
4. Promise的静态方法
Promise还有四个静态方法,分别是resolve、reject、all、race,下面我们一一介绍一下。
4.1 Promise.resolve()
除了通过new Promise()的方式,我们还有两种创建Promise对象的方法,Promise.resolve()相当于创建了一个立即resolve的对象。如下两段代码作用相同:
Promise.resolve('a');new Promise((res, rej)=> {res('a');
});
当然根据传入的参数不同,Promise.resolve()也会做出不同的操作。
- 参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
- 参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。
let thenable = {then: function(resolve, reject) {resolve(42);}
};
Promise.resolve方法会将这个对象转为 Promise对象,然后就立即执行thenable对象的then方法。
- 参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
- 不带有任何参数
Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
值得注意的一点是该静态方法是在本次事件轮询结束前调用,而不是在下一次事件轮询开始时调用。关于事件轮询可以看这里——>JavaScript 运行机制详解:再谈Event Loop
4.2 Promise.reject()
和Promise.resolve()类似,只不过一个是触发成功的回调,一个是触发失败的回调
4.3 Promise.all()
Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。
function asyncFun1() {return new Promise((res, rej)=> {setTimeout(()=> { res('a');}, 1000);});
}
function asyncFun2() {return new Promise((res, rej)=> {setTimeout(()=> { res('b');}, 1000);});
}
function asyncFun3() {return new Promise((res, rej)=> {setTimeout(()=> { res('c');}, 1000);});
}
Promise.all([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {console.log(val);
});
Promise.all([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {console.log(val); // ['a', 'b', 'c']
});
用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。
适用场景:打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。
4.4 Promise.race()
race()和all相反,all()是数组中所有Promise都执行完毕就执行then,而race()是一旦有一个Promise执行完毕就会执行then(),用上面的三个Promise返回值函数举例
Promise.race([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {console.log(val); // a
});
5. 链式调用经典例题
看了这么多关于Promise的知识,我们来做一道题巩固一下。
写一个类Man实现以下链式调用调用方式:
new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
打印:
'hello, lan' -(等待3s)--> 'lan eat apple' -(等待5s)--> 'lan eat banana'
思路:
- 在原型方法中返回this达到链式调用的目的
- 等待3s执行的效果可以用Promise & then实现
具体实现如下:
class Man {constructor(name) {this.name = name;this.sayName();this.rope = Promise.resolve(); // 定义全局Promise作链式调用
}sayName() {console.log(`hello, ${this.name}`);}sleep(time) {this.rope = this.rope.then(()=> {return new Promise((res, rej)=> {setTimeout(()=> {res();}, time*1000);});});return this;}eat(food) {this.rope = this.rope.then(()=> {console.log(`${this.name} eat ${food}`); });return this;}
}new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
ok!不知道你有没有看懂呢?如果能完全理解代码那你的Promise可以通关了,顺便来个小思考,下面这种写法可以吗?和上面相比有什么区别?:
class Man1345 {constructor(name) {this.name = name;this.sayName(); }sayName() {console.log(`hello, ${this.name}`);}sleep(time) { this.rope = new Promise((res, rej)=> {setTimeout(()=> {res();}, time*1000);}); return this;}eat(food) {this.rope = this.rope.then(()=> { console.log(`${this.name} eat ${food}`); });return this;}
}new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
简单的说,第二段代码的执行结果是
'hello, lan' -(等待3s)--> 'lan eat apple' ---> 'lan eat banana'
为什么会出现这种差别? 因为第二段代码每一次调用sleep都会new一个新的Promise对象,调用了两次sleep就new了两个Promise对象。这两个对象是异步并行执行,会造成两句eat同时显示。
和以下情况类似
var time1 = setTimeout(()=> {console.log('a');
}, 1000)
var time2 = setTimeout(()=> {console.log('b');
}, 1000)
// 同时输出 a b
抽象一点的讲解是:
// 第一段正确的代码的执行为
var p1 = new Promise().then('停顿3s').then('打印食物').then('停顿5s').then('打印食物');// 第二段代码的执行行为,p1、p2异步并行执行
var p1 = new Promise().then('停顿3s').then('打印食物');
var p2 = new Promise().then('停顿5s').then('打印食物');
总结
Promise的经常用到的地方:
- 摆脱回调地狱
- 多个异步任务同步
Promise是我们的好帮手,不过还有另一种方法也可以做到,那就是async&await,可以多多了解一下。
参考资料
ECMAScript 6 入门
通俗浅显的理解Promise中的then
大白话讲解promise