JavaScript 同步和异步

本篇博客整理了一下我对JavaScript同步和异步的简单理解,我认为还是比较详细和好懂的。其中部分借鉴了阮一峰前辈的博客(博客链接突然失效我找不到了),在这里先感谢阮一峰前辈以及其他前辈的解释~

一、为什么JavaScript是单线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、同步与异步任务

JavaScript 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个[执行栈](execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

三、事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。见下图:

怎么理解?

比如我们给一个按钮绑定了onclick点击事件,它的回调函数只有在用户点击了按钮之后才会调用,因此每次用户一点击,js主线程就会去任务队列查看并执行,这就是事件循环。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

五、setTimeout案例分析

我们来看一个setTimeout案例:

setTimeout(function(){
	console.log("setTimeout");
},3000);
console.log("哈哈代码");

在上面这段代码中,很容易想象到是先输出”哈哈代码“,再过3秒输出”setTimeout“。事实上也是如此。

这时候可能就会有人认为,这是因为setTimeout有3秒钟的延时,因此输出比“哈哈代码”要慢是正常的。

那么我们来对代码做一个简单的修改:

setTimeout(function(){
	console.log("setTimeout");
},0);

for(var i=0;i<100000;i++){
	 i += i;
}
console.log("哈哈代码");

在这段代码中,我将定时器的时间改为0,并且在中间加入了一段较为耗时的for循环。

按照道理来讲,JavaScript是从上到下执行的,应该是先输出“setTimeout”,然后经过一段时间(for循环的时间)后输出“哈哈代码”才是。

但事与愿违,实际上的结果是:先隔一段时间(执行for循环),再输出“哈哈代码”,然后才是“setTimeout”。

这是为什么呢?

实际上,我们在上面提到的js同步异步的知识已经可以理解这个现象了。我是这么理解的,setTimeout实际上在主线程中,只是将回调函数放入任务队列中,然后它就功成身退了。js主线程继续往下执行→for循环→“哈哈代码”。

在js主线程执行完全部的代码后,即空闲时,它会查看任务队列,有没有需要执行的任务。此时它就发现了一名“弃婴”,即setTimeout的回调函数,执行函数。另外,任务队列是队列,因此是先进先出的,先进来的任务,就先完成。

这样的机制在回调函数里面也是适用的,如下代码:

setTimeout(function(){
	setTimeout(function(){
		console.log("setTimeout02");
	},3000);
	console.log("setTimeout");
},3000);

var str = "";
for(var i=0;i<1000000;i++){
	 str += i;
}
console.log("哈哈代码");

输出结果是:先隔一段时间(执行for循环)→哈哈代码→“setTimeout”→“setTimeout02”。

我们可以知道在setTimeout的回调函数里面,他也是按照这样的机制来执行代码的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值