对单线程 Javascript 的理解
单线程来源
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
对页面交互的同步处理 js主要用途是与用户互动,以及操作DOM,若为多线程将会造成严重的同步问题
Web Workers 和 Service Workers 的理解
web worker就是一个后台执行JS文件的方法,能够给前端传递信息,前端也可以传递信息给web wokers。
- web workers是一个全新的上下文,与创建它的线程无关。
- 不可以执行dom操作
- 没有window这个对象
- 通过postMessage传递消息 主线程的阻塞并不会影响workers的异步执行,只是会影响它的输出,因为它是通过发送消息给主线程输出的,所以会等主线程执行完,才会按照顺序执行workers 返回的事件队列。 如果workers可以操作DOM的,那么很容易无法更新到最新的状态。
Service worker是一个注册在指定源和路径下的事件驱动worker。 它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。本质上可充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。
- 缺点:非常明显,开启service worker可能会导致浏览器的缓存数据大大增加。
异步事件机
不进入主线程、而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
回调函数,就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
异步就是利用消息队列和事件循环。
一句话概括就是: 工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。
- 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
- 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。
实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
为什么使用异步事件机制
不妨碍主线程工作,类似应急车道 A: I/O 类型的任务会有较长的等待时间。使用异步任务的方式,只要异步任务有了运行结果,再进行处理。这个过程中浏览器就不用处于等待状态,CPU 也可以处理其他任务。
在浏览器中,任务可以分为同步任务和异步任务两种。同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行后一个任务。异步任务进入"任务队列"的任务,当该任务完成后,"任务队列"通知主线程,该任务才会进入主线程排队执行。
在实际使用中异步事件可能会导致什么问题
回调地狱 怎么避免回调地狱?
1、写浅一些。别嵌套太深。
2、使用模块化技术,别一个模块里做很多事,而是分成不同模块。
3、对错误进行单独处理,而不是套很多错误处理的回调。
if (error) return console.error('Uhoh, there was an error', error)
比如这种风格。
关于 setTimeout、setInterval 的时间精确性
setTimeout和setInterval,这两个js函数都是用来定时执行。setTimeout执行一次,setInterval执行多次。
问题的出现,使用setInterval时,设置执行速度为1ms。这时setInterval就出现了延迟。它并没有严格按照1ms的速度执行。
原因:要从javascript的单线程机制说起。对于长时间执行的任务设置短暂的时间间隔,那么在第一次执行完成之前,可能会由于执行不断的迭代造成延迟。
因为setInterval的回调函数并不是到时后立即执行,而是等系统资源空闲下来后才会执行.而下一次触发时间则是在setInterval回调函数执行完毕之后才开始计时,所以如果setInterval内执行的计算过于耗时,或者有其他耗时任务在执行,setInterval的计时会越来越不准,延迟很厉害。
其实setTimeout和setInterval所谓的“异步调用”事实上是通过将代码段插入到代码的执行队列中实现的。
而如何计算插入的时间点呢?自然是要用到我们所说的timer,也就是计时器。当执行setTimeout和setInterval的时候,timer会根据你设定的时间“准确”地找到代码的插入点。
当队列“正常”地执行到插入点时,就触发timer callback,也就是我们设定的回调函数。事实上setTimeout和setInterval只是简简单单地通过插入代码到代码队列来实现代码的延迟执行(或者说异步执行)。所谓的异步只是一个假象——它同样运行在一个线程上!
(那要怎么提升精确度呢?)
A: 可以使用系统时钟来补偿计时器不准确性。如果你的定时器是一系列的,可以在每次回调任务结束的时候,根据最初的系统时间和该任务的执行时间进行差值比较,来修正后续的定时器时间。
对 EventLoop 的理解
介绍浏览器的 EventLoop
主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。
macrotask 和 microtask 的区别
二者任务都会被放置于任务队列中等待某个时机被主线程入栈执行,其实任务队列分为宏任务队列和微任务队列,其中放置的分别为宏任务和微任务。
-
macrotask(宏任务) 在浏览器端,其可以理解为该任务执行完后,在下一个macrotask执行开始前,浏览器可以进行页面渲染。触发macrotask任务的操作包括:
- script(整体代码)
- setTimeout、setInterval、setImmediate(Node独有)
- I/O、UI交互事件(浏览器独有)
- requestAnimationFrame(浏览器独有)
- postMessage、MessageChannel
-
microtask(微任务) 可以理解为在macrotask任务执行后,页面渲染前立即执行的任务。触发microtask任务的操作包括:
- Promise.then
- MutationObserver
- process.nextTick(Node环境)
setTimeout 和 Promise 在不同浏览器的执行顺序
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
}, 0);
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
}, 0)
console.log(7);
macrotask与microtask的运行机制如下:
- 执行一个macrotask(包括整体script代码),若js执行栈空闲则从任务队列中取
- 执行过程中遇到microtask,则将其添加到micro task queue中;同样遇到macrotask则添加到macro task queue中
- macrotask执行完毕后,立即按序执行micro task queue中的所有microtask;如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- 所有microtask执行完毕后,浏览器开始渲染,GUI线程接管渲染
- 渲染完毕,从macro task queue中取下一个macrotask开始执行 在单次的迭代中,event loop首先检查macrotask队列,如果有一个macrotask等待执行,那么执行该任务。当该任务执行完毕后(或者macrotask队列为空),event loop继续执行microtask队列。如果microtask队列有等待执行的任务,那么event loop就一直取出任务执行知道microtask为空。这里我们注意到处理microtask和macrotask的不同之处:在单次循环中,一次最多处理一个macrotask(其他的仍然驻留在队列中),然而却可以处理完所有的microtask。
对于js代码,其最终输出内容为:
1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6
可以从以下几个步骤来简单分析,具体执行步骤如下图所示: