
四、JavaScript是如何运行的-执行阶段
一、单线程
JS单线程,指的是在JS引擎中,解析执行JS代码的调用栈是唯一的,所有的JS代码都在这一个调用栈里按照调用顺序执行,不能同时执行多个函数。
JS最初被设计使用在浏览器上,作为浏览器上的脚本语言,需要与用户的操作互动以及操作DOM, 如果多线程的话,需要关注各个线程之间状态的同步问题。
单线程有什么问题:
- 我们常见的异步操作(定时器延迟、网络请求、网页事件等), 事件触发之后并不能立即得到结果,按照之前的运行模式,浏览器就会阻塞其他操作,等待相应结果,表现在页面中就是页面卡死,这是一个优秀的应用所不能允许发生的。
什么是同步异步
- 一般操作可以分为两个步骤,发起调用、得到结果;
- 发起调用,立马可以得到结果的是为同步;
- 发起调用,无法立即得到结果,需要额外操作才能得到结果的是为异步。
所以这就需要一种解决方案:
- 问题的本质是js引擎的单线程工作模式,只专注于一件事情, 必须【执行至完成】
- 而产生问题的那些操作往往不能直接得到 结果,必须经过额外操作才能得到结果 【异步问题】
一种思路:
- 可以把这些异步操作分发给其他模块,得到处理结果之后再把回调函数一块放入主线程执行。
- 这就是 事件循环(Event Loop)的主体思路。
- event loop 只是解决异步问题的一种思路 其他的思路还有:轮询、事件
二、web API模块及js执行线程
- 异步操作可以交给JS引擎之外的其他模块处理, 在浏览器中其他模块就是Web API模块
- 所以现在JS引擎中,执行栈遇到同步函数调用,直接执行得到结果后,弹出栈,继续下一个函数调用
- 遇到异步函数调用,将函数分发给Web API模块,然后该异步函数弹出栈,继续下一个函数调用,不会产生阻塞问题。
- web api模块内就有多个线程,每个异步操作处理模块都对应一个线程,http请求线程、事件处理线程、定时器处理线程等
- js执行的过程中,会有4个线程参与执行,但是只有js引擎线程执行脚本文件,其他三个线程只是协助,不参与代码的解析与执行。
- JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)。
- 事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推进事件队列,等待JS引擎线程执行。
- 定时器触发线程:主要控制计时器setInterval和延时器setTimeout,用于定时器的计时,计时完毕,满足定时器的触发条件,则将定时器的处理函数推进事件队列中,等待JS引擎线程执行。
- 注:W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms。
- HTTP异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。
- 注:浏览器对同一域名请求的并发连接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。
总结:永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行。
现在存在一个问题:异步操作分发给Web API模块处理之后,不能说不管了,主线程还是需要知道结果做后续操作的,Web API得到结果之后怎么通知主线程呢?
三、回调队列(callback queue)
- 回调队列, 也叫事件队列、消息队列。
- 这个模块就是用来帮助Web API模块处理异步操作的。
- 在Web API模块中,异步操作在相应的线程中处理完成得到结果之后,会把结果注入异步回调函数的参数中,并且把回调函数推入回调队列中。
- 但是,只推到回调队列里也不是个事儿,因为所有的JS执行都发生在主线程调用栈里面。这些异步操作拿到结果之后,带着回调函数推入了回调队列,需要在适当的时机进入主线程调用栈执行。
- 那么,谁知道什么时候是合适的时机呢?
- Event Loop 知道。
四、事件循环机制 Event Loop
Event Loop 不停地检查主线程调用栈和回调队列,当发现主线程空闲的时候,就把回调队列里第一个任务推入主线程执行。 以此不停地循环。
至此,一个异步操作,兜兜转转最终拿到了结果,成功执行并且没有阻塞其他的操作。
明确一些概念:
- 宏任务macro-task | task:按执行顺序分为同步任务和异步任务
- 同步任务:在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)。
1 | console.log('script start'); |
- 异步任务:不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(task queue),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax,DOM事件,setTimeout等。
1 | setTimeout(function() {console.log('setTimeout');}, 0); |
五、深入事件循环
事件循环可以理解成由三部分:
- 主线程执行栈
- 异步任务等待触发
- 回调队列
任务队列(task queue)就是以队列的数据结构对事件任务进行管理,特点是先进先出,后进后出。(其实就是回调队列)
JS引擎主线程执行过程中:
- 首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈。
- 当执行栈中的函数调用到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。
- 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(task queue)中,等待主线程读取执行。
- 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
- 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(Event Loop)的过程。
1 | console.log('script start'); |
- JS引擎主线程按代码顺序执行,当执行到console.log(‘script start’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行。
- 执行到setTimeout(function() { console.log(‘setTimeout’); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行。
- JS引擎主线程执行到console.log(‘script end’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end。
- JS引擎主线程上的任务执行完毕(输出script start和script end)后,主线程空闲,则开始读取任务队列中的事件任务,将该任务队里的事件任务推进主线程中,按任务队列顺序执行,最终输出setTimeout,所以输出的结果顺序为script start script end setTimeout。
- 以上便是JS引擎执行宏任务的整个过程。
5.1、微任务
微任务micro-task:ES6和node环境中出现的一个任务类型,如果不考虑es6和node环境的话,我们只需要理解宏任务事件循环的执行过程就已经足够了,但是到了es6和node环境,我们就需要理解微任务的执行顺序了。微任务(micro-task)的API主要有:Promise, process.nextTick
1 | Promise.resolve().then(function() { console.log('promise1') }) |
加入了微任务后,新的Event Loop变成以下这样:
- 检查宏任务中同步任务,有同步任务执行。
- 检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件(异步任务),推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务。
- 执行新宏任务的事件任务,再检查是否存在可执行的微任务,如此不断的重复循环。
1 | console.log('script start'); |
执行过程如下:
代码块通过语法分析和预编译后,进入执行阶段,当JS引擎主线程执行到console.log(‘script start’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行。
JS引擎主线程执行到setTimeout(function() { console.log(‘setTimeout’); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行。
JS引擎主线程执行到Promise.resolve().then(function() { console.log(‘promise1’); }).then(function() { console.log(‘promise2’); });,JS引擎主线程认为Promise是一个微任务,这把该任务划分为微任务,等待执行。
JS引擎主线程执行到console.log(‘script end’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end。
主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个Promise微任务,那么立刻执行,输出promise1和promise2
微任务执行完毕,主线程开始读取任务队列中的事件任务setTimeout,推入主线程形成新宏任务,然后在主线程中执行,输出setTimeout
输出结果:
1 | script |