本文章仅针对我自己在看书过程中对一些不太清楚的知识点进行查漏补缺——《你不知道的JavaScript(中卷)》第二部分异步和性能中的第一章”异步:现在与将来“、第二章”回调“、第三章”Promise“、第四章”生成器“
异步
事件循环
伪代码
// eventLoop是一个用作队列的数组
// (先进,先出)
var eventLoop = [ ];
var event;
// “永远”执行
while (true) {
// 一次tick
if (eventLoop.length > 0) {
// 拿到队列中的下一个事件
event = eventLoop.shift();
// 现在,执行下一个事件
try {
event();
}
catch (err) {
reportError(err);
}
}
}
解释
有一个用 while 循环实现的持续运行的循环,循环的每一轮称为一个 tick。对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。
一定要清楚,setTimeout(..) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。
JavaScript是单线程的理解
了解单线程之前先知道其他几个概念有助于理解单线程
异步
与同步处理相对,异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程
并行
一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的
并发
在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)
进程和线程
进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存
下面来看这样一个示例,可以理解为有2个任务并发执行
var a = 20;
function foo() {
a = a + 1;
}
function bar() {
a = a * 2;
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
假设JavaScript是多线程的,那么可能会发生以下情况
foo():
a. 把a的值加载到X
b. 把1保存在Y
c. 执行X加Y,结果保存在X
d. 把X的值保存在a
bar():
a. 把a的值加载到X
b. 把2保存在Y
c. 执行X乘Y,结果保存在X
d. 把X的值保存在a
1a (把a的值加载到X ==> 20)
2a (把a的值加载到X ==> 20)
1b (把1保存在Y ==> 1)
2b (把2保存在Y ==> 2)
1c (执行X加Y,结果保存在X ==> 22)
1d (把X的值保存在a ==> 22)
2c (执行X乘Y,结果保存在X ==> 44)
2d (把X的值保存在a ==> 44)
1a (把a的值加载到X ==> 20)
2a (把a的值加载到X ==> 20)
2b (把2保存在Y ==> 2)
1b (把1保存在Y ==> 1)
2c (执行X乘Y,结果保存在X ==> 20)
1c (执行X加Y,结果保存在X ==> 21)
1d (把X的值保存在a ==> 21)
2d (把X的值保存在a ==> 21)
还有多种排列组合的可能性,可见多线程编程是非常复杂的。因为如果不通过特殊的步骤来防止这种中断和交错运行的话,可能会得到出乎意料的、不确定的行为
而单线程的JavaScript则不需要考虑这么多不确定性,要么foo函数先执行,要么bar函数先执行,只会得到2种不同的结果
这种函数顺序的不确定性就是通常所说的竞态条件
Promise
如何判断一个值是Promise
(看了书才知道,原来我之前面试时候回答得全是错的~)
误区p instanceof Promise
Promise 值可能是从其他浏览器窗口(iframe 等)接收到的
库或框架可能会选择实现自己的 Promise,而不是使用原生 ES6 Promise 实现
thenable 鸭子类型(一个对象有then方法,有then方法但不是Promise的都被认为与Promise的编码不兼容)
如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
但在 ES6 之前,社区已经有一些著名的非 Promise 库恰好有名为 then(..) 的方法。这些库中有一部分选择了重命名自己的方法以避免冲突。而其他的那些库只是因为无法通过改变摆脱这种冲突,就很不幸地被降级进入了“与基于 Promise 的编码不兼容”的状态。
如果一个变量是 Object, 有 then 和 catch 方法, 就认为是 Promise
p !== null && p === "object" && typeof p,then === "function" && typeof p.catch === "function"
vue-next源码中是这样判断的
const isPromise = (val) => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
特殊的例子
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );
var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );
p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );
p1.then( function(v){
console.log( v );
} );
p2.then( function(v){
console.log( v );
} );
// A B <-- 而不是像你可能认为的B A
resolve传参
重点!!!
如果向 Promise.resolve(..) 传递一个非 Promise、非 thenable 的立即值,就会得到一个用这个值填充的 promise
而如果向 Promise.resolve(..) 传递一个真正的 Promise,就只会返回同一个 promise
如果向 Promise.resolve(..) 传递了一个非 Promise的 thenable 值,前者就会试图展开这个值,而且展开过程会持续到提取出一个具体的非类 Promise 的最终值
Promise的缺点——错误处理
首先要明确一点,以下代码中A中的错误B中无法捕获,其次,B中如果抛错就没法捕获了
new Promise((resolve, reject) => { // ...})
.then((res) => {
// A
}, (err) => {
// B
})
因此Promise 链中的任何一个步骤都没有显式地处理自身错误
任何 Promise 链的最后一步,不管是什么,总是存在着在未被查看的 Promise 中出现未捕获错误的可能性
以下是一种的解决方法,但并不是ES标准的一部分
Promsie 应该添加一个 done(..) 函数,从本质上标识 Promsie 链的结束。done(..) 不会创建和返回 Promise,所以传递给 done(..) 的回调显然不会报告一个并不存在的链接 Promise的问题。
var p = Promise.resolve(42);
p.then(function fulfilled(msg) {
// 数字没有string函数,所以会抛出错误
console.log(msg.toLowerCase());
}).done(null, handleErrors);
// 如果handleErrors(..)引发了自身的异常,会被全局抛出到这里
浏览器的捕获
浏览器可以知道所有对象被垃圾回收的时机,所以可以追踪Promise对象,如果它在被垃圾回收的时候有拒绝,有些浏览器就能确保这是一个真正的未被捕获的错误
无法取消的Promise
一旦创建了一个 Promise 并为其注册了完成或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程
很多开发者希望 Promise 的原生设计就具有外部取消功能,但这其实会违背了Promise的可信任性
谈谈Promise
生成器
生成器的作用
先通过下面一个例子来看看生成器的作用
var x = 1;
function* foo() {
x++;
yield; // 暂停!
console.log("x:", x);
}
function bar() {
x++;
}
ES5中我们没有办法在JavaScript中中断一个函数的运行,如没有办法在上面的foo函数中暂停,有了yield后我们可以这样实现一些操作
// 并没有执行生成器 *foo() ,而只是构造了一个迭代器 ,这个迭代器会控制它的执行
var it = foo();
// 启动了生成器 *foo() 并运行了 *foo() 第一行的 x++ 在 yield 语句处暂停
it.next();
x; // 2
bar(); // 通过 x++ 再次递增 x
x; // 3
it.next(); // 调用从暂停处恢复了生成器 *foo() 的执行 x: 3
除了暂停,yield与next组合起来也是一套双向消息传递系统,
每调用一次next,next函数中传递的参数替代前一个yield的值(第一次调用next没必要传值),代码执行到下一个yield之前或者没有yield则执行完毕,next的返回值value为下一个yield传递的值或者函数的返回值,
来看下面这个示例
function* foo(x) {
var y = x * (yield "Hello"); // <-- yield一个值!
return y;
}
var it = foo(6);
var res = it.next(); // 第一个next(),并不传入任何东西
res.value; // "Hello"
res = it.next(7); // 向等待的yield传入7
res.value; // 42
能看懂下面这个示例说明真的悟了~
function* foo() {
var x = yield 2;
z++;
var y = yield x * z;
console.log(x, y, z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; // 2 <-- yield 2
var val2 = it2.next().value; // 2 <-- yield 2
val1 = it1.next(val2 * 10).value; // 40 <-- x:20, z:2
val2 = it2.next(val1 * 5).value; // 600 <-- x:200, z:3
it1.next(val2 / 2); // y:300
// 20 300 3
it2.next(val1 / 4); // y:10
// 200 10 3
应用一:产生一系列值,其中每个值都与前面一个有特定的关系
闭包做法
var gimmeSomething = (function () {
var nextVal;
return function () {
if (nextVal === undefined) {
nextVal = 1;
} else {
nextVal = 3 * nextVal + 6;
}
return nextVal;
};
})();
gimmeSomething(); // 1
gimmeSomething(); // 9
gimmeSomething(); // 33
gimmeSomething(); // 105
生成器+迭代器
var something = (function () {
var nextVal;
return {
// for..of循环需要
[Symbol.iterator]: function () {
return this;
},
// 标准迭代器接口方法
next: function () {
if (nextVal === undefined) {
nextVal = 1;
} else {
nextVal = 3 * nextVal + 6;
}
return { done: false, value: nextVal };
},
};
})();
something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105
for (var v of something) {
console.log( v );
// 不要死循环!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
for of 的原理
假设 for (const a of target) { doSomething(a) } 就相当于以下代码
var iterator = target[Symbol.iterator]
while (true) {
var result = iterator.next()
if (result.done) break
doSomething(result.value)
}
这时候我们要再思考一个问题,可以直接for (const a of something) 进行遍历吗?答案是不行
我们要区分生成器和迭代器的概念,只有iterable才能进行for of,something只是生成器而不是迭代器,因此这样才行for (const a of something())
应用二:同步顺序的形式追踪流程控制,捕获异常
function foo(x, y) {
ajax("http://some.url.1/?x=" + x + "&y=" + y, function (err, data) {
if (err) {
// 向*main()抛出一个错误
it.throw(err);
} else {
// 用收到的data恢复*main()
it.next(data);
}
});
}
function* main() {
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
}
var it = main();
// 这里启动!
it.next();
仔细看这两行代码
var text = yield foo(11, 31);
console.log(text);
text可以接收到值,看似是通过阻塞同步代码实现的,实际上并不会阻塞整个程序,只是暂停了生成器本身的代码
并且在生成器内部有了看似同步的代码,但在背后foo的运行是异步的,书中是这样归纳好处的——
把异步作为实现细节抽象了出去,使得我们可以,以同步顺序的形式追踪流程控制:“发出一个 Ajax 请求,等它完成之后打印出响应结果。”
除此之外,try-catch也可以捕获到生成器中的错误,在异步代码中实现看似同步的错误处理在可读性和合理性方面都是一个巨大的进步
Promise+生成器
我们知道ajax是基于Promise的,因此上述例子可以改写为
function foo(x, y) {
return request("http://some.url.1/?x=" + x + "&y=" + y);
}
function* main() {
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
}
var it = main();
var p = it.next().value;
// 等待promise p决议
p.then(
function (text) {
it.next(text);
},
function (err) {
it.throw(err);
}
);
是不是比上述例子的阅读性要更强一点点,如果能不用再写Promise链那就更好了,下面再来看一种自动异步运行用户传入的生成器
自动异步运行生成器
function run(gen) {
var args = [].slice.call(arguments, 1),
it;
// 在当前上下文中初始化生成器
it = gen.apply(this, args);
// 返回一个promise用于生成器完成
return Promise.resolve().then(function handleNext(value) {
// 对下一个yield出的值运行
var next = it.next(value);
return (function handleResult(next) {
// 生成器运行完毕了吗?
if (next.done) {
return next.value;
}
// 否则继续运行
else {
return Promise.resolve(next.value).then(
// 成功就恢复异步循环,把决议的值发回生成器
handleNext,
// 如果value是被拒绝的 promise,
// 就把错误传回生成器进行出错处理
function handleErr(err) {
return Promise.resolve(it.throw(err)).then(handleResult);
}
);
}
})(next);
});
}
定义的 run(..) 返回一个 promise,一旦生成器完成,这个 promise 就会决议,或收到一个生成器没有处理的未捕获异常
生成器委托——在一个生成器中调用另一个生成器
以下是不用生成器委托而是用辅助函数run
function* foo() {
var r2 = yield request("http://some.url.2");
var r3 = yield request("http://some.url.3/?v=" + r2);
return r3;
}
function* bar() {
var r1 = yield request("http://some.url.1");
// 通过 run(..) "委托"给*foo()
var r3 = yield run(foo);
console.log(r3);
}
run(bar);
以下是通过yield *使用生成器委托
function* foo() {
var r2 = yield request("http://some.url.2");
var r3 = yield request("http://some.url.3/?v=" + r2);
return r3;
}
function* bar() {
var r1 = yield request("http://some.url.1");
// 通过 yeild* "委托"给*foo()
var r3 = yield* foo();
console.log(r3);
}
run(bar);
通过yield * 把迭代器实例控制委托给* foo()迭代器,等消耗完整个的 *foo() 迭代器,会再回到*bar()
yield * 暂停了迭代控制,而不是生成器控制。当你调用*foo() 生成器时,现在 yield 委托到了它的迭代器。但实际上,你可以 yield 委托到任意 iterable,yield *[1,2,3] 会消耗数组值 [1,2,3] 的默认迭代器。
消息委托例子
例子1——生成器委托
function* foo() {
console.log("inside *foo():", yield "B");
console.log("inside *foo():", yield "C");
return "D";
}
function* bar() {
console.log("inside *bar():", yield "A");
// yield委托!
console.log("inside *bar():", yield* foo());
console.log("inside *bar():", yield "E");
return "F";
}
var it = bar();
console.log("outside:", it.next().value);
// outside: A
console.log("outside:", it.next(1).value);
// inside *bar(): 1
// outside: B
console.log("outside:", it.next(2).value);
// inside *foo(): 2
// outside: C
console.log("outside:", it.next(3).value);
// inside *foo(): 3
// inside *bar(): D
// outside: E
console.log("outside:", it.next(4).value);
// inside *bar(): 4
// outside: F
例子2——数组迭代器
function* bar() {
console.log("inside *bar():", yield "A");
// yield委托给非生成器!
console.log("inside *bar():", yield* ["B", "C", "D"]);
console.log("inside *bar():", yield "E");
return "F";
}
var it = bar();
console.log("outside:", it.next().value);
// outside: A
console.log("outside:", it.next(1).value);
// inside *bar(): 1
// outside: B
console.log("outside:", it.next(2).value);
// outside: C
console.log("outside:", it.next(3).value);
// outside: D
console.log("outside:", it.next(4).value);
// inside *bar(): undefined
// outside: E
console.log("outside:", it.next(5).value);
// inside *bar(): 5
// outside: F
例子3——异常委托
function* foo() {
try {
yield "B";
} catch (err) {
console.log("error caught inside *foo():", err);
}
yield "C";
throw "D";
}
function* bar() {
yield "A";
try {
yield* foo();
} catch (err) {
console.log("error caught inside *bar():", err);
}
yield "E";
yield* baz();
// 注:不会到达这里!
yield "G";
}
function* baz() {
throw "F";
}
var it = bar();
console.log("outside:", it.next().value);
// outside: A
console.log("outside:", it.next(1).value);
// outside: B
console.log("outside:", it.throw(2).value);
// error caught inside *foo(): 2
// outside: C
console.log("outside:", it.next(3).value);
// error caught inside *bar(): D
// outside: E
try {
console.log("outside:", it.next(4).value);
} catch (err) {
console.log("error caught outside:", err);
}
// error caught outside: F
PS:4.6生成器并发、4.7形式转换程序 跳过了,有点吃不透了
手写生成器函数
function foo(url) {
// 闭包管理变量
// 管理生成器状态
var state;
// 生成器变量范围声明
var val;
function process(v) {
switch (state) {
// 状态1 起始
case 1:
console.log("requesting:", url);
// request(..)是一个支持Promise的Ajax工具
return request(url);
// 状态2 成功
case 2:
val = v;
console.log(val);
return;
// 状态3 失败
case 3:
var err = v;
console.log("Oops:", err);
return false;
}
}
// 构造并返回一个生成器
return {
next: function (v) {
// 初始状态
if (!state) {
state = 1;
return {
done: false,
value: process(),
};
}
// yield成功恢复
else if (state == 1) {
state = 2;
return {
done: true,
value: process(v),
};
}
// 生成器已经完成
else {
return {
done: true,
value: undefined,
};
}
},
throw: function (e) {
// 唯一的显式错误处理在状态1
if (state == 1) {
state = 3;
return {
done: true,
value: process(e),
};
}
// 否则错误就不会处理,所以只把它抛回
else {
throw e;
}
},
};
}
这个普通函数 foo(..)与生成器 *foo(..) 的工作几乎完全一样
调用 var it = foo("..") 和 it.next(..) ,就可以手工实例化生成器并控制它的迭代器 了,
我们也可以把它传给前面定义的工具 run(..)