消息队列和事件循环事件:页面是怎么活起来的?
想要在线程运行过程中能接收并执行新的任务,就需要采用事件循环机制
消息队列是一种数据结构,可以存放要执行的任务,符合先进先出的特点,也就是说要添加任务的话,添加到队列的尾部,要取出任务的话,从队列头部去取
IO线程:渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息。。接收到消息后,会将这些消息组装成任务发送给渲染主线程
- 添加一个消息队列
- IO 线程中产生的新任务添加进消息队列尾部
- 渲染主线程会循环从消息队列头部读取任务、执行任务
浏览器页面是由消息队列和事件循环系统来驱动的
消息类型
输入事件(鼠标滚动、点击、移动
微任务
文件读写
WebSocket
JS 定时器
- 页面相关的事件,
JS执行,解析DOM,样式计算,布局计算,CSS动画
页面使用单线程的缺点
如何处理高优先级任务
监控DOM节点的变化情况(节点的插入,修改,删除)微任务
消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了
执行效率
的问题。等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了
实时性
问题。
如何解决单个任务执行时长过久的问题
其中有个 JavaScript 任务因执行时间过久,占用了动画单帧的时间,这样会给用户制造了卡顿的感觉,这当然是极不好的用户体验。针对这种情况,
JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行
。至于浏览器是如何实现回调功能的,我们在后面的章节中再详细介绍。
WebAPI:setTimeout 是如何实现的?
setTimeout()是异步的宏任务
浏览器页面是由消息队列和事件循环系统来驱动的
浏览器怎么实现 setTimeout?如果要执行一段异步的JS 代码,也需要将执行任务添加到消息队列中去,但是消息队列中的任务是按照顺序执行的,为了保证回调函数能在指定的时间内执行,把 任务放到延迟队列中去
如何触发延迟列队中的任务,增加ProcessDelayTask 函数,该函数是专门用来处理延迟执行任务的。这里我们要重点关注它的执行时机,在上段代码中,处理完消息队列中的一个任务之后,就开始执行 ProcessDelayTask 函数。ProcessDelayTask 函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务
使用SetTimout 的一些注意事项
- 如果当前任务执行时间过久 会影响定时器任务的执行
- 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
- 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
- 延时执行时间有最大值 24.8天
- 使用setTimout 设置的回调函数中的 this 不符合 直觉,被setTimeout设置的回调函数是某个对象的方法,那么该方法中的this关键字将指向全局环境,而不是定义时候所在的那个对象
回调任务实时性并不是太好, 使用 JS 来实现动画,用
requestAnimationFrame
的工作机制,使用 requestAnimationFrame 不需要设置具体的时间,由系统来决定回调函数的执行时间,requestAnimationFrame 里面的回调函数是在页面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只执行一次,内如果页面未激活的话,requestAnimationFrame 也会停止渲染,这样既可以保证页面的流畅性,又能节省主线程执行函数的开销
WebAPI: XMLHTTPRequest是怎么实现的?
消息队列和主线程循环机制保证了页面有条不紊地运行
异步回调是指回调函数在主函数外执行,一般有2种方式
- 第一种是把异步函数做成一个任务,添加到消息队列的尾部
- 把异步函数添加到微任务队列中,可以在当前任务的末尾执行微任务
渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;IO 线程收到消息,渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。
XMLHTTPRequest遇到的问题
- 同源策略,跨域问题
- HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容,比如包含了 HTTP 资源,通过 HTTP 加载的图像、视频、样式表、脚本等,都属于混合内容,
通过HTML文件加载的混合资源,虽然给出警告,但大部分还是可以加载的
使用XMLHTTPRequest请求时候,浏览器认为这种请求可能是攻击者发起的,会阻止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53function GetWebData(URL){
/**
* 1:新建XMLHttpRequest请求对象
*/
let xhr = new XMLHttpRequest()
/**
* 2:注册相关事件回调处理函数
*/
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 0: //请求未初始化
console.log("请求未初始化")
break;
case 1://OPENED
console.log("OPENED")
break;
case 2://HEADERS_RECEIVED
console.log("HEADERS_RECEIVED")
break;
case 3://LOADING
console.log("LOADING")
break;
case 4://DONE
if(this.status == 200||this.status == 304){
console.log(this.responseText);
}
console.log("DONE")
break;
}
}
xhr.ontimeout = function(e) { console.log('ontimeout') }
xhr.onerror = function(e) { console.log('onerror') }
/**
* 3:打开请求
*/
xhr.open('Get', URL, true);//创建一个Get请求,采用异步
/**
* 4:配置参数
*/
xhr.timeout = 3000 //设置xhr请求的超时时间
xhr.responseType = "text" //设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")
/**
* 5:发送请求
*/
xhr.send();
}
宏任务和微任务:不是所有任务都是一个待遇
宏任务和微任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// Firefox和Chrome早期版本中带有前缀
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
// 选择目标节点
var target = document.querySelector('#some-id');
// 创建观察者对象
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type);
})
})
// 配置观察选项:
var config = { attributes: true, childList: true, characterData: true }
// 传入目标节点和观察选项
observer.observe(target, config);
// 随后,你还可以停止观察
observer.disconnect();
宏任务:包括整体代码script,setTimeout,SetInterval,I/O,UI交互事件
setImmediate(Node.js 环境)微任务:Promise、MutaionObserver、process.nextTick(Node.js 环境)
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之前,当前宏任务结束之后
微任务产生的时机和执行微任务的时机
产生 Promise、MutaionObserver,
当调用Promise.resolve() 或者 Promise.reject() 产生微任务MutaionObserver 可以用来监听DOM 变化,包括属性的变化、节点的增减、内容变化等
MutaionObserver 将响应函数改成异步调用,可以不用在每次DOM变化都触发异步调用,而是等多次DOM变化后,一次触发异步如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行
微任务结论
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
- 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以你在写代码的时候一定要注意控制微任务的执行时长。
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
MutationObserver 将响应函数改成异步调用,可以不用在每次
DOM
变化时候都触发异步调用,而是等多次DOM变化后,一次触发异步调用
, 并且还会使用一个数据结构来记录这期间所有的DOM变化,这样即使频繁的操作DOM,也不会对性能产生太大的影响1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
// 选择目标节点
var target = document.querySelector('#some-id');
// 创建观察者对象
var observer = new MutationObserver(function(mutations) {
// 异步函数
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// 配置观察选项:
var config = { attributes: true, childList: true, characterData: true }
// 传入目标节点和观察选项
observer.observe(target, config);
// 随后,你还可以停止观察
observer.disconnect();综上所述, MutationObserver 采用了“异步 + 微任务”的策略。通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题。
Promise: 使用Promise,告别回调函数
Promise 解决的是异步编码风格,消灭嵌套调用,合并多个任务的错误处理
异步编程模型,页面主线程发起了一个耗时的任务,并将任务交给另外一个进程去处理,这时页面主线程会继续执行消息队列中的任务。等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,并排队等待循环系统的处理。排队结束之后,循环系统会取出消息队列中的任务进行处理,并触发相关的回调操作
回调的痛点: 1. 多层嵌套的问题 2. 每种任务的处理结果存在2种可能性(成功或者失败),那么需要在每种任务执行结束后分别处理这两种可能性
Promise 通过回调函数延迟绑定``回调函数返回值穿透
和错误“冒泡”
技术解决了上面的问题
async/await:使用同步的方式去写异步代码
用同步代码实现异步逻辑
async/await 使用了
Generator
和Promise
两种技术Generator 为何能暂停和恢复,了解协程
协程是一种比线程更轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A协程,要启动 B协程,那么 A协程就需要将主线程的控制权交给 B协程,这就体现在 A协程暂停执行,B协程恢复执行;同样,也可以从 B协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A协程称为 B协程的父协程async/await 技术背后的秘密就是 Promise 和 生成器 应用,往底层说就是
微任务
和协程应用
。- async 是一个通过
异步执行
并隐式返回Promise
作为结果的函数 - async/await 背后原理
1
2
3
4
5
6
7
8
9
10
11
12
13async function foo(){
try{
let response1 = await fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}catch(err) {
console.error(err)
}
}
foo()- async 是一个通过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function foo() {
console.log(1)
// let promise_ = new Promise((resolve,reject){ resolve(100)})
// ran 然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。
//主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。 promise 回调函数是微任务
let a = await 100
console.log(a)
console.log(2)
}
// 父协程
console.log(0)
// 由于 foo 函数是被 async 标记过的,所以当进入该函数的时候,JavaScript 引擎会保存当前的调用栈等信息,
foo()
console.log(3)
// promise_.then((value)=>{ //回调函数被激活后 //将主线程控制权交给foo协程,并将vaule值传给协程})