JavaScript Promise 承诺执行原理浅析

1 参考文档

2 本文出发点

JS 中执行异步函数往往需要嵌套callback,多层 嵌套的callback使得代码难以阅读且容易出Bug。有没有一种办法可以明确等待异步函数执行完成后,下一个函数(代码块)再继续执行。

以微信小程序为例:

wx.request()
doAfterRequest()

为了不把doAfterRequest()放到wx.request()的回调函数中,于是就找到了Promise这样一种简单快捷方法,它可以这么做。

new Promise(function(resolve,reject) {
	wx.request({
		success (res) {
		    resolve(res)
		}
	}
}).then(res => {
	doAfterRequest(res)
})

3 简易理解Promise

关于Promise的标准解答,网上已有很多文章,详尽无比,诸如Promise有三个状态(pending、fulfilled、rejected),Promise与JS的事件驱动机制的关系。这里只做简单的理解,就四个字 “承诺执行”

new Promise()时,Promise承诺执行括号内的函数(在构造函数中执行这个承诺),并承诺通过resolve调用then接口,承诺通过reject调用catch接口。在ES6中,它还承诺最终无论成败一定调用finally接口。

他这个承诺具有这些特点:

  1. 承诺执行不表示立马执行完成,异步函数执行结束后,我们必须通过resolve()触发调用then接口
  2. then接口还是返回一个Promise对象,于是,我们可以一直then then写出链式调用的美感

第一个特点也说明,如果我们不调用resolve(),then接口是不会被调用,因为Promise不知道何时去调用,它无法知道异步函数(wx.request)什么时候结束。

4 简易理解Promise与JS事件驱动机制

先看网上的例子:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

使用node.js或者网页js在线运行一下,得到结果:

> "script start"
> "script end"
> "promise1"
> "promise2"
> "setTimeout"

简易理解:
(参考文档中有图)

同步任务 > 微任务 > 异步任务

  • setTimeout和Promise都是涉及新任务,肯定在当前任务(当前代码块)之后。所以先输出’script start’和’script end’。

  • setTimeout属于异步任务,Promise.then使用的是微任务,所以,先执行Promise.then,于是接着输出’promise1’和’promise2’。

  • 最后才是异步任务setTimeout的’setTimeout’输出。

稍微改造一下这个段代码,让它结合第一节承诺执行

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);
new Promise((r,j)=>{
	console.log("promise0")
  	r("") // 这里很关键,没有它,then不会被调用
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

JS在线输出结果:

> "script start"
> "promise0"
> "script end"
> "promise1"
> "promise2"
> "setTimeout"

重点在于,"promise0"在"script end"的前面,说明,第一步承诺执行是立即执行(是同步任务),且是在构造函数中执行。而then、catch则是先安排到微任务。

5 Promise简易链式调用

这里虽然代码简单,但是看完能加深对Promise的理解。

还是改造上面的例子。


改造1:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);
new Promise((r,j)=>{
	console.log("promise0")
  	r("")
}).then(function() {
  console.log('promise1');
  return new Promise(r => {
    console.log('promise1-1');
  })
}).then(function() {
  return new Promise(r => {
    console.log('promise2-2');
  })
  console.log('promise2');
});

console.log('script end');

JS在线输出结果:

> "script start"
> "promise0"
> "script end"
> "promise1"
> "promise1-1"
> "setTimeout"

关注点1:'promise2’和’promise2-2’没有出现。为什么?

原因在于,下面的代码中:

return new Promise(r => {
    console.log('promise1-1');
    // r("") //resolve不能漏了
  })

传入Promise构造函数的箭头函数(=>)没有执行resolve()。
回顾前面说的承诺执行的第一个特点,resolve必须由我们调用才能触发then接口。

关注点2:"promise1-1"在"setTimeout"的前面输出。

这是因为当执行到第一个then接口时,新的Promise被创建,它是当前的同步任务,所以执行。


改造2:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);
new Promise((r,j)=>{
	console.log("promise0")
  	r("")
}).then(function() {
  console.log('promise1');
  return new Promise(r => {
    console.log('promise1-1');
    r("")
  }).then(r => {
    console.log("promise1-then")
  })
}).then(function() {
  console.log('promise2'); 
  return null
}).then(function() {
  console.log('promise3'); 
});

console.log('script end');

JS 在线输出结果:

> "script start"
> "promise0"
> "script end"
> "promise1"
> "promise1-1"
> "promise1-then"
> "promise2"
> "promise3"
> "setTimeout"

关注点1: "promise1-then"先于其它"promise2"字符串输出

这跟Promise的实现有关,其实它等同于then写在外面:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);
new Promise((r,j)=>{
	console.log("promise0")
  	r("")
}).then(function() {
  console.log('promise1');
  return new Promise(r => {
    console.log('promise1-1');
    r("")
  })
}).then(r => {
    console.log("promise1-then")
}).then(function() {
  console.log('promise2'); 
  return null
}).then(function() {
  console.log('promise3'); 
});

