Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现一个完美符合Promise/A+规范的Promise #4

Open
forthealllight opened this issue May 16, 2018 · 21 comments
Open

实现一个完美符合Promise/A+规范的Promise #4

forthealllight opened this issue May 16, 2018 · 21 comments

Comments

@forthealllight
Copy link
Owner

forthealllight commented May 16, 2018

简要介绍:Promise允许我们通过链式调用的方式来解决“回调地狱”的问题,特别是在异步过程中,通过Promise可以保证代码的整洁性和可读性。本文主要解读Promise/A+规范,并在此规范的基础上,自己实现一个Promise.

一、Promise的使用

在了解Promise规范之前,我们知道主流的高版本浏览器已经支持ECMA中的Promise.

创建一个promise实例:

var p=new Promise(function(resolve,reject){
    setTimeout(function(){
       resolve("success")
    },1000);
    console.log("创建一个新的promise");
})
p.then(function(x){
  console.log(x)
})

//输出:
创建一个新的promise
success

上述是一个promise的实例,输出内容为,“创建一个promise”,延迟1000ms后,输出"success"。

从上述的例子可以看出,promise方便处理异步操作。此外promise还可以链式的调用:

var p=new Promise(function(resolve,reject){resolve()});
p.then(...).then(...).then(...)

此外Promise除了then方法外,还提供了Promise.resolve、Promise.all、Promise.race等等方法。

二、Promise/A+规范

Promise/A+规范扩展了早期的Promise/A proposal提案,我们来解读一下Promise/A+规范。

1.术语

(1)"promise"是一个对象或者函数,该对象或者函数有一个then方法

(2)"thenable"是一个对象或者函数,用来定义then方法

(3)"value"是promise状态成功时的值

(4)"reason"是promise状态失败时的值

我们明确术语的目的,是为了在自己实现promise时,保持代码的规范性(也可以跳过此小节)

2.要求

(1)一个promise必须有3个状态,pending,fulfilled(resolved),rejected当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态。当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。

promise英文译为承诺,也就是说promise的状态一旦发生改变,就永远是不可逆的。

(2)一个promise必须有一个then方法,then方法接受两个参数:

promise.then(onFulfilled,onRejected)

其中onFulfilled方法表示状态从pending——>fulfilled(resolved)时所执行的方法,而onRejected表示状态从pending——>rejected所执行的方法。

(3)为了实现链式调用,then方法必须返回一个promise

promise2=promise1.then(onFulfilled,onRejected)

三、实现一个符合Promise/A+规范的Promise

解读了Promise/A+规范之后,下面我们来看如何实现一个Promise,
首先构造一个myPromise函数,关于所有变量和函数名,应该与规范中保持相同。

1.v1.0 初始版本myPromise

