js 只在一个线程上运行,也就是同时只能运行一个任务,其他的任务只能在后面排队等待。js 只在主线程上运行,不代表 js 引擎就只有一个线程,事实上 js 引擎是有多个线程的,单个脚本都是在一个线程上运行,其他的线程在后面做辅助。单线程模式是 js 的一大特点,配合事件循环,js 可以实现不会阻塞的情况,这也是 Node 为什么可以用很少的资源,应付大流量访问的原因。
事件循环分浏览器和 node,大体上逻辑相似,但是内里逻辑又差别挺大的。 js 是单线程的,因为和它的用途有关系。js 设计的时候,就是为了和浏览器做交互使用的,在交互的时候,如果是多线程的话,就不知道 dom 究竟是听那个线程的操作了,如果两个线程一个前脚更新,一个后脚再变别的,究竟应该听谁的?所以设计和交互就决定了 js 是单线程的。为了利用多核的计算能力,出现了 web worker ,子线程是完全受主线程的控制的,而且不能操作 DOM,所以 JS 的单线程的特性是一直没发生变化的。
程序里的任务,可以分两种,一个是同步任务,一个是异步任务。 同步任务指的是没有被引擎挂起的,等待在主线程上运行的任务,只有前一个任务执行完,后一个任务才能执行的这种任务。 异步任务指的是被引擎挂起的任务,不进入主线程,在任务队列里的任务。引擎会不断的查询,只有当引擎认为某个任务可以执行了,才会把这个任务安排到主线程里去。
js 在运行的时候,除了主线程,引擎还会提供一个任务队列,里面是需要处理的异步任务。当主线程执行完所有的同步任务的时候,就会去查询异步任务里有没有返回结果,一旦有任务返回结果了这个异步任务的回调函数就会重新进去主线程开始执行,这时候就变成同步任务了。周而复始,一直这样检查异步任务队列,直到队列为空。
在浏览器中,为了让 js 代码和 dom 有序的执行,会在一个宏任务执行完以后在一下个宏任务执行之前对页面进行渲染,也就是 宏任务 -> 渲染 -> 宏任务 这样的一循环。 宏任务有
script 整体代码
setTimeout
setInterval
I/O
UI 交互事件
postMessage
MessageChannel
setImmediate(Node )
在当前的宏任务执行完后立即执行的任务,在某个宏任务执行完成以后,就会马上把这个宏任务产生的微任务全部执行掉。
Promise.then .... 等等的 promise 属性
Object.observe
MutationObserver
process.nextTick (node 环境)
Object.observe 是一个非常有意思的属性,一个异步监听 js 对象变化的方法
// 假设我们这里有个数据模型
var model = {};
// 我们来对它进行监听
Object.observe(model, function (changes) {
// 这个异步回调函数将被执行
changes.forEach(function (change) {
// 我们知道了都发生了哪些变化
console.log(change.type, change.name, change.oldValue);
});
});
浏览器是执行完一个宏任务,马上就执行这个宏任务所产生的微任务。
Node 的事件循环是通过 libuv 实现的
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Node 中的宏任务的执行顺序是这样的
- timers 定时器 本阶段执行已经安排的 setTimeout 和 setInterval 回调
- pending callbacks 待定回调,执行延迟到下个循环迭代的 I/O 回调。
- idle prepare 仅仅系统内部使用
- poll 轮询,检索新的 I/O 事件;执行与 I/O 相关的回调例如读取文件之类的,适当的条件下,node 会阻塞在这个阶段
- 检查阶段,执行 setImmediate 设定的 callback
- 关闭的回调函数,close callback 阶段,一些关闭的回调函数执行阶段,例如 socket.on('close', ...)
Node 和 浏览器不一样的地方在于,Node 执行微任务的时候,是在事件循环的间隙执行的。而且微任务就只有 promise.then 和 process.nextTick 且 process.nextTick 的优先级要高于 promise.then