js执行机制
JavaScript的运行机制
(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
主线程自上而下执行所有代码,同步任务直接进入到主线程被执行,而异步任务则进入到 Event Table
并注册相对应的回调函数,异步任务完成后,Event Table
会将这个函数移入 Event Queue
,主线程任务执行完了以后,会从Event Queue
中读取任务,进入到主线程去执行。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步
上述动作不断循环,就是我们所说的事件循环(Event Loop
)。
一个事件循环中有一个或者是多个任务队列
JavaScript中有两种异步任务:
宏任务(MacroTask)、微任务(MicroTask)
JavaScript
的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask
)和微任务(MicroTask
)。
宏任务,所有的同步任务代码都是MacroTask
(这么说其实不是很严谨,下面解释),setTimeout、setInterval、I/O、UI Rendering 等都是宏任务。
微任务,为什么说上述不严谨我却还是强调所有的同步任务都是 MacroTask
呢,因为我们仅仅需要记住几个 MicroTask
即可,排除法!别的都是 MacroTask
。MicroTask
包括:Process.nextTick、Promise.then catch finally(注意我不是说 Promise)、MutationObserver。
例1:
setTimeout(()=>{
console.log("setTimeout1");
Promise.resolve().then(data => {
console.log(222);
});
});
setTimeout(()=>{
console.log("setTimeout2");
});
Promise.resolve().then(data=>{
console.log(111);
});
Promise.resolve().then(data=>{
console.log(222);
});
运行结果为:
111
setTimeout1
222
setTimeout2
我们来详细说明一下, JS引擎是如何执行这段代码的:
- 主线程上没有需要执行的代码
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。
- 首先检查微任务队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输出 '111'。
- 此时microtask队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout的回调函数,立即执行回调函数输出 'setTimeout1',检查microtask 队列,发现队列不为空,执行promise的then回调,输出'222',microtask队列为空,进入下一个事件循环。
- 检查宏任务队列,发现有 setTimeout的回调函数, 立即执行回调函数输出'setTimeout2'。
例2:
console.log('script start');
setTimeout(function () {
console.log('setTimeout---0');
}, 0);
setTimeout(function () {
console.log('setTimeout---200');
setTimeout(function () {
console.log('inner-setTimeout---0');
});
Promise.resolve().then(function () {
console.log('promise5');
});
}, 200);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('script end');
运行结果为:
script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
我们来详细说明一下, JS引擎是如何执行这段代码的:
- 首先顺序执行完主进程上的同步任务,第一句和最后一句的console.log
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中(这个任务在下一次的事件循环中执行)。
- 接着遇到setTimeout 200,它的作用是在 200ms 后将回调函数放到宏任务队列中(这个任务在再下一次的事件循环中执行)。
- 同步任务执行完之后,首先检查微任务队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输出 'promise1',然后执行第二个promise的then回调,输出'promise3',由于第一个promise的.then()的返回依然是promise,所以第二个.then()会放到microtask队列继续执行,输出 'promise2';
- 此时microtask队列为空,进入下一个事件循环, 检查宏任务队列,发现有 setTimeout的回调函数,立即执行回调函数输出 'setTimeout---0',检查microtask 队列,队列为空,进入下一次事件循环.
- 检查宏任务队列,发现有 setTimeout的回调函数, 立即执行回调函数输出'setTimeout---200'.
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中,检查微任务队列,即 microtask 队列,发现此队列不为空,执行promise的then回调,输出'promise5'。
- 此时microtask队列为空,进入下一个事件循环,检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出,输出'inner-setTimeout---0'。代码执行结束.
### 为什么会需要event-loop
因为 JavaScript 是单线程的。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。
最后有一点需要注意的是:本文介绍的是浏览器的Event-loop,因此在测试验证时,一定要使用浏览器环境进行测试验证,如果使用了node环境,那么结果不一定是如上所说。