【浅谈JS执行机制】

八、JS的执行机制

8.1 进程和线程的概念

  • 进程和线程的概念:
    • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
    • 线程:每一个进程中,都会启动一个线程用来执行程序中的代码,这个线程被称之为主线程
    • 所以我们也可以说进程是线程的容器

image-20210719143102685

8.2 JavaScript的单线程

  • JavaScript是一门**单线程的语言,执行JavaScript代码只在一个单独的线程中执行

    • 也就是说,同一个时间只能做一件事
  • 单线程意味着:如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务,如果有其中一个任务执行时间过久,就会导致页面堵塞

  • 为了解决单线程问题,js将执行的任务分成了 同步任务 和 异步任务

  • 而其中异步任务又分为了宏任务微任务

8.3 同步任务和异步任务

  • 同步任务

    • 同步任务是指在主线程(执行栈)上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务

    • 当我们打开网站时,网站的渲染过程、元素的渲染,其实就是一个同步任务

  • 异步任务

    • 异步任务不进入主线程(执行栈)、而是先进入异步进程处理,当异步任务执行完毕就会进入**“任务队列”**中

    • 当主线程上的任务执行完毕,就会去任务队列中取出异步任务,然后放在主线程上执行,这个过程称为’事件循环

    • 像回调函数,什么时候被回调,这就是一个异步任务

  • 总结:JavaScript中优先执行所有的同步任务,当同步任务执行完成后,在执行异步任务

8.4 JS的执行机制流程图

image-20200602163713431

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行

(4)主线程不断重复上面的第三步。

8.5 宏任务和微任务

  • 异步队列中要执行的任务又分为了两类:宏任务微任务

    • 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等

    • 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等

  • 那么异步队列中宏任务和微任务的优先级又是怎样的?

    1. 先执行同步任务
    2. 执行异步队列中的微任务队列
    3. 当微任务队列执行完毕后,在执行宏任务队列
  • 宏任务微任务补充:

    • 可以将await关键字后面执行的代码,看做是包裹在(resolve,reject) => {函数执行}中的代码
    • awiat的下一条语句,可以看做是then(res => {函数执行})中的代码
    • 总结:await()这个过程是同步的,但是await的下一条语句会放到异步队列的微任务中
    • 注:使用async声明的函数,在调用时,依旧是同步任务,不会变成异步任务
  • 宏任务和微任务面试题

    image-20210719145324018

image-20210719151731404

8.6 循环中绑定事件

image-20210812202745410
  • 代码如上图所示,问题:为什么打印是3

    • 首先,我们需要知道JS中执行任务的顺序(同步任务和异步任务),优***先执行同步任务,然后在执行异步任务***

    • js中所有的事件绑定所对应的回调函数都是异步任务

    • 当绑定onclick事件后,不需要等待执行,继续执行下一个循环任务,每进行一次循环任务,全局变量的i都在不断的变化

    • 当点击的时候,外层循环已经结束,然后执行 console.log(i) 时,由于i不是私有变量,便会找到上一级window作用域全局的i,最终打印的i会指向全局变量的i,所以输出是3

  • 问题:为什么会出现上图中的情况?

    • 在for循环中使用var声明的变量是没有块级作用域的,只有全局作用域,也就是说每一次循环,用var声明的变量不会保存到本次循环中,而是保存到全局作用域

    • 模拟上图中for循环的基本流程

      • image-20210812202912824

  • 问题:为什么使用let声明变量可以解决问题?

    • 使用{}包裹起来的代码我们称之为代码块,{}里面就是块级作用域

    • let声明的变量具有块级作用域,也就是说每一次循环 使用let声明的变量 会保存到本次循环体中,而本次循环体就是一个块级作用域

    • 当访问变量时,优先访问自身作用域的下的变量,所以此时的i并不是外部全局变量下的i

    • 模拟for循环中使用let初始化变量的流程:

      image-20210812202941396

8.7 循环中按顺序处理异步任务

如果希望在循环中按顺序依次处理异步任务,最好使用 await + for 循环,而不是 await + forEach循环

  • 使用await + for循环可以按照顺序依次处理异步任务

    image-20210818153024615

坑:使用await + forEach循环***无法*** 按照顺序依次处理异步任务

  • 首先需要了解forEach的实现原理

    • 由下图可知,forEach方法中传入的回调函数是按顺序依次执行

    • 但是,即使回调函数函数体中有await,也不会阻止回调函数的执行

    • await只会阻塞当前函数体内代码的执行

image-20210818153456352

image-20210818160218435