浏览器中的事件循环

浏览器中的事件循环

总所周知 JS 运行在浏览器中,以单线程方式运行,每个window一个JS线程。那么浏览器是如何处理js中的I/O读取、用户点击、setTimeout等异步事件,并使其他js代码不被阻塞的呢?

浏览器中的事件循环就是其解决方式。简单来说浏览器中的事件循环的机制是将产生的异步事件产生的回调暂时存储在事件队列中,等到合适的时机再去执行队列中的异步事件的回调。

运行时概念

要了解浏览器中的事件循环,需要先弄明白两个重要的运行时概念。

执行栈,函数调用时形成一个调用帧,并压入栈中,当函数返回时,则帧弹栈。

队列

任务队列,每一个任务都包含一个处理该任务的函数,当任务产生时,任务及其处理函数会被作为一个整体推入任务队列中(例如:一个setTimeout,到达时间时,setTimeOut及其回调函数会作为一个任务被推入任务队列中)。任务队列按照先进先出的顺序执行。当任务队列里的任务需要被处理的时候(即调用任务的处理函数时),将会被移出队列,调用其处理函数,此时形成一个调用帧,并压入执行栈。

此时执行栈中的调用帧,直到执行栈为空,然后再去处理队列中的另一个任务。

浏览器任务

浏览器中的任务分为两种:task(macroTask 宏任务)和microtask(微任务)。不同的任务按照不同的规则执行。

task

一个事件循环里有多个task queue,其中的包含多个任务,每个任务严格的按照先进先出的顺序执行。在一个task执行结束后下一个task执行之前,浏览器可对页面进行重新渲染。 task queue中包含:

  • script整体代码
  • 浏览器事件(鼠标事件、键盘事件等)
  • 定时事件(setTimeout、setInterval、setImmediate)
  • I/O事件(资源读取等)
  • UI渲染
microTask

一个事件循环中包含一个microTask queue。 microTask queue包含:

  • Promise
  • Object.observe、MutationObserver

事件循环

执行至完成

一个任务完整的执行后,其他任务才会被执行。 即:执行栈中的调用帧,直到执行栈为空,然后再去处理队列中的另一个任务。

添加任务至队列

在浏览器中,当事件发生并且该事件绑定了事件监听时,该事件发生后的任务才会被添加至队列。

例如:为一个DOM元素button绑定onclick一个处理事件,只有当button元素上的click事件发生时,该事件发生后的任务会被添加至队列。

再例如: setTimeout 接受两个参数:待加入队列的任务和一个延迟。延迟代表了任务被添加至任务队列的时间,只有经过了延迟的时间,该任务才会被加入队列。添加至队列以后是否被处理,取决于队列里是否有其他任务。因此延迟的时间表示最少延迟时间,而非确切的等待时间。

事件循环进程模型

事件循环进程模型 步骤如下:

  1. 选择第一个进入到 task queue中的任务[task],如果task queue中没有任务,则直接进入第6步;
  2. 将当前事件循环的任务设置为第一步选出的[task];
  3. 执行任务[task];
  4. 将当前事件循环任务设置为null;
  5. 在task queue中删除执行完毕的[task]任务;
  6. microtask阶段:进入microtask检查点;
  7. 按照浏览器界面更新策略渲染界面;
  8. 返回第1步;
其中第6步,microtask阶段步骤如下:
  1. 设置microtask检查点标记为true;
  2. 重复检查microtask queue是否为空,若为空直接进入第3步;若不为空:
  • 选择microtask queue中的第一个[microtask];
  • 将当前事件循环的任务设置为第3步选出的[microtask];
  • 执行任务[microtask];
  • 将当前事件循环任务设置为null;
  • 在microtask queue中删除执行完毕的[microtask]任务;
  • 回到第2步
  1. 清理index database 事务;
  2. 设置microtask检查点的标记为false;
事件循环进程模型总结

