es6新特性 - 从Promise入门到深入原理
目录:
- 过去操作异步任务的缺陷(也是Promise出现的原因)
- Promise的基本认识和es6异步模型的了解
- Promise的全面使用和api详解(then, catch及Promise的静态方法all, race等)
- 探究Promise原理之手写部分Promise功能
1. 过去操作异步任务的缺陷
在es6的Promise出来之前, 我们大多数时候是在使用回调函数(callback)和事件来处理一些异步问题
1. 事件: 某个对象的属性是一个函数, 当发生一件事时, 运行该函数
2. callback回调函数: 运行某个函数以实现某个功能的时候, 传入一个函数作为参数, 当某个特定的条件下的时候, 就会触发该函数
从本质上来说, 事件和回调函数也没有什么太大的区别, 仅仅就是函数放置的位置不太一样而已
setTimeout(function() {
// 这个函数就是callback回调函数
}, 10)
$ajax({
url: '/api/getUserInfo',
success: function() {
// 成功的回调函数
},
error: function() {
// 失败的回调函数
}
})
var div = document.querySelector('#app');
div.addEventListener('click', function() {
// 点击div以后执行该函数
}, false)
我们也不能说回调函数不好, 但是回调函数确实是一把双刃剑, 随着业务的越来越复杂, 对大多数初中级工程师来说, 对回调函数的控制不够娴熟会造成一些致命打击, 我们来看看下面的一些问题
使用回调函数不当造成的问题(回调函数的缺陷)
问题1: 回调地狱
回调地狱: 某个异步操作需要等到之前的操作完成, 当这样的需求多了以后, 使的代码进入无尽的嵌套
// 来看下面这个需求:
// 1.我需要通过小明的学号请求到小明所在班级的信息
// 2. 然后通过小明所在班级信息中的班主任的工号请求到关于这个班主任的一些资料
// 3. 通过班主任的一些资料我希望拿到班主任毕业院校的id, 然后通过id去找到该院校的资料
// 利用回调函数大多数人势必会这样写
$ajax({
url: '/api/getClassInfo',
success: function(result) {
$ajax({
url: '/api/getTeacherInfo',
data: {
id: result.teacher.id
},
success: function(result) {
$ajax({
url: '/api/getSchoolInfo',
data: {
id: result.school.id
},
success: function(result) {
console.log(result.info);
}
})
}
})
}
})
这种情况是比较常见的: 每一次的网络请求都是根据之前的网络请求的结果, 我只写了三层, 其实如果你阅读的话已经会发现有一些不太容易读的地方了, 那就更不用说嵌套七八层了
回调地狱使得我们的代码非常的难以阅读, 同时导致代码可维护性大大降低
问题2:异步和同步之间的联系问题
当一个同步操作需要等待多个异步操作的结果, 这样会使得代码的逻辑变得相对来说比较复杂, 让人抓耳挠腮
// 来看一个问题, 小明同时像两个女神表白, 然后等待两个女神回应他,
// 每个女神都会思考一下才回应她, 而小明希望自己能够得到两个女神的回应以后再做打算
// 所以这个任务我们要怎么写呢
var response = [];
$ajax({
url: '/getResponse',
data: {
target: 'girl-1'
},
success: function(resp) {
response[1] =resp;
}
})
$ajax({
url: '/getResponse',
data: {
target: 'girl-2'
},
success: function(resp) {
response[2] =resp;
}
})
// 我们怎么能知道两个女神的回应什么时候来呢?
if(response.length == 2) {
// 这样肯定是不能行的, 由于之前是异步任务这里是同步任务, 所以这里会很快的就走掉
// 这里的代码不出意外的话无论怎样都会在数据到来之前执行
console.log(response); // 依旧是[]
}
// 所以我们必须监控response的变化, 可能就要使用到Object.defineProperty或者Proxy,
// 或者我们继续写回调 0.0, oh my god
// 这样会让代码相对来说更加的复杂
还有一些其他乱七八糟的小问题就不做赘述了, 这两个问题确实困扰了开发者们许久, W3C组织也是体会民间疾苦, 给我们推出了Promise
2. Promise的基本认识和es6异步模型的了解
针对之前在异步操作中容易出现的问题, 在ES6的新特性中, Promise如约而至
Promise 实际上是ES6提供的一个新的构造函数, 跟Date, Regex等标准构造函数一样, 我们需要用new构造Promise实例, 同时在Promise上有一些原型方法和一些静态方法供我们更加精准的来控制和处理异步任务
而在正式进入Promise之前, 我想先来说一说关于ES6提出的异步模型这个概念, 这个概念可能会有点晦涩, 但是他确实是进入Promise的必经之路, 异步模型理解的越深, Promise理解的越好, 当然, 如果你赶时间或者对这个异步模型压根不感兴趣, 可以直接下去看Promise的使用
ES6提出的异步模型
ES6官方是参考了大量的异步场景, 和这些场景容易发生的问题, 总结出了一套在所有异步场景下都可以游刃有余的一套异步模型, 啥是模型: 模型粗浅的理解你可以认为是解决方案
-
es官方认为, 任何一件可能发生异步操作的事情, 都会分为两个阶段: Unsettled和Settled
-
Unsettled: 未决阶段, 表示事情还在进行前期的处理(比如网络请求还在请求的过程中, dom事件绑定但是用户还未进行点击), 在处理中也就意味着这个事情还没有发生通向结果的那件事,就是还没有结果呗
-
Settled: 已决阶段, 事情的结果已经有了(比如网络请求的数据已经来到, 用户已经点击按钮), 既然有了结果, 那么就是已决阶段, 同时无论这个结果是好是坏, 整件事情都无法逆转
举个栗子: 你去找你同学玩, 你从出门到你同学家这中间是要花时间的, 是要有个走路的过程的, 这个过程称之为未决阶段, 你到了你同学家, 你同学说出来玩或者要写作业不出来玩, 无论你有没有得到你想要的答案, 这个结果是不可逆转的, 不出来和出来都代表你做这件事已经得到了一个结果了,或者说你中途摔跤了去医院了也代表这件事的结果已经来了今天你们玩不成了, 你也无法改变, 称之为已决
事情总是要从未决阶段(Unsettled)走向已决(Settled)的, 时间不可能凝固, 而未决阶段一定拥有控制通向已决阶段的能力
什么意思呢, 你去找他的路上, 你突然不想玩了, 你是不是可以直接回家, 并且马上获得结果就是今天玩不成, 而你突然遇到你同学他刚好也在外面, 你们愉快的玩耍是不是也是直接获得了结果呢
-
-
同时在这套异步模型中, 官方提出将异步处理分为两个阶段以外, 还提供三个状态: pedding和resolved, rejected
-
pedding: 挂起状态(或者称之为等待状态), 未决阶段的状态, 代表事情还在未决, 结果还没出来
-
resolved: 已处理(或者称之为处理成功), 已决阶段的状态, 代表事情已经产生结果, 而且这个结果是一个可以按照预定逻辑走下去的结果
-
rejected: 已拒绝(或者称之为处理失败), 已决阶段的状态, 代表事情已经产生结果, 但是这个结果跟预想的不太一样, 通常为发生错误
拿一个网络请求来说, 请求的过程中为未决阶段: 状态为pedding, 而请求到了数据状态码为OK则是resolved状态, 而请求失败500服务器错误则是rejected状态
由于我们之前说过, 未决阶段一定拥有控制通向已决阶段的能力, 所以:
1. 我们把事情推向已决阶段的resolved状态的方法叫做resolve, 推向该状态时可以传递一些数据 2. 我们把事情推向已决阶段的rejected状态的方法叫做reject, 通常会附带一些错误信息
-
-
当事情发生到已决阶段的某种状态的时候, 我们是要做后续处理的, 比如你跟你同学约到了在一起了你们是要决定接下来去哪玩玩什么的, 或者你们没有按照预想的约到一起那么你也要决定你下一步干嘛的, 不可能干等着活受罪
-
当事情发生到已决阶段的resolved状态的时候, 我们做的后续处理叫做thenable, 后续处理就是函数, thenable就是给他的称呼
-
当事情发生到已决阶段的rejected状态的时候, 我们做的后续处理叫做catchable
后续处理可以有许多个, 会形成一个处理队列, 这个后说, 使用你就知道了
-
根据上方的一套模型(一套对异步的处理方案), ES6设计出了Promise
Promise的基本了解和使用
之前有说过, Promise就是一个构造函数, 这哥们接收一个函数作为参数, 这个函数就是未决阶段的实现, 如下:
// 之前说到在未决阶段拥有将事情推向已决阶段的能力
// 所以这个函数参数又接收系统丢过来的resolve和reject参数用来手动将状态推向已决
var myPromise = new Promise((resolve, reject) => {
/*
这个函数中的代码会立即执行, 所以ajax请求等异步操作可以放在这儿
我们也称这个函数为未决阶段
通过调用resolve函数将Promise推向已决的resolved状态
通过调用rejected将Promise推向已决的rejected状态
resolve和reject都可以传递最多一个参数, 表示推向状态的数据
*/
})
console.log(myPromise); // 看看Promise实例
myPromise.then(result => {
/*
这是thenable函数, 如果当前的Promise已经是resolved状态, 该函数会立即执行,
如果当前是未决状态, 则会加入作业队列, 等待Promise状态变为resolved会立即执行
*/
}, err => {
/*
这是catchable函数, 如果当前的Promise已经是rejected状态, 该函数会立即执行,
如果当前是未决状态, 则会加入作业队列, 等待Promise状态变为rejected会立即执行
*/
})
上方的Promis实例打印如下,实例上有两个属性[[PromiseStatus]]
和[[PromiseValue]]
, 前者表示当前Promise实例的状态, 而后者表示当前Promise实例的已决结果 我们会发现一开始Promise实例状态为pedding, 由于还未到已决阶段, 所以Promise结果为undefined
我们来看一个具体案例
// 用Promise请求数据
let myPromise = new Promise(