众所周知,JavaScript(如下简称 JS) 是单线程语言,在 html5 中增长了 web workers,web workers 是新开了线程执行的,那么 JS 仍是单线程的吗?固然是,为何要设计成单线程?javascript
网上有不少说法,大部分都说是多个线程同时对一个dom操做(同时修改dom内容,一个线程增长属性,一个线程删除属性),会很是混乱,固然若是支持多线程就会相应的就要加入多线程的锁机制,那么 JS 就变得很是复杂了,想一想 JS 最开始设计的初衷就是用于用户交互,并且当时的原始需求是:功能不须要太强,语法较为简单,容易学习和部署,Brendan Eich 只用了10天,就设计完成了这种语言的初版,所以也不可能加入多线程这么复杂的技术。html
即便如今支持 web workers,因为没有多线程的机制,web workers 和执行线程只能经过 postMessage 来通讯,并且因为没有锁,web workers 没法访问 window 和 document 对象。html5
JS 的单线程是指一个浏览器进程中只有一个 JS 的执行线程,即同一时刻内只会有一段代码在执行。java
单线程如何实现异步?JS 设计了一个事件循环的方式。全部的代码执行均按照事件循环的方式进行。web
事件循环中分两种任务:一个是宏任务(Macro-Task),另外一个是微任务(Micro-Task)。常见的宏任务和微任务以下。promise
宏任务:script(总体代码)、setTimeout、setInterval、requestAnimationFrame、I/O、事件、MessageChannel、setImmediate (Node.js) 微任务:Promise.then、 MutaionObserver、process.nextTick (Node.js)浏览器
事件循环按下图的方式进行。bash
注意: 宏任务执行完后,须要清空当前微任务队列后才回去执行下一个宏任务,若是微任务里面产生了新的微任务,仍然会在当前事件循环里面被执行完,后面会举例说明。多线程
来个示例验证下上面的流程。dom
<script> console.log(1); setTimeout(function timeout1() { console.log(2); }, 0); Promise.resolve().then(function promise1() { console.log(3); setTimeout(function timeout2() { console.log(4); Promise.resolve().then(function promise2() { console.log(5); }); }, 0); return Promise.resolve() .then(function promise3() { console.log(6); return Promise.resolve().then(function promise4() { console.log(7); }); }) .then(function promise5() { console.log(8); }); }) console.log(9); </script>
<script> console.log(10); setTimeout(function timeout3() { console.log(11); }, 0); Promise.resolve().then(function promise6() { console.log(12); }); </script>
复制代码
按照上面流程梳理下执行流程:
[script1, script2]
[script2]
[script2, timeout1]
[promise1]
[script2, timeout1, timeout2]
[promise3]
[promise4]
[promise4,promise5]
[promise5]
[timeout1, timeout2, timeout3]
[promise6]
[timeout2, timeout3]
[timeout3]
[promise2]
setTimeout 的 delay 最小值在不一样浏览器的有差别,在 Chrome 74 上测试的结果是 2ms,Firefox 67 上测试的记过是 1ms。
最小值是什么意思?就是小于这个值后,浏览器按照0处理。好比在 Chrome 上,测试下面的代码:
setTimeout(function(){console.log(1)},1.99);
setTimeout(function(){console.log(2)},0);
复制代码
输出的结果为 一、2,而
setTimeout(function(){console.log(1)},2);
setTimeout(function(){console.log(2)},0);
复制代码
输出的结果为 二、1,说明 2ms 是有效的。
另外 setTimeout 是从调用开始计时,到了时间就放入宏任务队列,咱们来看下面的例子。
var s = Date.now()
setTimeout(function timeout1() {
console.log(1)
}, 200)
while (Date.now() - s <= 200) {
}
setTimeout(function timeout2() {
console.log(2)
}, 0)
复制代码
Date.now() - s <= 198
和 setTimeout 相同,调用开始计时,按 delay 时间将回调添加到宏任务队列中。那么 setInterval 是按 delay 不断的向宏任务队列添加任务,仍是须要等待已添加的任务执行完后再添加,仍是其余机制?
思考下面代码:
var start = Date.now()
var id = setInterval(function interval() {
var whileStart = Date.now()
console.log(whileStart - start) // 输出 interval1 调用的时间和最开始调用计时的时间差,即过了多久才调用
while (Date.now() - whileStart < 250) { // 至关于 sleep 250ms
}
}, 100)
setTimeout(function timeout() {
clearInterval(id)
console.log(Date.now() - start)
}, 400)
复制代码
打印的时间间隔是?
100
351
605
855
复制代码
为了更好的理解,用图示来解释上面的流程。