在事件循环中,首先从task queue中选择最先进入的task执行,每执行完一个task都会检查microtask queue是否为空,若不为空则执行完microtsk queue中的所有任务。然后再选择task queue中最先进入的task执行,以此循环。

总结上述步骤为流程图:

用代码解释

代码栗子1:
console.log('这是开始');

setTimeout(function cb() {
	console.log('这是来自第一个回调的消息');
}, 100);

console.log('这是一条消息');

setTimeout(function cb1() {
	console.log('这是来自第二个回调的消息');
}, 0);

Promise.resolve().then(function() {
	console.log('promise1');
}).then(function() {
	console.log('promise2');
});

console.log('这是结束');
复制代码
输出结果为:
    这是开始
    这是一条消息
    这是结束
    promise1
    promise2
    这是来自第二个回调的消息
    这是来自第一个回调的消息
复制代码
步骤解析:
  1. 初始状态队列的信息为:
    • task queue:run script;
    • microtask queue:【空】;
  2. 从task queue中拿出run script执行;执行完成后从task queue中删除run script任务;
    • 当前事件循环执行栈:run script;
    • task queue:setTimeout2 callback;
    • microtask queue:promise1 then;
输出:
    这是开始
    这是一条消息
    这是结束
复制代码
  1. 进入microtask检查点;从microtask queue中拿出promise1 then执行;将promise2 then推入microtask queue;执行完成后从microtask queue中删除promise1 then任务;

    • 当前事件循环执行栈:promise1 then;
    • task queue:setTimeout2 callback;
    • microtask queue:promise2 then;
输出:
    这是开始
    这是一条消息
    这是结束
    promise1
复制代码
  1. 查看microtask queue中是否还有任务;有则从microtask queue中拿出promise2 then执行;执行完成后从microtask queue中删除promise2 then任务;

    • 当前事件循环执行栈:promise2 then;
    • task queue:setTimeout2 callback;
    • microtask queue:【空】;
输出:
    这是开始
    这是一条消息
    这是结束
    promise1
    promise2
复制代码
  1. 到达100ms后,将setTimeout1 callback推入task queue;次步骤和3、4步无明确的前后关系,依据所设定的时间长短而定;setTimeout1 callback被推入task queue中以后不一定会立刻执行,因为task queue中可能存在其他任务尚未执行;因此setTimeout1 callback实际执行时间点>=100ms;
    • task queue:setTimeout2 callback、setTimeout1 callback;
    • microtask queue:【空】;
  2. microtask queue为空,继续取出task queue中的任务setTimeout2 callback执行,执行完成后从task queue中删除setTimeout2 callback任务;
    • 当前事件循环执行栈:setTimeout2 callback;
    • task queue:setTimeout1 callback;
    • microtask queue:【空】;
输出:
    这是开始
    这是一条消息
    这是结束
    promise1
    promise2
    这是来自第二个回调的消息
复制代码
  1. 检查microtask检查点,microtask queue为空,继续取出task queue中的任务setTimeout1 callback执行,执行完成后从task queue中删除setTimeout1 callback任务;

    • 当前事件循环任务:setTimeout1 callback;
    • task queue:【空】;
    • microtask queue:【空】;
输出:
    这是开始
    这是一条消息
    这是结束
    promise1
    promise2
    这是来自第二个回调的消息
    这是来自第一个回调的消息
复制代码
复杂的代码栗子2:
console.log('script start')
async function async1() {
    await async2();
    console.log('async1 end');
    setTimeout(function() {
    	console.log('async1 setTimeout')
    }, 0);
}
async function async2() {
    console.log('async2 end');
    setTimeout(function() {
    	console.log('async2 setTimeout')
    }, 0);
}
async1();

setTimeout(function() {
    Promise.resolve().then(function() {
    	console.log('setTimeout promise');
    })
	console.log('setTimeout');
}, 0);

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
	console.log('promise1')
})
.then(function() {
	console.log('promise2')
})

