很久之前我整理过关于JS执行机制的一些知识,并发表了一篇博客。但是前段时间进一步了解了JS的执行机制,才发现自己以前的一些理解可能并不完整。今天得闲将目前自己对JS执行机制的了解记录博客。
一、什么是进程和线程?
本质上来说,这两个名词都是CPU
工作时间片的一个描述;
进程描述了CPU
在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。线程是进程中的更小单位,描述了执行一段指令所需的时间;
把这些概念拿到浏览器来说,当你打开一个Tab
页,其实就是创建了一个进程,一个进程中可以有多个线程,如渲染线程,JS
引擎线程,HTTP
请求线程等等。比如,当你发出一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
二、JavaScript为什么是单线程?
JavaScript
作为浏览器的脚本语言,主要用途是与用户互动,操作DOM
等,这决定了它只能是单线程,否则会带来复杂的同步问题。
比如,上文提到了JS
的引擎线程和渲染线程,这两个线程是互斥的,JS
运行时可能会阻止UI
渲染。这其中的原因是因为JS
可以修改DOM
,如果在JS
执行的时候UI
线程也在工作,就可能导致不能安全的渲染UI
。假定JavaScript
同时有两个线程,一个线程在某个DOM
节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
这其实也是单线程的好处,得益于JS
是单线程运行的,可以达到节省内存,节约上下文切换时间,没有锁的问题的好处。
为了利用多核CPU
的计算能力,HTML5
提出Web Worker
标准,允许JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
。所以,这个新标准并没有改变JavaScript
单线程的本质。
三、什么是执行栈?
可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出原则。
当执行JS
代码时,首先会执行一个main
函数,然后执行我们的代码。根据先进后出原则,后执行的函数会先弹出栈。
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数没有释放,就会出现爆栈的问题。
function foo() {
foo();
}
foo();
四、同步与异步任务
JavaScript
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU
忙不过来,倒也算了,但是很多时候CPU
是闲着的,因为IO
设备(输入输出设备)很慢(比如Ajax
操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript
语言的设计者意识到,这时主线程完全可以不管IO
设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO
设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous
),另一种是异步任务(asynchronous
)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程,而进入"任务队列"(task queue
)的任务。只有主线程执行完毕,并且task queue
通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行。
(2)主线程之外,还存在一个"任务队列"(`task queue`)。只要异步任务有了运行结果,就在`task queue`之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取`task queue`,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
五、事件和回调函数
“任务队列"是一个事件的队列(也可以理解成消息的队列),