先放个测试题,压压惊
console.log('start');
const interval = setInterval(()=>{
console.log('setInterval');
},0);
setTimeout(()=>{
console.log('setTimeout 1');
Promise.resolve()
.then(()=>{
console.log('promise1');
})
.then(()=>{
setTimeout(()=>{
console.log('setTimeout 2');
clearInterval(interval);
},0);
})
},0);
Promise.resolve()
.then(()=>{
console.log('promise2');
});
末尾揭晓答案
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
1. 降低处理复杂性,简化开发,例如不用考虑竞争机制等。
2. 作为用于预处理与用户互动的脚本语言,可以更加容易地处理状态同步的问题。
3. JS核心维护人员自身的理解与设计。
4. 越简单越容易推广,快速上手。
并发处理能力,任务处于 I/O 等待状态,导致CPU处理资源的浪费。
于是JavaScript语言将任务的执行模分成两种:同步任务和异步任务。通过事件循环处理任务。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务:不进入主线程、而进入任务队列(Task queue),只有任务通知主线程,某个任务可以执行了,该任务才会进入主线程执行。
先看一段伪代码
// eventLoop是一个用作队列的数组,(先进,先出)
var eventLoop = [ ];
var event;
// “永远”执行
while (true){
if (eventLoop.length > 0){
// 拿到队列中的下一个事件
event = eventLoop.shift();
// 现在,执行下一个事件
try {
event();
}catch (err){
reportError(err);
}
}
}
这当然是一段极度简化的伪代码,只用来说明概念。不过它应该足以用来帮助大家有更好的理解。
再贴张流程图
事件循环的具体步骤
1. 同步任务直接放入到主线程执行,异步任务(点击事件,定时器,ajax等)挂在后台执行,等待I/O事件完成或行为事件被触发。
2. 系统后台执行异步任务,如果某个异步任务事件(或者行为事件被触发),则将该任务添加到任务队列的末端,每个任务会对应一个回调函数进行处理。
3. 执行任务队列中的任务具体是在执行栈中完成的,全部执行完毕后,去读取任务队列中的下一个任务,继续执行,是一个循环的过程,处理一个队列中的任务称之为tick。
请看下面一段代码
console.log('A'+ new Date());
setTimeout(function(){
console.log('B'+new Date());
},1000);
var end = Date.now()+3000;
while(Date.now()
A,B,C输出的顺序,以及输出的时间 ?
A会被立即输出,执行到setTimeout(...)时,将会等待1秒后在任务队列添加一个打印B的任务,然后继续往下执行。JS主线程会在while循环通过后继续往下执行,在等待3秒后C被打印,此时任务队列中还有个定时任务回调函数。JS执行栈执行完一个任务之后会再去任务队列取任务,所以C输出后。直接输出B。
PS:一定要清楚, setTimeout(..) 并 没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。如果这时候事件循环中已经有 20 个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么setTimeout(..) 定时器的精度可能不高。
JS是有两个任务队列的,一个叫做Macrotask Queue(Task Queue),一个叫做Microtask Queue;
Macrotask Queue:进行比较大型的工作,常见的有setTimeout,setInterval,用户交互操作,UI渲染等;
Microtask Queue:进行较小的工作,常见的有Promise,Process.nextTick;
两种任务同时出现,应该选择哪一个?
其实事件循环做的事情如下:
1. 检查Macrotask 队列是否为空,若不为空,则进行下一步,若为空,则跳到3;
2. 从Macrotask队列中取首个任务推入执行栈执行,执行完后进入下一步;
3. 检查Microtask队列是否为空,若不为空,则进入下一步,否则,跳到1(开始新的事件循环);
4. 从Microtask队列取首个任务执行,执行完后,跳到3;
简单来讲,整体的js代码这个macrotask先执行,同步代码执行完后有microtask执行microtask,没有microtask执行下一个macrotask,如此往复循环;
function timeProcessArray(items,process,callback){
var todo = items.concat();
setTimeout(function(){
var start = +new Date();
do{
process(todo.shift());
}while(todo.length > 0 && (+new Date() - start < 50));
if(todo.length>0){
setTimeout(arguments.callee,25);
}else{
callback();
}
},25);
}
如果一个函数运行时间过长,可以很容易地把它拆分成一系列更小的步骤,把每个独立的方法放在定时器中调用。
文首测试题的答案为:
start
promise2
setInterval
setTimeout 1
promise1
setInterval
setTimeout2