前言
了解前端开发的同学都知道,在浏览器中javascript主线程是单线程的。试想一下, 多个线程操作同一个dom,那岂不是很混乱?并且单线程可以节省资源和便于对上下文
context
的控制。也正是因为单线程的原因,js中处理异步调用的问题多数采用了回调函数
来作为解决方案。于是乎便产生js开发过程经常遇到的一个经典问题 回调地狱。
1.何为回调地狱
相信大家会经常遇到这样的场景:
- 表单的查询条件 需要ajax调用后台API来渲染
- 列表查询需要等待表单查询条件渲染完成后,根据其中的某一个值来发起ajax调用来获取后台数据
- ...
$.ajax({ //第一次调用
url: '/api_1',
type: 'post',
data: {...},
success(data){
$.ajax({ //第二次调用
url: '/api_2',
type: 'post',
data: {...}
success(data){
$.ajax({ //第三次调用
url: '/api_3',
type: 'post',
data: {...}
success(data){
}
})
}
})
}
})
复制代码
这样的编码方式就是所谓的回调地狱, 嵌套层次太多,不便于代码的阅读。而且对于日后代码的重构也是在挖坑。
Promise 正是为解决此问题而产生的,它已经成为ES6标准的一部分。
2. 神马是 Promise
面试的时候经常会被问到,promise是什么。可能你已经用过,但未必能真正说清楚。 简单来说:
1. Promise 是一种异步编程的解决方案。
2. Promise 本质类似数据库中的事物:有状态
3. Promise 的状态分为三种: pending(阻塞), fulfilled(成功或者完成), rejected(失败)
复制代码
到这里你可能又说了,这些概念到处都有,能不能说点人话
, 用自然语言来描述。想必面试的时候能够用自己的话,讲复杂的概念讲清楚的,也算是高人一个了。
2.1 Promise 来源于生活
2.1.1 初识Promise
在一个浪漫的夜晚, 你对暗恋了多年的女生表白。说我要给你一个承诺 ( Promise ), 这个时候你在等待你女神的回复 ( pending状态 ) , 你的女神也正好喜欢你,给你一个肯定的回答, 说我们在一起吧 ( fulfilled 状态 ) , 亦或者女神已经有喜欢的人,那只能拒绝你了 ( rejected 状态 )。而且大家都是好面子的人,只会表白一次。女神再给你回复后,不会再说第二次。也就是说 。只要这两种情况中的一种发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。 等到女神的回复后或兴奋亦或桑心。你会做出不同的反应 调用 then 方法。
演示过程:
- 代码片段1:
let p = new Promise((resolve, reject) => {
resolve('兴奋');
});
p.then((success) =>{
console.log(success);
},(err) => {
console.log( err );
});
复制代码
- 代码片段2
let p = new Promise((resolve, reject) => {
reject('表白失败了');
});
p.then((success) =>{
console.log(success);
},(err) => {
console.log( err );
});
复制代码
- 代码片段3
这里你的女神就不是女神了,而是女神经了吧。哈哈,你也不会任由她逗你玩。得到一个回复后,不再相信你女神了。
let p = new Promise((resolve, reject) => {
resolve('兴奋'); //这里只会处理一个反馈。
reject('表白失败了');
});
p.then((success) =>{
console.log(success);
},(err) => {
console.log( err );
});
复制代码
2.1.2 Promise中的异步调用
可故事总是峰回路转的,女神在给你答复前, 说你等等 ( 异步调用 )
let p = new Promise((resolve, reject) => {
setTimeout(()=>{ //同样可以用作ajax的异步回调中
resolve('兴奋');
}, 1000);
});
p.then((success) => {
console.log(success);
}, (err) => {
console.log(err);
});
复制代码
很多时候,女神都是比较害羞的。一般都会需要考虑考虑。说容我想几天再答复你吧。
2.1.3 链式调用(干掉回调地狱)
当获得女神的芳心后,经过一段时间的相处,女神会把你介绍她的父母。这个时候你就要好好捯饬捯饬了。一步一步(链式调用 p.then1.then2...)走向人生的巅峰。
- 代码演示
想让女神家长认识你,就先得认识女神,从女神那里获得认识女神家长的通关密码。知道女神家长都喜欢啥。哈哈哈~
假设: key1.txt文件中 存放着 key2.txt key2.txt文件中 存放着 小伙子,你很不错哦~
let fs = require('fs');
function getKey(){
fs.readFile('key1.txt', 'utf8', (err, data) => {
if(err){
console.log(err);
return;
}
console.log(data); //key2.txt
fs.readFile(data, 'utf8', (err, data) => {
if(err){
console.log(err);
return;
}
console.log(data); //小伙子你很不错哦~
})
})
}
复制代码
我们都知道readFile是一个异步调用的过程。如果想拿到上一步的结果,那就只能等待上一个执行结果完成后,再在回调函数了嵌套下一个执行函数。
改用promise 如何来实现呢?
function getKey(params){
return new Promise((resolve, reject) => {
fs.readFile(params, 'utf8', (err, data) => {
if(err){
reject(err);
}
resolve(data);
})
});
}
getKey('key1.txt').then((data) => {
return getKey(data);
}).then((data)=>{
console.log(data); //小伙子你很不错哦~
})
复制代码
当第一个then中返回一个promise,会将返回的promise的结果,传递到下一个then中。这就是比较著名的链式调用了。
从代码风格上来看,也显得更加直观了。
2.1.4 统一的错误处理函数 catch
在上面的代码中,then 函数中都只传了一个用来解析 resovle 状态的函数。那如果 readFile发生错误了。该如何处理呢?
getKey('key1.txt').then((data) => {
return getKey(data);
}).then((data)=>{
console.log(data); //小伙子你很不错哦~
}).
catch((err)=>{
console.log(err);
})
复制代码
调用resolve或reject并不会终结 Promise 的参数函数的执行。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
Promise的实现过程
很多时候,为了赶项目进度,知其然,然不知其所以然。相信上面的知识你已经早就了然于胸。阮一峰的ES6基础教程也有更为详细的API介绍。
下面我们就一层一层的剥开Promise的神秘面纱。看看女神到底长什么样子。
- 创建一个Promise类
class Promise{
}
复制代码
- Promise 中存在一个构造函数。通过前面的代码,我们可以看到:
Promise 新建后就会立即执行, 也就是 new Promise();
class Promise{
constructor(executor){
}
}
复制代码
- Promise 原型上还要有一个then函数
class Promise{
constructor(executor){ //示例:executor:(resovle, reject) => { resolve('兴奋') }, 初始化时,告诉promise, 什么状态调用什么函数
}
then(onFulFilled, onRejected){ //用来接收promise状态函数的返回结果
}
}
复制代码
- 有了基本的结构 Promise/A+ 规范中有这么一句话:
“value” is any legal JavaScript value (including undefined, a thenable, or a promise.
class Promise{
constructor(executor){
this.status = 'pending'; //默认是pending状态
this.value = undefined; //默认值
this.reason = undefined;
let resolve = (data) => {
if(this.status === 'pending'){ //还记得女神经那段吧。哈哈哈~
this.status = 'resolved';
this.value = data;
}
}
let reject = (err) => {
if(this.status === 'pending'){
this.status = 'rejected';
this.reason = err;
}
}
try{ //上述是例中 readFile这样的函数执行过程中就有可能遇到错误
executor(resolve, reject);
}catch(e){
reject(e);
}
}
then(onFulFilled, onRejected){
if(this.status === 'resolved'){
onFulFilled(this.value);
}
if(this.status === 'rejected'){
onRejected(this.reason);
}
}
}
复制代码
3. 小结
上面这段代码初探了Promise 内部实现。对于Promise 内部异步函数的调用,链式调用的实现 待续...., 欢迎批评指正。继续关注。