JS的执行机制

导读

JavaScript是一门单线程语言, 同一时间只能执行一个代码块。而且js在执行的时候,不会等待异步代码返回结果后在往下执行,会直接往下执行,异步代码的回调会进入一个任务队列,等到同步代码执行完成后会通过事件循环机制(Event Loop)将任务队列中的回调推入到主线程中执行。

本章我们分析下这背后的机制:js的同步,异步,宏任务,微任务,事件循环

为什么js是单线程的

假设是js是多线程的,js可以通过DOM API 操作dom, 现在有两个线程同时操作一个dom节点,一个线程删除该节点,一个线程修改操作该节点,那么到底该听谁的呢,所以js设计成单线程执行机制。虽然HTML5 提出了Web Worker标准,允许JavaScript可以创建多个子线程,但是子线程受主线程控制,并且不能操作dom, 所以并没有改变JavaScript是单线程执行的本质。

任务队列

单线程就意味着执行任务要排队,前一个任务结束才会执行后一个任务。如果是一个大量计算的代码那也可以接受,但是如果像Ajax这样的操作这样的等待就没有必要了,所以js的任务被分为了这两类:同步任务异步任务。”任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。

1. 同步任务

同步任务是指在主线程上排队执行的任务,只有前一个任务结束才会执行后一个任务。

2. 异步任务

异步任务是指不进入主线程,进入任务队列。当主线程的任务执行结束后,会看任务队列里面是否有待执行的异步任务,如果有的话就结束等待状态,将任务推入到主线程中进行执行。

事件和回调函数

任务队列就是一个事件队列(也被叫做消息队列),IO设备完成一项任务(比如),就会往任务队列中添加一个事件,主线程读取任务队列,就是读取里面有哪些事件。
除了IO设备事件外,还有用户的交互事件,比如鼠标的点击,移入移出等等,只要定义了回调函数,这些事件都会被添加到任务队列,等待主线程的读取。
什么是回调函数,就是异步任务进入主线程执行的代码,主线程在执行异步任务就是在执行回调函数。

宏任务和微任务

除了上面对js任务广义的定义为同步任务和异步任务,还对js进行了更精细的定义宏任务微任务

1. 宏任务

macro-task(宏任务)具体包含下面几个:

  • 整体代码script
  • 定时器 setTimeout、setInterval
  • I/O 输入输出
  • UI渲染
  • postMessage
  • MessageChannel
  • requestAnimationFrame
  • setImmediate(Node.js 环境)

2. 微任务

micro-task(微任务)具体包含下面几个

  • new Promise.then()
  • MutaionObserver
  • process.nextTick(Node.js 环境)

事件循环机制(EventLoop)

任务队列里面的任务什么时候执行,就是通过事件循环机制(EventLoop)来解决的。主线程读取任务队列,并且这个是不断重复的,这个机制就是事件循环机制(EventLoop)。具体规则如下:

  • 所有的代码作为宏任务进入主线程执行
  • 执行的过程中,同步立即执行,遇到宏任务并且定义了回调函数,就将回调函数加入到宏任务队列中,遇到微任务并且定义了回调函数,就将回调函数加入到微任务队列中
  • 当宏任务执行结束后,读取微任务列队,有就执行,并且全部执行完,清空微任务队列
  • 微任务全部执行完毕后,进行UI render渲染
  • 本轮宏任务执行完成,进行下一轮宏任务,回到第二步,直到清空宏任务队列和微任务队列

举例

console.log("1"); 

setTimeout(function () {
  console.log("2");
  setTimeout(function () {
    console.log("3");
  },0);
  new Promise(function (resolve) {
    console.log("4");
    resolve();
  }).then(function () {
    console.log("5");
  });
},0);

console.log("6");

new Promise(function (resolve) {
  console.log("7");
  resolve();
}).then(function () {
  console.log("8");
});

setTimeout(function () {
  console.log("9");
  setTimeout(function () {
    console.log("10");
  },0);
  new Promise(function (resolve) {
    console.log("11");
    resolve();
  }).then(function () {
    console.log("12");
  });
},0);


// 1, 6, 7, 8, 2, 4, 5, 9, 11, 12, 3, 10
复制代码

我们来分析一下:

  • 第一次事件循环
    • console.log(‘1’);同步代码立即执行,输出1
    • setTimeout 0秒后将回调函数推入宏任务列表,记作setTimeout1
    • console.log(“6”);同步代码立即执行,输出6
    • new Promise 中的executor是同步代码,执行console.log(“7”),输出7,then的回调函数进入微任务列队,记作then1
    • setTimeout 0秒后将回调函数推入宏任务列表,记作setTimeout2
    • 宏任务执行完成,执行微任务列队,then1,进入主线程执行console.log(“8”),输出8
    • 本轮宏任务执行完毕
  • 第二次事件循环
    • 读取setTimeout1 进入主线程执行,同步执行console.log(“2”),输出2
    • setTimeout 0秒后将回调函数推入宏任务列表,记作setTimeout3
    • new Promise 中的executor是同步代码,执行console.log(“4”),输出4,then的回调函数进入微任务列队,记作then2
    • 宏任务执行完成,执行微任务列队,then2,进入主线程执行console.log(“5”),输出5
    • 本轮宏任务执行完毕
  • 第三次事件循环
    • 读取setTimeout2 进入主线程执行,同步执行console.log(“9”),输出9
    • setTimeout 0秒后将回调函数推入宏任务列表,记作setTimeout4
    • new Promise 中的executor是同步代码,执行console.log(“11”),输出11,then的回调函数进入微任务列队,记作then3
    • 宏任务执行完成,执行微任务列队,then3,进入主线程执行console.log(“12”),输出12
    • 本轮宏任务执行完毕
  • 第四次事件循环
    • 读取setTimeout3 进入主线程执行,同步执行console.log(“3”),输出3
    • 本轮宏任务执行完毕
  • 第五次事件循环
    • 读取setTimeout4 进入主线程执行,同步执行console.log(“10”),输出10
    • 本轮宏任务执行完毕
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享