前言
本系列最开始是为了自己面试准备的.后来发现整理越来越多,差不多有十二万字符,最后决定还是分享出来给大家.
为了分享整理出来,花费了自己大量的时间,起码是只自己用的三倍时间.如果喜欢的话,欢迎收藏,关注我!谢谢!
文章链接
- 前端面试查漏补缺--(一) 防抖和节流
- 前端面试查漏补缺--(二) 垃圾回收机制
- 前端面试查漏补缺--(三) 跨域及常见解决办法
- 前端面试查漏补缺--(四) 前端本地存储
- 前端面试查漏补缺--(五) 渲染机制及重绘和回流
- 前端面试查漏补缺--(六) 浏览器缓存
- 前端面试查漏补缺--(七) XSS攻击与CSRF攻击
- 前端面试查漏补缺--(八) 前端加密
- 前端面试查漏补缺--(九) HTTP与HTTPS
- 前端面试查漏补缺--(十) 前端鉴权
- 前端面试查漏补缺--(十一) 前端软件架构模式MVC/MVP/MVVM
- 前端面试查漏补缺--(十二) 从输入URL到看到页面发生的全过程(含三握手,四挥手详解)
- 前端面试查漏补缺--(十三) 内存泄漏
- 前端面试查漏补缺--(十四) 算法及排序
- 前端面试查漏补缺--(十五) Event Loop
合集篇:
前端面试查漏补缺--Index篇(12万字符合集) 包含目前已写好的系列其他十几篇文章.后续新增值文章不会再在每篇添加链接,强烈建议议点赞,关注合集篇!!!!,谢谢!~
Event loop的初步理解
相信大家如果对Event loop有一定了解的话,大概都会知道它的大概步骤是:
- 1,Javascript的事件分为同步任务和异步任务.
- 2,遇到同步任务就放在执行栈中执行.
- 3,遇到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件.
上面的步骤说得没错,很对,但是太浅了.现在的面试应该不会这么简单,起码得加上宏任务,微任务.再整上几个Promise,async await,让你判断.否则都不好意思叫面试题!
为了避免被面试官吊起来打的情况,我们现在来详细地理一理Event Loop.
Event loop 相关概念
JS调用栈
Javascript 有一个 主线程(main thread)和 调用栈(call-stack),所有的代码都要通过函数,放到调用栈(也被称为执行栈)中的任务等待主线程执行。
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
WebAPIs
MDN的解释: Web 提供了各种各样的 API 来完成各种的任务。这些 API 可以用 JavaScript 来访问,令你可以做很多事儿,小到对任意 window 或者 element做小幅调整,大到使用诸如 WebGL 和 Web Audio 的 API 来生成复杂的图形和音效。
总结: 就是浏览器提供一些接口,让JavaScript可以调用,这样就可以把任务甩给浏览器了,这样就可以实现异步了!
任务队列(Task Queue)
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,如果存在"定时器",主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
同步任务和异步任务
Javascript单线程任务被分为同步任务和异步任务.
- 同步任务会在调用栈 中按照顺序等待主线程依次执行.
- 异步任务会甩给在WebAPIs处理,处理完后有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
宏任务(MacroTask)和 微任务(MicroTask)
在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
宏任务(MacroTask)
script(整体代码)、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O、UI Rendering。
微任务(MicroTask)
Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver(具体使用方式查看这里)
Event loop 执行过程
首先宏观上是按照这样的顺序执行.也就是前面在"Event loop的初步理解"里讲到的过程
注意:
- 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
- 在上图的Event Table里存放着宏任务与微任务,所以在它里面 还发生了一些更细致的事情.
前面介绍宏任务的时候,提过script也属于其中.那么一段代码块就是一个宏任务。故所有一般执行代码块的时候,先执行的是宏任务script,也就是程序执行进入主线程了,主线程再会根据不同的代码再分微任务和宏任务等待主线程执行完成后,不停地循环执行。
主线程(宏任务) => 微任务 => 宏任务 => 主线程
事件循环的顺序是从script开始第一次循环,随后全局上下文进入函数调用栈,碰到macro-task就将其交给处理它的模块处理完之后将回调函数放进macro-task的队列之中,碰到micro-task也是将其回调函数放进micro-task的队列之中。直到函数调用栈清空只剩全局执行上下文,然后开始执行所有的micro-task。当所有可执行的micro-task执行完毕之后。 接着浏览器会执行下必要的渲染 UI,然后循环再次执行macro-task中的一个任务队列,执行完之后再执行所有的micro-task,就这样一直循环。
注意: 通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。
例子分析
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
这里需要先理解async/await。
async/await 在底层转换成了 promise 和 then 回调函数。
也就是说,这是 promise 的语法糖。
每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。
async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。
关于73以下版本和73版本的区别
- 在73版本以下,先执行
promise1和promise2,再执行async1。 - 在73版本,先执行
async1再执行promise1和promise2。
主要原因是因为在谷歌(金丝雀)73版本中更改了规范,如下图所示:
- 区别在于
RESOLVE(thenable)和之间的区别Promise.resolve(thenable)。
在73以下版本中
- 首先,传递给
await的值被包裹在一个Promise中。然后,处理程序附加到这个包装的Promise,以便在Promise变为fulfilled后恢复该函数,并且暂停执行异步函数,一旦promise变为fulfilled,恢复异步函数的执行。 - 每个
await引擎必须创建两个额外的 Promise(即使右侧已经是一个Promise)并且它需要至少三个microtask队列ticks(tick为系统的相对时间单位,也被称为系统的时基,来源于定时器的周期性中断(输出脉冲),一次中断表示一个tick,也被称做一个“时钟滴答”、时标。)。
引用贺老师知乎上的一个例子
async function f() {
await p
console.log('ok')
}
简化理解为:
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
- 如果
RESOLVE(p)对于p为promise直接返回p的话,那么p的then方法就会被马上调用,其回调就立即进入job队列。 - 而如果
RESOLVE(p)严格按照标准,应该是产生一个新的promise,尽管该promise确定会resolve为p,但这个过程本身是异步的,也就是现在进入job队列的是新promise的resolve过程,所以该promise的then不会被立即调用,而要等到当前job队列执行到前述resolve过程才会被调用,然后其回调(也就是继续await之后的语句)才加入job队列,所以时序上就晚了。
谷歌(金丝雀)73版本中
- 使用对
PromiseResolve的调用来更改await的语义,以减少在公共awaitPromise情况下的转换次数。 - 如果传递给
await的值已经是一个Promise,那么这种优化避免了再次创建Promise包装器,在这种情况下,我们从最少三个microtick到只有一个microtick。
详细过程:
73以下版本
- 首先,打印
script start,调用async1()时,返回一个Promise,所以打印出来async2 end。 - 每个
await,会新产生一个promise,但这个过程本身是异步的,所以该await后面不会立即调用。 - 继续执行同步代码,打印
Promise和script end,将then函数放入微任务队列中等待执行。 - 同步执行完成之后,检查微任务队列是否为
null,然后按照先入先出规则,依次执行。 - 然后先执行打印
promise1,此时then的回调函数返回undefinde,此时又有then的链式调用,又放入微任务队列中,再次打印promise2。 - 再回到
await的位置执行返回的Promise的resolve函数,这又会把resolve丢到微任务队列中,打印async1 end。 - 当微任务队列为空时,执行宏任务,打印
setTimeout。
谷歌(金丝雀73版本)
- 如果传递给
await的值已经是一个Promise,那么这种优化避免了再次创建Promise包装器,在这种情况下,我们从最少三个microtick到只有一个microtick。 - 引擎不再需要为
await创造throwaway Promise- 在绝大部分时间。 - 现在
promise指向了同一个Promise,所以这个步骤什么也不需要做。然后引擎继续像以前一样,创建throwaway Promise,安排PromiseReactionJob在microtask队列的下一个tick上恢复异步函数,暂停执行该函数,然后返回给调用者。
具体详情查看(这里)。
Node.js的Event Loop
Node.js的运行机制如下。
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
Node 的 Event loop 分为 6 个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
Node的Event loop一共分为6个阶段,每个细节具体如下:
timers: 执行setTimeout和setInterval中到期的callback。pending callback: 上一轮循环中少数的callback会放在这一阶段执行。idle, prepare: 仅在内部使用。poll: 最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。check: 执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)的callback。close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。
关于Node.js的Event Loop更详细的过程可以参考这篇文章
补充: 线程和进程
在概念上
进程是应用程序的执行实例,每一个进程都是由私有的虚拟地址空间、代码、数据和其它系统资源所组成;进程在运行过程中能够申请创建和使用系统资源(如独立的内存区域等),这些资源也会随着进程的终止而被销毁。
而线程则是进程内的一个独立执行单元,在不同的线程之间是可以共享进程资源的,所以在多线程的情况下,需要特别注意对临界资源的访问控制。在系统创建进程之后就开始启动执行进程的主线程,而进程的生命周期和这个主线程的生命周期一致,主线程的退出也就意味着进程的终止和销毁。主线程是由系统进程所创建的,同时用户也可以自主创建其它线程,这一系列的线程都会并发地运行于同一个进程中。
比喻理解
一个进程好比是一个工厂,每个工厂有它的独立资源(类比到计算机上就是系统分配的一块独立内存),而且每个工厂之间是相互独立、无法进行通信。
每个工厂都有若干个工人(一个工人即是一个线程,一个进程由一个或多个线程组成),多个工人可以协作完成任务(即多个线程在进程中协作完成任务),当然每个工人可以共享此工厂的空间和资源(即同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等))。
到此你应该能初步理解了进程和线程之间的关系,这将有助于我们理解浏览器为什么是多进程的,而JavaScript是单线程。
浏览器是多进程的(一个窗口就是一个进程),每个进程包含多个线程.但JavaScript是单线程的.一个主线程(一个stack),多个子线程.
为什么JavaScript是单线程?
假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
浏览器中其他的线程:
js既然是单线程,那么肯定是排队执行代码,那么怎么去排这个队,就是Event Loop。虽然JS是单线程,但浏览器不是单线程。浏览器中分为以下几个线程:
- js线程
- UI线程
- 事件线程(onclick,onchange,...)
- 定时器线程(setTimeout, setInterval)
- 异步http线程(ajax)
其中JS线程和UI线程相互互斥,也就是说,当UI线程在渲染的时候,JS线程会挂起,等待UI线程完成,再执行JS线程
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
感谢及参考
- 一次弄懂Event Loop(这篇文章是写得真的非常详细,应该是全网最全的了,如果看完我写的,还不太理解的,强烈建议看这篇)
- event loop一篇文章足矣
求内推
如果有好的前端岗位,欢迎内推我,谢谢。
本科,三年运维,五年前端。 base 武汉 邮箱 bupabuku@foxmail.com