消息队列和事件循环
浏览器页面是由消息队列和事件循环系统来驱动的。
消息队列:单行道->先进先出。消息队列任务又被称为宏任务,每个宏任务队列又都包含了一个微任务队列。执行宏任务的时候,如果DOM
有变化,将变化提交到微任务中。当宏任务执行完再执行当前微任务。
消息队列和事件循环类似于观察者发布订阅模式,事件循环(单线程运行过程中能接收并执行新任务)是按顺序执行消息队列中任务的。for
主线程——>消息队列。主线程——>消息队列——>消息队列中微任务。
消息队列中任务:解析DOM
事件、重新布局事件、垃圾回收任务、IO
任务、点击事件。
chromium
目前采取的任务高度。
加载阶段:默认——>用户交互——>合成页面——>空闲:尽可能看到页面,页面解析,JS
优先级最高
用户交互:用户交互——>合成页面——>默认——>空闲
空闲阶段:默认,用户交互——>空闲——>合成页面
页面中大部分任务都是在主线程执行的:
- 渲染事件(解析
DOM
、计算布局、绘制) - 用户交互事件(鼠标点击、滚动页面、放大缩小等)
- JS脚本执行事件
- 网络请求完成,文件读写完成事件
渲染主线程:用户输入事件、合成任务、定时器、V8
垃圾回收、网络加载、HTML
解析,布局等、JS
回调。
由于渲染进程内部大多数任务都是在主线程上执行的,如JS
执行、DOM
、CSS
、计算布局、V8
垃圾回收等,能让这些任务在主线程有条不紊运行就要引入消息队列。在单消息队列下,任务需要花费时间过久的话,就会产生卡顿的感觉。
渲染进程内部会有多个消息队列,比如延迟和普通的消息队列。然后主线程采用for
循环,不断从这些任务队列中取出任务并执行任务。消息队列中的任务叫宏任务,消息队列中的任务是通过事件循环来执行的。setTimeout
的函数触发的回调函数也都在宏任务。
为了执行时间精度的控制,Promise
执行放到微任务,当前执行完执行微任务然后再执行宏任务,所以比setTimout
快。
微任务中的微任务,在微任务执行之后,继续执行微任务。
在当前宏任务中的JS
快执行完成时,JS
引擎准备退出全局执行上下文,并清空调用栈时,JS
引擎会检查全局执行上下文的微任务队列,然后顺序执行微任务。微任务是V8
引擎在创建全局执行上下文时创建的队列,当有微任务时再存放进去。
监听DOM
,MutationObserver
将响应函数改成异步,等多次DOM
变化后,合成一次触发异步调用,微任务通知变化,异步+微任务。
setTimeout
定时器需要在指定间隔才能调用回调函数。不能直接添加到消息队列,只能放在延迟队列。
消息队列执行完会执行延迟队列,嵌套调用最短时间为4ms
。setTimeout
是将延迟任务添加到延迟队列中。XMLHttpRequest
发起请求是由浏览器的网络进程去执行,再将结果利用IPC
方式通过渲染进程再添加到消息队列。
Promise
Promise
的出现是改变回调的编码风格,但它excutor
和then
里面用的还是回调函数。
模拟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);
- 先
new Bromise()
,Bromise
的构造函数会被执行 Bromise
的构造函数会执行Bromise
的executor
函数,然后在executor
中执行了resolve
- 执行
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
交替执行,生成器特性可以暂停,恢复执行。
- 生成器内部执行一段代码,遇到
yield
,JS
引擎返回后面的内容给外部,并暂停 - 外部函数可以通过
next
恢复函数执行。
(1)通过调用genDemo
创建一个协程gen
,创建后gen
协程没有立即执行。
(2)让gen
协程执行,要通过调用gen.next()
。
(3)当协程正在执行的时候,通过yield
来暂停gen
协程的执行,并返回主要信息给父协程。
(4)协程执行期,遇到return
,JS
引擎会结束当前协程,将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)
- 执行
console.log(0)
打印0
- 执行
foo
,由于被async
标记过,JS
引擎会保存当前调用栈,执行console.log(1)
- 执行
await 100
,JS
会创建一个promise
,let promise=new Promise(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)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。