浏览器工作原理与实践(四)——浏览器页面循环系统

Waxiangyu

消息队列和事件循环

浏览器页面是由消息队列和事件循环系统来驱动的。
消息队列:单行道->先进先出。消息队列任务又被称为宏任务,每个宏任务队列又都包含了一个微任务队列。执行宏任务的时候,如果DOM有变化,将变化提交到微任务中。当宏任务执行完再执行当前微任务。
消息队列和事件循环类似于观察者发布订阅模式,事件循环(单线程运行过程中能接收并执行新任务)是按顺序执行消息队列中任务的。for
主线程——>消息队列。主线程——>消息队列——>消息队列中微任务。
消息队列中任务:解析DOM事件、重新布局事件、垃圾回收任务、IO任务、点击事件。

chromium目前采取的任务高度。
加载阶段:默认——>用户交互——>合成页面——>空闲:尽可能看到页面,页面解析,JS优先级最高
用户交互:用户交互——>合成页面——>默认——>空闲
空闲阶段:默认,用户交互——>空闲——>合成页面

页面中大部分任务都是在主线程执行的:

  1. 渲染事件(解析DOM、计算布局、绘制)
  2. 用户交互事件(鼠标点击、滚动页面、放大缩小等)
  3. JS脚本执行事件
  4. 网络请求完成,文件读写完成事件

渲染主线程:用户输入事件、合成任务、定时器、V8垃圾回收、网络加载、HTML解析,布局等、JS回调。

由于渲染进程内部大多数任务都是在主线程上执行的,如JS执行、DOMCSS、计算布局、V8垃圾回收等,能让这些任务在主线程有条不紊运行就要引入消息队列。在单消息队列下,任务需要花费时间过久的话,就会产生卡顿的感觉。
渲染进程内部会有多个消息队列,比如延迟和普通的消息队列。然后主线程采用for循环,不断从这些任务队列中取出任务并执行任务。消息队列中的任务叫宏任务,消息队列中的任务是通过事件循环来执行的。setTimeout的函数触发的回调函数也都在宏任务。

为了执行时间精度的控制,Promise执行放到微任务,当前执行完执行微任务然后再执行宏任务,所以比setTimout快。
微任务中的微任务,在微任务执行之后,继续执行微任务。
在当前宏任务中的JS快执行完成时,JS引擎准备退出全局执行上下文,并清空调用栈时,JS引擎会检查全局执行上下文的微任务队列,然后顺序执行微任务。微任务是V8引擎在创建全局执行上下文时创建的队列,当有微任务时再存放进去。
监听DOMMutationObserver将响应函数改成异步,等多次DOM变化后,合成一次触发异步调用,微任务通知变化,异步+微任务。

setTimeout

定时器需要在指定间隔才能调用回调函数。不能直接添加到消息队列,只能放在延迟队列。
消息队列执行完会执行延迟队列,嵌套调用最短时间为4ms
setTimeout是将延迟任务添加到延迟队列中。XMLHttpRequest发起请求是由浏览器的网络进程去执行,再将结果利用IPC方式通过渲染进程再添加到消息队列。

Promise

Promise的出现是改变回调的编码风格,但它excutorthen里面用的还是回调函数。
模拟Promise:

function Bromise(executor) { 
    var onResolve_ = null;
    var onReject_ = null; 
    this.then = function (onResolve, onReject) { 
        onResolve_ = onResolve 
    }; 
    function resolve(value) { 
        setTimeout(()=>{ //延迟执行,否则this.then还未执行,onResolve还是null
            onResolve_(value) 
        },0) } 
    executor(resolve, null);
};

调用:
function executor(resolve, reject) { 
    resolve(100)
};
let demo = new Bromise(executor);
function onResolve(value){             
    console.log(value)
};
demo.then(onResolve);
  1. new Bromise(),Bromise的构造函数会被执行
  2. Bromise的构造函数会执行Bromiseexecutor函数,然后在executor中执行了resolve
  3. 执行resolve函数会触发this.then中设置的回调函数onResolve。由于 Bromise 采用了回调函数延迟绑定技术,所以在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行。

Promise通过回调函数延迟绑定、回调函数返回值穿透和错误 "冒泡" 技术解决了多层嵌套和错误捕获。

Generator

生成器Generator函数是一个带星号的函数,可以暂停执行和恢复执行的。
协程是比线程更加轻量级的存在,一个线程上可以存在多个协程,但同期只能执行一个协程,如果从A协程启动B协程,A就是B的父协程,协程由用户控制执行。

function* genDemo(){
    console.log('执行第一段');
    yield 'generator1';
    console.log('执行第二段');
    yield 'generator2';
    console.log('执行结束');
    return 'generator3'
}
console.log('main0');
let gen=genDemo();
console.log(gen.next().value);
console.log('main1');
console.log(gen.next().value);
console.log('main2');
console.log(gen.next().value);
console.log('main3');

输出结果全局和genDemo交替执行,生成器特性可以暂停,恢复执行。

  1. 生成器内部执行一段代码,遇到yieldJS引擎返回后面的内容给外部,并暂停
  2. 外部函数可以通过next恢复函数执行。
    (1)通过调用genDemo创建一个协程gen,创建后gen协程没有立即执行。
    (2)让gen协程执行,要通过调用gen.next()
    (3)当协程正在执行的时候,通过yield来暂停gen协程的执行,并返回主要信息给父协程。
    (4)协程执行期,遇到returnJS引擎会结束当前协程,将return后面的内容返回给父协程。

第一点:gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield gen.next 来配合完成的。
第二点:当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。

async

async/await:使用了同步的方式写异步代码。在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。

async function foo() {
    console.log(1)
    let a=await 100;
    console.log(a);
    console.log(2)
}
console.log(0)
foo();
console.log(3)
  1. 执行console.log(0)打印0
  2. 执行foo,由于被async标记过,JS引擎会保存当前调用栈,执行console.log(1)
  3. 执行await 100JS会创建一个promiselet promise=new Promise(resolve,reject){resolve(100)}JS引擎将任务提交给微任务队列promise
  4. JS引擎暂停当前协程执行,将主线程控制权交给父协程,同时将promise对象返回给父协程
  5. 主线程控制权给过父协程,父协程要调promise.then来监控promise状态的改变
  6. 接下来继续父协程流程执行,执行console.log(3),父协程执行结束,结束之前进入微任务检查,执行微任务,resolve(100)resolve回调函数被激活后,将主线程控制权交给foo函数的协程,将value值传给该协程
    7. foo协程激活后,把value值给a,然后foo协程继续执行console.log(a),console.log(2)
阅读 467

那就 javascript 吧
小白笔记,从一开始。
628 声望
28 粉丝
0 条评论
628 声望
28 粉丝
文章目录
宣传栏