消息队列和事件循环、宏任务和微任务

1、消息队列和事件循环

  1. 什么是事件循环和消息队列?c++

    页面中的大部分任务——包括渲染事件、用户交互事件、JavaScript 脚本执行事件、网络请求完成和文件读写完成事件等——都是在渲染进程的主线程上执行的,为了协调这些任务有条不紊地在主线程上执行,渲染进程引入了消息队列和事件循环机制。渲染进程内部会维护多个消息队列,好比延迟执行队列和普通地消息队列,而后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。json

  2. 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);  // 添加一个任务到消息队列中
    复制代码
  3. 消息队列中的任务类型浏览器

    • 内部消息类型:输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、定时器等。
    • 与页面相关的事件:JavaScript执行、解析DOM、样式计算、布局、CSS动画等。
  4. 页面使用单线程的缺点安全

    消息队列有“先进先出”的特色,放入消息队列中的任务,须要等前面的任务被执行完,才会被执行。因此要解决如下两个问题:网络

    (1) 如何处理高优先级的任务异步

    • 使用微任务。消息队列中的任务称为宏任务,每一个宏任务中都包含了一个微任务队列。等宏任务中的主要功能都完成后,渲染引擎不急着去执行下一个宏任务,而是执行当前宏任务中的微任务。

    (2) 如何解决单个任务执行时长太久的问题函数

    • 经过回调,让 JavaScript 任务滞后执行。
    • 使用 Web Worker,与 DOM 操做无关的任务能够放在 Web Worker 中执行。

2、setTimeout

  1. setTimeout方法是什么?工具

    setTimeout方法是一个定时器,用来指定某个函数在多少毫秒后执行。返回一个整数,表示定时器的编号,能够经过该编号来取消这个定时器。布局

  2. 浏览器怎么实现 setTimeout ?

    定时器设置的回调函数须要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,因此为了保障回调函数能在指定时间内执行,Chrome 中除了正常使用的消息队列以外,还有另一个消息队列,这个队列中维护了须要延迟执行的任务列表。setTimeout 任务就被添加到延迟执行队列中。

  3. 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);   // 将回调任务添加到延迟执行队列中
    复制代码
  4. 完善事件循环的代码

    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 结构。

  5. 取消定时器

    调用clearTimeout函数,传入须要取消的定时器的 ID。浏览器内部实现取消定时器的操做是直接从延迟队列delayed_incoming_queue中经过 ID 查找到对应的任务,而后将其从队列中删除。

  6. 使用 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 
    复制代码
  7. requestAnimationFrame实现的动画效果比setTimeout好的缘由是什么?

    (1) setTimeout 经过设置一个间隔时间来不断改变图像的位置,从而达到动画效果。可是用 setTimeout 实现的动画可能会出现卡顿、抖动的现象。有两个缘由:

    • setTimeout 的执行时间不是肯定的。在 JavaScript 中,setTimeout 任务被放进了延迟执行队列,主线程上的任务执行完后才会检查该队列中的任务是否须要开始执行,因此 setTimeout 的实际执行时间通常比其设定的时间晚一些。
    • 不一样设备的屏幕刷新频率可能会不一样,而 setTimeout 只能设置一个固定的时间间隔,这个时间不必定和屏幕的刷新时间相同。

    这两种状况致使 setTimeout 的执行步调和屏幕的刷新步调不一致,从而引发丢帧现象,致使动画卡顿。

    (2) requestAnimationFrame 是由系统来决定回调函数的执行时机的,它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引发丢帧现象。

    除此以外,requestAnimationFrame 还有CPU节能函数节流的优点。由于页面被隐藏或最小化时,requestAnimationFrame 会中止渲染,但 setTimeout 还会在后台继续执行动画任务。在高频率事件如 resize 和 scroll 中,requestAnimationFrame 能够保证在每一个刷新间隔内,函数只被执行一次。