console.log('script end');

关注点2: then可以返回null,也可以不返回。

其实这也是Promise的实现机制,then如果不返回,或者返回任意值或者非Promise对象,都将外包一层Promise对象。这样保证了then返回Promise对象。

实践一下就知道:

let thenReturn = new Promise((r,j)=>{	
  	r("")
}).then(function() {   
  return null // 这是给下一个then(function),里面的function的参数
})

console.log(thenReturn instanceof Promise);

输出:true

6 Promise代码解读

  • 代码取自:npm Promise page
  • 代码获取方法:从git下载代码或者使用命令npm install promise安装。
  • 内容基于最初的Promise设计,不包括ES6,如果需要了解ES6,可以吃参考代码中的promise/libs/es6-extensions.js
  • catch接口 属于ES6的内容,它也是间接地使用了then接口。

前文没有提到的,如何捕获失败结果呢,其实是在then接口的第二个参数。
就像这样:.then(function onResolve(){}, function onReject(){})

6-1 基础知识预备

  1. JS中一切皆对象,function也是对象
  2. 函数的this是动态指定的,指向调用它的对象
  3. 当new 一个function时,它本身就是构造函数
  4. function的prototype指向原型对象
  5. Function.prototype.bind()将方法的this绑定到新的对象
  6. JS的事件驱动机制,promise所依赖的asap库就是基于这个机制来安排任务的

asap 内部通过process.nextTick(node.js)或者 setImmediate(浏览器) setTimeout(默认)其中之一布置任务到任务队列。

6-2 从构造器到resolve/reject结果

function Promise(fn) {
  ......
  this._h = 0;
  this._i = 0;
  this._j = null;
  this._k = null;
  doResolve(fn, this);
}
function doResolve(fn, promise) {
  var done = false;
  // 直接调用fn,并传入两个函数参数
  var res = tryCallTwo(fn, function (value) {
    if (done) return;
    done = true;
    resolve(promise, value);
  }, function (reason) {
    if (done) return;
    done = true;
    reject(promise, reason);
  });
  // 如果调用返回结果是错误,直接结束;或者走到trace。
  if (!done && res === IS_ERROR) {
    done = true;
    reject(promise, LAST_ERROR);
  }
}

Promise trace依赖 promise/libs/rejection-tracking.js。本文认为构造函数总是正确执行

tryCallTwo正式调用fn,并将两个函数参数传递进fn(fn_resolve, fn_reject)中。当我们在fn内的异步函数的结果回调函数中调用fn_resolve(res)或者fn_reject(res),就是走到这两个作为参数的函数。

注意这里命名的一致性,会导致混淆。Promise内部也有resolve/reject函数,fn的参数也命名为resolve/reject。为了区别,我们把fn的参数名称改成fn_resolve和fn_reject。

于是得到这样的定义:

var fn_resolve = function (value) {
    if (done) return;
    done = true;
    resolve(promise, value);
  }
 var fn_reject = function (reason) {
    if (done) return;
    done = true;
    reject(promise, reason);
  })

再看resolve的代码-1(resolve传入的参数是字符串):

function resolve(self, newValue) {  
  ......   
  self._i = 1;
  self._j = newValue;
  finale(self);
}

finale的作用是直接结束,如果有handle被调用过,则调用handle。而handle的调用,来自then()接口。
我们假设这里一定会使用then接口。

干脆在这看一下finale():

function finale(self) {
  if (self._h === 1) {
    handle(self, self._k);
    self._k = null;
  }
  if (self._h === 2) {
    for (var i = 0; i < self._k.length; i++) {
      handle(self, self._k[i]);
    }
    self._k = null;
  }
}

可以看到没什么特别的,如果有handle执行过,_h是1或者2,于是再次调用handle。
then()接口的工作内容是安排一个任务,但是这个任务还没插入任务队列,只是暂时记录在Promise对象中缓存起来(self._k)。调用一次then缓存一个任务。

注意区别于后文的链式调用,链式调用是不同的Promise的then接口被调用一次,这里是同一个then接口被调用多次,比如下面就是then被调用多次:

let p = new Promise(r => {
  console.log("in Promise")
  r("r")
})

p.then(r => {
  console.log("then1")
})

p.then(r => {
  console.log("then2")
})

p.then(r => {
  console.log("then3")
})

finale就是根据then的调用情况,决定是结束还是再次调用handle。当finale再次调用handle时,它使用asap库往JS微任务队列中插入一个任务或者多个任务(任务个数与then调用的次数有关)。

当同步任务执行完后,微任务进入同步任务中得到执行机会。

伪代码大概是这样的:

new Promise(fn) // fn在同步任务中立即执行
.then(task) // 一个task被缓存起来
.then(task) // 一个task被缓存起来
......

这里有一种特殊的情况,假设fn内部没有调用异步函数(如:wx.request),那么fn_resolve()会在then接口之前被调用。此时,then中的task是直接插入JS的微任务队列。