function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    function resolve(value){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

同时,需要在myPromise的原型上定义链式调用的then方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

上述就是一个初始版本的myPromise,在myPromise里发生状态改变,然后在相应的then方法里面根据不同的状态可以执行不同的操作。

var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//输出1

但是这里myPromise无法处理异步的resolve.比如:

var p=new myPromise(function(resolve,reject){setTimeout(function(){resolve(1)},1000)});

p.then(function(x){console.log(x)})
//无输出

2.v2.0基于观察模式实现

为了处理异步resolve,我们修改myPromise的定义,用2个数组onFullfilledArray和onRejectedArray来保存异步的方法。在状态发生改变时,一次遍历执行数组中的方法。

function myPromise(constructor){
    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    self.onFullfilledArray=[];
    self.onRejectedArray=[];
    function resolve(value){
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
          self.onFullfilledArray.forEach(function(f){
                f(self.value);
                //如果状态从pending变为resolved,
                //那么就遍历执行里面的异步方法
          });
        
       }
    }
    function reject(reason){
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
          self.onRejectedArray.forEach(function(f){
              f(self.reason);
             //如果状态从pending变为rejected, 
             //那么就遍历执行里面的异步方法
          })
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

对于then方法,状态为pending时,往数组里面添加方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "pending":
        self.onFullfilledArray.push(function(){
             onFullfilled(self.value)
        });
        self.onRejectedArray.push(function(){
             onRejected(self.reason)
        });
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

这样,通过两个数组,在状态发生改变之后再开始执行,这样可以处理异步resolve无法调用的问题。这个版本的myPromise就能处理所有的异步,那么这样做就完整了吗?

没有,我们做Promise/A+规范的最大的特点就是链式调用,也就是说then方法返回的应该是一个promise。

3.v3.0then方法实现链式调用

要通过then方法实现链式调用,那么也就是说then方法每次调用需要返回一个primise,同时在返回promise的构造体里面,增加错误处理部分,我们来改造then方法

myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                try{
                   let temple=onFullfilled(self.value);
                   resolve(temple)
                }catch(e){
                   reject(e) //error catch
                }
             });
             self.onRejectedArray.push(function(){
                 try{
                   let temple=onRejected(self.reason);
                   reject(temple)
                 }catch(e){
                   reject(e)// error catch
                 }
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
            try{
              let temple=onFullfilled(self.value);
              //将上次一then里面的方法传递进下一个Promise的状态
              resolve(temple);
            }catch(e){
              reject(e);//error catch
            }
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
            try{
               let temple=onRejected(self.reason);
               //将then里面的方法传递到下一个Promise的状态里
               resolve(temple);   
            }catch(e){
               reject(e);
            }
        })
        break;
      default:       
   }
   return promise2;
}

这样通过then方法返回一个promise就可以实现链式的调用:

p.then(function(x){console.log(x)}).then(function(){console.log("链式调用1")}).then(function(){console.log("链式调用2")})
//输出
1
链式调用1
链式调用2

这样我们虽然实现了then函数的链式调用,但是还有一个问题,就是在Promise/A+规范中then函数里面的onFullfilled方法和onRejected方法的返回值可以是对象,函数,甚至是另一个promise。

4.v4.0 then函数中的onFullfilled和onRejected方法的返回值问题

特别的为了解决onFullfilled和onRejected方法的返回值可能是一个promise的问题。

(1)首先来看promise中对于onFullfilled函数的返回值的要求

I)如果onFullfilled函数返回的是该promise本身,那么会抛出类型错误

II)如果onFullfilled函数返回的是一个不同的promise,那么执行该promise的then函数,在then函数里将这个promise的状态转移给新的promise
III)如果返回的是一个嵌套类型的promsie,那么需要递归。

IV)如果返回的是非promsie的对象或者函数,那么会选择直接将该对象或者函数,给新的promise。

根据上述返回值的要求,我们要重新的定义resolve函数,这里Promise/A+规范里面称为:resolvePromise函数,该函数接受当前的promise、onFullfilled函数或者onRejected函数的返回值、resolve和reject作为参数。

下面我们来看resolvePromise函数的定义:

function resolvePromise(promise,x,resolve,reject){
  if(promise===x){
     throw new TypeError("type error")
  }
  let isUsed;
  if(x!==null&&(typeof x==="object"||typeof x==="function")){
      try{
        let then=x.then;
        if(typeof then==="function"){
           //是一个promise的情况
           then.call(x,function(y){
              if(isUsed)return;
              isUsed=true;
              resolvePromise(promise,y,resolve,reject);
           },function(e){
              if(isUsed)return;
              isUsed=true;
              reject(e);
           })
        }else{
           //仅仅是一个函数或者是对象
           resolve(x)
        }
      }catch(e){
         if(isUsed)return;
         isUsed=true;
         reject(e);
      }
  }else{
    //返回的基本类型,直接resolve
    resolve(x)
  }
}

改变了resolvePromise函数之后,我们在then方法里面的调用也变成了resolvePromise而不是promise。

myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                setTimeout(function(){
                  try{
	                   let temple=onFullfilled(self.value);
	                   resolvePromise(temple)
	                }catch(e){
	                   reject(e) //error catch
	                }
                })
             });
             self.onRejectedArray.push(function(){
                setTimeout(function(){
                   try{
	                   let temple=onRejected(self.reason);
	                   resolvePromise(temple)
	                 }catch(e){
	                   reject(e)// error catch
	               }
                })
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
           setTimeout(function(){
               try{
	              let temple=onFullfilled(self.value);
	              //将上次一then里面的方法传递进下一个Promise状态
	              resolvePromise(temple);
	            }catch(e){
                  reject(e);//error catch
               }
           })
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
           setTimeout(function(){
             try{
               let temple=onRejected(self.reason);
               //将then里面的方法传递到下一个Promise的状态里
               resolvePromise(temple);   
             }catch(e){
               reject(e);
             }
           })
        })
        break;
      default:       
   }
   return promise2;
}

这样就能处理onFullfilled各种返回值的情况。

var p=new Promise(function(resolve,reject){resolve("初始化promise")})
p.then(function(){return new Promise(function(resolve,reject){resolve("then里面的promise返回值")})}).then(function(x){console.log(x)})

//输出
then里面promise的返回值

到这里可能有点乱,我们再理一理,首先返回值有两个:

  • then函数的返回值——>返回一个新promise,从而实现链式调用

  • then函数中的onFullfilled和onRejected方法——>返回基本值或者新的promise

这两者其实是有关联的,onFullfilled方法的返回值可以决定then函数的返回值。

四、检测是否完全符合Promise/A+规范

npm install -g promises-aplus-tests 

具体用法请看promise test然后

promises-aplus-tests  myPromise.js

结果为:

这里写图片描述
说明我们的实现完全符合Promise/A+规范。

完整代码的地址

https://github.com/forthealllight/promise-achieve

五、最后补充Typescript实现的Promise/A+规范(可以忽略此节)

interface IConstructor{
  (resolve:IResolve,reject:IReject):void
}
interface IResolve {
    (x:any):void
}
interface IReject {
    (x:any):void
}
function myPromise(constructor:IConstructor):void{
  let self:object=this;
  self.status="pending";
  self.value=undefined;//if pending->resolved
  self.reason=undefined;//if pending->rejected
  self.onFullfilledArray=[];//to deal with async(resolved)
  self.onRejectedArray=[];//to deal with async(rejeced)
  let resolve:IResolve;
  resolve=function(value:any):void{
    //pending->resolved
    if(self.status==="pending"){
      self.status="resolved";
      self.value=value;
      self.onFullfilledArray.forEach(function(f){
        f(self.value);
      })
    }
  }
  let reject:IReject;
  reject=function(reason:any):void{
    if(self.status==="pending"){
      self.status="rejected";
      self.reason=reason;
      self.onRejectedArray.forEach(function(f){
        f(self.reason);
      })
    }
  }
  //According to the definition that the function "constructor" accept two parameters
  //error catch
  try {
    constructor(resolve,reject);
  } catch (e) {
    reject(e);
  }
}

单纯的写个工具函数,用ts还是有点影响可读性。

@pomelovico
Copy link

请教一个问题,在v4版本中resolvePromise函数需要4个参数,为啥在then方法的实现里只传了一个promise参数呢

@Yangfan2016
Copy link

Yangfan2016 commented Jan 2, 2019

v4.0 版本中加的 setTimeout 是起什么作用的, v3.0 里没有提及

@forthealllight
Copy link
Owner Author

v4.0 版本中加的 setTimeout 是起什么作用的, v3.0 里没有提及

你好,then方法返回的也是一个promise,也是一个异步的,这里用setTimeout包裹,使得then方法被调用后,不会阻塞后续的同步任务。

@taokexia
Copy link

请教一个问题,在v4版本中resolvePromise函数需要4个参数,为啥在then方法的实现里只传了一个promise参数呢

推荐去看一下作者的源码,这块代码好像写错了。

