事件循环之浏览器事件循环

date
Mar 21, 2022
slug
event-loop-in-browsers
status
Published
tags
Interview
Browsers
summary
事件循环之浏览器事件循环
type
Post

概念

事件循环是什么?

  • 可以把事件循环理解成我们编写的 JavaScript 和浏览器或者 Node 之间的一个桥梁。
    • 浏览器的事件循环是我们编写的 JavaScript 代码和浏览器API调用(setTimeout|AJAX|监听事件等)的一个桥梁,桥梁之间他们通过回调函数进行沟通。
    • Node 的事件循环是一个我们编写的 JavaScript 代码和系统调用(file system|network等) 之间的一个桥梁,桥梁之间他们通过回调函数进行沟通。
    •  

进程和线程

  • 线程和进程是操作系统中的两个概念:
    • 进程( process ): 计算机已经运行的程序;
    • 线程( thread ): 操作系统能够运行运算调度的最小单位;
  • 简单解释:
    • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);
    • 线程:每一个进程中,都会启动一个线程来执行程序中的代码,这个线程被称之为主线程;
    • 所以我们也可以说进程是线程的容器;
  • 生活例子类比:
    • 操作系统类似于一个工厂
    • 工厂中有很多的车间,这个车间就是进程
    • 每个车间可能有一个以上的工人在工厂,这个工人就是线程
 

多进程多线程开发

  • 操作系统是如何做到同时让多个进程(边听歌,边写代码,边查阅资料)同时工作的呢?
    • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换
    • 当我们的进程中的线程获取到时间片时,就可以快速的执行我们编写的代码
    • 对于用户来说是感受不到这种快速切换的
 

浏览器和JavaScript

  • 我们经常说JavaScript时单线程的,但是JavaScript 的线程应该有自己的容器进程:浏览器或者 Node
  • 浏览器是一个进程吗,它里面只有一个线程吗?
    • 目前多数的浏览器其实是多进程的,当我们打开一个tab页面时就会开一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
    • 每个进程中又有很多的线程,其中包括执行 JavaScript 代码的线程
  • 但是 JavaScript 的代码执行是在一个单独的线程中执行的
    • 这就意味着 JavaScript 的代码,在同一个时刻只能做一件事
    • 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞
 

JavaScript执行流程

  • 对于普通的 JavaScript 代码的执行,会有一个函数调用栈的概念,调用的函数会被压入到栈中,先进栈的后出栈。
 

浏览器的事件循环

  • 如果在执行 JavaScript 代码的过程中,有异步操作呢?
    • 在代码执行过程中插入一个 setTimeout 函数的调用
    • 这个函数被放到调用栈中,执行会立即结束,并不会阻塞后续代码的执行
  • 那么,传入的函数,会在什么时候被执行呢?
    • 事实上,setTimeout是调用了 web api (计时器线程负责),在合适的时机,会将回调函数加入到一个事件队列中
    • 事件队列中的函数,会被放入到调用栈中,在调用栈中被执行
 

宏任务与微任务

  • 但是事件循环中并非只维护着一个队列,事实上浏览器中是有两个队列:
    • 宏任务队列(macrotask queue): ajax | setTimeout |setInterval | DOM监听 | UI Rendering 等
    • 微任务队列(microtask queue): Promise的then 回调,Mutation Observer API | queueMicroTask() 等
  • 那么事件循环对于两个队列的优先级是怎么样的呢?
    • main script 中的代码优先执行(编写的顶层 script 代码)
    • 在执行任何一个宏任务之前(不是队列,是一个宏任务), 都会先查看微任务队列中是否有任务需要执行
      • 也就是宏任务执行之前,必须保证微任务队列是空的
      • 如果不为空,那么就优先执行微任务队列中的任务(回调)
 

⚠️提示

async、await 是Promise的一个语法糖:
  • 我们可以将awit 关键字后面执行的代码,看作是包裹在 (resolve, reject) ⇒ {函数执行} 中的代码,类似于new Promise 的构造函数,就是立即执行
  • awit 的下一条语句,可以看作是 then(res ⇒ {函数执行}) 中的代码