resolve的代码-2(resolve传入的参数是对象或者函数):

function resolve(self, newValue) {  
  ...... 
  if (
    newValue &&
    (typeof newValue === 'object' || typeof newValue === 'function')
  ) {
    var then = getThen(newValue);
    if (then === IS_ERROR) {
      return reject(self, LAST_ERROR);
    }
    if (
      then === self.then &&
      newValue instanceof Promise
    ) {
      self._i = 3;
      self._j = newValue;
      finale(self);
      return;
    } else if (typeof then === 'function') {
      doResolve(then.bind(newValue), self);
      return;
    }
  }
}

再看看fn_resolve的参数的类型是一个函数或者对象,那么Promise要注意这个参数有无名为"then"这样的函数。如果有,将这个"then"的this指向重新绑定到新的对象中,然后调用,类似于Promise构造函数中的fn一样调用。(由于很少触及到这里,我们就略过它了)

6-3 从then接口到链式调用

链式调用的内部过程是这样的:

  1. then添加一个任务缓存起来,这个任务索引了下一个Promise对象
  2. fn_resolve调用,then任务被安排的微任务队列中
  3. 微任务得到执行,执行then参数指定的函数
  4. 微任务尾部,调用resolve,触发下一个Promise对象的then任务进入微任务队列中
  5. 重复3-4
  6. 如果第4步中,下一个Promise对象的then接口没有被调用,则直接结束整个调用链

开始看代码:

从 6-2 知道:then接口的作用是在安排一个任务到微任务队列,并且这个任务的执行必定在fn_resolve()之后。

从 前文 知道:then接口一定返回一个新的Promise对象。

then接口的实现:

Promise.prototype.then = function(onFulfilled, onRejected) {
  // 这是一个保险做法,但是什么情况下this对象会被改变了,除非有人执行了bind操作
  if (this.constructor !== Promise) {
    return safeThen(this, onFulfilled, onRejected);
  }
  var res = new Promise(noop);
  handle(this, new Handler(onFulfilled, onRejected, res));
  return res;
};

onFulfilled:fn_resolve之后调用它
onRejected:fn_reject之后调用它

可以看到return的是一个new Promise(),且构造函数的函数体是空的。它的构造函数什么也不做,这个新的Promise对象的唯一用途就是用于调用它的then接口。

function Promise(fn) {
  this._h = 0;
  this._i = 0;
  this._j = null;
  this._k = null;
  if (fn === noop) return;
}

这个看似构造函数什么都不错的新的Promise在调用它的then接口时,同样会缓存一个task,保存到Promise._k队列中。

这个时候,来到第一个步骤:1. then添加一个任务缓存起来,这个任务索引了下一个Promise对象

function handle(self, deferred) {
  while (self._i === 3) {
    self = self._j;
  }
  if (Promise._l) {
    Promise._l(self);
  }
  // 缓存任务
  if (self._i === 0) {
    if (self._h === 0) {
      self._h = 1;
      self._k = deferred;
      return;// 缓存完成后返回
    }
    if (self._h === 1) {
      self._h = 2;
      self._k = [self._k, deferred];
      return; // 缓存完成后返回
    }
    self._k.push(deferred);
    return;
  }
  handleResolved(self, deferred);
}

来到第二个步骤:2. fn_resolve调用,then任务被安排的微任务队列中
通过asap库将一个函数放入微任务队列,参数deferred.promise就是新的Promise。
先通过tryCallOne执行上一个then任务主体,然后在任务的尾部,通过resolve触发下一个Promise的then任务。

function handleResolved(self, deferred) {
  asap(function() {
    ......
    var ret = tryCallOne(cb, self._j);
    if (ret === IS_ERROR) {
      reject(deferred.promise, LAST_ERROR);
    } else {
      resolve(deferred.promise, ret);
    }
  });
}

上面的tryCallOne就是第三个步骤:3. 微任务得到执行,执行then参数指定的函数

上面的resolve(deferred.promise, ret);
就是第四个步骤:4. 微任务尾部,调用resolve,触发下一个Promise对象的then任务进入微任务队列中

关于resolve的内容,前面已经分析过了。resolve决定了是否继续还是退出调用链。

7 微信小程序添加finally支持

发现小程序在IOS运行时,Promise不会调用finally,调试发现,Promise.prototype.finally 是 undefined。Android上没有问题。

将npm Promise page(参考上一小节)中的finally.js提取出来:

'use strict';

Promise.prototype.finally = function (f) {
  return this.then(function (value) {
    return Promise.resolve(f()).then(function () {
      return value;
    });
  }, function (err) {
    return Promise.resolve(f()).then(function () {
      throw err;
    });
  });
};

将其放到 libs/promise/finally.js中。
在app.js顶部添加:require("libs/promise/finally.js")

发布了7 篇原创文章 · 获赞 0 · 访问量 247

猜你喜欢

转载自blog.csdn.net/weixin_45866432/article/details/104388565