JS的单线程的由来
JavaScript 作为浏览器语言,主要用途就是与用户互动(操作Dom),而操作Dom就只能是单线程,否则就会引起复杂的同步问题,比如一个线程在某个Dom上添加内容,而另一个线程删除了这个节点,这时浏览器就不知道以哪个线程为准了。
为了利用多核CPU的计算能力,HTML5提出Web
Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
单线程意味着一旦有代码报错或者循环调用(栈溢出)就会阻塞后面代码的执行,我们在浏览器中常常看到下面这样的报错,其实就是栈溢出导致的阻塞。
任务队列
由于JS的单线程,这就意味着所有任务都得排队。
JavaScript语言的设计者将需要等待的任务定义为异步任务( asynchronous ),不需要等待的业务定义为同步任务( synchronous )
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution
context stack)。 - 主线程之外,还存在一个”任务队列”(task
queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。 - 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
下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
Node.js运行机制
- V8引擎解析JavaScript脚本
- 解析后的代码,调用Node API
- libuv库负责Node
API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎 - V8引擎再将结果返回给用户
事件驱动/事件循环/线程池
- 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
- 主线程之外,还维护了一个”事件队列“(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
- 主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
- 主线程不断重复上面的第三步。