@majunchang
Copy link

第二个案例为什么会先输出一个undefined呢 原生的直接输出1 没有undefined

@boboox
Copy link

boboox commented Apr 19, 2019

prmoise === x 的时候应该reject(new TypeError()) 而不是throw吧?

@Tmac-1
Copy link

Tmac-1 commented Apr 24, 2019

你好,问一下为什么第一个案例无法处理异步的resolve啊

@VRCuspid
Copy link

您好,我想问一下为什么要保存this呀, const self = this;
=..=

@ningrixin
Copy link

什么第一个案例无法处理异步的resolve啊

因为执行then的时候还没有resolve,status还是pending所以不会执行onFullfilled

@cunjieliu
Copy link

prmoise === x 的时候应该reject(new TypeError()) 而不是throw吧?

外层有try catch

@cunjieliu
Copy link

您好,我想问一下为什么要保存this呀, const self = this;
=..=

不做处理的话,会发生隐式绑定,导致this丢失问题

@342x
Copy link

342x commented May 20, 2019

第2个案列中,为什么会先输出个undefined的

@mallocLNode
Copy link

mallocLNode commented Jun 6, 2019

第二个案例为什么会先输出一个undefined呢 原生的直接输出1 没有undefined

第2个案列中,为什么会先输出个undefined的

因为switch没有使用break的话,匹配到了会一直往下执行的,作者漏贴了吧。推荐去把源码下下来。
另外还发现在v3 case "pending"后面也有错的

   let temple=onRejected(self.reason);
   reject(temple)

应该改为

   let temple=onRejected(self.reason);
   resolve(temple)

@Yxiuchao
Copy link

Yxiuchao commented Sep 1, 2019

您好!
有typescript实现的then方法吗?

@JayTam
Copy link

JayTam commented Nov 25, 2019

你好,问一下为什么第一个案例无法处理异步的resolve啊

因为 new myPromise() 执行完同步构造函数进行实例化之后,立马又执行了同步的then方法,constructorthen在一个tick中执行了,没有实现异步。

new myPromise((resolve, reject) => {
    setTimeout(()=>{
       // 过了1s执行了resolve,把状态修改成`onFulfilled`,
      //  但是then已经在初始化Promise后立即执行了,then应该在执行了resolve()之后执行
        resolve("end");
    }, 1000);
}).then((data)=>{
    // 这个匿名回调函数不会执行,因为then执行的时候,状态还是pending
    console.log(data);
})

@cjinhuo
Copy link

cjinhuo commented Dec 3, 2019

你好,在第二个案例中switch少了个break;
onRejected(self.reason)
});
// 少了个break;所以导致他们说的undefined
case "resolved":

@h2190697689
Copy link

第二个案例为什么会先输出一个undefined呢 原生的直接输出1 没有undefined

case “pending” 那缺少个break

@forthealllight
Copy link
Owner Author

你好,在第二个案例中switch少了个break;
onRejected(self.reason)
});
// 少了个break;所以导致他们说的undefined
case "resolved":

文章写的代码可能有误,直接看源码里面的,那个是正确的额

@forthealllight
Copy link
Owner Author

第二个案例为什么会先输出一个undefined呢 原生的直接输出1 没有undefined

case “pending” 那缺少个break

我手写的文章里面可能有误,直接看我给的源码地址,那里面是对的

@97Yates05
Copy link

有个问题,执行器传入的reslove应该是能接受promise或者theable对象并且不会直接将值传入的,但是按照作者的实现,执行器里的reslove只是单纯的传值,这个不是promiseA+的标准吗。。

@dbwcooper
Copy link

dbwcooper commented Mar 29, 2021

@forthealllight resolvePromise 函数使用的地方应该少传了三个参数,

 let temple=onFullfilled(self.value);
 resolvePromise(temple)

应该是这样?

 let temple=onFullfilled(self.value);
 resolvePromise(promise2, temple, resolve, reject)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests