JavaScript单线程
开始这篇文章之前有几个问题
1、为什么JavaScript是单线程的
2、从for循环的面试题来理解JavaScript单线程
JavaScript为什么是单线程。
我们知道JavaScript作为浏览器的脚本语言,主要用途是与用户互动,以及操作DOM。如果以多线程的方式操作这些DOM,则可能出现操作的冲突。假设有两个线程同时操作一个DOM元素,线程1要求浏览器删除DOM,而线程2却要求修改DOM样式,这时浏览器就无法决定采用哪个线程的操作。当然,我们可以为浏览器引入“锁”的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行。
由于JavaScript 是单线程的,在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行。那么对于类似I/O等耗时的任务,就没必要等待他们执行完后才继续后面的操作。在这些任务完成前,JavaScript完全可以往下执行其他操作,当这些耗时的任务完成后则以回调的方式执行相应处理。这些就是JavaScript与生俱来的特性:异步与回调。
从for循环的面试题来理解JavaScript单线程
开始之前我们先预热下一些基本概念:
- 并行: 同一时刻内多任务同时进行,多线程实现;
- 并发,同一时间段内,多任务同时进行着,但是某一时刻,只有某一任务执行,单线程可实现;
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
- 堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);
- 栈(stack):后进先出的顺序存储数据结构,通常存储函数参数和基本类型值变量(按值访问);
- 队列(queue):先进先出顺序存储数据结构。
- 任务队列:一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。
- 除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。如果有定时器,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
- 回调函数(callback): 那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
- 事件循环: 主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环(Event Loop)。
宿主环境为JavaScript创建线程时,会创建堆(heap)和栈(stack):
- 堆内存储JavaScript对象
- 栈内存储执行上下文
同步任务:
- 执行栈内,执行上下文的同步任务按序执行
- 执行完即退栈
异步任务:
- 异步任务执行时,该异步任务进入等待状态(不入栈)
- 与此同时,通知线程“当触发该异步事件的时候(或该异步操作响应返回时),需向任务队列插入一个事件
- 当实际上异步事件触发或异步操作响应返回时,线程向任务队列插入相应的回调事件
- 当执行栈清空后,线程从任务队列取出一个事件消息,其对应异步任务(函数)结束等待状态,进入执行栈,执行回调函数
- 如果该事件消息未绑定回调,则执行完任务后退栈,这个消息会被丢弃
线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick,事件循环流转一次称为一次tick)
其实上面这些可以用一个图来概括,也就是JavaScript事件循环:
for (var i = 0; i < 5; i++) {
console.log(i);
}
结果: 0,1,2,3,4
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
结果: 5,5,5,5,5
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
结果:0,1,2,3,4
for (var i = 0; i < 5; i++) {
(function() {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
结果:5,5,5,5,5
for (var i = 0; i < 5; i++) {
setTimeout((function(i) {
console.log(i);
})(i), i * 1000);
}
结果:0,1,2,3,4
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
结果:2,3,5,4,1
promise是微任务,当主线程执行完毕,微任务会排在宏任务前面先去执行
微任务:promise、process.nextTick()
最后看一个复杂的题目
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
setTimeout(() => {
console.log(5)
}, 5)
setTimeout(() => {
console.log(4)
}, 4)
setTimeout(() => {
console.log(3)
}, 3)
setTimeout(() => {
console.log(2)
}, 2)
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log(0)
}, 0)
结果是?
0,1,2,3,4,5
3,2,1,0,4,5
1,0,2,3,4,5
In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.
底层代码:
double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
规范有些地方也不是全对啊~