JS拾荒のPromise实现难点

1,107 阅读6分钟
  • resolve的是一个promise
  • then中return了一个promise
  • 防止循环引用
  • 延迟设计
    • 初衷猜想
  • 关于异常捕获
  • 值的穿透

pre-notify

previously :

最近发现掘金上又多了很多promise相关的文章,于是乎几个月前写的东东拿出来看了看,又理了理,于是乎就有了这么一篇文。

resolve的是一个promise

promise允许resolve里可以是一个promise。

比如说

假如我们有一个promise1,这个promise1resolve的也是一个promise,我们姑且称之为promise2,那么这个promise2的结果将作为我们promise1的结果(状态和值)。

let p = new Promise(function(resolve,reject){
    resolve(new Promise(function(resolve,reject){
    	resolve('a');
    }))
});

>>> p.value
<<< a

并且这个promise可以无限的在resolve中嵌套下去。

而我们只需要记住我们最终拿到的promise的结果是最里层的promise的结果即可。


实现

function resolve(value){
    // value可能是一个promise
    if(value!==null&&(typeof value==='object'||typeof value==='function')){
      return value.then(resolve,reject);
    }
   ...
  }

思维模型图大概长这样

就像一个个钩子,绑定和定义的顺序是从右往左,执行时是从左往右

then中return了一个promise

正常情况下如果then中return的是一个普通值,那么会走下一个then中的成功回调,并且这个return的值会作为回调的参数传入。

但如果是一个promise,则会根据这个promise的状态来决定走下一个then中的哪个回调,并且以这个promise的值作为回调的参数传入。

p.then(function(data){
    return new Promise(function(resolve,reject){
    	resolve(100);
    })
}).then(function(data){
    console.log(data);
})

<<<
100

其次这个返回的promise里也可像上面的说过的一样在这个promise里的resolve里继续嵌套promise

p.then(function(data){
    return new Promise(function(resolve,reject){
    	resolve(new Promise(resolve,reject){
    	    resolve(200);
        });
    })
}).then(function(data){
    console.log(data);
})

<<<
200

实现:

这大概是Promise实现最难的一部分,主要是通过一个resolvePromise的方法来支持我们上述的功能,先上完整代码

function resolvePromise(p2,x,resolve,reject){
  // 注意:有可能解析的是一个第三方promise

  if(p2 === x){ //防止循环引用
    return reject(new TypeError('Error:循环引用')); //让promise2失败
  }

  let called; // 防止第三方的promise出现可能成功和失败的回调都调用的情况

  if(x!==null||(typeof x === 'object')||typeof x === 'function'){
    // 进来了只能说明可能是promise
    try{
      let then = x.then;
      if(typeof then === 'function'){
        then.call(x,function(y){
          if(called) return; //这里可能交由的是第三方promise来处理,故可能被调用两次
          called = true;
          // p.then(function(){return new Promise(){resolve(new Promise...)}}) return中的promise的resolve又是一个promise,即y又可能是一个promise
          resolvePromise(p2,y,resolve,reject);
        },function(err){
          if(called) return;
          called = true;
          reject(err);
        });
      }else{
        resolve(x); //可能只是组键值对,像这样{then:1}
      }
    }catch(e){
      // Object.define({},'then',{value:function(){throw Error()}})
      if(called) return; // 防止第三方promise失败时 两个回调都执行 导致触发两次reject注册的回调
      called = true;
      reject(e);
    }
  }else{ //说明是个普通值 让p2成功
    resolve(x);
  }
}

其中尤其要重视的是这一部分

if(typeof then === 'function'){
    then.call(x,function(y){
      resolvePromise(p2,y,resolve,reject);
    },function(err){
      if(called) return;
      called = true;
      reject(err);
    });
}

这里,此时then中成功回调的这个y参数即有可能是我们所说的resolve里嵌套promise的情况,

故我们需要将这个y传入resolvePromise方法再次进行解析,这样不断递归,直到这个y变成一个普通值,我们以这个普通值来resolve我们then中返回的promise。

