什么是事件循环 Eventloop

简介: 什么是事件循环 Eventloop

什么是事件循环 Eventloop

同步编程

我们先不着急明白事件循环是什么。先从它的起源入手。大家都知道JavaScript是同步的,也就是单线程,原因是因为如果不使用单线程,在操作DOM时可能会出现一些问题,比如我们有两个事件,一个是删除div,一个是添加div,他们的执行顺序不同,导致的结果也将截然不同。比如当前有div1和div2,如果先执行删除后添加,那么得到的就是div1和div2,但是如果是先执行添加后删除,那么得到的还是div1和div2。为了避免这种逻辑上的混乱,因此规定JavaScript是单线程的。

异步编程

但是如果JavaScript如果只是单线程的话,那也会有问题。比如我们执行多个任务,其中一个任务执行需要的时间很长,比如读取大文件或巨量的数据,此时就会造成阻塞,导致后面的任务无法执行,必须等待,造成程序假死(实际在执行,只是其中一个任务耗时特别长),用户体验极差。于是JavaScript就有了异步任务的概念。

所谓的异步任务,和同步任务不同,同步任务是一个任务和它的回调函数完成了,才执行下一个任务。但是异步任务却是,一个任务执行了,还没执行回调函数,就直接开始执行下一个任务。

举个不是很恰当的例子,就是一堆人在食堂排队买饭吃,第一个人买完饭(第一个任务执行了),但是还没开始吃(回调函数还没执行);第二个人就开始买饭(第二个任务就执行了),还没开始吃,第三个人就又开始买饭了(第三个任务就执行了)。

宏任务(macro task)

整体代码的script(外层的同步代码)、setTimeout、setInterval、DOM监听、UI渲染相关事件、ajax等。

微任务(micro task)

promise、async await、process.nextTick等就是微任务。

事件循环 eventloop

说了这么多,那事件循环究竟是什么呢?事件循环,简单理解就是代码的执行流程。而理解事件循环就是理解所谓的同步代码、异步代码或者说宏任务、微任务的执行的先后顺序。

事件循环的执行顺序

执行顺序

先执行同步代码(主任务中的宏任务),遇到异步宏任务将异步宏任务放进宏任务队列,遇到异步微任务放进微任务队列。所有同步代码执行后再将异步微任务调入主线程中执行,执行完毕后,再将宏任务队列中的宏任务调入到队列中执行。

练习 1

同步、异步微任务、异步宏任务的执行顺序

Promise.resolve().then(()=>{
  console.log('1')  
  setTimeout(()=>{
    console.log('2')
  },0)
});
setTimeout(()=>{
  console.log('3')
  Promise.resolve().then(()=>{
    console.log('4')    
  })
},0);
console.log('5');

让我们分析下这道题目中的代码执行顺序,为了方便,我给每个任务都给个编号:

1、先从最外层开始看,先找同步代码:

// => 任务(1)
console.log('5');

2、再看异步代码,先看异步微任务:

// => 任务(2)
Promise.resolve().then(()=>{
  console.log('1')  
  setTimeout(()=>{
    console.log('2')
  },0)
});

3、最后看异步宏任务:

// => 任务(3)
setTimeout(()=>{
  console.log('3')
  Promise.resolve().then(()=>{
    console.log('4')    
  })
},0);

此时我们的任务列表可以分为 3 条,最外层同步代码:(1);异步微任务:(2);异步宏任务:(3)

4、此时按照顺序执行(1)(2),输出 5 1 又遇到宏任务

// => 任务(4)
setTimeout(()=>{
    console.log('2')
},0)

再次放进宏任务队列中,此时任务列表的已经没有了最外层同步代码和异步微任务,只剩下异步宏任务:(3)(4)

5、按照顺序执行(3),输出3时,又遇到异步微任务

// => 任务(5)
Promise.resolve().then(()=>{
    console.log('4')    
  })

放进微任务队列中,此时任务列表变成了,异步微任务:(5);异步宏任务:(4)

6、此时按照顺序执行就直接输出了 4 2

完整流程

Promise.resolve().then(()=>{  // => (2)
  console.log('1')  // =>  (2-1)
  setTimeout(()=>{  // =>  (4)
    console.log('2')  // => (4-1)
  },0)
});
setTimeout(()=>{  // => (3)
  console.log('3')  // => (3-1)
  Promise.resolve().then(()=>{  // (3-2)
    console.log('4')  // => (3-2-1)  
  })
},0);
console.log('5'); // => (1)
---控制台最终结果---
5
1
3
4
2

练习 2