练习

习题一:

 
setTimeout(function(){
	console.log('set1');

	new Promise(function(resolve){
		resolve();
	}).then(function(){
		new Promise(function(resolve){
			resolve();
		}).then(function(){
			console.log('then4')
		});
		console.log('then2')
	})
})

new Promise(function(resolve){
	console.log('pr1')
	resolve()
}).then(function(){
	console.log('then1')
});

setTimeout(function(){
	console.log('set2')
});

console.log(2)

queueMicrotask(() => {
	console.log('queueMicrotask1')
});

new Promise(function(resolve){
	resolve();
}).then(function(){
	console.log('then3')
})
 
  • 分析
    • 首先第一部分是一个setTimeout 函数,因此他的回调函数将会被放到宏任务队列中,等待被执行。
      • notion image
    • 然后是一个new Promise, 它的构造函数将会被立即执行,因此首先输出打印 pr1,紧接着resolve() 被执行,对应的then 函数回调 then1 被放到了微任务队列。
      • notion image
    • 接着还是个setTimeout。同样的,它的回调函数会被添加到宏任务队列,此时没有输出打印。
      • notion image
    • 接着是一个正常的log函数执行,此时输出打印2。
      • notion image
    • 接下来是一个queueMicotask函数,它的回调函数将会被添加到微任务队列,没有额外输出打印。
      • notion image
    • 然后又是一个new Promise, 它的构造函数 resolve() 将会被立即执行,因此对应的then 函数回调会被添加到微任务队列,没有额外输出打印。
      • notion image
    • 此时main script 全部执行完毕,接下来将会优先查看微任务队列中是否有回调函数需要执行,此时微任务队列中有 then1 queuetask1 then3 三个回调函数,因此将依次执行这三个函数,则输出打印 then1, queuetask1, then3。
      • notion image
         
    • 当前微任务队列已空,则查看宏任务队列,执行 set1 回调函数,首先执行log(’set1’), 则输出打印set1, 然后继续执行 new Promise 的构造函数resolve(),将对应的then 回调函数添加到微任务队列,暂且将这个回调函数命名为cb1。
      • notion image
    • 此时微任务队列中有cb1 回调函数,因此优先执行cb1 函数。 则 new Promise 的构造函数resolve()被执行,对应的then 回调函数 then4 被添加到了微任务队列,然后继续执行后面的log(then2),则输出打印then2。
      • notion image
    • 此时微任务队列中还有then4 这个回调函数,则执行函数,输出打印then4。
      • notion image
         
    • 最后,微任务队列为空,宏任务队列中只剩set2 函数,则执行set2, 输出打印set2。
      • notion image
         
  • 结论
    • 最后的输出结果就是:pr1 2 then1 queuetask1 then3 set1 then2 then4 set2
 

习题二:

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')
  • 分析
    • 首先是async1 和async2 两个函数的定义,然后是log函数执行,则输出打印 script start, 接下来代码继续执行 setTimeout,将setTimeout 回调函数添加到宏任务队列。
      • notion image
    • 接着是async1 函数被调用,则执行打印,输出打印 async1 start, awit 后面的 async2 函数被调用且立即执行,则输出打印 async2。 awit 下面一句则相当于then 函数回调,它会被添加到微任务队列,此时async1 函数调用结束。
      • notion image
    • 接下来是 new Promise, 则立即执行构造函数,输出打印 promise1, 然后执行resolve(), 即将后面then 的回调函数添加到微任务队列。然后执行最后一个打印语句,则输出打印 script end。 main script 执行完毕。
      • notion image
    • main script 执行完毕之后则优先检查微任务队列是否有回调函数,依次执行,因此打印输出 async1 end, 然后再是 promise2。
      • notion image
    • 最后微任务队列中也为空,则查看宏任务队列,执行setTimeout 回调函数,则打印输出setTimeout。
      • notion image
  • 结论
    • 最后输出的结果就是: script start, async1 start, async2, promise1, script end, async1 end, promise2 setTimeout
    •  

© xk_wan 2021 - 2024