防止循环引用

如果我们在一个then中return了一个promise,且这个promise还恰巧是then后返回的promise本身,那么这个then返回的promise永远不可能会转换状态。So为了防止出现这种情况我们会直接reject it。

如果then中返回的promise是它本身就reject it
let p = new Promise(function(resolve,reject){
  resolve();
});
var p2 = p.then(function(){
  return p2; //<---看这里!!
});
p2.then(function(){

},function(e){
  console.log(e); //会走这里
});

实现:

主要是resolvePromise方法中的这么一句

if(p2 === x){ //防止循环引用
    return reject(new TypeError('Error:循环引用')); //让promise2失败
}

延迟设计

在promise的设计中,即使promise里没有异步resolve,通过promisethen注册的回调也只会在标准的同步代码执行完成后才会执行

let p = new Promise(function(resolve,reject){
  resolve(222);
});
p.then(function(data){
  console.log(data);
})
console.log(111);

<<<
111
222

我们可以在代码中这样实现

...
promise2 = new Promise(function(resolve,reject){
  // 因为返回值可能是普通值也可能是一个promise,其次也可能是别人的promise,故我们将它命名为x,
  setTimeout(function(){
    try{
      let x = onFulfilled(self.value);
      resolvePromise(promise2,x,resolve,reject);
    }catch(e) {
      reject(e);
    }
  });
});
...

即让then回调执行之前套上一层setTimeout,让它在下一轮执行。 (但实际上原生的promise实现是将其作为微任务而不是宏任务执行的)。

初衷猜想

这么设计很重要的一个原因在我看来是因为执行then回调时,我们会调用resolvePromise这个方法,而调用这个方法时我们需要将then新返回的promise传入,但我们能在一个new的过程中拿到自己吗?

像这样

let a = new A(){
    console.log(a);
}

这样显然拿到的是undefined

So,为了拿到这个新返回的promise,故我们在外面套了一层setTimeout这样的东东。

关于异常捕获

一个then,只要它有return,那么下一个then就会走成功的回调,即使这个return的是一个状态为失败态的promise。

p.then(function(data){
    return new Promise(function(resolve,reject){
    	reject('失败');
    })
})
.then(function(data){
    console.log('会走成功');
},function(err){

})

<<<
会走这里

只有一种情况下一个then会走失败的回调,那就是此次的回调执行是抛出了异常,

p.then(function(data){
    throw Error('出现错误!')
})
.then(function(data){
    console.log('会走成功');
},function(err){
    console.log(err)
})

<<<
出现错误!

实现: 我们在两个地方使用过try catch

一个是executor执行的时候

 try{ // 正因为executor执行是同步的,故我们能使用try catch
    executor(resolve,reject);
  }catch(e){
    reject(e);
  }

一个是我们then所注册的回调执行的时候

...
promise2 = new Promise(function(resolve,reject){
  setTimeout(function(){
    try{
      let x = onFulfilled(self.value);
      resolvePromise(promise2,x,resolve,reject);
    }catch(e) {
      reject(e);
    }
  });
});
...

值的穿透

promise允许我们使用空then(即使这样做并没有什么意义),而这些省略了回调的then原本改接收的参数会被向下传递直到遇到一个不是空的then。

var p = new Promise(function(resolve,reject){
  resolve(100)
});
p.then().then().then(function(data){
  console.log(data);
},function(err){
  console.log(err)
});

<<<
100 //也允许异常向下传递

实现:

Promise.prototype.then = function(onFulfilled,onRejected){
  // 成功和失败默认不传,则让他们穿透直到有传值的then
  onFulfilled = typeof(onFulfilled)==='function'?onFulfilled:function(value){
    return value;
  };
  onRejected = typeof(onRejected)==='function'?onRejected:function(err){
    throw err;
  };
  ...

源码

仓库:点我获取


时间或许并不是平白的流过,只要一直在尝试着,TA似乎能让有些事逐渐变得清晰

--- end ---