浏览器工作原理——浏览器中的 setTimeout和XMLHttpRequest 是怎么实现的
setTimeout + XMLHttpRequest 浏览器循环系统是怎么工作的,粗时间颗粒度的任务
循环 + 任务队列
关于 setTimeout
setTimeout
不是由 ECMAScript 维护,而是由 host environment 提供,具体遵循的规范由 whatwg 维护
关于 setTimeout 的几点描述
- If timeout is less than 0, than timeout to 0
- If nesting(嵌套) level is greater than 5, and timeout is less than 4, than set timeout to 4
嵌套层级5级 + timeout 小于 4ms,设置 timeout 4ms1
2
3
4
5
6
7
8
9
10
11
12
13
14
15setTimeout(()=>{
// level 1
setTimeout(()=>{
// level 2
setTimeout(()=>{
// level 3
setTimeout(()=>{
// level 4
setTimeout(()=>{
// level 5
},0)
},0)
},0)
},0)
},0) - Increment nesting level by one
- let task’s timer nesting level be nesting level
setTimeout 的使用
浏览器中的 setTimeout 是怎么实现的
chromium 中 setTimeout 的实现
1 | // 用户转发的最大间隔时间 |
分析 chromium 中 setTimeout 的实现
三个常量
-
maxTimerNestingLevel = 5
嵌套层级最多是 5 -
minimumInterval=0.004
最小延迟 std::max(oneMillisecond, interval * oneMillisecond)
在 1ms 和 延尽时间之间取一个最大值。也就是说,在不满足嵌套层级的情况下,最小延迟时间是 1ms1
2
3
4
5
6
7
8
9
10
11
12// Version 91.0.4472.114 (Official Build) (x86_64)
setTimeout(console.log, 3, 3)
setTimeout(console.log, 2, 2)
setTimeout(console.log, 1, 1)
setTimeout(console.log, 0, 0)
/**
1
0
2
3
**/
浏览器页面是由消息队列和事件循环系统来驱动的。
渲染进程中所有运行在主线程上的任务都需要先添加到消息队列中,事件循环系统再按照顺序执行消息队列中的任务。如下:
- 接收到 html 文档数据时,渲染引擎会将
解析 DOM 事件
添加到消息队列; - 用户改变窗口大小时,渲染引擎会将
重新布局
事件添加到消息队列中; - 触发 JavaScript 垃圾回收机制时,渲染引擎会将
垃圾回收任务
添加到消息队列; - 执行一段异步 JavaScript 代码时,也会将
执行任务
添加到消息队列
setTimeout 定时器,用来指定回调函数参数多少毫秒后执行。返回一个整数,作为定时器编号。同时可以通过这个编号取消定时器。** setTimeout 需要在指定时间执行回调函数,而消息队列中任务是按顺序先进先出。Chrome 的解决办法是维护了另外一个需要延迟的执行任务的消息队列。这个消息队列中包括了定时器和 Chromium 内部一些需要延迟的任务 **
1 | // chrominum 中延迟代码的定义 |
模拟代码 —— 创建回调任务,并添加至延迟队列中
创建回调任务 delayTask
回调函数、发起时间、延迟时间
1 | struct DelayTask { |
如上通过定时器发起的任务就添加至了延迟队列,那么事件循环系统如何触发延迟队列?
1 | void ProcessDelayTask() { |
ProcessDelayTask 函数根据发起时间和延迟时间计算出到期任务,然后依次执行这些任务。
1 | // 清除定时器 |
取消定时器浏览器实现方式是从 delayed_incoming_queue 延迟队列中,通过 ID 查找对应任务,然后删除。
使用 setTimeout 的注意事项
如果当前任务执行时间过久,会影响定时器任务的执行
很多因素会导致回调函数执行比设定的预期值要久
当前任务执行时间过久从而导致定时器设置的任务被延后执行1
2
3
4
5
6
7
8function bar() {console.log('bar')}
function foo() {
setTimeout(bar, 0);
for(let i=0; i<5000; i++) console.log(i)
}
foo();执行 foo 函数所消耗的时长是 500 毫秒,这也就意味着通过 setTimeout 设置的任务会被推迟到 500 毫秒以后再去执行,而设置 setTimeout 的回调延迟时间是 0。
如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
也就是说在定时器函数里面嵌套调用定时器,也会延长定时器的执行时间1
2function cb() {setTimeout(cb, 0);}
setTimeout(cb, 0)
1 | static const int kMaxTimerNestingLevel = 5; |
定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒.所以,一些实时性较高的需求就不太适合使用 setTimeout 了
未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
未被激活的页面中定时器最小值大于 1000 毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量延时执行时间有最大值
Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2 147483647 毫秒(大约 24.8 天)时就会溢出,那么相当于延时值被设置为 0 了,这导致定时器会被立即执行。如果将延时值修改为小于 2147483647 毫秒的某个值,那么执行时就没有问题了。使用 setTimeout 设置的回调函数中的 this 不符合直觉
1
2
3
4
5
6var name= 1;
var MyObj = {
name: 2,
showName: function(){ console.log(this.name); }
}
setTimeout(MyObj.showName,1000)这段代码在编译的时候,执行上下文中的 this 会被设置为全局 window,如果是严格模式,会被设置为 undefined。
浏览器中的 XMLHttpRequest 是怎么实现的
XMLHttpRequest 提供了从 Web 服务器获取数据的能力,如果你想要更新某条数据,只需要通过 XMLHttpRequest 请求服务器提供的接口,便可以获取到服务器的数据,然后操作 DOM 更新页面内容,整个过程只需要更新见面的一部分就可以了,不用刷新整个页面,这样既有效率又不会打扰用户。
同步回调、异步调用
1 | let callback = function() { |
如上,将函数 callback 作为参数传递给函数 doWork,那么作为参数的函数 callback 就是 回调函数。
如上,回调函数 callback 是在主函数 doWork 返回之前执行的,这个回调过程称为 同步回调。
异步回调
1 | let callback = function() { |
如上,doWork 函数中使用了 setTimeout 函数让 cb 在主函数 doWork 执行完后延迟 1 秒执行。callback 没有在函数内部调用。回调函数在主函数外部执行的过程称为 异步回调
系统调用栈
消息队列与主线程循环机制保证了页面有条不紊地运行
当循环系统在执行一个任务的时候,都要为这个任务维护一个 系统调用栈
系统调用栈的信息可以通过 chrome://tracing/
抓取
也可以通过 Performance 来抓取它的核心调用信息。
img
XMLHttpRequest 运作机制
** XMLHttpRequest 的用法 ** 使用 XMLHttpRequest 来请求数据
1 | function getWebData(url) { |
- 创建 XMLHttpRequest 对象,来执行实际的网络请求操作
let xhr = new XMLHttpRequest()
- 为 xhr 对象注册回调函数
后台执行任务可以通过回调函数来告诉执行结果
XMLHttpRequest 的回调函数主要有以下几种:- ontimeout 监控超时请求,如果后台请求超时了,调用该函数
- onerror 监控出错信息,如果后台请求出错,调用该函数
- onreadystatechange 监控后台请求过程中的状态。如:监控 http 头加载完成的信息、http 响应体消息、数据加载完成消息等。
- 配置基本的请求信息
xhr.open
配置基础的请求信息,包括:请求地址、请求方法、请求方式
配置其它可选信息 xhr.timeout = 3000 配置超时信息
其它可选配置信息 xhr.responseText = “text” 下表为返回数据类型类型 描述 “” 将 responseText 设置为空字符串,默认类型 UTF-16 字符串 “text” 返回 UTF-16字符串文本 “json” response 是一个 JavaScript 对象 “document” response 是一个 DOM 对象 “blob” response 是一个包含二进制数据的 Blob 对象 “arraybuffer” response 是一个包含二进制数据的 JavaScript ArrayBuffer 如果 xhr.responseText = json 那么系统会自动将服务器返回的数据转换为 JavaScript 对象格式 其它可选配置 xhr.setRequestHeader 来添加自己专用的请求头属性 - 发起请求
经过 2、3 步,一切准备就绪后 xhr.send() 发起请求
渲染进程会将请求发送给网络进程,然后网络进程负责资源下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,会根据相关状态调用回调函数。- 网络请求超时:xhr.ontimeout
- 请求出错:xhr.onerror
- 正常接收: xhr.onreadystatechange 返馈相应状态
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
43var xhr = new XMLHttpRequest(); // 1. 新建 XMLHttpRequest 网络请求对象
var url = 'http://img-ads.csdn.net/2018/201811150919211586.jpg '
// 处理请求过程中的状态(onreadystatechange 监控后台请求过程中的状态)
function handler() {
switch (xhr.readyState) {
case 0:
console.log('请求初始化');
break;
case 1:
console.log('OPENED');
break;
case 2:
console.log('HEADERS_RECEIVED');
break;
case 3:
console.log('LOADING');
break;
case 4:
if(this.state === 200 || this.state === 304) console.log(this.responseText);
console.log('DONE');
break;
}
}
function callOtherDomain() {
if (xhr) {
/**
* 2. 注册相关回调
*/
xhr.onreadystatechange = handler
xhr.ontimeout = function(e) {console.log('request timeout')}
xhr.onerror = function(e) {console.log('error')}
// 3. 配置基础请求信息
xhr.open('GET', url, true)
xhr.timeout = 3000
xhr.responseText = 'json'
// 4. 发送请求
xhr.send()
}
}
callOtherDomain()Access to XMLHttpRequest at ‘https://time.geekbang.org/' from origin ‘http://localhost:4000' has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
由于跨域导致访问失败HTTPS 混合内容的问题
HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容,比如包含了 HTTP 资源,通过 HTTP 加载的图像、视频、样式表、脚本等,都属于混合内容。Mixed Content: The page at ‘https://www.ximalaya.com/waiyu/18797993/243864198' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint ‘http://img-ads.csdn.net/2018/201811150919211586.jpg'. This request has been blocked; the content must be served over HTTPS.
通过 HTML 文件加载的混合资源,虽然给出警告,但大部分类型还是能加载的。而使用 XMLHttpRequest 请求时,浏览器认为这种请求可能是攻击者发起的,会阻止此类危险的请求。
XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中