JavaScript 事件循环机制 学习笔记

762 阅读7分钟

线程与进程

概念:

  1. 进程是CPU资源分配的最小单位
  2. 线程是CPU调度得最小单位

浏览器内核

浏览器默认有三个进程:

一. 浏览器主进程(Browser进程)

作用:

  • 负责浏览器界面显示,用户交互(前进、后退,关闭等)
  • 负责各个页面的管理,如创建和销毁其它进程
  • 将Render进程中得到的内存中的bitmap,绘制到用户界面上
  • 网络资源的管理,如下载

分类:

  • GUI渲染线程
  .主要负责页面的渲染,解析HTML,CSS,构建DOM树,布局和绘制等。
  .当界面需要重绘或者由于某种操作引发回流时,将执行该线程
  .该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,JS引擎才会去执行GUI渲染。
  • JS引擎线程
  .该线程当然是主要负责处理 JavaScript脚本,执行代码。
  .也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS引擎线程的执行。
  .当然,该线程与 GUI渲染线程互斥,当 JS引擎线程执行 JavaScript脚本时间过长,将导致页面渲染的阻塞。
  • 事件触发线程
  .主要负责将准备好的事件交给JS引擎线程执行
  .比如setTimeout定时器计数结束,ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待JS引擎线程的执行。
  • 定时器触发线程
  .顾名思义,负责执行异步定时器一类的函数的线程,如:setTimeout,setInterval。
  .主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS殷勤线程执行。
  • HTTP请求线程
 .顾名思义,负责执行异步请求一类的函数的线程,如:promise,axios,ajax等。
 .主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。

二. GPU进程
用于3d绘制等,如为dom元素添加css3样式-webkit-transform:translateZ(0),将使浏览器的渲染从cup转向gpu,开启gpu加速。

三. 浏览器渲染进程(Render进程)也被称为浏览器内核。
浏览器渲染进程(浏览器内核),主要负责页面的渲染、JS执行以及事件的循环。

Browser进程,Render进程,GPU进程三者是如何协作的呢?

Browser进程收到用户的请求,首先由UI线程处理,而且将相应的任务转给IO线程,他随机将该任务传递给Render进程;
Render进程的IO线程经过简单解释后交给渲染线程,渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染,最后Render进程将结果由IO线程传递给Browser进程;
Browser进程接收到结果并将结果绘制出来;

同步任务和异步任务

  • 同步任务 即可以立即执行的任务,例如 console.log() 打印一条日志、声明一个变量或者执行一次加法操作等。

  • 异步任务 相反不会立即执行的事件任务。异步任务包括宏任务和微任务(后面会进行解释~)。

    每个宏任务都包含了一个微任务队列:

常见的异步操作:

  1. Ajax
  2. DOM的事件操作
  3. setTimeout
  4. Promise的then方法
  5. Node的读取文件

事件循环完整的执行过程

  • 代码开始执行,JavaScript 引擎对所有的代码进行区分。
  • 同步代码被压入栈中,异步代码根据不同来源加入到宏任务队列尾部,或者微任务队列的尾部。
  • 等待栈中的代码被执行完毕,此时通知任务队列,执行位于队列首部的宏任务。
  • 宏任务执行完毕,开始执行其关联的微任务。
  • 关联的微任务执行完毕,继续执行下一个宏任务,直到任务队列中所有宏任务被执行完毕。
  • 执行下一个任务队列。
   //写出下面代码的运行结果:
      console.log("1");
        setTimeout(function() {
            console.log("2")
        }, 0)
        setTimeout(function() {
            console.log("3")
        }, 200)
        console.log("4");

宏任务和微任务

异步任务:宏任务和微任务,宏任务队列可以有多个,微任务队列只有一个

宏任务和微任务的执行方式在浏览器和 Node 中有差异。

在微任务中 process.nextTick 优先级高于Promise

当异步任务进入栈执行时,是宏任务还是微任务

  1. 由于执行代码都是全局任务script,而全局任务属于宏任务,所以当栈为空,同步任务任务执行完毕时,会先执行微任务队列里的任务
  2. 微任务队列里的任务全部执行完毕后,会读取宏任务队列中拍最前的任务
  3. 执行宏任务的过程中,遇到微任务,依次加入微任务队列
  4. 栈空后,再次读取微任务队列里的任务,依次类推

案例分析:

      setTimeout(function() {
            console.log(1)
            Promise.resolve().then(() => {
                console.log(2)
            })
        }, 0)
        setTimeout(function() {
            console.log(3)
        }, 200)

        new Promise(function(resolve, reject) {
            console.log(4)
            resolve()
        }).then(() => {
            console.log(5)
        })
        console.log(6);
  1. 我们遇到的第一个宏任务setTimeout,放到我们异步队列里面,我们暂时记为setTimeout1,直接加入宏任务队列
  2. 再次遇到了一个宏任务setTimeout,放到我们异步队列里面,我们暂时记为setTimeout2,直接加入宏任务队列
  3. 接下来是一个promise对象,在实例化对象中console.log(4)属于同步任务所以,直接打印4,但是then()属于微任务,并且加入微任务队列中
  4. 最后遇到打印语句,直接打印6

  1. 栈空后,先执行微任务队列中的then(),输出5,此时微任务队列为空

6. 然后我们执行最靠前的宏任务 setTimeout1
7. 先执行console.log(1),此时我们打印出来1,后面有一个then(),加入到微任务。

8. 然后执行微任务队列中的then()方法,输出2,此时微任务队列为空;

9. 继续读取宏任务队列的最靠前的任务 setTimeout2。
10. 直接执行打印语句,打印日志 3。执行完毕。

先通过一个小案例来说明一下async函数:

        async function async1() {
            console.log('async1 start') 
            await async2()
            console.log('async1 end') 
        }
        async function async2() {
            console.log('async2') 
        }
        async1();
        console.log('script end') 

await 要等的东西分为两种情况:

  • 是promise对象
  • 不是promise对象
  1. 如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
  2. 如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果。

小案例的实现过程分析:

  1. 首先代码是自上而下执行,所以先调用的async1(),那么就先执行async1函数,先打印async1 start;
  2. await 后面是等待的async2函数的返回值,从右向左的。先打印async2,后打印的 script end;
  3. 最后是打印 async1 end

所以小伙伴们大家来看一下:

//大家开动脑筋看一道比较经典的面试题:
        async function async1() {
            console.log('async1 start') //2
            await async2()
            console.log('async1 end') // 6
        }

        async function async2() {
            console.log('async2') //3
        }

        console.log('script start') // 1

        setTimeout(function() {
            console.log('setTimeout') //8
        }, 0)
        async1();

        new Promise(function(resolve) {
            console.log('promise1') //4
            resolve();
        }).then(function() {
            console.log('promise2') //7
        })

参考:

8 张图帮你一步步看清 async/await 和 promise 的执行顺序
事件循环机制的那些事
从 薛定谔的猫 聊到 Event loop
微任务、宏任务与Event-Loop