JavaScript事件循环

本文深入探讨JavaScript的浏览器内核线程、事件循环机制、定时器、宏任务与微任务,以及Promise和async/await的执行顺序。详细解释了JavaScript如何处理同步和异步任务,以及在不同场景下的任务调度和执行流程。
摘要由CSDN通过智能技术生成

1.浏览器内核有多种线程在工作:

  1. GUI渲染线程:
    负责渲染页面,解析html,css构成DOM树等
    和js引擎线程是互斥的,当JS引擎在工作时,GUI线程会被挂起,GUI更新被放在JS任务队列中,等待JS引擎线程空闲继续执行
  2. js引擎线程:
    单线程工作,负责解析JavaScrip脚本
    和GUI线程互斥,JS运行时间过长会导致页面阻塞。
    js引擎是单线程。每次只能执行一个任务,其他任务需要按照顺序排队等待被执行,只有当前任务执行完成之后才会往下执行下一个任务。

2.javaScript事件循环机制

分为两种:1.浏览器事件循环 2.Node事件循环

  1. 浏览器Event Loop是HTML中定义的规范;Node Event Loop是由libuv库实现的。
    浏览器部分:

  2. JavaScript有一个main thread 主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈等主线程执行。

  3. JS调用栈:
    JS调用栈是后进先出的数据结构。当函数被调用时,会把函数加入到栈顶,执行完成之后就从栈顶溢出该函数,直到栈内被清空。

  4. 同步任务、异步任务
    JavaScript单线程中的任务分为同步和异步。

    1. 同步任务会在调用栈中按照顺序等待主线程执行。
    2. 异步任务则会在异步有了结果后将注册的回调函数添加到任务队列等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。

Event Loopl 事件循环:

调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,就会去任务队列中按照顺序读取一个任务放入栈中。每次栈被清空,都会区读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,形成了事件循环。

3.定时器setTimeout

定时器会开启一个定时器触发线程来触发计时,定时器会在等待了指定时间后,把事件放入任务队列中等待读取到主线程里执行。

  1. 意义:
    定时器指定的演示毫秒数其实并不准确,因为计时器只是在到了指定的时间时将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中事件全部执行完成以后,才回去读取定时器的事件到主线程中,中间可能会存在耗时比较久的任务,所以不能保证在指定的时间执行。
  2. setTimeout 0的作用:
    浏览器会在执行完当前任务队列中的任务,再执行setTimeout队列中积累的任务。
    通过设置任务在0毫秒后执行,能够改变任务执行的先后顺序,延迟该任务发生,使之异步执行。

4.宏任务、微任务

ES6中microtask称为jobs,macrotask称为task
宏任务是由宿主发起的,微任务是JS自身发起。
ES5之后,JS引入了Promise,这样,不需要浏览器,JS殷勤自身也能够发起异步任务了。
JavaScript单线程中的任务可以细分为宏任务和微任务。

  1. macro-task:
    script(整体代码)、setTimeout、setInterval、setImmedate、I/O、UI rendering
  2. micro-task:
    process.nextTick、Promise的回调、Object.Observe、MutationObserver的回调

5.进一步说Promise:

  1. Promise构造函数是同步执行的,then方法时异步执行的。
  2. 构建Promise对象的时候,需要传入一个executor函数;
  3. Promise构造函数执行时立即调用executor函数,resolve和reject两个函数作为参数传递给executor,resolve和reject函数被调用时,状态发生变化。
  4. 所以在executor函数中调用resolve函数后,会触发promise.then设置的回调函数,而调用reject函数后,会触发promise.catch设置的回调函数。
new Promise(resolve=>{
      console.log(1);
      resolve(3);
  }).then(num=>{
      console.log(num);
  });
  console.log(2);
//输出顺序:1 2 3 


let a1=new Promise(()=>{
    setTimeout(()=>{
        console.log(1);
    },1000)
    console.log(2);
})
console.log(3);
//输出顺序:2 3 1

let a=new Promise((resolve,reject)=>{
    console.log(1);
    resolve("张三");
    console.log(2);
})
a.then(res=>{
    console.log("成功"+res);
},reason=>{
    console.log("失败"+reason);
})
console.log(3);
// 1 2 3 成功张三 
// 先进行一个整段代码宏任务,遇到resolve()、then()放入微任务
// 输出1 2 3 resolve会触发then(),输出 成功 张三

6.promise和async/await的执行顺序:

  1. async/await
    async/await是一种编写异步代码的新方法,之前的异步方案是回调和promise。
    async/await是建立在promise基础上的。
    async/await像promise一样,也是非阻塞的。
    async/await让异步代码看起来、表现起来更显同步代码。

很多人以为await是等右边的表达式 执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。

  1. await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面的js栈的代码。
  2. 等本轮事件循环执行完毕之后又会跳回async函数等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将promise放入promise队列。
    顺序解析:
