【JavaScript】JS执行机制及Event Loop

1. JS的单线程概念

  • JavaScript语言特点就是单线程,其主要用途是与用户互动和DOM操作。因此它只能是单线程,否则会带来复杂的同步问题。

  • 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但这并不是真正意义上的多线程,其子线程由主线程完全控制,且子线程不能对DOM进行操作。

  • js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。这个循环机制又称为Event Loop(事件循环)

  • 同步任务都在主线程上执行,形成一个执行栈(execution context stack);如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行;

2. 任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

2.1 同步任务&异步任务

任务可以分成两种:

  • 同步任务 (synchronous):在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务 (asynchronous):不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

此时,js运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

  • (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  • (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • (4)主线程不断重复上面的第三步。
    只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。

哪些是异步任务

  • setTimeout 和 setInterval
  • DOM 事件
  • Promise
  • 网络请求
  • I/O

2.2 宏任务 & 微任务

但按照异步和同步的划分方式,并不准确,更准确的任务划分方式: 宏任务 & 微任务

宏任务(Macrotask) 微任务(Microtask)
setTimeout requestAnimationFrame(有争议)
setInterval MutationObserver(浏览器环境)
MessageChannel Promise.[ then/catch/finally ]
I/O,事件队列 process.nextTick(Node环境)
setImmediate(Node环境) queueMicrotask
script(整体代码块)

按照这种分类方式,JS的执行机制是

  • 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
  • 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完

示例:

 setTimeout(function(){
     console.log('定时器开始啦')
 });
 
 new Promise(function(resolve){
     console.log('马上执行for循环啦');
     for(var i = 0; i < 10000; i++){
         i == 99 && resolve();
     }
 }).then(function(){
     console.log('执行then函数啦')
 });

 console.log('代码执行结束');

执行结果:【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】

首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
遇到 new Promise直接执行,打印"马上执行for循环啦"
遇到then方法,是微任务,将其放到微任务的【队列里】
打印 "代码执行结束"
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"
到此,本轮的event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"

3. EventLoop 事件循环

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
Event Loop事件循环

  • 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
  • 同步任务会直接进入主线程依次执行;
  • 异步任务会再分为宏任务和微任务;
  • 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  • 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
  • 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
  • 上述过程会不断重复,这就是Event Loop事件循环;

4. 定时器

除了异步任务的事件,定时器事件也能放入任务队列,根据定时器函数setTimeout()setInterval()定时或者周期执行定时器事件。
当定时器函数设置事件为0时:setTimeout(fn,0),该定时器事件将在主线程空闲时立即执行,即在同步任务、任务队列的事件均处理完成后,立即执行。因此,定时器函数是把定时器事件加入"任务队列",但必须等到当前任务栈执行结束,主线程才执行其指定的回调函数。

5 示例

function add(x, y) {
    
    
  console.log(1)
  setTimeout(function() {
    
     // timer1
    console.log(2)
  }, 1000)
}
add();

setTimeout(function() {
    
     // timer2
  console.log(3)
})

new Promise(function(resolve) {
    
    
  console.log(4)
  setTimeout(function() {
    
     // timer3
    console.log(5)
  }, 100)
  for(var i = 0; i < 100; i++) {
    
    
    i == 99 && resolve()
  }
}).then(function() {
    
    
  setTimeout(function() {
    
     // timer4
    console.log(6) 
  }, 0)
  console.log(7)
})

console.log(8)

执行结果: 1,4,8,7,3,6,5,2

猜你喜欢

转载自blog.csdn.net/qq_38987146/article/details/115090073