Event loop (JavaScript中的执行机制)

进程、线程

  • 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
  • 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。

浏览器内核

  • 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。
  • 浏览器内核有多种线程在工作。
    • GUI 渲染线程:
      • 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
      • 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
    • JS 引擎线程:
      • 单线程工作,负责解析运行 JavaScript 脚本。
      • 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
    • 事件触发线程:
      • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
    • 定时器触发线程:
      • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
      • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
    • http 请求线程:
      • http 请求的时候会开启一条请求线程。
      • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

javascript 堆(heap)、栈(stack)、队列(queues)

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

  • heap是没有结构的,数据可以任意存放。heap用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象
  • stack是有结构的,每个区块按照一定次序存放(后进先出),stack中主要存放一些基本类型的变量和对象的引用,存在栈中的数据大小与生存期必须是确定的。可以明确知道每个区块的大小,因此,stack的寻址速度要快于heap。
  • queues(消息队列):一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。

浏览器中的Event Loop

Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。

在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。

task (MacroTask、宏任务)

  • setTimeout
  • setInterval
  • I/O
  • script代码块
  • setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)
  • UI Rendering

jobs (MicroTask、微任务)

  • nextTick
  • callback
  • Promise
  • process.nextTick
  • Object.observe(废弃)
  • MutationObserver

Event loop

==事件循环的顺序,决定了JavaScript代码的执行顺序。== 它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的宏任务,这样一直循环下去。其中每一个任务的执行,无论是微任务还是宏任务,都是借助函数调用栈来完成。

一段代码块就是一个宏任务。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
不同任务类型会进入对应的event queue,比如setTimeout和setInterval会进入相同的宏任务Event queue

浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。

具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。

总结

  • 首先要明白的是event loop是由javascript宿主环境(像浏览器)来实现的,js引擎他不关心也不知道event
    loop机制的运行和存在,他只负责从事件队列里面读取事件来执行,他不会也不会知道怎样向事件队列中push事件任务,这些都由宿主来完成。理解这第一点很重要要先知道是谁在做这件事情。
  • 第二点就是一个宿主环境 只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。相同任务源的任务,只能放到一个任务队列中。不同任务源的任务,可以放到不同任务队列中。然后js引擎做的事就是不断的去读取这些队列里面的任务来执行,
  • 任务可分为宏任务和微任务;他们的执行过程和顺序:js引擎逐句的执行script整体代码,当遇到异步任务时,js的运行环境就会在适时的时候将这些事件任务push到相应的队列中去,等待着被js引擎去执行,而如果异步没有产生回调(callback)或者说是事件任务,那他就不会push到队列里面去,当js执行栈执行完成后,然后他把微任务队列中的任务读取过,并进行执行,在这执行过程中如果有产生新的异步任务也会按照上述的方式进行处理,当微任务执行完成后他会去读取宏任务队列中的任务并执行,然后周而复始的反复执行,直到把队列中的任务全部执行完。
  • macro-task(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;
    micro-task(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver
  • 至于队列中的优先级,有一个大概的顺序process.nextTick > promise.then > setTimeout > setImmediate;优先级执行顺序可能还会和具体的宿主环境有关;边城大神也说的对对于异步我们更不应该去依赖他们的执行顺序既然是异步就当作无序的来处理。

参考资料

JavaScript 运行机制详解:再谈Event Loop
JS浏览器事件循环机制
这一次,彻底弄懂 JavaScript 执行机制
一次弄懂Event Loop(彻底解决此类面试问题)
一篇文章教会你Event loop——浏览器和Node
JavaScript中的执行机制
JavaScript的事件队列(Event Queue)
关于javascript 的event loop如何理解event queue的优先级?

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复