每篇一句,送给爱学习的你
学了东西总归要记下来的,等老了以后再回过头来看很有意思,就像你现在打开朋友圈,去看三年前自己发的朋友圈,总会觉得自己当时好幼稚啊,哈哈哈,怎么会发这么傻的朋友圈,三年后再看自己写的文章,哈哈哈,自己当时好菜啊,这么多理解错误的地方。
今天的介绍对象Promise
Promise(期约),一个表示将来的动作,因为js是单线程,但是Promise表示将来才发生的事情,js不会等着它有结果了,再去执行后面的代码,所以呢,这里就会产生异步执行这样一种操作。等Promise有结果了,我们再来执行它里面定义的回调。而我们的Promise呢,也是ES6里面提出的一种,异步编程新的解决方案。
优点
- 可以链式调用,解决回调地狱问题
- 支持定义多个回调
- 支持异常穿透,只需要定义一个catch就可以捕获链式调用中任何阶段抛出的错误
- 支持回调函数的后定义。(先执行Promise,再给Promise的返回值定义回调函数,不像传统的异步回调,必须在写异步方法的时候,就要定义好回调)
- 想不到拉,剩下的优点谷歌一下吧。 缺点
- Promise执行进度无法追踪,处于pending状态不知道是程序刚开始,还是快结束了
- 一旦开始无法取消(后面会介绍一种方法,可以中断Promise的链式调用)
- Promise里面的错误如果不定义回调,无法反映到外部
今天要学习的重点有以下这些
- promise的状态:promiseState
- promise的结果:promiseResult
- promise原型上的方法:then,catch
- Promise私有方法:resolve,reject,all,race
- Promise的参数是个立即执行函数,也就是同步方法,但是then方法是异步的,属于微任务(宏任务,微任务也算是js事件循环中的重点吧,这块我有空的时候,也写篇文章记录一下,最近真的太忙拉,每天看react源码到12点,眼睛通红,为了生活还得继续啊)
备注:
- 小写的promise是Promise类构造出来的实例,而大写的Promise呢是构造函数,大家要分清哦。
- 立即执行函数,就是new Promise时,传给Promise的参数
promiseState
介绍
promiseState就是用来表示用Promise构造出来的实例的状态,当这个状态发生变化的时候,就会调用相应的回调函数(成功的回调或者失败的回调),这俩个回调函数是通过then方法去绑定的。
状态的取值有三种:
- pending:表示的是初始状态
- fulfilled/resolved:表示已成功(这俩个状态都表示一个意思已成功,后面我就用fulfilled来举例)
- rejected: 表示已失败(内部抛出错误也是这个状态)
状态的变化有俩种:
- 从pending状态变成fulfilled
- 从pending状态变成rejected
注意:状态的变化是不可逆的,一旦状态发生变化,该实例对象的状态将不再改变。也就是不会发生从fulfilled变成rejected,也不会从rejected变成fulfilled。在代码实现上就如下代码片段
if(promiseState !== 'pending) return;
promiseResult
介绍
promiseResult是用来表示promise实例的结果,这个结果是由Promise构造函数内部的resolve方法或者reject方法进行赋值的。promiseResult的值会被作为实参传递给回调函数中,当调用resolve方法,那么promiseResult就会被当做成功的回调参数,让抛出错误或者调用reject方法,那么promiseResult就会被当做失败的回调参数。
promiseResult取值有四种方式
- 当为初始状态的时候,promiseResult的值为undefined
- 当调用resolve方法的时候,resolve方法的参数即为promiseResult的值
- 当调用reject方法的时候,reject方法的参数即为promiseResult的值
- 当立即执行函数抛出错误的时候,抛出的错误对象,即为promiseResult的值
then,catch
介绍
then方法是给promise实例注册回调函数用的,then方法传入1或者2个函数作为参数(当然不传也是可以的,但是不传没有任何意义,这里就不做过多解释),第一个参数作为成功的回调函数,第二个参数作为失败的回调,并且then方法的返回值也是一个promise对象,这也是Promise支持链式调用的核心,因为返回的是promise对象,所以它就有then方法,所以它就可以继续调用then方法。(哈哈哈,一想到这里就想到了俄罗斯套娃,虽然这更像是接火车)。当为同一个promise实例注册多个then方法,这些then方法都会按顺序依次执行,不会是后面的then方法覆盖前面的then方法。
当然then方法最重要是还是里面的回调函数,因为回调函数的返回值可能是一个js的正常数据类型,但是它也可能是一个Promise对象。
- 当执行的是成功的回调函数时
- 返回值为正常js数据类型,那么它就直接作为then方法返回的promise实例的promiseResult的值(哎,语文没学好,这句话怎么写都感觉不得劲。。。),并且状态为成功
- 如果回调函数的返回值是一个promise对象,那么该promise对象返回的状态和结果即作为then函数返回的状态和结果 这里描述的有点复杂,但是代码其实并不复杂
- 当执行的是失败的回调函数时
- 返回值是正常js数据类型,那么它状态为失败,结果为返回值
- 返回值是promise对象,那么它的状态还是失败,值为promise对象
// onResolved方法就是传入then方法的成功的回调
// 这里的resolve和reject方法是Promise立即执行函数里面的参数
result = onResolved(this.promiseResult)
if(result instanceof Promise){
// 回调函数返回值是promise对象,则根据promise对象的then方法来确定then方法的返回值类型
result.then((v) => {
resolve(v);
}, (r) => {
reject(r);
})
} else {
// 回调函数不是promise对象,则直接返回成功状态的promise对象
resolve(result);
}
catch方法是给promise实例对象注册失败回调函数的地方,它只接收一个函数作为参数,这个函数就是失败的回调函数,失败有俩种情况。
- 调用了reject方法
- 是执行过程中抛出了错误
这两种情况都会在catch方法中的回调函数中进行处理,上面说的异常穿透,指的就是在链式调用过程中,只需要在链的最后边,加上一个catch方法,它就能捕获链中任意位置传报错,从而执行错误回调。catch的方法参数也比较简单,它和then方法的第二个回调函数基本上是一样的。
私有方法resolve,reject
介绍
这里的resolve和reject方法是Promise构造函数的私有方法,区别于promise实例对象原型上的resolve和reject方法,这是的resolve和reject方法是用来快速生成一个成功或失败的promise对象的。而原型上的方法是用来改变promise实例的状态和结果的。
resolve方法生成一个成功状态的promise对象,状态为成功。也就是promiseState='fulfilled',promiseResult='成功',reject方法也是类似的,生成一个状态为失败的promise对象。
const promise = Promise.resolve('成功');
私有方法all,race
介绍
all方法,接收一个由promise对象组成的数组,all方法的返回值也是一个promise对象。返回值有两种场景
- 当数组中的promise对象全都返回成功的promise对象时,all方法返回的promise对象,状态为成功,结果为所有promise对象返回结果组成的数组,并且这个结果的顺序要和传入的promise对象数组的顺序保持一致。
- 当数组中的promise对象,有一些返回的是失败的promise对象,则all方法返回的是第一个状态改变为失败的promise对象。
race方法,接收一个由promise对象组成的数组,race方法的返回值也是一个promise对象。race有赛跑的意思,所以race的返回值,就是数组中第一个改变状态的promise对象。
立即执行函数excutor
介绍
excutor为传入Promise的参数,它是个函数,有俩个方法resolve和reject,为什么叫立即执行函数呢?这是因为在js执行的时候,当执行到new Promise的时候,这个参数也就是这个函数会同步的被执行,所以叫它立即执行函数,这个在js的事件循环(eventLoop)中也是一个重点,只要考到事件循环,必有Promise的立即执行函数,then方法。所以这里也单独拎出来介绍一下这个立即执行函数
// 立即执行函数
let excutor = (resolve, reject) => {
resolve('OK');
}
// 创建promise实例对象p
let p = new Promise(excutor);
知识点讲完啦
哈哈哈,是不是还浑浑噩噩的没听懂?没关系,接下来我就来手写实现一下这个Promise,以上知识都是脑子里面记住的哦,我可没有去翻笔记,去粘贴复制过来的哈。如果你也能手写Promise的话,那么恭喜你这个知识点,你已经熟练掌握啦。手写实现上面所有的函数,我大概需要20-30分钟。不知道你们花多长时间。哈哈哈,评论区比比看谁效率高(咳咳,咱不说快,以后男孩子们要记住说效率高)
手写Promise涉及的知识点
- 函数的异步执行
- 原型原型链
- this的指向和箭头函数
知识点不是很多,所以手写实现的话也比较简单。这就看基本功扎不扎实了。
手写实现Promise和它的函数们
介绍
知道每个函数是干什么的,和它的特性,手写就比较简单拉,我这边就使用ES6+语法了哈,现在不会还有人会写var了吧,var估计也只能出现出现在面试题里面去为难为难别人了。下面是编码时间(show time)。
Promise函数
// 定义常量,要不然下面拼写出错了,很难找出原因
// 很多大牛往往也会因为拼写出错而调试很久。哈哈哈
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected'
// 创建Promise构造函数,当然使用类也是一样的哈,本质上没什么区别
function Promise(excutor) {
this.promiseState = PENDING;
this.promiseResult = undefined;
// 缓存then方法的回调
this.callbacks = [];
// 为resolve和reject函数传递外部的this进行使用
// 当然这里使用箭头函数也是可以的,就不用传递this拉
const self = this;
// 成功时改变状态和结果的方法
function resolve(data) {
if (self.promiseState !== PENDING) return;
self.promiseState = FULFILLED;
self.promiseResult = data;
// 下面的将缓存的回调,在状态改变之后进行调用
// 模仿一下微任务
setTimeout(() => {
self.callbacks.forEach(item => {
const {onResolved} = item;
onResolved(data);
});
});
}
// 失败时改变状态和结果的方法
function reject(data) {
if (self.promiseState !== PENDING) return;
self.promiseState = REJECTED;
self.promiseResult = data;
// 下面的将缓存的回调,在状态改变之后进行调用
// 模仿一下微任务
setTimeout(() => {
self.callbacks.forEach(item => {
const {onRejected} = item;
onRejected(data);
});
});
}
// 这里是处理立即执行函数,如果执行抛出错误,直接将promise对象的状态改成rejected
try {
excutor(resolve, reject)
} catch (error) {
reject(error);
}
}
// 定义原型上的then方法
Promise.prototype.then = function(onResolved, onRejected) {
// 这里处理一下异常穿透,和then方法只传一个参数或不传参数的情况
if(typeof onResolved !== 'function'){
// 这里是箭头函数的简写方式拉,意思是当onResolved不是一个函数的时候,直接把参数返回
onResolved = value => value;
}
if(typeof onRejected !== 'function'){
onRejected = reason => {
// 如果onRejected不是一个函数,我们直接将参数,通过throw抛出去就好了
// 因为我们在下面的公共方法里面已经做了处理,所以直接抛错就可以了
throw reason;
}
}
// then方法的返回值是一个promise对象,
// 所以这里要使用new去创建一个promise对象,这样才能支持链式调用
return new Promise((resolve, reject) => {
const self = this;
function commonFn(type){
try{
const res = type(self.promiseResult);
// 判断返回值是不是promise对象
if(res instanceof Promise){
// 如果是Promise对象的话,那么它就可以调用then方法,
// 这个then方法的参数,也就是promise对象的结果,用它来生成外部then方法的返回值
res.then(v => {
resolve(v);
}, r =>{
reject(r);
});
} else {
// 如果不是promise对象,直接返回成功的promise对象就好了
resolve(res);
}
} catch(err){
reject(err);
}
}
// 当状态改变成成功的时候执行then方法成功的回调函数onResolved
if (this.promiseState === FULFILLED) {
// 这里模仿一下then方法是个微任务,我不会创建微任务,就创建一个宏任务替代一下吧。
setTimeout(() => {
// 这里函数更下面的逻辑差不多,就封装了公共方法,看不懂的,可以把这个公共方法注释掉,看下面注释掉的代码
commonFn(onResolved);
});
// // 参数是promise对象promiseResult的值
// const res = onResolved(this.promiseResult);
// // 判断返回值是不是promise对象
// if(res instanceof Promise){
// // 如果是Promise对象的话,那么它就可以调用then方法,
// // 这个then方法的参数,也就是promise对象的结果,用它来生成外部then方法的返回值
// res.then(v => {
// resolve(v);
// }, r =>{
// reject(r);
// });
// } else {
// // 如果不是promise对象,直接返回成功的promise对象就好了
// resolve(res);
// }
}
if(this.promiseState === REJECTED){
// 这里模仿一下then方法是个微任务,我不会创建微任务,就创建一个宏任务替代一下吧。
setTimeout(() => {
// 这里面的方法,和状态为fulfilled差不多,所以封装一个公共方法吧
commonFn(onRejected);
});
}
// 那如果执行到then方法的时候,promise状态还未改变,我们就需要对其进行缓存,
// 等promise状态改变的时候,我们再把缓存的函数拿出来执行,
// 所以缓存,就给promise对象创建一个callbacks的数组进行存储吧
// 这也是promise可以后指定回调的关键地方
if(this.promiseState === PENDING){
// 当状态为pending的时候,缓存回调函数。
this.callbacks.push({
onResolved: () => {commonFn(onResolved)},
onRejected: () => {commonFn(onRejected)}
})
}
});
}
// 写catch方法,上面已经完整的写好了then方法,这里我们就可以直接调用then方法的错误回调就好了
Promise.prototype.catch = function(onRejected){
// 这里直接将then方法错误回调的返回值返回出去就好了。
// 因为已经做了错误穿透,所以then方法第一个参数直接传个undefined就行了
return this.then(undefined, onRejected);
}
// 写私有方法resolve,这里区别原型上的resolve方法哦
Promise.resolve = function(param){
// 这里应该不用多说了吧,返回的是promise对象,Promise的所有方法都返回的是promise对象
return new Promise((resolve, reject) => {
if(param instanceof Promise){
param.then(v=> {
resolve(v);
}, r => {
reject(r);
})
} else {
resolve(param);
}
})
}
// 私有方法reject,返回失败的promise对象
Promise.reject = function(param){
return new Promise((_, reject) => {
// 这里就不用判断它是不是promise对象了,直接返回错误的对象就好了
// 元素的Promise也是这么干的
reject(param);
})
}
// 私有方法all
Promise.all = function(promises){
return new Promise((resolve, reject) => {
// 定义一个数组缓存成功的值
let arr = [];
let count = 0;
promises.forEach((item, index) => {
item.then(value => {
arr[count] = value;
count++;
// 当全部成功的时候,给promise对象改变状态和赋值。
if(count === promises.length){
resolve(arr);
}
}, reason => {
// 当有一个失败的时候,直接返回错误对象
reject(reason);
})
})
})
}
// 私有方法race
Promise.race = function(promises){
return new Promise((resolve, reject) => {
promises.forEach((item) => {
// 谁先改变状态,返回值就是谁
item.then(value => {
resolve(value);
}, reason => {
reject(reason);
})
})
})
}
测试用例
注意:测试用例,一定要在浏览器环境下测试,服务器环境(node)在异步状态下,打印出来的一直都是pending状态,在浏览器上也要等异步时间过了,在去点开查看,要不然,还是pending,目前没搞懂这块打印的机制,有哪位大佬知道的话,可以评论区告诉我一下,让我弥补一下空缺,感激不尽。
测试用例不能一起解开注释去测试,只能一个一个测哈,定义变量太麻烦了,我就没一起编写测试用例了
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./index.js"></script>
<title>Promise</title>
</head>
<body>
<script>
// 测试1: 同步异步的resolve,resolve方法
// let p = new Promise((resolve, reject) => {
// // resolve('OK');
// // reject('error');
// // throw 'err';
// // 异步
// // setTimeout(() => {
// // // resolve('OK');
// // // reject('error');
// // }, 200)
// });
// console.log(p);
// 测试2:传入的值为promise对象时,输出的值
// let p = Promise.resolve( new Promise((resolve, reject) => {
// resolve('333');
// }));
// let p = Promise.reject( new Promise((resolve, reject) => {
// reject('333');
// }));
// 测试3:测试then方法的返回值, 为正常js类型和为promise对象
// let p = Promise.resolve('123');
// let p = Promise.reject('123');
// const res = p.then((value) => {
// console.log(value);
// // return 'ok';
// // return new Promise((resolve, reject) => {
// // // resolve('成功啦');
// // // reject('失败啦');
// // })
// }, (reason) => {
// return new Promise((resolve, reject) => {
// // resolve('成功啦');
// // reject('失败啦');
// // throw 'oh on';
// });
// })
// console.log(res);
// 测试4:给promise指定多个回调,和异常穿透
// let p = Promise.resolve('123');
// let p = Promise.reject('123');
// p.then((value) => {
// console.log(111, value);
// return '成功啦'
// }, (reason) => {
// console.log(222, reason);
// return '失败啦'
// });
// p.then((value) => {
// console.log(222, value);
// });
// p.catch((reason) => {
// console.log("555", reason);
// });
// 测试5: 测试all方法和race方法
// const p1 = new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('111');
// })
// })
// const p2 = new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('222');
// // reject('222');
// })
// })
// const p3 = Promise.resolve('3333');
// // const res = Promise.race([p1, p2, p3]);
// const res = Promise.all([p1, p2, p3]);
// console.log(res);
</script>
</body>
</html>
写在最后
没想到这次失算了,代码加测试用例加注释,竟然画了2个多小时,哈哈哈,平常写Promise只要20-30分钟,没想到写注释这么画时间,哦,这后面还包括了测试的时间,测试出来了几个bug,已经修复了,哈哈哈。不会有人测试用例打印出的结果看不懂吧?如果看不懂说明知识点没掌握透哦。也可能是我写了bug,哈哈哈,有问题大家评论区说吧。
码了五千字,花了一天时间,希望能给不了解promise的小伙伴,带来一些理解吧。技术大佬就随便看看吧。嘿嘿。能给点指导就更好了。
分享一件高兴的事
找了之前带我的同事要到了react源码的教学视频,白嫖的,爽歪歪,这几个月每天晚上又有事干了。 希望我学完以后,能搞个react源码的专题,哈哈哈。