消息队列和事件循环
最简单的场景:使用单线程处理安排好的任务。处理逻辑如下图所示:
这个模型的缺点是只能处理所有已经安排好的任务,无法处理在线程运行过程中的新任务。
事件循环
在线程中引入事件循环,能在线程运行过程中接收并执行新的任务。
但是这个模型中,所有的任务都是来自于线程内部,如果另一个线程想让主线程执行一个任务,此模型是无法做到的。
消息队列
△ 消息队列:是一种数据结构,可以存放要执行的任务,符合先进先出的特点。
这个模型做了如下改造:
- 添加一个消息队列
- IO线程中产生的新任务添加进消息队列的尾部
- 渲染主线程会循环地从消息队列头部中读取任务,执行任务
由于是多个线程操作同一个消息队列,所以在添加任务和取出任务时还会加上一个同步锁。
跨进程处理任务模型
渲染进程专门有一个IO线程用来接收其他进程传进来的消息,接收到这些消息后,会将这些消息组装成任务发送给渲染主进程
退出机制
当确定要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。如果设置了,那么就直接就中断当前的所有任务,退出线程。
如何处理高优先级的任务
典型场景:监控DOM节点的变化情况,然后根据这些变化来处理相应的业务逻辑。
但当DOM发生变化,如果采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性。针对这种情况,微任务就应运而生了。
通常我们把消息队列中的任务成为宏任务,每个宏任务中都包含了一个微任务队列。在执行宏任务的过程中,如果DOM有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。
(微任务依然运行在当前宏任务的执行环境中,这个特性会导致宏任务与微任务有一些本质上的区别)
等宏任务中的主要功能都直接完成之后,这时候渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,这样便解决了实时性的问题。
如何解决单个任务执行时长过久的问题
JS通过回调功能来规避这种问题,即让JS任务滞后执行。
WebAPI: setTimeOut是如何实现的?
setTimeOut是一个定时器,用来指定某个函数在多少毫秒之后执行。它会返回一个整数,表示定时器的编号,同时你还可以通过该编号来取消这个定时器。
浏览器实现方法
首先,因为它们需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,所以为了保证回调函数能在指定时间内执行,你不能将定时器的回调函数直接添加到消息队列中。
在Chrome中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和Chromium内部一些需要延迟执行的任务。所以当通过JavaScript创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。
处理完消息队列的一个任务后,延迟任务监控函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。
调用clearTimeout函数时,便是通过ID查找到对应的任务,然后再将其从队列中删除掉就可以了。
注意事项
-
如果当前任务执行时间过久,会影延迟到期定时器任务的执行
-
如果setTimeout存在嵌套调用,那么系统会设置最短时间间隔为4毫秒
(是因为在Chrome中,定时器被嵌套调用5次以上,系统会判断该函数方法被阻塞了,然后就会把最小间隔时间设为4毫秒)
所以,一些实时性较高的需求就不太适合使用setTimeout了,比如你用setTimeout来实现JavaScript动画就不是一个很好的主意 -
未激活的页面标签,setTimeout执行最小间隔是1000毫秒
-
延时执行时间有最大值2147483647 (32个bit能存放的最大数字)
-
回调函数中的this
var name= 1; var MyObj = { name: 2, showName: function(){ console.log(this.name); } } setTimeout(MyObj.showName,1000);如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的this关键字将指向全局环境(window/严格环境undefined),而不是定义时所在的那个对象。
解决办法如下:
- 将回调函数放在匿名函数中执行
//箭头函数 setTimeout(() => { MyObj.showName() }, 1000); //或者function函数 setTimeout(function() { MyObj.showName(); }, 1000);- 使用bind方法
setTimeout(MyObj.showName.bind(MyObj), 1000)
WebAPI:XMLHttpRequest是怎么实现的?
同步回调 VS 异步回调
△ 回调函数:将一个函数作为参数传递给另外一个函数,那作为参数的这个函数就是回调函数。
△ 同步回调:回调函数 callback 是在主函数返回之前执行的
let callback = function(){
console.log('i am do homework')
}
function doWork(cb) {
console.log('start do work')
cb()
console.log('end do work')
}
doWork(callback)
使用场景:在当前主函数的上下文中执行回调函数
△ 异步回调:回调函数在主函数外部执行的过程
let callback = function(){
console.log('i am do homework')
}
function doWork(cb) {
console.log('start do work')
setTimeout(cb,1000)
console.log('end do work')
}
doWork(callback)
使用场景:第一种是把异步函数做成一个任务,添加到信息队列尾部;第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了。
XMLHttpRequest运作机制
运作机制如上图所示,渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用IPC来通知渲染进程;渲染进程接收到消息之后,会将xhr的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。
XMLHttpRequest的两个坑
- 跨域问题
- HTTPS混合内容的问题
△ HTTP混合内容:HTTPS 页面中包含了不符合HTTPS安全要求的内容。
宏任务与微任务
异步回调的两种方式
第一种是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。
举例:setTimeout和XMLHttpRequest的回调函数都是这么实现的。
第二种方式的执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。
基于微任务的技术有MutationObserver(监控某个DOM节点)、Promise以及以Promise为基础开发出来的很多其他的技术。
宏任务
△ 宏任务:指的是消息队列中的任务,通过事件循环系统来执行的。它可以满足大部分的日常需求,无法满足对时间精度要求较高的需求。
微任务
△ 微任务:就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
每个宏任务都关联了一个微任务队列。当JavaScript执行一段脚本时,v8引擎在为其创建全局执行上下文的同时也会在内部创建一个微任务队列。
通常情况下,在当前宏任务中的JavaScript快执行完成时,也就在JavaScript引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。