console.log('script end')
复制代码
输出结果为:
    script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout
	setTimeout promise
	async1 setTimeout
复制代码
步骤解析:

async函数是promise的一个语法糖,简单理解为:await中的语句相当于在promise.resolve()中;await后面的语句相当于.then中的语句

  1. 初始状态的队列信息
    • task queue:run script;
    • microtask queue:【空】;
  2. 【task 阶段】执行run script;根据上述对await的解释,async1中的await async2()直接执行,async1中的await 后面的语句相当于promise then去处理
    • 当前事件循环执行栈:run script;
输出:
	script start
复制代码

-- 2.1 执行到调用async1()语句,在async1中执行await async2,async2中的语句直接执行,async2 setTimeout callback被推入task queue中;async1中的await async2后面的语句相当于promise then被推入microtask queue中; ​ - task queue:async2 setTimeout callback; ​ - microtask queue:async1中的await async2后面的语句;

输出:
	script start 
	async2 end
复制代码

-- 2.2. 执行到setTimeout将setTimeout callback 推入 task queue中; ​ - task queue:async2 setTimeout callback、setTimeout callback; ​ - microtask queue:async1中的await async2后面的语句; -- 2.3. 执行到promise,执行resolve,将promise then1推入microtask queue; ​ - task queue:async2 setTimeout callback、setTimeout callback; ​ - microtask queue:async1中的await async2后面的语句、promise then1;

输出:
	script start
	async2 end 
	Promise
复制代码

-- 2.4. 执行输出script end;

输出:
	script start
	async2 end
	Promise
	script end
复制代码
  1. 【microtask 阶段】执行async1中的await async2后面的语句,输出内容,并将async1 setTimeout callback推入task queue中

    • 当前事件循环执行栈:async1中的await async2后面的语句;
    • task queue:async2 setTimeout callback、setTimeout callback、async1 setTimeout callback;
    • microtask queue:promise then1;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
复制代码
  1. 【microtask 阶段】执行promise then1,输出内容,并将promise then2推入microtask queue中
    • 当前事件循环执行栈:promise then1;
    • task queue:async2 setTimeout callback、setTimeout callback、async1 setTimeout callback;
    • microtask queue:promise then2;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
复制代码
  1. 【microtask 阶段】执行promise then2,输出内容,microtask queue为空
    • 当前事件循环执行栈:promise then2;
    • task queue:async2 setTimeout callback、setTimeout callback、async1 setTimeout callback;
    • microtask queue:【空】;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
复制代码
  1. 【task 阶段】microtask queue为空,进入task阶段,执行async2 setTimeout callback,输出内容
    • 当前事件循环执行栈:async2 setTimeout callback;
    • task queue:setTimeout callback、async1 setTimeout callback;
    • microtask queue:【空】;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout 
复制代码
  1. 【task 阶段】microtask queue为空,执行setTimeout callback,将setTimeout promise推入microtask queue,输出内容
    • 当前事件循环执行栈:setTimeout callback;
    • task queue:async1 setTimeout callback;
    • microtask queue:setTimeout promise;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout 
复制代码
  1. 【microtask 阶段】检查microtask queue,执行setTimeout promise,输出内容
    • 当前事件循环执行栈:setTimeout promise;
    • task queue:async1 setTimeout callback;
    • microtask queue:【空】;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout
	setTimeout promise 
复制代码
  1. 【task 阶段】microtask queue为空,执行async1 setTimeout callback,输出内容
    • 当前事件循环执行栈:async1 setTimeout callback;
    • task queue:【空】;
    • microtask queue:【空】;
输出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout
	setTimeout promise
	async1 setTimeout
复制代码
参考:

什么是浏览器的事件循环(Event Loop)?

HTML标准-事件循环

MDN-并发模型与事件循环

一次弄懂Event Loop

转载于:https://juejin.im/post/5c6a25b151882562a12ada20

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值