单线程意味着什么?
意味着,正常情况下,JS引擎会按照代码书写的逻辑,依次调用函数,在任意时间点,有且只能有一个函数被执行。外层函数必须等到内层函数处理完毕有返回值之后才能继续执行。
为什么要单线程?
JS最初被设计使用在浏览器上,作为浏览器上的脚本语言,需要与用户的操作互动以及操作DOM, 如果时多线程的话,需要关注各个线程之间状态的同步问题。
想象一下,js引擎中可以同时执行2个函数,如果两个函数操作同一个对象,那么到底以哪个为准?
然后,操作DOM结构,在线程A上已经删了某节点,线程B同时还在对该节点一顿操作,这就尴尬了。
而做多线程的状态同步又是得不偿失的,因此就直接用单线程了。
有什么问题?
假如,某个函数耗时比较久,那么调用它的外层函数必须安安静静的等待这个函数执行完成才能继续执行, 如果,这个函数出错了,那么外层的函数也没办法继续执行了。
function foo(){ bar() console.log('after bar') } function bar(){ while(true) {} } foo()
比如这个栗子,foo函数中调用了bar函数,那么foo函数必须等待bar函数执行完毕才能继续执行后续代码,然而bar函数是个无限执行的函数哟,回不来的,那么foo函数等到花儿都谢了也没办法执行后面的代码的。
当然这是比较极端的情况,但是在前端的业务场景中有几类常见的case,在此是有问题的:
- 定时器延迟操作
- 网络请求
- 网页事件等
这些就是我们常见的异步操作
, 事件触发之后并不能立即得到结果,按照之前的运行模式,浏览器就会阻塞其他操作,等待相应结果,表现在页面中就是页面卡死,这是一个优秀的应用所不能允许发生的。
同步&异步
关于同步和异步操作,借用朴灵大神的说法:
一般操作可以分为两个步骤,
- 发起调用
- 得到结果
发起调用,立马可以得到结果的是为
同步
发起调用,无法立即得到结果,需要额外操作才能得到结果的是为
异步
因此当前的模型在异步操作
中是有问题的
解决方案?
问题的本质是js引擎的单线程工作模式,只专注于一件事情, 必须【执行至完成】
而产生问题的那些操作往往不能直接得到 结果,必须经过额外操作才能得到结果 【异步问题】
思路:可以把这些异步操作
分发给其他模块
,得到处理结果之后再把回调函数一块放入主线程执行。
这就是 事件循环(Event Loop)的主体思路。
event loop 只是解决异步问题的一种思路 其他的思路还有:
- 轮询
- 事件
Web API's
上一章节提到,异步操作
可以交给JS引擎之外的其他模块
处理, 在浏览器中其他模块
就是Web API模块
Web API
其实就是上述的 JS runtime
提供的一系列宿主环境的特性集合。
在浏览器中,主要包括以下能力:
- Event Listeners
- HTTP request
- Timing functions
完美的cover了上述产生异步问题的几类case
因此至此,我们可以得出以下的视图:
JS引擎中,执行栈遇到同步函数调用,直接执行得到结果后,弹出栈,继续下一个函数调用
遇到异步函数调用,将函数分发给Web API
模块,然后该异步函数弹出栈,继续下一个函数调用,不会产生阻塞问题。
问题来了?
这些异步操作分发给Web API
模块处理之后,不能说不管了,主线程
还是需要知道结果做后续操作的,Web API
得到结果之后怎么通知主线程呢?
这就需要其他的模块帮忙了。
这个地方提到了
主线程
,就意味着还有其他的辅助线程。
是的,JS是
单线程执行
的,但是并不意味着浏览器内核是单线程的。
事实上,web api模块内就有多个线程,每个异步操作处理模块都对应一个线程
http请求线程、事件处理线程、定时器处理线程等
回调队列 (callback queue)
回调队列, 也叫事件队列、消息队列。
这个模块就是用来帮助Web API
模块处理异步操作的。
在Web API
模块中,异步操作在相应的线程中处理完成得到结果之后,会把结果注入异步回调函数的参数中,并且把回调函数推入回调队列中。
但是,只推到回调队列里也不是个事儿,因为前面说到了,所有的JS执行都发生在主线程调用栈里面。这些异步操作拿到结果之后,带着回调函数推入了回调队列,需要在适当的时机进入主线程调用栈执行。
那么,谁知道什么时候是合适的时机呢?
Event Loop 知道。
回调队列
队列是一个FIFO,先进先出的存储结构,
这样意味着异步操作的回调函数会按照进入队列的顺序被执行,而不是调用的顺序被执行
Event Loop
Event Loop 不停地检查主线程调用栈和回调队列,当发现主线程空闲的时候,就把回调队列里第一个任务推入主线程执行。 以此不停地循环。
至此,一个异步操作,兜兜转转最终拿到了结果,成功执行并且没有阻塞其他的操作。
overview
一个完整的图示如下:
至此,本文在宏观结构上按照浏览器执行js文件的步骤,分析了浏览器环境的一些简单机制。 更多的执行细节,比如词法分析、作用域构建等在后续文章中会继续深入。