JavaScript单线程模型与事件循环

702 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、单线程模型

JavaScript 是单线程的,异步非阻塞的。

单线程模型

单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。

注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合,例如GUI渲染线程、定时器线程、http异步线程等。

JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。

如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

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

异步非阻塞

所谓异步非阻塞,就是当主线程遇到了setTimout、Ajax请求等比较耗时的操作时,直接交给webapi处理,这是异步。而此时主线程并不会等待返回结果,而是继续运行同步代码,这是非阻塞

二、任务队列与事件循环

JavaScript异步非阻塞的机制是依靠事件循环支持的。

任务队列

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,可分为微任务和红宏任务)。为了方便理解,这里假设只存在一个队列。)

事件循环

首先,主线程会去执行所有的同步任务,此时可能会遇到需要异步处理的任务,例如setTimeout、Ajax请求等,这需要交给其他线程处理,当其他线程处理完之后,再将回调函数放入任务队列

等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

 console.log('主线程开始!')
 ​
 setTimeout(function() {
     console.log('setTimeout完成!')
 }, 10);
 ​
 console.log('主线程结束!')
 ​
 //主线程开始!
 //主线程结束!
 //setTimeout完成!

简述上述代码的过程,首先同步执行整个代码,输出主线程开始!,接着遇到setTimeout,交给定时器线程处理,此时并不会等待结果,而是继续同步运行,输出主线程结束!,此时setTimout的回调函数已经被放在任务队列中了(准确说是10ms之后),将回调函数从任务队列中取出来,在主线程中运行,输出setTimeout完成!

image.png