JavaScript 运行机制

1.单线程

避免多线程同时操作DOM,带来复杂的同步问题,同一个时间只能做一件事,提高效率。(Web Worker允许创建多个子线程但不得操作DOM,还是单线程)

2.任务队列

任务排队等待一个一个执行。如果遇到很慢的,如网络读取数据,挂起处于等待中的任务,先运行排在后面的任务。挂起的任务出了结果,再回头继续执行。

  • 同步任务(synchronous):主线程上排队执行的任务。
  • 异步任务(asynchronous):不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行机制: (主线程 执行栈 任务队列)

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。这个过程会不断重复。

3.事件和回调函数

任务队列(事件的队列):

  • IO设备完成一项任务,就在任务队列添加一个事件。主线程来读取的就是这些事件。
  • 还包括用户产生的事件click、scroll等,事件发生时就进入任务队列,等待主线程读取。

回调函数(callback):是挂起来的代码,主线程执行异步任务,就是执行对应的回调函数。

任务队列是一个先进先出的数据结构,只要执行栈一清空,任务队列上第一位的事件就自动进入主线程。(定时器功能使某些事件按时间返回主线程)

4.事件循环(Event Loop)

主线程产生堆(heap)和栈(stack),栈中调用外部API,向任务队列中加入事件(click、load、done)。栈中执行完,主线程就读取任务队列,依次执行回调函数。

da078fa3eadf3db4bf455904ae06f84b.png

例如Ajax。指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取任务队列。

5.定时器

定时器功能主要由setTimeout()和setInterval()这两个函数来完成。

定时器等到同步任务和任务队列现有的事件都处理完,才会得到执行。

定时器只是将事件插入了任务队列,必须等执行栈,执行完,主线程才会去执行它指定的回调函数。(当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在指定的时间执行)

如果setTimeout加入队列的阻塞时间大于两个setTimeout执行的间隔时间,那么先加入任务队列的先执行,尽管它里面设置的时间可能比另一个setTimeout的要大。

6.Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

20c8ec0e32db9d302955408622f7c91e.png

Node.js的运行机制:

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API。
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

Node.js提供的任务队列方法:
process.nextTick方法,指定的任务总是发生在所有异步任务之前(本次触发)。
setImmediate方法,指定的任务总是在下一次Event Loop时执行(下次触发)。

Reference:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
http://www.ruanyifeng.com/blog/2014/10/event-loop.html