function testSometing() {
    console.log("执行testSometing");
    return "testSometing";
}

async function testAsync() {
    console.log("执行testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");
    const v1 = await testSometing();//关键点1
    console.log(v1);
    const v2 = await testAsync();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise = new Promise((resolve)=> { console.log("promise start.."); resolve("promise");});//关键点2
promise.then((val)=> console.log(val));

console.log("test end...")

test start 
执行testSometing
promise start...
test end...
testSometing
执行testAsync
promise
hello async
testSomething hello async

分析:

  1. 执行到const v1 = await testSomting();的时候,会先将await后面的函数执行一遍,打印出“执行testSometing”的字符串,之后跳出整个async函数执行后面的;
  2. promise表达式是同步的,继续执行就可以,打印出“promise start …”;
  3. 遇到promise.then()方法,是异步的,加入到promise队列;
  4. 继续执行输出“test end…”;至此第一次事件循环结束。
  5. 跳回async函数中,const v1 = await testSomting();函数右边的返回值是非promise的,可以继续执行async函数中的代码,输出“testSomething”;
  6. 遇到const v2 = await testAsync();的时候,会先将await后面的函数执行一遍,输出“执行testAsync”,跳出函数,向下执行。
  7. 事件循环到了promise队列,执行then语句,输出promise;
  8. 回到test函数,继续执行。

将testSometing()改为异步函数:

async function testSometing() {
    console.log("执行testSometing");
    return "testSometing";
}

async function testAsync() {
    console.log("执行testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");
    const v1 = await testSometing();
    console.log(v1);
    const v2 = await testAsync();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise = new Promise((resolve)=> { console.log("promise start.."); resolve("promise");});//3
promise.then((val)=> console.log(val));

console.log("test end...")

test start...
执行testSometing
promise start...
test end
promise
testSometing
执行testAsync
hello async
testSometing hello async

分析:
promise的then方法比consolelog(v1)先执行:因为testSometing函数加了async,返回的是一个promise对象,需要等它的resolve,所以将当前promise推入队列,之后跳出test函数执行后续代码。

之后就开始执行promise的任务队列了,因为then()这个promise对象鲜卑推入队列,所以先执行。

3:

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout') 
},0)  
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
}).then(function(){
    console.log('promise2')
})
console.log('script end')

script start  //代码从上到下执行,先打印,再调用async1()
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout

分析:

  1. console.log(‘script start’),打印,之后执行async1()
  2. 执行async1(),console.log(‘async1 start’),打印
  3. await async2(),先将await后面的函数执行一遍,console.log(‘async2’),打印之后跳出async1(),向下执行
  4. 执行promise(),console.log(‘promise1’),then()推入promise队列,
  5. console.log(‘script end’),打印,第一次事件循环结束
  6. 跳入async1(),await async2(),async2返回的是一个promise对象,需要等它的resolve,所以将当前promise推入队列,之后跳出async1()执行后续代码。
  7. 执行promise的.then(),console.log(‘promise2’)
  8. 跳入async1(),await async2(),async2返回的是一个promise对象,被它resolve之后,返回的是resolve的值,不再是promise,继续向下执行console.log(‘async1 end’)
  9. 执行定时器事件

7.执行顺序:

第一次事件循环(读取任务对列-执行)中,JavaScript引擎会把整个script代码当作一个宏任务执行,执行之后,再检测本次循环中是否存在微任务,存在的话就依次从微任务的队伍队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS的执行顺序就是每次事件循环中的宏任务-微任务。

分析执行顺序1:
console.log(1);
setTimeout(function() {
    console.log(2);
})
var promise = new Promise(function(resolve, reject) {
    console.log(3);
    resolve();
})
promise.then(function() {
    console.log(4);
})
console.log(5);

setTimeout和Promise被称为任务源,来自不同的任务源注册的回调函数会被放入不同的任务队列中。

  1. 第一次事件循环,整段代码作为宏任务进入主线程执行。console.log(1)
  2. 遇到了setTimeout,等过了指定时间,放入宏任务的任务队列,等待同步任务和现有的任务队列中的事件全部执行完,才读取计时器中的事件到主线程。
  3. 遇到了Promise,将then函数放入到微任务的任务队列中。
  4. 整个事件循环以后,检测微任务中的任务队列中是否存在任务,存在就执行。

第一次的循环结果:1.3.5.4

  1. 接着再到宏任务的任务队列中按顺序取出一个宏任务到栈中让主线程执行,那么在这次循环中的宏任务就是 setTimeout 注册的回调函数,执行完这个回调函数,发现在这次循环中并不存在微任务,就准备进行下一次事件循环。
  2. 检测到宏任务队列中已经没有了要执行的任务,那么就结束事件循环。
    最终的结果就是 1,3,5,4,2。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值