文章目录
1 参考文档
- 深入理解JavaScript事件循环机制
- (转)帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)
- wx.request
- Promise是什么
- npm Promise page
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接口。
他这个承诺具有这些特点:
- 承诺执行不表示立马执行完成,异步函数执行结束后,我们必须通过resolve()触发调用then接口
- 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 基础知识预备
- JS中一切皆对象,function也是对象
- 函数的this是动态指定的,指向调用它的对象
- 当new 一个function时,它本身就是构造函数
- function的prototype指向原型对象
- Function.prototype.bind()将方法的this绑定到新的对象
- 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接口到链式调用
链式调用的内部过程是这样的:
- then添加一个任务缓存起来,这个任务索引了下一个Promise对象
- fn_resolve调用,then任务被安排的微任务队列中
- 微任务得到执行,执行then参数指定的函数
- 微任务尾部,调用resolve,触发下一个Promise对象的then任务进入微任务队列中
- 重复3-4
- 如果第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")