什么是事件循环和消息队列?c++
页面中的大部分任务——包括渲染事件、用户交互事件、JavaScript 脚本执行事件、网络请求完成和文件读写完成事件等——都是在渲染进程的主线程上执行的,为了协调这些任务有条不紊地在主线程上执行,渲染进程引入了消息队列和事件循环机制。渲染进程内部会维护多个消息队列,好比延迟执行队列和普通地消息队列,而后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。json
C++代码模拟事件循环和消息队列跨域
// 队列
class TaskQueue {
public:
Task takeTask(); // 取出队列头部的一个任务
void pushTask(Task task); // 添加一个任务到队列尾部
}
TaskQueue task_queue;
void ProcessTask(); // 执行任务
bool keep_running = true;
void MainThread() {
for(;;) {
Task task = task_queue.takeTask(); // 从消息队列中读取一个任务
ProcessTask(task);
if(!keep_running) // 若是设置了退出标志,呢么直接退出线程循环
break;
}
}
Task clickTask;
task_queue.pushTask(clickTask); // 添加一个任务到消息队列中
复制代码
消息队列中的任务类型浏览器
页面使用单线程的缺点安全
消息队列有“先进先出”的特色,放入消息队列中的任务,须要等前面的任务被执行完,才会被执行。因此要解决如下两个问题:网络
(1) 如何处理高优先级的任务异步
(2) 如何解决单个任务执行时长太久的问题函数
setTimeout
方法是什么?工具
setTimeout
方法是一个定时器,用来指定某个函数在多少毫秒后执行。返回一个整数,表示定时器的编号,能够经过该编号来取消这个定时器。布局
浏览器怎么实现 setTimeout ?
定时器设置的回调函数须要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,因此为了保障回调函数能在指定时间内执行,Chrome 中除了正常使用的消息队列以外,还有另一个消息队列,这个队列中维护了须要延迟执行的任务列表。setTimeout 任务就被添加到延迟执行队列中。
C++ 模拟实现延迟队列
DelayedIncomingQueue delayed_incoming_queue; // 源码中延迟队列的定义
// 模拟实现一个回调任务
struct DelayTask {
int64 id;
CallBackFunction cbf;
int start_time;
int delay_time;
};
DelayTask timerTask;
timerTask.cbf = showName;
timerTask.start_time = getCurrentTime(); // 获取当前时间
timerTask.delay_time = 200; // 设置延迟时间
delayed_incoming_queue.push(timerTask); // 将回调任务添加到延迟执行队列中
复制代码
完善事件循环的代码
void ProcessDelayTask() {
// 从delayed_incoming_queue中取出已经到期的定时器任务
// 依次执行这些任务
}
TaskQueue task_queue;
void ProcessTask(); // 执行任务
bool keep_running = true;
void MainThread() {
for(;;) {
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);
// 执行延迟队列中的任务
ProcessDelayTask();
if(!keep_running) // 若是设置了退出标志,那么直接退出线程循环
break;
}
}
复制代码
每处理完消息队列中的一个任务以后,就开始执行延迟队列中到期的任务。等到期的任务执行完成以后,再继续下一个循环过程。这里的延迟队列其实是一个 hashmap 结构。
取消定时器
调用clearTimeout
函数,传入须要取消的定时器的 ID。浏览器内部实现取消定时器的操做是直接从延迟队列delayed_incoming_queue
中经过 ID 查找到对应的任务,而后将其从队列中删除。
使用 setTimeout 的一些注意事项
(1) 若是当前任务执行太久,会影响延迟到期定时器任务的执行。
(2) 若是 setTimeout 存在嵌套调用,那么系统会设置最短期间隔为 4 毫秒。
(3) 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及下降耗电量。
(4) 延迟执行时间有最大值。Chrome、Safari、Firefox 都是以32 个 bit来存储延时值的,延迟值大于 32 bit 能够存放的最大数字时会溢出,致使定时器会被当即执行。
(5) 使用 setTimeout 设置的回调函数中的 this 不符合直觉。
var name = 1;
var myObj = {
name: 2,
showName: function() {
console.log(this.name);
}
}
setTimeout(myObj.showName, 1000); // 延迟1秒执行,结果是:1
setTimeout(myObj.showName(), 1000); // 当即执行,结果是:2
setTimeout(function() {
myObj.showName(); // 延迟1秒执行,结果是:2
}, 1000);
setTimeout(() => {
myObj.showName(); // 延迟1秒执行,结果是:2
}, 1000);
setTimeout(myObj.showName.bind(myObj), 1000); // 延迟1秒执行,结果是2
复制代码
用requestAnimationFrame
实现的动画效果比setTimeout
好的缘由是什么?
(1) setTimeout 经过设置一个间隔时间来不断改变图像的位置,从而达到动画效果。可是用 setTimeout 实现的动画可能会出现卡顿、抖动的现象。有两个缘由:
这两种状况致使 setTimeout 的执行步调和屏幕的刷新步调不一致,从而引发丢帧现象,致使动画卡顿。
(2) requestAnimationFrame 是由系统来决定回调函数的执行时机的,它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引发丢帧现象。
除此以外,requestAnimationFrame 还有CPU节能和函数节流的优点。由于页面被隐藏或最小化时,requestAnimationFrame 会中止渲染,但 setTimeout 还会在后台继续执行动画任务。在高频率事件如 resize 和 scroll 中,requestAnimationFrame 能够保证在每一个刷新间隔内,函数只被执行一次。
系统调用栈
消息队列和主线程循环机制保证了页面有条不紊地执行。当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈。这个系统调用栈相似于 JavaScript 的调用栈,只不过是用 C++ 语言来维护的。能够经过 Chrome 开发者工具的 Performance 抓取核心调用信息。
什么是回调函数?
将一个函数做为参数传递给另一个函数,做为参数的这个函数就是回调函数。
XMLHttpRequest 运做机制
(1) 建立 XMLHttpRequest 对象;
(2) 为 xhr 对象注册回调函数:ontimeout
、onerror
、onreadystatechange
;
(3) 打开请求:open()
;
(4) 配置基础的请求信息;
(5) 发起请求
XMLHttpRequest 示例代码
function getDataByXhr(url) {
// 1.新建 XMLHttpRequest 请求对象
let xhr = new XMLHttpRequest()
// 2.注册事件回调函数
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case 0: // 请求未初始化。还没有调用open()方法
console.log('请求未初始化');
break;
case 1: // 请求已启动。已经调用open()方法,但还没有调用send()方法
console.log('OPENED');
break;
case 2: // 请求已发送。已经调用send()方法,但还没有接收到响应
console.log('HEADERS_RECEIVED');
break;
case 3: // 正在接收。已经接收到部分响应数据
console.log('LOADING');
break;
case 4: // 请求完成。已经接收到所有响应数据
if (xhr.status == 200 || xhr.status == 304) {
console.log(xhr.responseText);
}
console.log('DONE')
break;
}
}
xhr.ontimeout = function(e) { console.log('timeout', e) }
xhr.onerror = function(e) { console.log('error', e) }
// 3.打开请求
xhr.open('GET', url, true); // open()方法的第三个参数设置为true,表示异步请求
// 4.配置参数
xhr.timeout = 3000 // 设置请求的超时时间
xhr.responseType = 'json' // 设置响应返回的数据格式
// xhr.setRequestHeader()
// 5.发送请求
xhr.send();
}
复制代码
XMLHttpRequest 使用过程当中可能遇到的问题
(1) 跨域问题
(2) HTTPS 混合内容的问题
setTimeout 和 XMLHttpRequest 工做机制的区别
关于消息队列
WHATWG 规范定义了在主线程的循环系统中,能够有多个消息队列,好比鼠标事件队列,IO 完成消息队列,渲染任务队列,而且能够给这些消息队列排优先级。但浏览器目前只实现了消息队列和延迟执行队列。
什么是宏任务?
消息队列中的任务称为宏任务。
宏任务的执行过程
为何须要微任务?
由于 JavaScript 代码不能掌控宏任务添加到队列中的位置,难以控制开始执行任务的时间。对时间精度要求较高的需求,宏任务难以胜任,因此须要微任务。
什么是微任务?
微任务是一个须要异步执行的函数,执行时机是在主函数执行结束以后、当前宏任务结束以前。
微任务是如何产生的?
产生微任务有两种方式。
Promise.resolve()
或者Promise.reject()
的时候,也会产生微任务。执行微任务队列的时机
微任务和宏任务
Mutation Event 和 MutationObserver 监听 DOM 变化
(1) Mutation Event 采用观察者模式监听 DOM 变化。当 DOM 有变更时就马上触发相应的事件,这种方式属于同步回调。可是这种实时性形成了严重的性能问题。
(2) MutationObserver 将事件的响应函数改为异步调用,不是在每次 DOM 变化都触发异步调用,而是等屡次 DOM 变化后,一次触发异步调用。每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。当执行到检查点的时候,V8 引擎就会按顺序执行这些微任务。
MutationObserver 经过异步调用和减小触发次数解决同步操做的性能问题,经过微任务解决实时性问题。
参考: