事件和回调函数的缺陷
我们习惯于使用传统的回调或者事件处理来解决异步问题
事件: 某个对象的属性是一个函数, 当发生一件事时, 运行该函数
dom.onclick = function() {
// ...执行的代码
}
回调:运行某个函数以实现某个功能的时候, 传入一个函数作为参数, 当某个特定的条件下的时候, 就会触发该函数
dom.addEventListner('click', callback);
本质上,事件和回调没有本质上的区别, 只是把函数放置的位置不同而已, 一直以来这个模式都运作良好,直到前端工作越来越复杂,目前该模式主要面临以下两个问题
- 回调地狱: 某个异步操作需要等待之前的异步操作完成, 无论是回调还是事件都会陷入不断的嵌套,
ajax({
url, method, data,
success: res => { // 当某个请求数据的接口需要用到上一次请求回来的数据
if(res.data) { // 的时候, 我们就会生成回调地狱, 代码非常的难看
ajax({
url, method, res.data.data,
success: res => {
// ajax....
}
})
}
}
})
- 异步之间的联系: 某个异步操作需要等待多个异步操作的结果, 对这种联系的处理, 会让代码复杂度增加
// 小明像10个女神表白, 每个女神会思考部分时间以后再进行回复, 当所有女神回复完毕以后, 小明统计日志
for(let i = 0, len = girArr.length; i < len; i++) {
getResponse() // ..表白函数执行, 每个表白函数中有一个setTimeout的异步来决定女神的思考时间,思考完成以后会推入result数组
}
if(reslut.length === 10) {
// ...开始记录日志 这样会显得相当麻烦, 且不符合设计模式的单一原则
}
Promise
为了解决上述的问题, ES6引入了Promise, 这一块概念文字较多(建议多读几遍至少对es6处理异步的通用模型有一定的基础认识,如果时间紧迫不想阅读可以直接划到下面看promise的实例讲解)
我们先来看看ES6对异步处理的通用模型
ES6官方参考了大量的异步场景, 总结出了一套异步的通用模型, 该模型几乎可以覆盖所有的异步场景, 甚至是某些同步场景
值得注意的是, 为了兼容旧系统, ES6并不打算抛掉过去的做法, 只是基于该模型推出了一个全新的api,使用该api会让异步处理更加的简洁和优雅
理解该API, 最重要的是理解他的异步模型,
- ES6将某一件可能发生异步操作的事情, 分成两个阶段: unsettled和settled
- unsettled: 未决阶段, 表示事情还在进行前期的处理, 并没有发生通向结果的那件事(比如监听一个dom的点击事件, 这时候用户还未点击)
- settled: 已决阶段, 事情已经有了一个结果, 不管这个结果是好是坏, 整件事情无法逆转(比如用户触发点击事件已经产生了结果)
事件总是从未决阶段逐步发展到已决阶段的, 未决阶段拥有控制何时通向已决阶段的能力
- 同时ES6将事情划分为三种状态: padding, resolved, rejected
- padding: 挂起, 处于未决阶段, 最终结果还没有出来
- resolved: 已处理, 已决阶段的一种状态, 表示整件事情已经产生结果, 并且是一个可以按照正常逻辑进行下去的结果。
- rejected: 已拒绝, 已决阶段的一种状态, 表示整件事情已经产生结果, 并且是一个无法按照正常逻辑走下去的结果, 通常用于表示有错误
因为未决阶段有能力决定事情的走向, 所以未决阶段可以决定事情的最终走向
- 我们将把事情变为resolved状态的过程叫做resolve, 推向该状态时, 可以传递一些数据
- 我们把事情变为rejected状态的过程叫做reject, 推向该状态时, 通常传递一些错误
始终记住, 无论是阶段还是状态都是不可逆转的
- 当事情发展到已决阶段以后, 通常需要进行后续处理, 不同的已决状态, 决定了不同的后续处理
- resolved: 后续处理为thenable
- rejected: 后续处理为catchable
后续处理可能会有很多个, 因此会形成作业队列, 这些后续处理会按照顺序, 当状态到达后依次执行
ES6把上述一系列的概念和操作, 取了一个很优雅的名字叫做Promise(承诺)
Pormise的基本使用
Pormise是一个构造类, 他接受一个函数作为参数, 如下
const promise = new Promise((resolve, rejected) => {
/*
未决处理的阶段
ajax请求等异步操作可以放在这儿
通过调用resolve函数将Promise推向已决的resolved状态
通过调用rejected将Promise推向已决的rejected状态
resolve和reject都可以传递最多一个参数, 表示推向状态的数据
*/
})
promise.then(result => {
/*
这是thenable函数, 如果当前的Promise已经是resolved状态, 该函数会立即执行,如果当前是未决状态, 则会加入作业队列, 等待Promise状态变为resolved会立即执行
*/
}, err => {
/*
这是catchable函数, 如果当前的Promise已经是rejected状态, 该函数会立即执行, 如果当前是未决状态, 则会加入作业队列, 等待Promise状态变为rejected会立即执行
*/
})
同时Promise可以有多个then和catch并列进行,适用于对同一个Promise的多种处理
promise.then(res => {
console.log('这个res我要存入缓存')
})
promise.then(res => {
console.log('这个res我要用来获取新一轮的数据');
})
Promise的细节
- 未决阶段的处理函数是同步的, 会立即执行
const promise = new Promise((resolve, reject) => {
console.log('helloworld, 我是未决阶段的处理函数');
/* 这个函数是同步的会立即执行 */
})
- thenable和catchable函数是异步的, 就算是立即执行, 也会放到event queue中等待执行, 并且加入的是微任务
const promise = new Promise((resolve, reject) => {
console.log('padding状态, 但是我要立马将他推入resolve');
resolve('hello');
})
setTimeout(() => {
console.log('我是宏任务的计时器')
},0)
promise.then(res => {
console.log(res);
})
console.log('我是callstack的执行栈任务');
/*
执行结果肯定是
padding状态, 但是我要立马将他推入resolve
我是callstack的执行栈任务
hello
我是宏任务的计时器
*/
如果对js执行机制还不是很清楚的话, 可以查看我写的js执行机制和ui多线程的博客
- promise.then可以只添加thenable函数, promise.catch也可以只添加catchable函数
promise.then(resulte => {
/* thenable */
}).catch(error => {
/* catchable */
})
- 在未决阶段的处理函数中, 如果发生未捕获的错误, 会将状态推向rejected, 并会被catchable捕获,(比如请求数据的时候网断了)
const promise = new Promise((resolve, reject) => {
throw new Error('xxx'); // 会导致promise 直接触发reject状态
})
- 一旦状态推向了已决阶段, 无法再对状态做任何更改
const promise = new Promise((resolve, reject) => {
resolve();
resolve(); //这个是无效的, 因为上面的结果已经确定了,结果是不可逆转的
})
Promise并没有消除回调, 只是让回调变得可控
下面我们来看几个Promise的实例:
- 小明表白女神事件, 小明发出表白申请,因为要看看小明是不是自己心仪的男生, 所以女神会思考三秒钟以后才会告诉小明答应还是不答应
/* 小明向女神表白, 如果随机数大于0.1 则表白失败, 反之表白成功 */
const promise = new Promise((resolve, reject) => {
console.log('此时为padding状态,该函数是同步会立即执行');
setTimeout(() => {
if(Math.random() < 0.1) {
/* 表白成功啦, 同时代表事件已经处理, 从未决阶段变成已决阶段, 整件事情已经产生结果(小明获得女神芳心), 并且这个结果是按照正常逻辑走下去的结果, 所以我们需要用到resolve将Promise状态推向resolve状态*/
resolve(true);
}else {
/* 表白失败, 同样代表事件已经处理, 从未决阶段变成已决阶段, 整件事情已经产生结果(小明心碎了), 这个结果也是按照正常逻辑走下去的结果(女神本来就可能同意也可能拒绝), 所以我们同样需要用到resolve将Promise状态推向resolve状态, 但是根据需求我们可能传递的参数不一样*/
resolve(false);
}
}, 3000)
})
promise.then(result => {
/*当事件处于resolved状态就会进入thenable, 我们看到无论女神答应与否其实这个都是按照正常逻辑走下去的状态, 我们通过then接受到这个状态, 同时根据不同的参数处理不同的结果*/
console.log(result);
if(result) {
console.log('小明表白成功, 恭喜恭喜')
}else {
console.log('今天的我你爱理不理, 明天的我你高攀不起')
}
}).catch(err => {
/*当事情进入了rejected状态的话会触发catchable, 代表发生了一些错误, 比如网络请求失败*/
console.log(err);
})
- 在网络请求中使用Promise来请求数据
/* 在网络请求中使用Promise */
const ajaxPromise = new Promise((resolve, reject) => {
/* 开启ajax请求 */
ajax({
url: 'www.xxx.com',
method: 'POST',
data: {
name: 'tommy'
},
/* 在成功的回调函数中, 不管返回的结果是什么我们都会用resolve将Promise的状态推入resolved状态 */
success: function(data) {
resolve(data);
},
fail: function(err) {
/* 而如果触发了失败的回调函数, 那么可能是网络或者服务器或者自身出问题了,我们应该将状态推入rejected状态 */
rejected(err);
}
})
})
/* 只要Promise状态变为resolved状态就会立马触发thenabale的函数, 只要变为rejected状态马上就会触发catchable函数 */
ajaxPromise.then(data => {
console.log(data);
/* 拿到data做一系列处理 */
}).catch(err => {
console.log(err);
/* 查看捕获的错误 */
})
Promise的串联
当后续的Promise需要用到之前的promise的处理结果时, 就需要用到promise的串联
在promise对象中, 无论是then方法还是catch方法, 都是有返回值的,返回的是一个全新的promise对象, 他的状态满足下面的规则
- 如果当前的Promise是未决(padding)状态, 那么得到的新的Promise也是未决状态
- 如果当前的Promise是已决状态, 那么会运行相应的后续处理函数, 并将后续处理函数的结果(返回值)作为resolved的状态数据, 应用到新的Promise中, 如果后续处理函数发生错误, 则把返回值作为rejected的状态数据, 应用到新的Promise中.
后续的Promise一定会等到前面的Promise有了后续处理结果后才会变成已决状态
const promise = new Promise((resolve, reject) => {
resolve(1);
})
const promise2 = promise.then(res => res * 2);
promise2.then(res => {
console.log(res);
})
/* 直接输出promise2拿到的是padding状态的promise对象, 因为then和catch都是异步的, 同时promise被resolve推向了resolved状态, 那么势必会执行then后面的函数, promise2也会变为resolved状态, 同时promise的then的返回结果会作为promise2的resolved处理函数的参数被放进then中, 也就是上面的res */
如果返回的是一个Promise对象, 那么会把这个Promise对象拆解开并且把状态数据传递给新的Promise对象
const promise = new Promise((resolve, reject) => {
resolve(1);
})
const promise2 = promise.then(res => {
return new Promise((resolve, reject) => {
resolve(2);
})
})
promise2.then(res => {
console.log(res); // 输出2
})
Promise的其他api
原型成员(实例成员
- then: 注册一个后续处理函数, 当Promise为resolved状态时运行该函数,
- catch: 注册一个后续处理函数, 当Promise为rejected状态时运行该函数
- finally: [ES2018]注册一个后续处理参数(无参), 当Promise为已决时运行该函数
const promise = Promise((resolve, reject) => {
resolve();
})
promise.finally(() => {
console.log('helloworld');
})
构造函数成员(静态成员)
- resolve(数据): 该方法返回一个resolved状态的Promise, 传递的数据作为状态数据。
特殊情况: 如果传递的数据是Promise, 则直接返回传递的Promise对象
Promise.resolve(1); //
- reject(数据): 该方法返回一个rejected状态的Promise, 传递的数据为状态数据
Promise.reject(2); //
- all(iterable): 这个方法返回一个新的Promise对象, 该promise对象在iterable参数对象里所有的promise对象都成功的时候才会被触发成功, 一旦有任何一个在iterable里的promise对象失败则立马触发该promise对象的失败, 这个新的promise对象在触发成功状态以后, 会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值, 顺序跟iterable保持一致,如果这个新的promise对象触发了失败状态, 那么他会把iterable里第一个触发失败的promise对象的失败信息作为他的失败错误信息, Promise.all方法常被用于处理多个promise对象的状态集合。
let proms = [];
for(let i = 0; i < 10; i ++) {
proms.push(new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i);
}, 3000)
}))
}
console.log(proms);
Promise.all(proms).then(res => {
console.log('全部请求完成',res);
}).catch(err => {
console.log(err);
})
- race(iterable): 当iterable参数里的任意一个子promise被成功或者失败以后, 父类promise马上也会用子promise的成功返回值或失败详情作为参数调用父类promise绑定的相对应的句柄, 并返回该promise对象
let proms = [];
for(let i = 0; i < 10; i++) {
proms.push(new Promise((resolve, reject) => {
reject('我是第一个error' + i);
}))
}
Promise.race(proms).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})