- JavaScript中的异步,是指程序一部分现在运行,而一部分则在将来运行——现在和将来有时间间隔,在这段间隔中,程序没有活跃执行。
- 程序现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
一、分块的程序
- 程序中将来执行的部分并不一定在现在运行的部分执行完之后就立刻执行。比如:Ajax发送请求,需要等待数据返回之后,才会执行“将来”的部分。
- 从现在到将来的“等待”,最简单的方法是使用一个通常称为回调函数的函数。(不是唯一的方案)
举个例子
function now() {
return 21;
}
function later() { // 将来执行的部分
answer = answer * 2;
console.log("Meaning of life:", answer);
}
var answer = now();
setTimeout(later, 1000); // Meaning of life: 42
- 这个程序有两个块:现在执行的部分,以及将来执行的部分。
对异步机制的理解:
- 任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax响应式)时执行,就是在代码创建了一个将来执行的块,也由此在这个程序引入了异步机制。
异步控制台
console.*
方法并不是JavaScript正式的一部分,而是宿主环境添加到JavaScript中的。
某些浏览器的console.log()
并不会把传入的内容立即输出,可能出现异步情况。
这种情况比较少见,最好的选择是使用断点调试。
二、事件循环
- JavaScript引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是web浏览器。
- 宿主环境提供了一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript引擎,这种机制被称为事件循环。
- JavaScript引擎本身没有时间的概念,只是需要一个按需执行JavaScript任意代码判断的环境。
以setTimout为例
- 它所做的是设定一个定时器。当定时器到时,环境会把你的回调函数放到事件循环中。在未来某个时刻的
tick
就会摘下并执行回调。 - 如果这时候事件循环中有其他项目了,那么该回调函数就会排到最后面,等待前面的执行完成。
- 这也是**
setTimeout()
定时器精度不高**的原因。
ES6精确指定了事件循环的工作细节,主要是Promise
的引入。
因为这项技术要求对事件循环队列的调度运行能够直接进行精确控制。
三、并行线程
-
术语”异步“和”并行“常常被混为一谈,但实际上它们的意义完全不同。
- 异步是关于现在和将来的时间间隙,而并行是关于能够同时发送的事情。
- 并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行。
- 事件循环把自身工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。
-
并行线程的交替执行和异步事件的交替调度,其粒度是完全不同的。
- 前者语句层面,后者函数(事件)层面。
竞态条件
- 同一段代码执行顺序存在不确定性,这种执行顺序的不确定是在函数(事件)顺序级别上。
- 这种函数顺序不确定就是通常所说的竞态条件。
代码说明
var a = 1;
var b = 2;
function foo() { // 异步
a++;
b = b + 3;
a = b + 3;
}
function bar() { // 异步
b--;
a = 8 + b;
b = a * 2
}
ajax("http://some.url.1", foo);
ajax("http://some.url.2", bar);
- 上面
foo()
和bar()
函数是异步执行的,而且执行顺序是不确定的。 - 可能
foo()
也可能是bar()
,取决于哪个ajax请求先得到结果。 - 也就是说
foo()
和bar()
互相竞争,看谁先运行。所以无法可靠预测a和b的结果。
四、并发
- 有多个”进程“同一段时间内执行就出现了并发,虽然它们不是并行执行。这与运算级的并行相对。(不是严格意义上的进程,而是对于JavaScript来说的异步过程。)
- 比如:用户滚动页面更新列表。如果滚动地特别快的话,等待第一个响应返回结果的时候,已经触发了多次滚动事件。在这段时间内,就有多个异步任务执行,而且还没有得到结果。
- JavaScript一次只能处理一个事件,所以要么先处理响应结果,要么先下一个发出请求。单线程事件循环是并发的一种形式。(
foo()
和bar()
的例子也是并发)
非交互
- 如果”进程“间没有互相影响,那么不确定性是完全可以接受的。比如前面
foo()
和bar()
的例子。
交互
- 常见的情况是,”进程“之间需要互相交流,那么需要进行协调以避免竞态。
五、任务
- 在ES6中,有一个新的概念建立在事件循环之上,叫做任务队列。
- 任务队列中挂载事件循环队列每个
tick
之后的一个队列。
(其他相关解释:执行宏任务时,如果产生微任务,先将微任务放置到微任务队列,在本次宏任务执行结束时,执行微任务队列中的微任务。如果在微任务执行过程中,又产生微任务,就继续添加到微任务队列末尾,也是在本次宏任务执行。)