// 任务A
setTimeout(()=>{
    console.log(1)
},20)
// 任务B
setTimeout(()=>{
    console.log(2)
},0)
// 任务C
setTimeout(()=>{
    console.log(3)
},10)
// 任务D
setTimeout(()=>{
    console.log(4)
},10)
// 任务E
console.log(5)

按照队列顺序排列,最外层同步任务:E;异步宏任务:A、B、C、D。

虽然任务的执行顺序如上,但是setTimeout具有延时效果,因此真正的输出情况应该是:

---控制台打印---
5
2
3
4
1

练习 3

console.log(1)
new Promise((resolve,reject)=>{
    console.log(2)
    resolve()
}).then(res=>{
   console.log(3) 
})
console.log(4)

值得注意的是:上面一个promise中,console.log(2)属于宏任务,console.log(3) 才属于微任务。

第一轮是先执行同步代码(最外层的宏任务),先输出1,代码执行到promise时立即输出2,resolve将.then()中的代码放入到了微任务中,宏任务继续输出4,最后执行微任务输出3。

所以最终的输出是:

---控制台打印---
1
2
4
3

练习 4 setTimeout和promise的执行顺序

setTimeout(()=>{
    console.log(1)
},0)
new Promise((resolve,reject)=>{
    console.log(2)
    resolve()
}).then(()=>{
    console.log(3)
}).then(()=>{
    console.log(4)
})
console.log(5)

先执行同步代码(主栈中的宏任务),遇到setTimeout放入宏任务列表,执行到promise,马上输出2,resolve()将then()中的两部分代码丢入到微任务中,输出5,第一轮宏任务结束,开始执行第一轮留下的微任务,输出3和4,第一轮循环结束。开始第二轮宏任务setTimeout输出1

2
5
3
4
1

练习 5 setTimout和promise执行顺序变种题

setTimeout(()=>{
    console.log(1)
},0)
new Promise((resolve,reject)=>{
    console.log(2)
    for(let i = 0;i < 1000;i++){
        if(i === 10){
           console.log(10)
        }
        i === 999 && resolve()
    }
    console.log(3)
}).then(()=>{
    console.log(4)
})
console.log(5)

其实解题思路和一样,先执行同步代码(主栈中的宏任务),setTimeout放进宏任务列表中,遇到promise直接输出2,循环时输出10,resolve()将.then()中的代码放进微任务列表中,输出3,再输出5,第一轮宏任务结束,开始执行遗留下的微任务,直接输出4。第一轮事件循环结束,开始第二轮宏任务,setTimout输出1

---控制台打印---
2
10
3
5
4
1

在chrome的控制台console中输出结果的话,会发现每相邻的两个事件循环之间都会有一个undefined进行隔离

---控制台打印---
2
10
3
5
4
undefined
1

练习 6 setTimeout和promise相互嵌套

console.log(1)
setTimeout(()=>{
    console.log(2)
    Promise.resolve().then(()=>{
        console.log(3)
    })
},0)
new Promise((resolve,reject)=>{
    console.log(4)
    setTimeout(()=>{
        console.log(5)
        resolve(6)
    },0)
}).then(res=>{
    console.log(7)
    setTimeout(()=>{
        console.log(res)
    })
})

老规矩,先执行同步代码(主栈的宏任务),输出1,遇到setTimeout

setTimeout(()=>{
    console.log(2)
    Promise.resolve().then(()=>{
        console.log(3)
    })
},0)

放入宏任务列表中,遇到promise直接输出4,又遇到setTimeout

setTimeout(()=>{
  console.log(5)
    resolve(6)
},0)

放进宏任务列表中,第一轮宏任务结束,且没有微任务,结束第一轮事件循环。

开始第二轮宏任务,setTimeout输出2,resolve将console.log(3)放进微任务中,第二轮宏任务结束,执行第二轮微任务,输出3,结束第二轮事件循环。

开始第三轮宏任务,执行下一个setTimeout输出5,同时resolve(6)将

res=>{
 console.log(7)
 setTimeout(()=>{
     console.log(res)
 })
}

放进微任务列表中,第三轮宏任务结束,执行第三轮微任务,输出7,将

setTimeout(()=>{
     console.log(res)
 })

放进宏任务列表中,第三轮事件循环结束。开始第四轮宏任务,直接输出res,就是6

所以输出顺序为:

---控制台打印---
1
4
2
3
5
7
6

练习 7

async function async1(){
    console.log(1)
    await async2()
    console.log(2)
}
async function async2(){
    new Promise((resolve)=>{
        console.log(3)
        resolve()
    }).then(()=>{
        console.log(4)
    })
}
console.log(5)
setTimeout(()=>{
    console.log(6)
},0)
async1()
new Promise((resolve)=>{
    console.log(7)
    resolve()
}).then(()=>{
    console.log(8)
})
console.log(9)

