Javascript总结

205 阅读8分钟

对单线程 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(事件循环)。

image.png 上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。

macrotask 和 microtask 的区别

二者任务都会被放置于任务队列中等待某个时机被主线程入栈执行,其实任务队列分为宏任务队列和微任务队列,其中放置的分别为宏任务和微任务。

  • macrotask(宏任务)  在浏览器端,其可以理解为该任务执行完后,在下一个macrotask执行开始前,浏览器可以进行页面渲染。触发macrotask任务的操作包括:

    • script(整体代码)
    • setTimeoutsetIntervalsetImmediate(Node独有)
    • I/OUI交互事件(浏览器独有)
    • requestAnimationFrame(浏览器独有)
    • postMessageMessageChannel
  • 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

可以从以下几个步骤来简单分析,具体执行步骤如下图所示:

image.png