js 事件循环(Event Loop)机制

先放个测试题,压压惊

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');
   });
末尾揭晓答案

JS单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

单线程的优劣势

优势

1. 降低处理复杂性,简化开发,例如不用考虑竞争机制等。

2. 作为用于预处理与用户互动的脚本语言,可以更加容易地处理状态同步的问题。

3. JS核心维护人员自身的理解与设计。

4. 越简单越容易推广,快速上手。

明显的劣势

并发处理能力,任务处于 I/O 等待状态,导致CPU处理资源的浪费。

于是JavaScript语言将任务的执行模分成两种:同步任务和异步任务。通过事件循环处理任务。

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务:不进入主线程、而进入任务队列(Task queue),只有任务通知主线程,某个任务可以执行了,该任务才会进入主线程执行。

事件循环(Event Loop)

先看一段伪代码

// 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()<end){}
console.log('C'+new Date());

A,B,C输出的顺序,以及输出的时间 ?

A会被立即输出,执行到setTimeout(...)时,将会等待1秒后在任务队列添加一个打印B的任务,然后继续往下执行。JS主线程会在while循环通过后继续往下执行,在等待3秒后C被打印,此时任务队列中还有个定时任务回调函数。JS执行栈执行完一个任务之后会再去任务队列取任务,所以C输出后。直接输出B。

PS:一定要清楚, setTimeout(..) 并 没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。如果这时候事件循环中已经有 20 个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么setTimeout(..) 定时器的精度可能不高。

ES6事件循环

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


猜你喜欢

转载自blog.csdn.net/i13738612458/article/details/80213836