JavaScript运行机制:event-loop
我们从javascript的单线程->任务队列->事件和回调函数->事件环,一步一步讲解javascript的执行机制。
一、JavaScript是单线程
JavaScript语言的特点是单线程,同一时间只能做一件事,JavaScript之所以是单线程,跟他的用途有关,作为浏览器脚本语言,JavaScript的主要用途是操作DOM,假如同时有两个js线程同时操作一个DOM,一个添加一个删除,浏览器就懵了吧。
二、任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后面的任务就必须等待。
IO读写操作一般是IO设备比较慢,这些任务耗时将很久,主线程将长时间处于等待结果返回的状态。
JavaScript语言的设计者意识到主线程完全可以不管处于等待队列中的任务,先去运行后面的任务,当处于等待中的任务返回结果,再继续执行此任务。
所有的任务可以分两种:同步任务、异步任务。同步任务是在主线程上排队的任务,一个任务执行完毕,才会执行下一个任务;异步任务是指,不进入主线程,而进入任务队列,只有任务队列通知主线程,某个异步任务可以执行了,主线程才会来执行此异步任务的回调。
异步执行的运行机制如下:
-
所有同步任务都在主线程上执行,形成一个执行栈;
-
主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件;
-
一旦执行栈中所有同步任务执行完毕,系统就会读取任务队列,哪个任务结束了等待状态,就进入执行栈,开始执行。
-
主线程不断重复上一步。
三、事件和回调函数
"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
四、Event Loop
主线程从任务队列中读取事件,这个过程是循环不断的,这个运行机制被称为Event Loop(事件环)
五、先来看一下浏览器的事件环
主线程运行的时候,产生堆和栈,heap就是堆,堆里面是存的是各种对象和函数,stack是栈,var a=1就存储在栈内;dom事件,ajax请求,定时器等异步操作的回调会被放到任务队列callback queue中,这个队列时先进先出的顺序,主线程执行完毕之后会依次执行callback queue中的任务,当这些任务是等待结束状态,就进入主线程被执行。
1、浏览器的宏任务和微任务
异步任务中分“宏任务(macro-task)”和“微任务(micro-task)”机制 macrotask 和 microtask 两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完,这就是浏览器中Event Loop对宏任务和微任务的执行机制。
两个类别的具体分类如下:
macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering,mesageChannel micro-task: Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver 我们用下面一段代码来检验一下是否理解浏览器事件环:
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('then1');
})
},0)
Promise.resolve().then(()=>{
console.log('then2');
Promise.resolve().then(()=>{
console.log('then3');
})
setTimeout(function(){
console.log('setTimeout2')
},0)
})
复制代码
执行结果是then2 then3 setTimeout1 then1 setTimeout2 首先代码里面的setTimeout和Promise都是异步任务,js从上到下执行代码,分别将这两个异步任务放到了宏任务队列和微任务队列,主线程先到微任务队列中,所以先输出了then2,然后在微任务队列中有添加一个then3的promise任务,在宏任务中添加了一个setTimeout2的定时器任务,现在接着去执行微任务队列,所以输出了then3,开始执行第一个宏任务,输出setTimeout1,并且在微任务队列又天机then1的promise任务,所以转去执行微任务,输出then1,再去执行一个宏任务,就是之前放进去的setTimeout2.