Javascript零碎之事件循环机制
kekobin opened this issue · comments
单线程和异步
js的任务分为 同步 和 异步 两种,它们的处理方式也不同,同步任务是直接在主线程上排队执行,异步任务则会被放到任务队列中,若有多个任务(异步任务)则要在任务队列中排队等待,任务队列类似一个缓冲区,任务下一步会被移到调用栈(call stack),然后主线程执行调用栈的任务。
单线程是指js引擎中负责解析执行js代码的线程只有一个(主线程),即每次只能做一件事,而我们知道一个ajax请求,主线程在等待它响应的同时是会去做其它事的,浏览器先在事件表注册ajax的回调函数,响应回来后回调函数被添加到任务队列中等待执行,不会造成线程阻塞,所以说js处理ajax请求的方式是异步的。
总而言之,检查调用栈是否为空,以及确定把哪个task加入调用栈的这个过程就是事件循环,而js实现异步的核心就是事件循环。
macrotask & microtask
macrotask
包含执行整体的js代码,事件回调,XHR回调,定时器(setTimeout/setInterval/setImmediate),IO操作,UI render
microtask
更新应用程序状态的任务,包括promise回调,MutationObserver,process.nextTick,Object.observe
其中setImmediate和process.nextTick是nodejs的实现。
事件处理过程
大致如下图所示:
总结起来,一次事件循环的步骤包括:
- 检查macrotask队列是否为空,非空则到2,为空则到3
- 执行macrotask中的一个任务
- 继续检查microtask队列是否为空,若有则到4,否则到5
- 取出microtask中的任务执行,执行完成返回到步骤3
- 执行视图更新
这里需要注意,由于整体script就是一个宏任务,所以一开始在主线程执行完后,一定是去执行一次微任务,如下面的执行顺序一样。
记住: 事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。
由一个例子开始
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
这是常见的同步与异步的逻辑,那么就有一个逻辑的执行先后问题。可以想想上面的执行结果会是怎样呢?
任务队列
所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中执行;而异步任务会通过任务队列( Event Queue )的机制来进行协调。
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
所以可以看到上面首先会输出: 'script start'、script end',即一定是主线程的同步任务全部执行完后才会去执行异步任务。
现在来分析下上面代码的执行流程:
- 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
- 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
- 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函1. 数,将其分到微任务 Event Queue 中,记为 then2
- 遇到 console.log,输出 script end
- 整体script宏任务执行完后,执行所有的微任务,即首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有的微任务
- 然后继续取出一个宏任务进行执行,即执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout。
特别注意一下示例情况:
setTimeout(function() {console.log('timer1')}, 0)
setTimeout(function() {console.log('timer2')}, 0)
new Promise(function executor(resolve) {
console.log('promise 1')
setTimeout(() => {
resolve();
}, 50) // 这里的延时会使得then回调在setTimeout之后执行,因为延迟时间比所有setTimeout都长。
console.log('promise 2')
}).then(function() {
console.log('promise then')
})
console.log('end')
new Promise 中的代码无论是否在 resolve()前还是后,都会立即执行,只是then回调才会被推到微任务队列中。
额外知识点集
V8引擎
- emory Heap(内存堆) — 内存分配地址的地方
- Call Stack(调用堆栈) — 代码执行的地方
Runtime(运行时)
javascript运行时,一方面引用引擎自身提供的api进行解析,一方面使用浏览器提供的web api(DOM、AJAX、setTimeout等)进行工作.
调用栈
JavaScript是一种单线程编程语言,这意味着它只有一个调用堆栈。因此,它一次只能做一件事。
调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,它就会将其放置到栈顶,当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。
"堆栈溢出",当你达到调用栈最大的大小的时候就会发生这种情况,而且这相当容易发生,特别是在你写递归的时候却没有全方位的测试它。
参考
深入理解JavaScript事件循环机制
深入理解js事件循环机制(浏览器篇)
JavaScript 运行机制详解:再谈Event Loop