虽然今年已经18年,可是今天仍是要继续聊聊ES6的东西,ES6已通过去几年,但是咱们对于ES6的语法到底是掌握了什么程度,是了解?会用?仍是精通?相信你们和我同样都对本身有着一个提高的心,对于新玩具可不能仅仅了解,对于其中的思想才是最吸引人的,因此接下来会经过一篇文章,来让你们对于Promise
这个玩具作到精通的程度!!!
npm
此处打开一瓶冰阔落~~~编程
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最先提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise
对象。promise
嗝~~~~~bash
首先,咱们经过字面能够看出来Pormise
是一种解决方案,并且还有两种传统的解决方案·回调函数
和事件
,ok,那么咱们就来先聊聊这两种方案。app
回调函数想必你们都不陌生,就是咱们常见的把一个函数当作参数传递给另一个函数,在知足了必定的条件以后再去执行回调,好比咱们想要实现一个在三秒后去计算1到5的和,那么:dom
// 求和函数
function sum () {
return eval([...arguments].join('+'))
}
// 三秒后执行函数
function asyncGetSum (callback) {
setTimeout(function(){
var result = callback(1,2,3,4,5);
console.log(result)
},3000)
}
asyncGetSum(sum);
复制代码
这样的实现就是回调函数,可是若是我要实如今一段动画,动画的执行过程是小球先向右移动100px,而后再向下移动100px,在向左移动100px,每段动画持续时间都是3s.异步
dom.animate({left:'100px'},3000,'linear',function(){
dom.animate({top:'100px'},3000,'linear',function(){
dom.animate({left:'0px'},3000,'linear',function(){
console.log('动画 done')
})
})
})
复制代码
这样就会看到造成了一个回调嵌套,也就是咱们常说的回调地狱
,致使代码可读性十分差。async
事件处理就是jQuery
中的on
绑定事件和trigger
触发事件,其实就是咱们常见的发布订阅模式,当我订阅了一个事件,那么我就是订阅者,若是发布者发布了数据以后,那么我就要收到相应的通知。异步编程
// 定义一个发布中心
let publishCenter = {
subscribeArrays:{}, // 定义一个订阅者回调函数callback
subscribe:function(key,callback){
// 增长订阅者
if(!this.subscribeArrays[key]){
this.subscribeArrays[key] = [];
}
this.subscribeArrays[key].push(callback)
},
publish:function(){
//发布 第一个参数是key
let params = [...arguments];
let key = params.shift();
let callbacks = this.subscribeArrays[key];
if(!callbacks || callbacks.length === 0){
// 若是没人订阅 那么就返回
return false
}
for( let i = 0 ; i < callbacks.length; i++ ){
callbacks[i].apply( this, params );
}
}
};
// 订阅 一个wantWatermelon事件
publishCenter.subscribe('wantWatermelon',function(){console.log('恰西瓜咯~~')})
//触发wantWatermelon事件 好咯 能够看到 恰西瓜咯
publishCenter.publish('wantWatermelon')
复制代码
恰西瓜中~~~函数
嗝~ok,吃完咱们进入正题,看到上面异步编程如此如此如此麻烦,对于我这种头大用户,固然是拒绝的啊,还好咱们有Pormise
(Pormise
大法好),下面咱们就来经过实现一个Promise
去更深的了解Promise
的原理,首先咱们了解一下PromiseA+
,它是一种规范,用来约束你们写的Promise
方法的,为了让你们写的Promise
杜绝一些错误,按照咱们所指望的流程来走,所以就出现了PromiseA+
规范。
咱们根据PromiseA+
文档来一步一步的看Promise
有什么特色。
首先咱们看文档的2.1节,题目是Promise states,也就是说讲的是Promise
的状态,那么都说了些什么呢,咱们来看一哈:
- 一个promise只有三种状态,pending态,fulfilled态(完成态),rejected(拒绝态)
- 当promise处于pending态时,可能转化成fulfilled或者rejected
- 一旦promise的状态改为了fulfilled后,状态就不能再改变了,而且须要提供一个不可变的value
- 一旦promise的状态改为了rejected后,状态就不能再改变了,而且须要提供一个不可变的reason
ok,那么咱们就开始写咱们本身的Promise
,咱们先看看一段正常Promise
的写法
// 成功或者失败是须要提供一个value或者reason
let promise1 = new Promise((resolve,rejected)=>{
// 能够发现 当咱们new Promise的时候这句话是同步执行的 也就是说当咱们初始化一个promise的时候 内部的回调函数(一般咱们叫作执行器executor)会当即执行
console.log('hahahha');
// promise内部支持异步
setTimeout(function(){
resolve(123);
},100)
// throw new Error('error') 咱们也能够在执行器内部直接抛出一个错误 这时promise会直接变成rejected态
})
复制代码
根据咱们上面的代码还有PromiseA+规范中的状态说明,咱们能够知道Promise
已经有了下面几个特色
promise
有三种状态 默认pending
态 pending
能够变成fulfilled
(成功态)或者rejected
(失败态),而一旦转变以后就不能在变成其余值了promise
内部有一个value
用来存储成功态的结果promise
内部有一个reason
用来存储失败态的缘由promise
接受一个executor
函数,这个函数有两个参数,一个是resolve
方法,一个是reject
方法,当执行resolve
时,promise
状态改变为fulfilled
,执行reject
时,promise
状态改变为rejected
new Promise
执行的时候内部的executor
函数执行promise
内部支持异步改变状态promise
内部支持抛出异常,那么该promise
的状态直接改为rejected
咱们接下来继续看PromiseA+文档:
promise
必需要有一个then
方法,用来访问它当前的value
或者是reason
- 该方法接受两个参数
onFulfilled
(成功回掉函数),onRejected
(失败回调函数)promise.then(onFulfilled, onRejected)
- 这两个参数都是可选参数,若是发现这两个参数不是函数类型的话,那么就忽略 好比
promise.then().then(data=>console.log(data),err=>console.log(err))
就能够造成一个值穿透onFulfilled
必须在promise
状态改为fulfilled
以后改为调用,而且呢promise
内部的value
值是这个函数的参数,并且这个函数不能重复调用onRejected
必须在promise
状态改为rejected
以后改为调用,而且呢promise
内部的reason
值是这个函数的参数,并且这个函数不能重复调用onFulfilled
和onRejected
这两个方法必需要在当前执行栈的上下文执行完毕后再调用,其实就是事件循环中的微任务(setTimeout
是宏任务,有必定的差别)onFulfilled
和onRejected
这两个方法必须经过函数调用,也就是说 他们俩不是经过this.onFulfilled()
或者this.onRejected()
调用,直接onFulfilled()
或者onRejected()
then
方法能够在一个promise
上屡次调用,也就是咱们常见的链式调用- 若是当前
promise
的状态改为了fulfilled
那么就要按照顺序依次执行then
方法中的onFulfilled
回调- 若是当前
promise
的状态改为了rejected
那么就要按照顺序依次执行then
方法中的onRejected
回调then
方法必须返回一个promise
(接下来咱们会把这个promise
称作promise2
),相似于promise2 = promise1.then(onFulfilled, onRejected);
- 若是呢
onFulfilled()
或者onRejected()
任一一个返回一个值x
,那么就要去执行resolvePromise
这个函数中去(这个函数是用来处理返回值x
遇到的各类值,而后根据这些值去决定咱们刚刚then
方法中onFulfilled()
或者onRejected()
这两个回调返回的promise2
的状态)- 若是咱们在
then
中执行onFulfilled()
或者onRejected()
方法时产生了异常,那么就将promise2
用异常的缘由e
去reject
- 若是
onFulfilled
或者onRejected
不是函数,而且promise
的状态已经改为了fulfilled
或者rejected
,那么就用一样的value
或者reason
去更新promise2
的状态(其实这一条和第三条一个道理,也就是值得穿透问题)
好吧,咱们总结了这么多规范特色,那么咱们就用这些先来练练手
/**
* 实现一个PromiseA+
* @description 实现一个简要的promise
* @param {Function} executor 执行器
* @author Leslie
*/
function Promise(executor){
let self = this;
self.status = 'pending'; // 存储promise状态 pending fulfilled rejected.
self.value = undefined; // 存储成功后的值
self.reason = undefined; // 记录失败的缘由
self.onfulfilledCallbacks = []; // 异步时候收集成功回调
self.onrejectedCallbacks = []; // 异步时候收集失败回调
function resolve(value){
if(self.status === 'pending'){
self.status = 'fulfilled';// resolve的时候改变promise的状态
self.value = value;//修改为功的值
// 异步执行后 调用resolve 再把存储的then中的成功回调函数执行一遍
self.onfulfilledCallbacks.forEach(element => {
element()
});
}
}
function reject(reason){
if(self.status === 'pending'){
self.status = 'rejected';// reject的时候改变promise的状态
self.reason = reason; // 修改失败的缘由
// 异步执行后 调用reject 再把存储的then中的失败回调函数执行一遍
self.onrejectedCallbacks.forEach(element => {
element()
});
}
}
// 若是执行器中抛出异常 那么就把promise的状态用这个异常reject掉
try {
//执行 执行器
executor(resolve,reject);
} catch (error) {
reject(error)
}
}
Promise.prototype.then = function(onfulfilled,onrejected){
// onfulfilled then方法中的成功回调
// onrejected then方法中的失败回调
let self = this;
// 若是onfulfilled不是函数 那么就用默认的函数替代 以便达到值穿透
onfulfilled = typeof onfulfilled === 'function'?onfulfilled:val=>val;
// 若是onrejected不是函数 那么就用默认的函数替代 以便达到值穿透
onrejected = typeof onrejected === 'function'?onrejected: err=>{throw err}
let promise2 = new Promise((resolve,reject)=>{
if(self.status === 'fulfilled'){
// 加入setTimeout 模拟异步
// 若是调用then的时候promise 的状态已经变成了fulfilled 那么就调用成功回调 而且传递参数为 成功的value
setTimeout(function(){
// 若是执行回调发生了异常 那么就用这个异常做为promise2的失败缘由
try {
// x 是执行成功回调的结果
let x = onfulfilled(self.value);
// 调用resolvePromise函数 根据x的值 来决定promise2的状态
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
}
if(self.status === 'rejected'){
// 加入setTimeout 模拟异步
// 若是调用then的时候promise 的状态已经变成了rejected 那么就调用失败回调 而且传递参数为 失败的reason
setTimeout(function(){
// 若是执行回调发生了异常 那么就用这个异常做为promise2的失败缘由
try {
// x 是执行失败回调的结果
let x = onrejected(self.reason);
// 调用resolvePromise函数 根据x的值 来决定promise2的状态
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
}
if(self.status === 'pending'){
//若是调用then的时候promise的状态仍是pending,说明promsie执行器内部的resolve或者reject是异步执行的,那么就须要先把then方法中的成功回调和失败回调存储袭来,等待promise的状态改为fulfilled或者rejected时候再按顺序执行相关回调
self.onfulfilledCallbacks.push(()=>{
//setTimeout模拟异步
setTimeout(function(){
// 若是执行回调发生了异常 那么就用这个异常做为promise2的失败缘由
try {
// x 是执行成功回调的结果
let x = onfulfilled(self.value)
// 调用resolvePromise函数 根据x的值 来决定promise2的状态
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
})
self.onrejectedCallbacks.push(()=>{
//setTimeout模拟异步
setTimeout(function(){
// 若是执行回调发生了异常 那么就用这个异常做为promise2的失败缘由
try {
// x 是执行失败回调的结果
let x = onrejected(self.reason)
// 调用resolvePromise函数 根据x的值 来决定promise2的状态
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
})
}
})
return promise2;
}
复制代码
一鼓作气,是否是以为以前总结出的特色十分有效,对着特色十分顺畅的就撸完了代码~
那么就让咱们接着来看看promiseA+文档里还有些什么内容吧
resolvePromise
这个函数呢会决定promise2
用什么样的状态,若是x
是一个普通值,那么就直接采用x
,若是x
是一个promise
那么就将这个promise
的状态当成是promise2
的状态- 判断若是
x
和promise2
是一个对象,即promise2 === x
,那么就陷入了循环调用,这时候promise2
就会以一个TypeError
为reason
转化为rejected
- 若是
x
是一个promise
,那么promise2
就采用x
的状态,用和x
相同的value
去resolve
,或者用和x
相同的reason
去reject
- 若是
x
是一个对象或者是函数 那么就先执行let then = x.then
- 若是
x
不是一个对象或者函数 那么就resolve
这个x
- 若是在执行上面的语句中报错了,那么就用这个错误缘由去
reject
promise2
- 若是
then
是一个函数,那么就执行then.call(x,resolveCallback,rejectCallback)
- 若是
then
不是一个函数,那么就resolve
这个x
- 若是
x
是fulfilled
态 那么就会走resolveCallback
这个函数,这时候就默认把成功的value
做为参数y
传递给resolveCallback
,即y=>resolvePromise(promise2,y)
,继续调用resolvePromise
这个函数 确保 返回值是一个普通值而不是promise
- 若是
x
是rejected
态 那么就把这个失败的缘由reason
做为promise2
的失败缘由reject
出去- 若是
resolveCallback
,rejectCallback
这两个函数已经被调用了,或者屡次被相同的参数调用,那么就确保只调第一次,剩下的都忽略掉- 若是调用
then
抛出异常了,而且若是resolveCallback
,rejectCallback
这两个函数已经被调用了,那么就忽略这个异常,不然就用这个异常做为promise2
的reject
缘由
咱们又又又又又又总结了这么多,好吧不说了总结多少就开撸吧。
/**
* 用来处理then方法返回结果包装成promise 方便链式调用
* @param {*} promise2 then方法执行产生的promise 方便链式调用
* @param {*} x then方法执行完成功回调或者失败回调后的result
* @param {*} resolve 返回的promise的resolve方法 用来更改promise最后的状态
* @param {*} reject 返回的promise的reject方法 用来更改promise最后的状态
*/
function resolvePromise(promise2,x,resolve,reject){
// 首先判断x和promise2是不是同一引用 若是是 那么就用一个类型错误做为Promise2的失败缘由reject
if( promise2 === x) return reject(new TypeError('typeError:大佬,你循环引用了!'));
// called 用来记录promise2的状态改变,一旦发生改变了 就不容许 再改为其余状态
let called;
if( x !== null && ( typeof x === 'object' || typeof x === 'function')){
// 若是x是一个对象或者函数 那么他就有多是promise 须要注意 null typeof也是 object 因此须要排除掉
//先得到x中的then 若是这一步发生异常了,那么就直接把异常缘由reject掉
try {
let then = x.then;//防止别人瞎写报错
if(typeof then === 'function'){
//若是then是个函数 那么就调用then 而且把成功回调和失败回调传进去,若是x是一个promise 而且最终状态时成功,那么就会执行成功的回调,若是失败就会执行失败的回调若是失败了,就把失败的缘由reject出去,作为promise2的失败缘由,若是成功了那么成功的value时y,这个y有可能仍然是promise,因此须要递归调用resolvePromise这个方法 直达返回值不是一个promise
then.call(x,y => {
if(called) return;
called = true;
resolvePromise(promise2,y,resolve,reject)
}, error=>{
if(called) return
called = true;
reject(error)
})
}else{
resolve(x)
}
} catch (error) {
if(called) return
called = true;
reject(error)
}
}else{
// 若是是一个普通值 那么就直接把x做为promise2的成功value resolve掉
resolve(x)
}
}
复制代码
finnnnnnnnnally,咱们终于经过咱们的不懈努力实现了一个基于PromiseA+规范的Promise
!
最后呢为了完美,咱们还要在这个promise
上实现Promise.resolve
,Promise.reject
,以及catch
,Promise.all
和Promise.race
这些方法。
Promise.resolve = function(value){
return new Promise((resolve,reject)=>{
resolve(value)
})
}
Promise.reject = function(reason){
return new Promise((resolve,reject)=>{
reject(reason)
})
}
Promise.prototype.catch = function(onRejected){
return this.then(null,onRejected)
}
Promise.all = function(promises){
return new Promise((resolve,reject)=>{
let arr = [];
let i = 0;
function getResult(index,value){
arr[index] = value;
if(++i == promises.length) {
resolve(arr)
}
}
for(let i = 0;i<promises.length;i++){
promises[i].then(data=>{
getResult(i,data)
},reject)
}
})
}
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i = 0 ; i < promises.length ; i++){
promises[i].then(resolve,reject)
}
})
}
复制代码
恰完西瓜来口糖,语法糖是为了让咱们书写promise的时候可以更加的快速,因此作了一层改变,咱们来看一个例子,好比当咱们封装一个异步读取图片的宽高函数
// 原来的方式
let getImgWidthHeight = function(imgUrl){
return new Promise((resolve,reject)=>{
let img = new Image();
img.onload = function(){
resolve(img.width+'-'+img.height)
}
img.onerror = function(e){
reject(e)
}
img.src = imgUrl;
})
}
复制代码
是否是以为怎么写起来有点舒服但又有点不舒服,好像我每次都要去写执行器啊!为何!好的,没有为何,既然不舒服 咱们就改!
// 实现一个promise的语法糖
Promise.defer = Promise.deferred = function (){
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd
}
复制代码
有了上面的语法糖咱们再看一下那个图片的函数怎么写
let newGetImgWidthHeight = function(imgUrl){
let dfd = Promise.defer();
let img = new Image();
img.onload = function(){
dfd.resolve(img.width+'-'+img.height)
}
img.onerror = function(e){
dfd.reject(e)
}
img.url = imgUrl;
return dfd.promise
}
复制代码
是否是发现咱们少了一层函数嵌套,呼~~ 得劲~~
npm install promises-aplus-tests -g
复制代码
既然咱们都说了咱们是遵循promiseA+规范的,那至少要拿出点证据来是否是,否则是否是说服不了你们,那么咱们就用promises-aplus-tests这个包来检测咱们写的promise
究竟怎么样呢!安装完成以后来跑一下咱们的promise
最终跑出来咱们所有经过测试!酷!晚餐再加个鸡腿~