3、XMLHttpRequest

  1. 系统调用栈

    消息队列和主线程循环机制保证了页面有条不紊地执行。当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈。这个系统调用栈相似于 JavaScript 的调用栈,只不过是用 C++ 语言来维护的。能够经过 Chrome 开发者工具的 Performance 抓取核心调用信息。

  2. 什么是回调函数?

    将一个函数做为参数传递给另一个函数,做为参数的这个函数就是回调函数

    • 回调函数在主函数返回以前执行的回调过程称为同步回调,回调函数在当前主函数的上下文中执行。
    • 回调函数在主函数外部执行的过程称为异步回调,通常有两种方式:
      • 第一种是把异步函数作成一个任务,添加到消息队列尾部;
      • 第二种是把异步函数添加到微任务队列中,微任务在当前任务的末尾处执行。
  3. XMLHttpRequest 运做机制

    (1) 建立 XMLHttpRequest 对象;

    (2) 为 xhr 对象注册回调函数:ontimeoutonerroronreadystatechange

    (3) 打开请求:open()

    (4) 配置基础的请求信息;

    (5) 发起请求

    • 渲染进程将请求发送给网络进程;
    • 网络进程下载请求资源,接收到数据以后,用进程间通讯 IPC 通知渲染进程;
    • 渲染进程将 xhr 的回调函数封装成任务并添加到消息队列中;
    • 主线程循环系统执行到该任务的时候,根据相关的状态来调用对应的状态函数。
  4. 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();
    }
    复制代码
  5. XMLHttpRequest 使用过程当中可能遇到的问题

    (1) 跨域问题

    (2) HTTPS 混合内容的问题

    • HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容。好比 HTTP 资源,包括经过 HTTP 加载的图像、视频、样式表、脚本等。
    • 经过 HTML 文件加载混合资源时,浏览器会针对 HTTPS 混合内容显示警告,但大部分类型依然能够加载。而使用 XMLHttpRequest 请求时,浏览器会认为这种请求多是攻击者发起的,会阻止此类请求。
  6. setTimeout 和 XMLHttpRequest 工做机制的区别

    • setTimeout 直接将延迟任务添加到延迟队列中。
    • XMLHttpRequest 发起请求时,由浏览器的其余进程或线程去执行,而后再将执行结果利用 IPC 的方式通知渲染进程,以后渲染进程再将对应的消息添加到消息队列中。

4、宏任务和微任务

  1. 关于消息队列

    WHATWG 规范定义了在主线程的循环系统中,能够有多个消息队列,好比鼠标事件队列,IO 完成消息队列,渲染任务队列,而且能够给这些消息队列排优先级。但浏览器目前只实现了消息队列和延迟执行队列。

  2. 什么是宏任务?

    消息队列中的任务称为宏任务。

  3. 宏任务的执行过程

    • 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
    • 而后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
    • 当任务执行完成以后,删除当前正在执行的任务,并从对应的消息队列中删除这个 oldestTask;
    • 最后统计执行完成的时长等信息。
  4. 为何须要微任务?

    由于 JavaScript 代码不能掌控宏任务添加到队列中的位置,难以控制开始执行任务的时间。对时间精度要求较高的需求,宏任务难以胜任,因此须要微任务。

  5. 什么是微任务?

    微任务是一个须要异步执行的函数,执行时机是在主函数执行结束以后、当前宏任务结束以前。

  6. 微任务是如何产生的?

    产生微任务有两种方式。

    • 第一种方式是使用 MutationObserver 监控某个 DOM 节点,而后再经过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
    • 第二种方式是使用 Promise,当调用Promise.resolve()或者Promise.reject()的时候,也会产生微任务。
  7. 执行微任务队列的时机

    • 一般状况下,在当前宏任务中的 JavaScript 快执行完成时,也就是在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,而后按照顺序执行队列中的微任务。(WHATWG 把执行微任务的时间点称为检查点)
    • 若是在执行微任务的过程当中,产生了新的微任务,一样会将该任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程当中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
  8. 微任务和宏任务

    • 微任务和宏任务是绑定的,每一个宏任务在执行时,会建立本身的微任务队列。
    • 微任务的执行时长会影响到当前宏任务的时长。
    • 在一个宏任务中分别建立一个用于回调的宏任务和微任务,不管什么状况下,微任务都早于宏任务执行。
  9. Mutation Event 和 MutationObserver 监听 DOM 变化

    (1) Mutation Event 采用观察者模式监听 DOM 变化。当 DOM 有变更时就马上触发相应的事件,这种方式属于同步回调。可是这种实时性形成了严重的性能问题。

    (2) MutationObserver 将事件的响应函数改为异步调用,不是在每次 DOM 变化都触发异步调用,而是等屡次 DOM 变化后,一次触发异步调用。每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。当执行到检查点的时候,V8 引擎就会按顺序执行这些微任务。

    MutationObserver 经过异步调用和减小触发次数解决同步操做的性能问题,经过微任务解决实时性问题


参考:

  1. 【极客时间】浏览器原理,李兵。
  2. 【CSDN】深刻理解 requestAnimationFrame: blog.csdn.net/vhwfr2u02q/…
相关文章
相关标签/搜索