美文网首页
JavaScript 的并发处理 -- Event Loop 的

JavaScript 的并发处理 -- Event Loop 的

作者: web前端_ElonWu | 来源:发表于2019-03-01 14:14 被阅读97次

前言: 本文是我通过阅读文章和视频得到一些个人理解;(链接:信息来源以供参考)。如果我有误读的地方, 非常欢迎指出。

首先我们看一下下面的代码:

    setTimeout(() => {
        console.log('第1个定时器回调')
    }, 100)
    setTimeout(() => {
        console.log('第2个定时器回调')
    }, 0)
    
    console.log('打印3')

先预测一下,打印结果。然后复制代码运行验证一下。

如果你的答案是依次打印 3 、2 、1 那么恭喜你答对了。

但是需要问 2 个为什么:

1. js 不是单线程吗?为什么可以一边执行代码, 一边执行定时器?

其实 JS 确实是单线程,也就是说 JS 代码只能一行行压入执行栈中去执行;完成后执行下一行。而定时器,其实不是 js 在执行; 它是 window 也就是浏览器提供的 API;浏览器负责定时器的执行; 完成后再由 JS 的执行栈去完成回调。这里就引出第二个问题, 回调在什么时候执行呢?

2. 为什么延迟为 0ms 的定时器不是最先打印?到底执行顺序是怎么定的?

第一个问题我们知道了, 不论定时器设定了多久的延迟; 都会交给浏览器去处理;然后再把回调函数传递回来。但是,不是直接传递到 JS 的执行栈; 而是传到 "任务对列" 中去排列; 等 JS 执行栈里事情都做完了,才会从 "任务对列" 中依次选出排在最前面的回调,加入只能栈去执行。

event-loop.gif

以上面的代码为例, 其过程如下:

  1. JS 运行, 从上往下执行代码;
  2. 执行到第一个定时器;把执行过程交给浏览器;浏览器开始到计时了;
  3. 执行栈不等它、继续向下走;发现第二个定时器,还是交给浏览器; 0ms 后就完成了; 浏览器把回调函数放到任务对列里排队;
  4. 执行栈不会立刻去管任务对列,因为它还要继续忙着往下走,执行 '打印3'
  5. 此时,执行栈忙完了,才会去任务对列里找事情做;发现队列里有一个回调函数(第二个定时器的); 执行它 '第2个定时器回调'
  6. 此时执行栈、任务对列都空了; 这点事可能几微秒就处理完了;就开始等着;
  7. 浏览器终于完成了倒计时 100ms, 才把第一个定时器的回调放入任务对列;
  8. 执行栈正闲着,于是从任务对列里拿出这个回调, 执行 '第1个定时器回调'
  9. 最终执行栈、任务对列、浏览器都没事做了。执行结束。

除了定时器, promise, Ajax 请求, 事件监听,所有异步操作都是同样的道理。一旦 JS 执行到直接异步操作, 都一律抛给浏览器;等浏览器响应之后,将回调函数加入"任务对列";等到 JS 执行栈闲下来,才会从任务对列取出,并执行回调函数。

下面是一个相对复杂的例子,练习把这个执行过程再捋一遍


    setTimeout(() => {
        console.log('第1个定时器回调')
    }, 2000)
    
    console.log(1)
    
    document.addEventListener('click', () => {
        console.log('click')
    })
    
    console.log(2)
    
    Promise.resolve(10).then((val) => {
        console.log(val)
    })
    console.log(3)

js 执行后,不断点击页面;可以看到打印信息如下:


event-loop.jpg

其过程如下:

  1. JS 运行, 从上往下执行代码;
  2. 执行到定时器,交给浏览器处理; 开始倒计时;
  3. 继续执行打印 1;
  4. 继续执行到事件绑定; 交给浏览器;
  5. 继续执行打印 2;
  6. 继续执行到 Promise, 交给浏览器;立刻resolve了10, 将回调函数加入任务对列;
  7. 继续执行打印 3;
  8. 此时执行栈也许只用了几微秒,就把事情都做完了;你还没来得及做任何一次点击;此时任务对列里只有 Promise 的回调;此时已经闲下来的执行栈,从任务对列拿出这个回调,执行打印 10;
  9. 此时,你不但点击页面;浏览器的事件监听将每次点击的回调加入任务对列;执行栈是如此之快,它不断从任务对列拿出点击事件的回调执行;执行后执行栈空了,它又去任务对列寻找;如此循环;
  10. 在你点击第三次和第四次之间,定时器倒计时结束,把回调加入到任务对列;你的第四次点击,因此,排在执行栈处理了定时器回调之后('第1个定时器回调');
  11. 等你停止点击之后,执行栈将任务对列都执行空了。浏览器仍旧在监听点击事件; 如果你继续点击,浏览器会往任务对列添加回调; 并触发执行栈继续工作,执行一个个回调。

从这里可以发现, 为什么当单线程的 JS 进行异步操作时,并不会阻断整个页面,因为浏览器帮助你把异步的操作都完成,并把回调函数排列在任务队列。JS 只需要关注主线程中的工作; 然后在闲下来之后,不断执行任务队列即可。

但实际上,也可以发现并非完全不影响; 当某一个回调函数的任务太复杂,它会一直拖住执行栈;让消息队列中排队的其他回调函数无法执行;此时就会出现诸如页面渲染卡顿、交互事件响应慢;定时器超时等现象。

写在最后

浏览器运行 Js 和在 Node 中,event-loop 的机制会有区别;如果对 Node 的 event-loop 有兴趣,可以查看官网的介绍和这两篇文章《浏览器与Node的事件循环(Event Loop)有何区别?》《Nodejs探秘:深入理解单线程实现高并发原理》

相关文章

网友评论

      本文标题:JavaScript 的并发处理 -- Event Loop 的

      本文链接:https://www.haomeiwen.com/subject/hgrduqtx.html