javascript运行机制解析

JS浏览器事件循环机制

进程和线程

  • 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
  • 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。

浏览器内核

浏览器是多进程的,浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。浏览器内核有多种线程在工作:

  1. GUI渲染线程:
    • 负责渲染页面,解析HTML,CSS构成DOM树等,当页面重绘或回流时调用该线程。
    • 和JS引擎线程互斥,当JS引擎线程在工作的时候,GUI渲染线程会被挂起,GUI更新被放入在JS任务队列中,等待JS引擎线程空闲的时候继续执行。
  2. JS引擎线程:
    • 单线程工作,负责解析运行javascript脚本。
    • 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
  3. 事件触发线程:
    • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
  4. 定时器触发线程:
    • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
    • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待JS引擎处理。
  5. http请求线程:
    • http 请求的时候会开启一条请求线程。
    • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

JS引擎是单线程

JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。

JS事件循环机制

JavaScript事件循环机制分为浏览器和Node事件循环机制,这里主要讲的是浏览器部分。
Javascript 有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。这里先了解JS执行环境及作用域,可以看这篇:浅谈JS执行环境和作用域

  1. JS调用栈
    • JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
  2. 同步任务、异步任务
    • JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
  3. Event loop
    • 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。

事件循环机制

事件循环下的宏任务和微任务

通常我们把异步任务分为宏任务与微任务,它们的区分在于:

  • 宏任务(macro-task):一般是JS引擎和宿主环境发生通信产生的回调任务,比如 setTimeout,setInterval是浏览器进行计时的,其中回调函数的执行时间需要浏览器通知到JS引擎,网络模块,I/O处理的通信回调也是。整体script代码、setTimeout、setInterval、setImmediate、DOM事件回调,ajax请求结束后的回调都是。执行顺序:整体script代码>setTimeout>setInterval>setImmediate
  • 微任务(micro-task):一般是宏任务在线程中执行时产生的回调。比如process.nextTick、Promise、Object.observe(已废弃), MutationObserver(DOM监听),都是JS引擎自身可以监听到的回调。执行顺序:process.nextTick>Promise>Object.observe

在事件循环中,任务一般都是由宏任务开始执行的(JS代码的加载执行),在宏任务的执行过程中,可能会产生新的宏任务和微任务,这时微任务会被添加到当前任务队列,当前宏任务执行完,会执行任务队列中的微任务,当前队列的微任务执行完毕,再执行新的宏任务。
事件循环图示

典型案例

  • 案例说明一
setImmediate(function(){
  console.log(1);
},0);
setTimeout(function(){
  console.log(2);
},0);
new Promise(function(resolve){
  console.log(3);
  resolve();
  console.log(4);
}).then(function(){
  console.log(5);
});
console.log(6);
process.nextTick(function(){
  console.log(7);
});
console.log(8);
复制代码

根据js的运行原理解释上面代码的执行顺序:

代码都是从script主程序开始执行;
创建setImmediate宏任务;
创建setTimeout宏任务;
执行到Promise,输出3、4,当前宏任务(script主程序)的程序还没有执行完,创建微任务Promise.then的回调;
执行console.log(6)输出6;
创建微任务process.nextTick;
执行console.log(8)输出8;
执行任务队列中的微任务,按照优先级:process.nextTick > Promise.then 依次输出 7,5;
微任务执行完毕,执行宏任务,按照优先级: setTimeout > setImmediate  依次输出2,1
//  3 4 6 8 7 5 2 1
复制代码
  • 案例说明二
setTimeout(function(){     
  console.log(1);   
},0)   
async function async1(){    
  console.log(2);    
  await async2();    
  console.log(3);   
}   
async function async2(){    
  console.log(4);   
}   
async1();   
new Promise(function(resolve,reject){    
  console.log(5);    
  resolve();   
}).then(function(e2){    
  console.log(6);   
})   
console.log(7);
复制代码

根据js的运行原理解释上面代码的执行顺序:

代码都是从script主程序开始执行;
创建setTimeout宏任务;
执行到async1(),等待await返回值后(此时打印了2,4)继续执行主程序代码,await后的代码被阻断,加入任务队列等待执行;
执行Promise输出5,当前宏任务(script主程序)的程序还没有执行完,创建微任务Promise.then的回调;
执行console.log(7)输出7;
执行任务队列中的微任务,按照顺序,依次打印await后的代码输出3,Promise.then的回调6;
微任务执行完毕,执行宏任务setTimeout,打印1。
// 2 4 5 7 3 6 1
复制代码

注意async函数里await右边的语句会立即执行,下面的代码进行等待状态,await返回值以后才会继续执行下一条语句。

参考文档: 参考文档1参考文档2

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享