js的单线程模型与游览器的进程/线程息息相关,在了解js单线程与异步的时候,建议先看看这篇文章
单线程/异步
js为什么是单线程
- js能够操作dom,如果js是多线程,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,这时就会出现问题。
- 虽然可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行。
js为什么需要异步
- 由于GUI线程和JS引擎线程互斥,所以当js执行的时候,页面渲染就会被挂起,如果js执行时间过长,或者因为I/O操作(同步ajax请求)的等待,就会让页面'卡死',造成渲染阻塞
js如何实现异步
- 通过事件驱动机制,来实现异步任务等待,当js主线程空闲后,自动去拿异步任务进行执行
js的事件驱动机制
- 事件驱动机制(event driven)通过事件循环(event loop)和事件队列(event queue)来实现。
- 事件队列,也称消息队列,是一个先进先出的队列,它里面存放着各种事件消息。事件循环是指主线程重复从消息队列中取消息、执行的过程。
- js将程序分为同步任务和异步任务,同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,事件触发线程管理着消息队列,只要异步任务有了运行结果,就在消息队列之中放置一个事件消息(通常都关联着回调函数)。
- 一旦执行栈中的所有同步任务执行完毕(此时js引擎空闲),系统就会去读取事件队列,将可运行的异步任务添加到执行栈中,开始执行。
- js引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
- 贴两张图表示


js的异步编程模型
传统异步回调的问题
- 代码可读性
- 流程控制
- 异常和错误处理
异步编程的变革
- Promise
- Generator
- Async/await
执行机制
setTimeout/setInterval
- 定时器并不是由js引擎计数的,因为js引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此是通过单独线程来计时并触发的。
- 当使用setTimeout/setInterval时,定时器线程会开始计时,计时完成后就会将特定的事件推入事件队列中。
- 虽然很多定时器设置时间为0毫秒后就推入事件队列,但实际上W3C在HTML标准中规定,低于4ms的时间间隔算为4ms。
- setTimeout/setInterval每次都精确的隔一段时间推入一个事件 。但是,事件的实际执行时间不一定准确,因为存在代码执行时间。
- 如果setInterval代码在再次添加到队列之前还没有完成执行,就会导致定时器代码连续运行好几次,而之间没有间隔,造成累积效应。
- 当把浏览器最小化显示等操作时,setInterval并不是不执行程序, 它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行。
- 当函数执行时,如果发现同一个定时器已经有多个在等待执行的任务,只会执行1次,后面的会被忽略掉,示例代码如下:
var sleep = function(time) {
var date = new Date();
while(new Date() - date <= time) {}
}
var time = new Date();
var a = setInterval(function(){
sleep(200);
console.log(new Date() - time);
if(new Date() - time > 700) clearInterval(a);
}, 100);
// 303
// 506
// 708
目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame。
macrotask与microtask
起手示例
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start
// script end
// promise1
// promise2
// setTimeout
为什么呢?因为Promise涉及到一个新的概念:microtask。其实,js的任务任务类型分为两者:macrotask和microtask。
概念描述
macrotask(宏任务)——可以理解为是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中取到,放入执行栈中执行的任务)
- 每一个task会从头到尾执行完毕,过程中不会执行其它
- 浏览器为了能够使得js内部task与dom任务能够有序的执行,会在一个task执行结束后,在下一个task执行开始前,对页面进行重新渲染 (task->渲染->task->...)
microtask(微任务)——可以理解是在当前task执行结束后立即执行的任务
- 也就是在当前task任务后,下一个task之前,在渲染之前
- 在一个宏任务执行完后,就会将在它执行期间产生的所有微任务都执行完毕
类型归属
- macrotask——主代码块,setImmediate,setTimeout/setInterval
- microtask——promise,process.nextTick
运行机制
- 执行一个宏任务,主栈中没有就从事件队列中获取。
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
- 宏任务执行完毕后,立即依次执行当前微任务队列中的所有微任务。
- 当微任务执行完毕,开始检查渲染,然后GUI线程接管渲染。
- 渲染完毕后,JS线程继续接管,开始下一个宏任务。

再看看如下代码:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
结果输出为:1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
补充
- 宏任务中的任务都是放在一个事件队列中,而这个队列由事件触发线程维护。
- 微任务中的任务都是放在到微任务队列中,等待当前宏任务执行完毕后执行,而这个队列由js引擎线程维护。
- 在node环境的微任务执行中,process.nextTick的优先级高于promise。
- 在node环境的宏任务执行中,setImmediate的优先级高于setTimeout。
注意
- 关于promise,官方版本中,是标准的微任务形式。但在polyfill中,一般都是通过setTimeout模拟的,所以是宏任务形式。
- 有一些浏览器执行结果不一样,它们可能把microtask当成macrotask来执行了, 有些浏览器可能并不标准。
- HTML5新特性MutationObserver属于微任务,优先级小于Promise,一般是Promise不支持时才会这样做。
- HTML5新特性MessageChannel属于宏任务,优先级是:setImmediate->MessageChannel->setTimeout。
参考
- https://segmentfault.com/a/1190000004322358
- http://www.imweb.io/topic/58e3bfa845e5c13468f567d5
- http://www.cnblogs.com/mininice/p/4298952.html
- https://juejin.im/post/59e85eebf265da430d571f89
- http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html
- https://juejin.im/post/5a6ad46ef265da3e513352c8
网友评论