JavaScript执行机制

40 阅读5分钟

1、JS为什么是单线程

js的代码只能在一个线程上运行,也就说,js同时只能执行一个js任务,js的主要用途是与用户互动和操作DOM。如果一段JS代码并行在两个线程上运行,一个线程在DOM上添加内容,另一个线程在删除DOM,就会存在操作冲突,所以为了避免复杂性,JS选择单线程执行

2、浏览器的多线程

•js引擎线程:用于解释执行js代码、用户输入、网络请求、运行JS代码的线程,叫做主线程,同步的代码直接在主线程运行,异步代码交给其他线程运行

•GUI渲染线程:绘制用户界面,与JS主线程互斥(因为js可以操作DOM,进而会影响到GUI的渲染结果);

•http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;

•定时触发器线程:setInterval、setTimeout等待时间结束后,会把执行函数推入任务队列中;

•浏览器事件处理线程:将click、mouse等UI交互事件发生后,将要执行的回调函数放入到事件队列中。

虽然异步的逻辑交给其他线程去执行,但是最终所有的回调还是会回到主线程完成执行~

3、同步任务和异步任务

JS在执行过程中会产生两种任务,分别是:同步任务和异步任务;

•同步任务:比如声明语句、for、赋值等,读取后依据从上到下从左到右,立即执行。

•异步任务:比如 ajax 网络请求,setTimeout 定时函数等都属于异步任务。异步任务会通过任务队列(Event Queue)的机制(先进先出的机制)来进行协调。

那么异步任务是怎么由任务队列协调回到主线程执行的呢?我们继续往下看~

4、任务队列

可以理解为一个静态的队列存储结构,非线程,只做存储,里面存的是一堆异步成功后的回调函数,先成功的异步的回调函数在队列的前面,后成功的在后面。

注意:异步成功后,才把其回调函数扔进队列中,而不是一开始就把所有异步的回调函数扔进队列

执行流程:下图流程解析

1.主线程执行所有同步任务,异步任务交给其他线程执行

2.其他线程执行完异步任务,将回调函数放入任务队列中

3.主线程任务执行完毕,读取任务队列中是否有任务要执行,有则主线程开始执行异步任务

4.主线程会不断重复第3个流程

image.png

我们看到任务队列中包含了多种异步任务回调函数,这些任务是否有执行顺序的区分呢?继续往下看

5、事件循环

任务队列中的任务也分为两种,分别是:宏任务(Macro-take)和微任务(Micro-take),这两个任务分别维护一个队列,都是采用先进先出的策略进行执行。

•宏任务主要包括:scrip(JS 整体代码)、setTimeout、setInterval、setImmediate、UI 交互

•微任务主要包括:Promise(重点关注)、process.nextTick(Node.js)、MutationObserver(Dom监听)

image.png

1.异步任务会区分宏任务、微任务类型,分别放到对应的任务队列,两个任务队列均遵循先进先出的机制

2.主线程执行完同步任务会检查任务队列中是否有未完成的微任务,有则执行全部的微任务,执行完之后继续下一个宏任务

3.主线程会不断地读取任务队列中的任务,循环2流程

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

浏览器为了能够使宏任务DOM渲染有序的进行,会在一个宏任务执行后,查看是否有微任务需要执行,执行完所有微任务之后,GUI渲染线程开始工作,对页面进行渲染;也就是当一个宏任务执行完会立刻执行一个微任务,然后进行页面渲染

微任务 -> GUI渲染 -> 宏任务 -> ...

为了更好的理解任务的执行顺序,我们看以下两个示例解析

示例1:

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");

执行顺序:

1.先执行同步任务:输出script start

2.遇到异步任务setTimeout,放入宏任务队列

3.遇到Promise,加入微任务队列(两个回调promise1、promise2)

4.同步代码执行:输出script end

5.同步代码执行完毕,执行微任务队列:输出promise1,then 之后产生一个微任务,加入微任务队列(promise2)

6.执行微任务,输出: promise2

7.微任务:执行微任务队列(promise1)

8.执行渲染操作,更新界面

9.执行宏任务,输出:setTimeout

示例2:

new Promise((resolve, reject) => {
    resolve(1)
    new Promise((resolve, reject) => {
        resolve(2)
    }).then(data => {
        console.log(data)
    })
}).then(data => {
    console.log(data)
})
console.log(3)

执行顺序:

1.先同步任务new Promise,没有打印,执行下一个同步任务new Promise,还是没有打印

2.执行.then,异步微任务,放入微任务队列

3.执行外部的.then,异步微任务放入微任务队列

4.同步任务,打印3

5.执行微任务,先执行内部的回调输出2,在执行下一个微任务,打印外部的回调输出1