Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浏览器和NodeJS中不同的Event Loop #3

Open
runzhq opened this issue Jul 30, 2018 · 1 comment
Open

浏览器和NodeJS中不同的Event Loop #3

runzhq opened this issue Jul 30, 2018 · 1 comment

Comments

@runzhq
Copy link
Owner

runzhq commented Jul 30, 2018

填坑。。

1、先从setTimeout和setInterval说起

这两个方法是BOM提供的功能,均接收两个参数:

setTimeout(fn,ms)
setInterval(fn,ms)

  • 返回值是一个数字,可以通过这个数字,使用 clearTimeout(id)clearInterval(id) 来取消定时器。
  • 为什么使用这两个定时器后函数的运行的时间并不是按照我们的期待呢?
  • 由于浏览器内核中的事件循环模型!一个主线程调用栈+多个任务队列
  • 执行的顺序大致可以描述为:依次执行主线程调用栈中的同步任务,遇到定时器就注册它们,当达到设置的时间时(触发条件),将回调函数添加到对应的任务队列中,它与主线程是独立,当主线程调用栈空闲的时候,从任务队列中按照先进先出的顺序依次读取一个任务放到主线程中去执行。
  • 这样就会有一个问题,当我们使用setInterval时,本意是想让函数隔一段固定的时间调用一次,但是在实际执行时,函数只是隔一段时间被加入到任务队列中,而最终的执行时间往往跟这个设定的时间间隔没有关系,比如执行间隔会变小。有一种解决方案是使用setTimeout的链式调用来实现setInterval。
setTimeout(function () {
        //一些处理
        setTimeout(arguments.callee, interval);//调用自己
}, interval)

2、浏览器中的EL

首先需要清楚的事情:

  • 同步任务直接进入主执行栈(call stack)中执行
  • 等待主执行栈中任务执行完毕,由EL将异步任务推入主执行栈中执行
  • 任务队列根据类别的不同可以有多个task

(1)task(macrotask)的种类

  • script代码
  • setTimeout/setInterval
  • I/O
  • UI交互
  • setImmediate(nodejs环境中)

(2)microtask(一个EL中只有一个microtask队列,通常下面几种任务被认为是microtask)

  • promise(promise的then和catch才是microtask,本身其内部的代码并不是)

  • MutationObserver

  • process.nextTick(nodejs环境中)

  • 循环过程:
    任意选择一个task(宏任务)队列=>选择最早进入的一个task执行=>设为currently running task=>运行该task=>currently running task 置为 null=>当前task出队=>Microtasks=>更新渲染=>如果这是一个worker event loop,但是task队列中没有任务,并且WorkerGlobalScope对象的closing标识为true,则销毁EL,中止这些步骤,然后 run a worker(没太明白。。。)

  • 简短版循环过程:

一个宏任务,所有微任务(,更新渲染),一个宏任务,所有微任务(,更新渲染)......

3、NodeJS中的EL

Node中的EL由 libuv库 实现,它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力。

image
图中每个阶段都有自己的回调函数队列,进入该阶段执行队列中所有的回调(或者最大限制数量的回调),然后清理nextTickQueue/microtasks,进入下一阶段。

  • Timers Queue setTimeout() setInterval()设定的回调函数
  • I/O Queue 几乎所有的回调,除了timers、check、close阶段的回调
  • Check Queue setImmediate() 设定的回调函数
  • Close Queue 比如 socket.on('close', ...)

timers: poll阶段来控制什么时候执行timers callbacks
I/O : 处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。
poll: (迷。。。。)获取新的I/O事件。这里有一个最大事件数的限制。由于其它各个阶段的操作都有可能导致新的事件发生,并使得内核向poll queue中添加事件,所以在poll阶段处理事件的同时可能还会有新的事件产生,最终,长时间的调用回调函数将会导致定时器过期,所以在poll阶段与定时器会有"合作"。

1、有timer=>poll进入空闲状态=>EL检查timers=>有timer到期=>回到 timers 阶段执行timers queue
2、无timer=>poll queue不为空=>执行队列中的callback,直到为空或者达到上限
=========> poll queue空=>有setImmediate()(check)=>结束poll阶段进入check阶段
=====================>没有setImmediate()(check)=>event loop将阻塞在poll等待callbacks加入

check: poll队列闲置下来或者是代码被setImmediate调度,EL会马上进入check phase
close: 关闭I/O的动作,比如文件描述符的关闭,连接断开等;如果socket突然中断

  • 循环过程

同步任务=>同步任务中的异步操作发出异步请求=>执行process.nextTick()
开始循环

  1. 清空当前循环内的 Timers Queue,清空NextTick Queue,清空Microtask Queue
  2. 清空当前循环内的 I/O Queue,清空NextTick Queue,清空Microtask Queue
  3. poll情况比较复杂(前面已经分析过了)
  4. 清空当前循环内的 Check Queue,清空NextTick Queue,清空Microtask Queue
  5. 清空当前循环内的 Close Queue,清空NextTick Queue,清空Microtask Queue
  6. 进入下一轮循环

优先级:
nextTick > microtask
setTimeout/setInterval > setImmediate 在同一个I/O cycle中,immediate 总比 timeout 更早被调度
关于process.nextTick:
Node.js 最终选择的实现方法是将 microtask queue 的任务通过一个 runMicrotasks 对象暴露给上游,然后通过 nextTick 方法把它们推进了 nextTickQueue,也就是说最终 microtask queue 的任务变成了 nextTickQueue 的任务,所以我们用 promise.then 和 process.nextTick 可以实现相同的效果。

参考

浏览器和NodeJS中不同的Event Loop
setTimeout说事件循环
JavaScript定时器原理及高级使用

@du1wu2lzlz
Copy link

期待总结哦

@runzhq runzhq changed the title 浏览器和NodeJS中不同的Event Loopz 浏览器和NodeJS中不同的Event Loop Aug 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants