先综述一下:
浏览器在执行js时有一个渲染主线程, 但是其他IO线程, 网络进程等等也可以向主线程发送一些任务, 必须输入输出, 下载等等, 就引入了消息队列
消息队列中存放的是其他线程和进程发送过来的任务, 每一个任务都是一个宏任务, 浏览器在执行时, 每次从队列首部取出最老的一个任务开始执行
setTimeout异步任务 : 存放在延迟消息队列中, 在浏览器执行完消息队列中的一个最老的任务后, 会取出延迟消息队列中到期的延迟任务, 开始执行. (所以setTimeout
可能会有延迟, 不一定到期之后就会立刻被执行)
微任务就是在消息队列中的每个宏任务被执行时, 浏览器都会为其创建该宏任务对应的微任务队列, 宏任务执行完毕会执行其对应的微任务. 所以微任务的执行时间也会影响到宏任务的总体执行时长
下面详细讲述( 浏览器工作原理与实践 的笔记)
文章目录
消息队列和事件机制
渲染进程下有一个渲染主线程主要来执行任务, 还可以IO线程来添加任务
一个比较好的接收其他线程发送的消息的线程模型就是 - 消息队列
消息队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnqqbxQF-1636881722554)(http://blog.poetries.top/img-repo/2019/11/29.png)]
流程如下:
- 添加一个消息队列;
- IO 线程中产生的新任务添加进消息队列尾部;
- 渲染主线程会循环地从消息队列头部中读取任务,执行任务。
IO线程也用来接收其他进程发送过来的网络消息, 比如网络资源下载完成等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4GnhAKJd-1636881722556)(http://blog.poetries.top/img-repo/2019/11/30.png)]
消息队列中的任务类型
消息队列中的任务类型主要有: 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。
除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。
以上这些事件都是在主线程中执行的,所以在编写 Web 应用时,你还需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。
微任务的引入
在消息队列中的所有任务都是单线程的, 必须要等前一个任务完成之后才能去执行下一个任务.
这个时候引入了微任务, 消息队列中的每个任务都是一个宏任务, 每个宏任务都包含一个微任务队列. 如果有高优先级的任务可以放在某个宏任务对应的微任务队列中
setTimeout的实现
setTimeout
是一个定时器, 指定多少毫秒之后执行. 如果我们想要执行一段异步任务, 不能立刻将其加入任务队列, 而是要在指定时间之后将其加入到任务队列中
延迟消息队列
在 Chrome 中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。
所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中
void ProcessTimerTask(){
// 从 delayed_incoming_queue 中取出已经到期的定时器任务
// 依次执行这些任务
}
TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;
void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);
// 执行延迟队列中的任务
ProcessDelayTask()
if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}
在上段代码中,处理完消息队列中的一个任务之后,就开始执行 ProcessDelayTask
函数。ProcessDelayTask
函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。
setTimeout的问题
当前任务执行时间过久, 影响延迟到期定时器任务的执行
function bar() {
console.log('bar')
}
function foo() {
setTimeout(bar, 0);
for (let i = 0; i < 5000; i++) {
let i = 5+8+8+8
console.log(i)
}
}
foo()
即使setTimeout
的延迟时间是0, 但是由于消息队列中的任务for循环执行时间太久会阻碍延迟消息任务的执行, for循环执行大约花费500ms, 所以setTimeout
会在500ms后执行
如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
function cb() { setTimeout(cb, 0); }
setTimeout(cb, 0);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yfx1e4AL-1636881722558)(http://blog.poetries.top/img-repo/2019/11/34.png)]
上图中的竖线就是定时器的函数回调过程,从图中可以看出,前面五次调用的时间间隔比较小,嵌套调用超过五次以上,后面每次的调用最小时间间隔是 4 毫秒。之所以出现这样的情况,是因为在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。
未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
除了前面的 4 毫秒延迟,还有一个很容易被忽略的地方,那就是未被激活的页面中定时器最小值大于 1000 毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。这一点你在使用定时器的时候要注意。
延时执行时间有最大值
那就是 Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,这导致定时器会被立即执行。
function showName(){
console.log(" 极客时间 ")
}
var timerID = setTimeout(showName,2147483648);// 会被理解调用执行
使用 setTimeout 设置的回调函数中的 this 不符合直觉
如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。这点在前面介绍 this 的时候也提过,你可以看下面这段代码的执行结果:
var name= 1;
var MyObj = {
name: 2,
showName: function(){
console.log(this.name);
}
}
setTimeout(MyObj.showName,1000)
这里输出的是 1,因为这段代码在编译的时候,执行上下文中的 this 会被设置为全局 window,如果是严格模式,会被设置为 undefined。
那么该怎么解决这个问题呢?通常可以使用下面这两种方法。
第一种是将MyObj.showName放在匿名函数中执行,如下所示:
// 箭头函数
setTimeout(() => {
MyObj.showName()
}, 1000);
// 或者 function 函数
setTimeout(function() {
MyObj.showName();
}, 1000)
第二种是使用 bind 方法,将 showName 绑定在 MyObj 上面,代码如下所示:
setTimeout(MyObj.showName.bind(MyObj), 1000)
宏任务和微任务
页面中的大部分任务都是在主线程上执行的,这些任务包括了:
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件。
宏任务的执行过程
- 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
- 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
- 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
- 最后统计执行完成的时长等信息。
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如后面要介绍的监听 DOM 变化的需求
微任务
异步回调
- 第一种是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数
- 第二种方式的执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。
微任务的执行过程
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
- 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以-你在写代码的时候一定要注意控制微任务的执行时长。
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
Promise告别回调函数
异步编程模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5a5lW97z-1636881722561)(http://blog.poetries.top/img-repo/2019/11/43.png)]
使用promise封装xhr
第一步封装XHR
//makeRequest 用来构造 request 对象
function makeRequest(request_url) {
let request = {
method: 'Get',
url: request_url,
headers: '',
body: '',
credentials: false,
sync: true,
responseType: 'text',
referrer: ''
}
return request
}
//[in] request,请求信息,请求头,延时值,返回类型等
//[out] resolve, 执行成功,回调该函数
//[out] reject 执行失败,回调该函数
function XFetch(request, resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.ontimeout = function (e) { reject(e) }
xhr.onerror = function (e) { reject(e) }
xhr.onreadystatechange = function () {
if (xhr.status = 200)
resolve(xhr.response)
}
xhr.open(request.method, URL, request.sync);
xhr.timeout = request.timeout;
xhr.responseType = request.responseType;
// 补充其他请求信息
//...
xhr.send();
}
XFetch(makeRequest('https://time.geekbang.org'),
function resolve(data) {
console.log(data)
}, function reject(e) {
console.log(e)
})
第二步解决回调地狱问题
function XFetch(request) {
function executor(resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.open('GET', request.url, true)
xhr.ontimeout = function (e) { reject(e) }
xhr.onerror = function (e) { reject(e) }
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.responseText, this)
} else {
let error = {
code: this.status,
response: this.response
}
reject(error, this)
}
}
}
xhr.send()
}
return new Promise(executor)
}
var x1 = XFetch(makeRequest('https://time.geekbang.org/?category'))
var x2 = x1.then(value => {
console.log(value)
return XFetch(makeRequest('https://www.geekbang.org/column'))
})
var x3 = x2.then(value => {
console.log(value)
return XFetch(makeRequest('https://time.geekbang.org'))
})
x3.catch(error => {
console.log(error)
})
- 首先我们引入了 Promise,在调用 XFetch 时,会返回一个 Promise 对象。
- 构建 Promise 对象时,需要传入一个executor 函数,XFetch 的主要业务流程都在 executor 函数中执行。
- 如果运行在 excutor 函数中的业务执行成功了,会调用 resolve 函数;如果执行失败了,则调用 reject 函数。
- 在 excutor 函数中调用 resolve 函数时,会触发 promise.then 设置的回调函数;而调用 reject 函数时,会触发 promise.catch 设置的回调函数
模拟实现promise的构造函数
由于Promise是由V8实现的, 所以手动模拟实现以下, 起名为Bromise
function Bromise(executor) {
var onResolve_ = null
var onReject_ = null
// 模拟实现 resolve 和 then,暂不支持 rejcet
this.then = function (onResolve, onReject) {
onResolve_ = onResolve
};
function resolve(value) {
//setTimeout(()=>{
onResolve_(value)
// },0)
}
executor(resolve, null);
}
执行下面这段测试代码
function executor(resolve, reject) {
resolve(100)
}
// 将 Promise 改成我们自己的 Bromsie
let demo = new Bromise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
会报错
Uncaught TypeError: onResolve_ is not a function
at resolve (<anonymous>:10:13)
at executor (<anonymous>:17:5)
at new Bromise (<anonymous>:13:5)
at <anonymous>:19:12
是由于 Bromise 的延迟绑定导致的,在调用到 onResolve_ 函数的时候,Bromise.then 还没有执行
所以在执行onResolve_
的时候, 可以使用setTimeout
延迟执行
function resolve(value) {
setTimeout(()=>{
onResolve_(value)
},0)
}