Javascript中的异步与性能(一) -- 事件循环,并行与任务

可以把 JavaScript 程序写在单个 .js 文件中,但是这个程序几乎一定是由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。

JavaScript 引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是Web 浏览器。经过最近几年(不仅于此)的发展,JavaScript 已经超出了浏览器的范围,进入了其他环境,比如通过像 Node.js 这样的工具进入服务器领域。实际上,JavaScript 现如今已经嵌入到了从机器人到电灯泡等各种各样的设备中。

(一)事件循环

  • 它们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JavaScript引擎,这种机制被称为事件循环。换句话说,JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。
  • 程序通常分成了很多小块,在事件循环队列中一个接一个地执行。严格地说,和你的程序不直接相关的其他事件也可能会插入到队列中。
// eventLoop是一个用作队列的数组
// (先进,先出)
var eventLoop = [];
var event;
// “永远”执行
while (true) {
	// 一次tick
	if (eventLoop.length > 0) {
		// 拿到队列中的下一个事件
		event = eventLoop.shift();
		// 现在,执行下一个事件
		try {
			event();
		}
		catch (err) {
			reportError(err);
		}
	}
}

setTimeout(…) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。因此,setTimeout(…) 定时器的精度可能不高,大体说来,只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。

(二)并行线程

术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。
并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。
示例代码:

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 );

①多线程的情况
解析内存中foo和bar的数据存储
线程 1(X 和 Y 是临时内存地址):
foo():
a. 把a的值加载到X
b. 把1保存在Y
c. 执行X加Y,结果保存在X
d. 把X的值保存在a
线程 2(X 和 Y 是临时内存地址):
bar():
a. 把a的值加载到X
b. 把2保存在Y
c. 执行X乘Y,结果保存在X
d. 把X的值保存在a
1,2线程按某一顺序并行运行:
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)
a 的结果将是 44。
如果1,2线程按照其它顺序执行:
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)
a 的结果将是 21。

所以,多线程编程是非常复杂的。因为如果不通过特殊的步骤来防止这种中断和交错运行的话,可能会得到出乎意料的、不确定的行为,通常这很让人头疼。

②单线程的情况
根据 JavaScript 的单线程运行特性,如果 foo() 运行在 bar() 之前,a 的结果是 42,而如果bar() 运行在 foo() 之前的话,a 的结果就是 41。
JavaScript 从不跨线程共享数据,这意味着不需要考虑并发线程这一层次的不确定性。但是这并不意味着 JavaScript 总是确定性的,foo() 和 bar() 的相对顺序改变可能会导致不同结果(41 或 42)。

在 JavaScript 的特性中,这种函数顺序的不确定性就是通常所说的竞态条件(race condition),foo() 和 bar() 相互竞争,看谁先运行。具体来说,因为无法可靠预测 a 和 b的最终结果,所以才是竞态条件。

(三)任务

事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,你需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。
任务和 setTimeout(…0) hack 的思路类似,但是其实现方式的定义更加良好,对顺序的保证性更强:尽可能早的将来。

console.log( "A" ); 
setTimeout( function(){ 
	console.log( "B" ); 
}, 0 ); 
// 理论上的"任务API" 
schedule( 
	function(){ 
		console.log( "C" ); 
		schedule( function(){ 
		console.log( "D" ); 
	} ); 
} );

可能你认为这里会打印出 A B C D,但实际打印的结果是 A C D B。因为任务处理是在当前事件循环 tick 结尾处,且定时器触发是为了调度下一个事件循环 tick。

(四)总结

JavaScript程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。
一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个tick。用户交互、IO 和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一个或多个后续事件。
并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Funnee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值