当前位置: 首页 > Web前端 > JavaScript

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

时间:2023-03-27 02:19:34 JavaScript

消息队列和事件循环浏览器页面是由消息队列和事件循环系统驱动的。消息队列:单行道->先进先出。消息队列任务也称为宏任务,每个宏任务队列包含一个微任务队列。执行宏任务时,如果DOM发生变化,则将变化提交给微任务。当执行宏任务时,执行当前的微任务。消息队列和事件循环类似于观察者发布订阅模式。事件循环(单线程运行时可以接收并执行新任务)按顺序执行消息队列中的任务。对于主线程-->消息队列。主线程-->消息队列-->消息队列中的微任务。消息队列中的任务:解析DOM事件、重排事件、垃圾回收任务、IO任务、点击事件。Chromium当前占用的任务高度。加载阶段:默认-->用户交互-->合成页面-->空闲:尽量看页面,页面解析,JS优先用户交互:用户交互-->合成页面-->默认-->空闲空闲阶段:默认情况下,用户交互-->空闲-->合成页面中的大部分任务都在主线程上执行:渲染事件(解析DOM、计算布局、绘制)用户交互事件(鼠标点击、滚动页面、放大缩小等)JS脚本执行事件网络请求完成、文件读写完成事件渲染主线程:用户输入事件、合成任务、定时器、V8垃圾回收、网络加载、HTML解析、布局等。,JS回调。由于渲染进程内部的大部分任务都是在主线程上执行的,比如JS执行、DOM、CSS、计算布局、V8垃圾回收等,要让这些任务在主线程上有序运行,就需要引入消息队列。单消息队列下,如果任务耗时太长,会有卡顿的感觉。渲染进程内部有多个消息队列,比如延迟消息队列和普通消息队列。然后主线程用一个for循环不断的从这些任务队列中取出任务并执行。消息队列中的任务称为宏任务,消息队列中的任务通过事件循环执行。setTimeout函数触发的回调函数也在宏任务中。为了控制执行时间精度,将Promise的执行放在microtask中,microtask先于macrotask执行,因此比setTimout更快。microtask中的microtask在microtask执行完后继续执行microtask。当当前宏任务中的JS即将执行完毕,JS引擎即将退出全局执行上下文并清空调用栈时,JS引擎会检查全局执行上下文的微任务队列,并然后依次执行微任务。Microtasks是V8引擎在创建全局执行上下文时创建的队列,当有microtasks时存储。监听DOM,MutationObserver将响应函数改为异步,多次DOM变化后,合成一个触发异步调用,microtask通知变化,异步+microtask。setTimeout定时器需要在指定的时间间隔调用回调函数。不能直接加入消息队列,只能放入延迟队列。消息队列执行完后,会执行延迟队列,嵌套调用的最小时间为4ms。setTimeout是将延迟任务添加到延迟队列中。XMLHttpRequest发起的请求由浏览器的网络进程执行,然后使用IPC通过渲染进程将结果添加到消息队列中。PromisePromise的出现是改变了回调的编码风格,但它仍然使用了excutor中的回调函数然后。模拟Promise:functionBromise(executor){varonResolve_=null;varonReject_=null;this.then=function(onResolve,onReject){onResolve_=onResolve};functionresolve(value){setTimeout(()=>{//延迟执行,否则this.then还没执行完,onResolve还是nullonResolve_(value)},0)}executor(resolve,null);};call:functionexecutor(resolve,reject){resolve(100)};letdemo=newBromise(executor);functiononResolve(value){console.log(value)};demo.then(onResolve);首先newBromise(),会执行Bromise的构造函数Bromise的构造函数会执行Bromise的executor函数,然后在executor中执行resolve。执行resolve函数会触发this.then中设置的回调函数onResolve。由于Bromise采用了回调函数延迟绑定技术,当resolve函数执行时,回调函数还没有绑定,所以只能延迟回调函数的执行。Promise通过回调函数延迟绑定、回调函数返回值穿透和错误“冒泡”技术解决多层嵌套和错误捕获。GeneratorGenerator函数是带星号的函数,可以暂停执行和恢复执行。协程比线程更轻量级。一个线程上可以存在多个协程,但同时只能执行一个协程。如果协程B是从协程A启动的,那么A就是B的父协程。协程程序是在用户控制下执行的。function*genDemo(){console.log('执行第一段');产量'generator1';console.log('执行第二段');产量'generator2';console.log('执行结束');返回'??generator3'}console.log('main0');letgen=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交替执行,生成器可以暂停功能,以恢复执行。生成器在内部执行一段代码。遇到yield时,JS引擎向外部返回如下内容,通过next暂停外部函数恢复函数执行。(1)通过调用genDemo创建协程gen,gen协程创建后并不会立即执行。(2)通过调用gen.next()让gen协程执行。(3)协程执行时,通过yield暂停gen协程的执行,将主要信息返回给父协程。(4)协程执行过程中,如果遇到return,JS引擎会结束当前协程,将return后的内容返回给父协程。第一点:gen协程和父协程是在主线程上交互执行的,不是并发的。他们之前的切换是通过yield和gen.next的配合完成的。第二点:在gen协程中调用yield方法时,JavaScript引擎会保存gen协程当前的调用栈信息,并恢复父协程的调用栈信息。同理,在父协程中执行gen.next时,JavaScript引擎会保存父协程的调用栈信息,并恢复gen协程的调用栈信息。asyncasync/await:以同步方式编写异步代码。使用同步代码在不阻塞主线程的情况下实现对资源的异步访问,让代码逻辑更加清晰。该技术背后的秘密是Promise和生成器应用程序,在低级别上是微任务和协程应用程序。asyncfunctionfoo(){console.log(1)leta=await100;控制台日志(一);console.log(2)}console.log(0)foo();console.log(3)executeconsole.log(0)打印0并执行foo,因为被async标记,JS引擎会保存当前调用栈,执行console.log(1)executeawait100,JS会创建一个promise,让promise=newPromise(resolve,reject){resolve(100)},JS引擎将任务提交到微任务队列promise。JS引擎暂停当前协程的执行,将主线程的控制权交给父协程,并将promise对象返回给父协程。主线程控制权交给了父协程,父协程需要调整promise.then来监听promise的状态变化后,继续执行父协程进程,执行console.log(3),父协程执行结束,结束前进入微任务检查,执行微任务,resolve(100),resolve回调函数被激活后,主线程控制权交给foo函数的协程,而value传递给协程7.foo协程启动后,将value赋值给a,然后foo协程继续执行console.log(a),console.log(2)