【JavaScript】Event Loop 事件循环

单线程模型

单线程模型指的是,JavaScript只能在一个线程上运行,也就是说只能同时指向一个任务,其他任务都必须在后面排队等待。注意:虽然JavaScript只在一个线程上运行,但并不代码JavaScript引擎只有一个线程。事实上,JavaScript引擎有多个线程,单个脚本只能在一个线程上运行(主线程),其他线程都是在后台配合。

  • JS引擎线程:也叫JS内核,负责解析执行JS脚本程序的主程序,例如Charome的V8引擎
  • 事件触发线程:属于浏览器内核线程,主要用于控制事件,例如鼠标、键盘等,当事件被触发后,就会把事件的处理函数推进事件队列,等待JS引擎执行
  • 定时器触发线程:主要控制setTimeoutsetInterval,用来计时,计时完毕后,则把定时器处理的函数推进事件队列,等待JS引擎执行
  • HTTP异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎执行

疑问?

  • JavaScript为什么要采用单线程,而不是多线程?
    不想让浏览器变得复杂(避免复杂性),同时避免 DOM 渲染冲突,因为多线程需要共享资源、且可能修改彼此运行的结果

  • 该模式会导致的问题?
    如果单个任务耗时长,会拖延整个程序的执行,可能导致浏览器无响应(假死)

  • JavaScript是如何解决这个问题的?
    因为单线程的原因,CPU很多时候都闲着的,并且因为IO操作(输入输出)很慢(比如Ajax操作从网络读取数据),这时CPU可以完全不管IO操作,挂起等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是JavaScript内部采用的“事件循环”机制(Event Loop)

宏任务

可以分成两类:同步任务(synchronous)和异步任务(asynchronous)

  • 同步任务:没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

  • 异步任务:被引擎放一边、不进行主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如Ajax操作从服务器得到了结果),那么该任务(通过回调函数的形式)才能进入主线程执行。排在异步任务后面的代码,不用等到异步任务结束就会马上运行,也就是说,异步任务不具有“堵塞”效应

微任务

微任务是ES6的Promise和Node环境下的process.nextTick(笔主对Node未深入学习,所以这里只是简单说明有这个东西)

注意

微任务的执行在宏任务的同步任务之后,在异步任务之前

任务队列和事件循环

JavaScript运行时,除了一个正在运行的主线程(又称为“调用栈(call stack)”),引擎还提供了一个任务队列(task queue),里面是各种需要处理当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列)

Event Loop开始时会从全局栈,一行一行执行,遇到函数调用会把它压入调用栈中,被压入的函数叫做帧(Frame),当函数返回后会从调用栈中弹出

1.主线程会去依次执行所有的同步任务,并检查所有的异步任务和微任务,并将其放入对应的任务队列(异步任务放入消息队列,宏任务放入微任务队列)
2.等到同步任务全部执行完,查看任务队列中的异步任务和微任务,遵循先执行微任务再异步任务的方式,将任务放入调用栈开始执行
3.等任务执行完,引擎再轮询检查任务队列,将下一个任务队列中将要执行的任务放入调用栈开始执行。
4.引擎不停检查(“事件轮询”),一旦任务队列清空,程序就结束执行。

代码例子:

console.log('start')

setTimeout(function () {
  console.log('timeout')
}, 0)

new Promise(function (resolve) {
  console.log('promise')
}).then(function () {
  console.log('promise resolved')
})

console.log('end')

执行结果

分析一下上面代码

  • console.log('start') 放入调用栈,执行打印start,弹出调用栈
  • 遇到setTimeout放入调用栈,将回调函数放入消息队列,等待执行
  • 遇到new Promise放入调用栈,其回调函数并不会被放入任务队列,因此会同步的执行,打印promise,但是当执行resolve后,.then会把其内部的回调函数放入微任务队列
  • 执行到了最底部的代码,打印出end。这时,主执行栈清空了,开始寻找微任务队列里有没有可执行代码
  • 发现了微任务队列中有之前放进去的代码,先执行微任务队列中的代码,打印出promise resolved,第一次循环结束
  • 再开始第二次循环,从消息队列开始,检查宏任务队列是否有可执行代码,发现有一个,打印timeout

参考文档

2分钟了解 JavaScript Event Loop | 面试必备
在浏览器输入 URL 回车之后发生了什么(超详细版)

posted @ 2020-06-07 09:31  努力挣钱的小鑫  阅读(305)  评论(0编辑  收藏  举报