深度理解事件循环——Event Loop

一.浏览器的多线程:
浏览器的每一个tab页都会开启一个浏览器渲染引擎实例(注:每个渲染引擎是相互独立的),每个渲染引擎多个线程
1.GUI渲染线程,
* 负责解析html css => 构建DOM树、 CSSOM、Render树 => layout布局  =>绘制.
* js 操作dom 引起界面发生变化时候,会触发重绘 或回流 ,此时会触发GUI线程执行.
* js线程和GUI线程是互斥的,js线程在执行时候,GUI渲染引擎会被挂起保存在一个队列,等js引擎空闲时候,再执行。
2.JS线程,
*也叫js内核,如chrome 的V8 ,主要用来解析和执行js代码。
* js代码作为一个个任务,由任务队列统一调度分配,JS引擎一直等待任务队列中任务到来,有任务过来时,JS引擎线程便开始执行。
3.事件触发线程
*当js代码执行到事件任务(如点击事件,鼠标移动事件等)时候,会将该事件任务添加到事件触发线程,并开始监听该事件。当对应的事件被触发时,事件的回调函数会被放入js 的task 的任务队列,等待js引擎来执行。
4.定时器触发线程
*当js引擎执行到setTimeout/setInterval ,会把定时器任务添加到定时器线程,定时触发器执行倒计时,当倒计时结束,将定时任务中的回调函数添加到js 线程 task  队列, 等待js引擎执行。
5.异步请求线程
*当发送异步 ajax 请求时,浏览器会开启异步请求线程http请求,当检测到请求的状态发生变化时候,会将回调函数添加到task 任务队列,等待js引擎执行。
二 js单线程:
javascript 作为浏览器脚本语言,在设计之初就是用来与用户互动以及操作dom,这就注定了他只能是单线程的。想象一下,如果js 是多线程,一个线程在插入dom,另一个线程在删除dom,此时浏览器要以哪个为准,假如能以其中一个为准,还要增加一些类似 “读锁”、“写锁”之类的操作,很容易造成浏览器卡顿,结果不可预料。
当然,为了利用多核CPU 的计算能力,HTML5 也提出了 Web Work的标准,允许javascript 创建多个线程工作,但是子线程完全受主线程控制,且不可操作dom,更多执行一个cpu密集型的计算任务。这并没有改变js 单线程的本质。
三 执行栈;
当前执行一段js代码时候,js引擎会先创建一个执行上下文(用来评估和执行当前javascript环境);
执行栈 是用来存储代码运行时创建的所有执行上下文栈结构,它拥有后进先出(LIFO—last in first out)的特点。

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

上面代码如何进栈和出栈,首先js引擎会在全局创建一个执行上下文, main() 压入到栈,当执行到first ()函数,js引擎为first h函数创建一个执行上下文压入到执行栈顶部。
在first内部 调用second()函数时,javascript 引擎会为second函数创建一个新的执行上下文,压入到当前执行栈的顶部。当seond执行完毕,它的执行上下文会从执行栈弹出,将控制权交给first 函数。当first 函数执行完毕,它的执行上下文也会从执行栈弹出,将控制权交给全局执行上下文 main(),一旦全部执行完毕,js引擎会将全局执行上下文移出执行栈。此时 执行栈为空。

从上图,我们可以清晰看到各个函数执行上下文 进栈出栈情况。
四 同步和异步;
1.
函数执行返回结果时,如果函数调用者能够拿到预期的结果(就是函数计算的结果),那么这个函数就是同步的. 例console.log(1)
2. 函数返回的的时候,调用者还不能立即得到预期的返回结果,需要通过一定的方法才能得到。这就是异步执行,例如
setTimeout(()=>console.log(1),1000);  当这段代码执行完之后, 并没有打印出来对应的内容, 而是间隔1s 之后打印。
异步方法的调用 有点类似把MQ消息的传递给浏览器的其他线程,一旦执行完毕就立即返回,调用者继续执行后续的操作。 此时,异步方法调用产生的真实任务, 已经真实的在执行了,整个过程不过阻塞调用者后续的运行。详细内容看浏览器线程;
五. 宏任务和微任务
其实事件循环就是 代码在执行时候进栈和出栈的循环,上面我们说到了setTimeout/setInterval ,另外还有点击事件,ajax请求,promise 等等。这些进栈出栈的任务队列,可以分为宏任务 (macro-task)和微任务(micro-task);
一般来说 宏任务包括 ,
* script (整体代码)
*setTimeout & setInterval 
*UI  事件
* 页面渲染
* I/O 
微任务包括:
*promise
*process.nextTick
*MutationObserver
一轮Event-Loop 触发时顺序。
1.执行主线程的第一个任务,也是第一个宏任务,如 script 代码块,如遇到setTimeout /setInterval  将其放到对应的定时器线程,等倒计时结束,其回调函数作为放到宏任务队列。
2.遇到micro-task,将其放到微任务队列.
3.等执行完第一个 宏任务,取出微任务队列中所有的任务,依次执行完成,遇到宏任务继续放到宏任务队列,遇到微任务放到为人队列,直到清空微任务队列里面。
4.再取出宏任务队列中的 第一个任务执行。
注:很多人以为是由 then 方法来触发它保存回调,而事实上 then 方法即不会触发回调,也不会将它放到微任务,then 只负责注册回调,由 resolve/reject 将注册的回调放入微任务队列,由事件循环将其取出并执行.
如下例


new Promise(resolve => {
console.log('promise start')
  resolve();
})
  .then(() => {
    new Promise(resolve => {
      resolve();
    })
      .then(() => {
        console.log("log: 内部第一个then");
        return Promise.resolve();
      })
      .then(() => console.log("log: 内部第二个then"));
  })
  .then(() => console.log("log: 外部第二个then"));

执行的顺序:
1). 整段代码作为一个宏任务执行,同步代码new出promise的实例,称为promise1,当promise1 resolve 时候,将它的第一个 then回调函数注册到微任务队列,第二个then 中的注册到第一个then返回的promise的内部队列中,。
2). 宏任务执行完,执行栈从微任务队列取出任务,也即第一个then中的回调函数,先执行其中的同步代码 new Promise 生成实例promise2 , 当promise2状态 resolve,把promise2 的then 中的回调函数注册到微任务,此时,promise1 第一个then 中 

发布了16 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42317878/article/details/104783562