这篇文章总结一下JS本身单线程异步的局限性(promise出现的原因),以及实现一个简易的Promise。
单线程与异步
JavaScript是一个单线程执行的语言,在不考虑异步编程的情况下,它执行的顺序就是一个eventLoop的简单循环。比如书写一段简单的JS代码:
// 声明两个变量,和一个函数
var demoVariA = 100;
var demoVariB = 200;
// 函数的功能是把入参的两个数值相加
function addTowNum (a, b) {
return a + b;
}
addTowNum(demoVariA, demoVariB);
那么在不考虑预解析的情况下(变量函数作用域提升),我们试着把上面这段代码执行顺序用一个eventLoop来简单表示出来。
// 代码的执行队列
eventLoop = [
'赋值100给内存demoVariA',
'赋值200给内存demoVariB',
'申请堆栈内存声明为addTowNum,赋值为一个函数',
'执行函数'
];
// 现在JS代码需要分析执行
// 这里不考虑任何异步的情况,包括宏任务和微任务
while (eventLoop.length > 0) {
// 这里表示对词法token进行编译借的过程的抽象
var event = eventLoop.shift();
try {
event();
} catch (error) {
report(error);
}
}
我们可以看到,不需要考虑多线程共享数据时线程执行先后对程序造成的影响,不需要使用进程锁的概念,词法的执行顺序是多么的清晰,单线程编程是多么的令人愉快。直到异步进入到我们的编程世界。
同样是上面的例子,我们把demoVariA
,demoVariB
的数据请求改为异步的请求获取值asyncGetSomeValue
,事情就完全不一样了。
var demoVariA = asyncGetSomeValue('pathA');
var demoVariB = asyncGetSomeValue('pathB');
function addTowNum (a, b) {
return a + b;
}
addTowNum(demoVariA, demoVariB); // undefined + undefined = NaN
asyncGetSomeValue必须在某个异步操作之后,再获取到值,因此按照JS的常规做法,我们必须把demoVariA
,demoVariB
放到回调当中去获取值,我们把代码修改如下:
var demoVariA;
var demoVariB;
asyncGetSomeValue('pathA', function (responseValue) {
demoVariA = responseValue;
});
asyncGetSomeValue('pathB', function (responseValue) {
demoVariB = responseValue;
});
function addTowNum (a, b) {
return a + b;
}
现在问题来了addTowNum
,该放到哪里去执行才能a和b同时获取到呢。有的同学此时会说,简单,我把demoVariB
获取放到demoVariA
获取的回调下,再去执行addTowNum
。
var demoVariA;
var demoVariB;
asyncGetSomeValue('pathA', function (responseValue) {
demoVariA = responseValue;
asyncGetSomeValue('pathB', function (responseValue) {
demoVariB = responseValue;
addTowNum(demoVariA, demoVariB);
});
});
function addTowNum (a, b) {
return a + b;
}
实际上,demoVariB
的请求并没有依赖到demoVariA
,因此把demoVariB
的取值请求放到demoVariA
的后面的做法是错误的,这样会导致addTowNum
这个函数的调用时间将会变成两个异步请求费时的总和。这两个获取值得请求严格意义上应该是并发的概念。我们要做的是需要声明一个为gate
的函数,当你传入的所有变量存在的时候,去执行传入的函数。
/**
* @param {function} gateFunction 所有变量存在后执行的函数
* @param 剩余变量
*/
function gate (gateFunction) {
var testArray = arguments.slice(1);
// 全部变量都存在的时候,才执行gateFunction
if (testArray.every(variable => variable)) {
gateFunction.apply(this, testArray);
}
}
此时加入gate
函数,我们上面的修改为异步的例子才算简单完成。实际业务编程当中,我们还要考虑asyncGetSomeValue的异常抛错等问题。
var demoVariA;
var demoVariB;
asyncGetSomeValue('pathA', function (responseValue) {
demoVariA = responseValue;
gate(addTowNum, a, b);
});
asyncGetSomeValue('pathB', function (responseValue) {
demoVariB = responseValue;
gate(addTowNum, a, b);
});
function addTowNum (a, b) {
return a + b;
}
通过这个简单的例子我们不难发现异步编程的问题:
- 多异步返回的执行顺序不可控。
- 多异步的异常错误处理非常繁杂。
- 多异步嵌套,会导致回调地狱。
我们急需要一个能够保证异步执行顺序,保证执行和抛出错误的异步处理的保证范式来解决这些问题。ES6给我们的答案就是Promise(承诺)。
Promise的特性
都是将来
Promise的使用方法不做阐述,想要了解的可以去查询一下MDN。
Promise的回调than有一个非常重要的特性,那就是无论是现在还是将来,统一都是将来。我们来猜一下下面代码的执行顺序:
var promise = new Promise(function (resolve, reject) {
console.log(1);
setTimeout(() => {
console.log(2);
}, 0)
resolve(3);
});
promise.then(val => console.log(val))<