第一轮宏任务(同步代码)开始,先输出5,遇到

setTimeout(()=>{
    console.log(6)
},0)

放进宏任务中,执行async1,输出1,执行async2,输出3,将

console.log(4)

放进宏任务中,执行async1,输出1,执行async2,输出3,将

console.log(4)

console.log(2)

放入微任务中,遇到promise,输出7,将

console.log(8)

放入微任务中,输出9,第一轮宏任务结束。开始遗留的微任务,输出4、2、8,第一轮事件循环结束。

开始第二轮事件循环,开始第二轮宏任务,输出6,事件循环结束。

---控制台打印---
5
1
3
7
9
4
2
8
6

练习 8

async function async1() {
    console.log('5');
    await async2();
    console.log('6');
}
async function async2() {
    console.log('7');
}
setTimeout(()=>{
    console.log(1)
})
new Promise(function(resolve){
    console.log(2);
    resolve();
}).then(function(){
    console.log(3)
});
console.log(4);
async1();

执行第一轮宏任务(同步代码)

setTimeout(()=>{
    console.log(1)
})

放入宏任务列表,遇到promise,输出2,console.log(3)放入微任务,输出4,执行async1,输出5,执行async2,输出7,console.log(‘6’)放入微任务中,第一轮宏任务结束,开始执行遗留的微任务,输出3和7。第一次事件循环结束,第二次宏任务开始,输出1。

所以,执行顺序为:

---控制台打印---
2
4
5
7
3
6
1

总结

其实事件循环本身并不难,总结一下:

(1)先执行主栈中的宏任务(同步代码),遇到setTimeout之类的宏任务就放进下个循环事件的宏任务列表中,遇见promise或者async await(其实就是对promise的封装)之类的微任务,就放进当前循环的微任务列表中。

(2)宏任务执行完毕后,执行当前循环中的微任务。完成当前事件循环中的所有微任务后,当前事件循环结束。

(3)开启下一轮循环后,重复上诉操作,注意每个setTimeout本身是一个宏任务,而非多个setTimeout为一个宏任务。

(4)当然,在我们的日常工作中基本上都使用了async await的操作将异步变成同步的写法,大家很容易忘记这部分的知识点。不过在我们的工作中,有些仍然会遇到直接使用promise或者多个setTimeout的情况,这时候代码的执行顺序常常使我们困惑,因此熟悉事件循环还是有一定必要的。

目录
相关文章
|
7月前
|
安全 API
muduo源码剖析之EventLoop事件循环类
EventLoop.cc就相当于一个reactor,多线程之间的函数调用(用eventfd唤醒),epoll处理,超时队列处理,对channel的处理。运行loop的进程被称为IO线程,EventLoop提供了一些API确保相应函数在IO线程中调用,确保没有用互斥量保护的变量只能在IO线程中使用,也封装了超时队列的基本操作。
88 0
|
JavaScript
26 # eventloop 执行顺序
26 # eventloop 执行顺序
61 0
|
2月前
|
JavaScript 数据库
事件循环
【10月更文挑战第28天】
40 3
|
1月前
|
存储 JavaScript 前端开发
事件循环的原理是什么
事件循环是一种编程机制,用于在单线程环境中处理多个任务。它通过维护一个任务队列,按顺序执行每个任务,并在任务之间切换,从而实现并发处理。在每个循环中,事件循环检查是否有新的任务加入队列,并执行就绪的任务。
|
3月前
|
存储 JavaScript 前端开发
JavaScript:事件循环机制(EventLoop)
【9月更文挑战第6天】JavaScript:事件循环机制(EventLoop)
41 5
|
4月前
|
存储 前端开发 JavaScript
事件循环机制是什么
【8月更文挑战第3天】事件循环机制是什么
40 1
|
6月前
|
调度 C++ 开发者
C++一分钟之-认识协程(coroutine)
【6月更文挑战第30天】C++20引入的协程提供了一种轻量级的控制流抽象,便于异步编程,减少了对回调和状态机的依赖。协程包括使用`co_await`、`co_return`、`co_yield`的函数,以及协程柄和awaiter来控制执行。它们适合异步IO、生成器和轻量级任务调度。常见问题包括与线程混淆、不当使用`co_await`和资源泄漏。例如,斐波那契生成器协程展示了如何生成序列。正确理解和使用协程能简化异步代码,但需注意生命周期管理。
118 4
|
7月前
|
前端开发 JavaScript UED
|
JavaScript 前端开发 调度
25 # eventloop 执行流程
25 # eventloop 执行流程
58 0
|
前端开发 JavaScript
说说你对事件循环的理解?
说说你对事件循环的理解?