JS的单线程的由来

JavaScript 作为浏览器语言,主要用途就是与用户互动(操作Dom),而操作Dom就只能是单线程,否则就会引起复杂的同步问题,比如一个线程在某个Dom上添加内容,而另一个线程删除了这个节点,这时浏览器就不知道以哪个线程为准了。

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

单线程意味着一旦有代码报错或者循环调用(栈溢出)就会阻塞后面代码的执行,我们在浏览器中常常看到下面这样的报错,其实就是栈溢出导致的阻塞。
image.png

任务队列

由于JS的单线程,这就意味着所有任务都得排队。

JavaScript语言的设计者将需要等待的任务定义为异步任务( asynchronous ),不需要等待的业务定义为同步任务( synchronous )

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

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

具体来说,异步执行的运行机制如下。

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

下7种会放入异步任务队列:

  • setTimeout(延时器)和
    setInterval(定时器)
  • DOM事件
  • ES6中的Promise
  • Ajax请求
  • process.nextTick(node,在当前执行栈同步任务执行完之后)
  • I/O(node)
  • setImmediate(node,在当前Event Loop执行完之后)

宏任务 & 微任务

宏任务(macro-task):整体代码script、setTimeOut、setInterval、setImmediate、
I/O、UI rendering
微任务(micro-task):promise.then、process.nextTick

任务执行顺序

script(主程序代码-同步)—>process.nextTick—>Promises…——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering

事件和回调函数

“任务队列”是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了。主线程读取”任务队列”,就是读取里面有哪些事件。

“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

Event Loop

image.png

Node.js运行机制

image.png

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

事件驱动/事件循环/线程池

image.png

  1. 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈execution context stack)。
  2. 主线程之外,还维护了一个”事件队列“(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
  3. 主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
  4. 主线程不断重复上面的第三步。