背景
几年前接触到了Event Loop大致了解了它的原理,最近又发看了一些相关资料,有了更深层次的理解,在这里记录一下。
浏览器
浏览器内核工作时主要的线程包括:
JS执行引擎线程
GUI页面渲染线程
计时器触发线程
事件监听线程
异步http网络请求线程。
JavaScript单线程原因
JS引擎是单线程,因为作为浏览器脚本语言,它的主要任务是:
嵌入动态文本于HTML页面
对浏览器事件做出响应
读写HTML元素
在数据被提交到服务器之前验证数据
检测访客的浏览器信息
控制cookies,包括创建和修改
基于Node.js技术进行服务器端编程
假如多线程的话会带来相当复杂的同步问题,例如:(两个线程一个appendChild了一个div,另外一个却在removeChild这个div,或者浏览器在运行一个复杂的图像算法时,调用堆栈有方法需要执行,就会被阻塞,无法执行其他任何代码)。单线程控制GUI在大多数GUI编程中也是普遍现象,JavaSpript是Netscape公司在1995年首次运用在浏览器页面上,早期设计成单线程也是顺理成章的。这里以Process-per-site-instance这种浏览器默认的进程模式展开。
主体内容
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
下图就是对这个模型的一个描述,
可以看到堆(FILO先进后出)、栈(FIFO先进后出)和队列,其中函数调用栈用来处理函数的执行顺序,任务队列则来处理其他代码的执行(如执行栈清空后执行的异步代码),有点类似于操作系统的进程调度,js在执行时也是将任务分为同步任务和异步任务,首先,同步任务在主线程上排队并依次执行形成一个执行栈(execution contexts),接着异步任务则进入任务队列(即上图的Queue),任务队列(task queue)又被分为(Macro-Task和Micro-Task)。
macro-task(宏任务)包括:
script(整体代码)
setTimeout
setInterval
setImmediate
requestAnimationFrame
I/O
UI rendering
micro-task(微任务)包括:
Promise.then catch finally
process.nextTick
MutationObserver
代码示例
setTimeout(function () {
console.log('【宏任务setTimeout1执行】macro_setTimeout_1');
}, 1000)
new Promise(function (resolve) {
console.log('【script执行】global_promise');
resolve();
}).then(function() {
console.log('【微任务promise .then执行】micro_then');
})
process.nextTick(function() {
console.log('【微任务nextTick执行】micro_nextTick');
})
setTimeout(function () {
console.log('【宏任务setTimeout2执行】macro_setTimeout_1');
}, 0)
执行结果如下:
可以看到,第一次执行时,js引擎会把整个script作为一个宏任务去执行,执行结束再去执行本次循环中的微任务,结束后再去执行宏任务队列中的下一条任务,反复循环,直至结束。如下流程图所示
总结:
- 事件循环的主要工作是监视调用堆栈和回调队列,调用堆栈为空,事件循环则从队列取出第一个时间并将其推送到调用堆栈,在调用堆栈会运行它。
- 异步实质是不阻塞事件循环。
- setTimeout执行后,是在截止设置的时间时再把回调方法推入执行队列的。
国外大神视频链接:
https://vimeo.com/96425312