详解JavaScript 执行机制

简介: 详解JavaScript 执行机制

详解JavaScript 执行机制

热身

/* 先打印1, 3, 2s后打印2 */
console.log(1)
setTimeout(() => {
  console.log(2)
}, 1000)
console.log(3)


/* 先打印1, 3, 后打印2 */
console.log(1)
setTimeout(() => {
  console.log(2)
}, 0)
console.log(3)

第一个例子的话不难理解,定时器函数就是1s后才调用回调函数.

而第二个例子则可能优点小问题,JavaScript从上到下执行,那么遇到0s的计时器函数,就应该先输出2才对啊。这就是因为后面要提到的JavaScript执行机制导致的啦,因为setTimeout是异步任务。

JavaScript是单线程

JavaScript的核心特征就是单线程,即同一时间只能做一件事。

为什么它是单线程呢?因为JavaScript作为浏览器脚本语言,它的主要用途就是与用户互动、操作DOM。既然如此,如果它不是单线程的话,假如一个线程在DOM节点上添加内容,同时另一个线程删除这个节点。可以看出,如果JavaScript不是单线程的话,那么将会导致同步问题。

任务队列

JavaScript是单线程语言,这也就导致了如果有一个任务等待很长的时间,这个时候就会导致阻塞,程序就会“卡死”,用户体验非常差。所以JavaScript需要异步任务。


那么,为什么JavsScript明明是单线程的,为什么能异步呢?这是因为浏览器是多线程的,通过事件循环 Event Loop即可实现异步。


所有任务都可以分成两种。

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完,才能执行后一个任务
  • 异步任务:不进入主线程,而是进入任务队列的任务。当异步任务的触发条件满足时,异步任务才会进入任务队列,而当主线程空了,就会去任务队列中取异步任务到主线程中执行


常见异步任务

  • JS事件
  • AJAX请求
  • setTimeout和setInterval
  • Promise(Promise定义部分为同步任务,回调部分为异步任务)


Event Loop事件循环机制1

  1. 所有同步任务进入主线程,而异步任务则是进入 Event Table注册回调函数
  2. 当异步任务的触发条件满足时,异步任务注册的回调函数将会从 Event Table移入到任务队列 Event Queue
  3. 当主线程中的所有同步任务执行完毕后,系统就会去看看 Event Queue中看看有没有回调函数,有的话就推到主线程中
  4. 主线程不断重复上面的步骤

preview

这就是Event Loop

宏任务和微任务

异步任务又可以进行更精细的划分为宏任务和微任务。

宏任务

setTimeout、setInterval、requestAnimationFrame

  • 当宏任务队列中的任务全部都执行完之后,如果微任务队列不为空,则先执行微任务队列中的所有任务

微任务

Promise回调部分、process.nextTick

  • 在上一个宏任务队列执行完毕后如果有微任务就会执行微任务队列中的所有元素

Event Loop事件循环机制2

  1. 首先执行 script下的同步任务
  2. 执行过程中,如果遇到异步任务,则需要把它放到对应的任务队列中(遇到宏任务,则放到宏任务中;遇到微任务,则放到微任务队列中)
  3. 同步任务执行完毕,查看微任务队列

    • 如果存在微任务,则将微任务队列全部执行(包括执行微任务中产生的新微任务)
    • 如果不存在微任务,则查看宏任务队列,执行第一个宏任务,宏任务执行完后,又看看微任务队列是否有任务,有的话,又先全部执行完微任务队列,重复上述操作,知道宏任务队列为空。

preview

练手1

console.log(1)

setTimeout(() => {
  console.log(2)
}, 0)

new Promise((resolve, reject) => {
  for (let i = 3; i < 6; i++) {
    console.log(i)
  }
  resolve()
}).then(() => {
  console.log(6)
})

console.log(7)

打印顺序:1, 3, 4, 5, 7, 6, 2


解析:

  1. 首先,程序从上往下走,直接输出1,遇到 setTimeout后,把它放到宏任务队列中

    • 此时,宏任务队列中为[setTimeout](这里用数组表示任务队列,左边代表先进入的任务队列)
  2. 继续往下跑,遇到 Promise,因为Promise定义部分为同步任务,依次输出3, 4, 5,遇到 Promise.then(),把它放到微任务队列中

    • 此时,宏任务队列为[setTimeout]
    • 此时,微任务队列为[Promise.then()]
  3. 输出7后,执行微任务队列中全部的任务,输出6, 再执行宏任务队列中的任务,输出2

练手2

题目是本人自己想的,分析有误请见谅(希望评论指示)

console.log(1)

setTimeout(() => {
  console.log(2)
  Promise.resolve().then(() => {
    console.log(3)
  })
}, 10)

const p = new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((value) => {
  console.log(value)
  Promise.resolve().then(() => {
    console.log(6)
  })
  setTimeout(() => {
    console.log(7)
  })
}).then(() => {
  console.log(8)
})

console.log(9)

输出顺序:1, 4, 9, 5, 6, 8, 7, 2, 3


解析:

  1. 先输出1, 遇到定时器,但是此时并不满足触发条件,所以2(后面还有其他的内容)只能存放在 Event Table

    • 此时, Event Table中有 [2(后面还有其他的内容)] Event Table中没有顺序,满足触发条件后,就会进入对应的任务队列
  2. 输出4,5(后面还有内容)进入微任务队列

    • 此时, Event Table中有 [2(后面还有其他的内容)]
    • 微任务队列为[5(后面还有内容)]
  3. 8进入微任务队列

    • 此时, Event Table中有 [2(后面还有其他的内容)]
    • 微任务队列为[5(后面还有内容), 8]
  4. 输出9, 然后执行微任务中的任务

    1. 输出5, 6进入微任务队列, 7进入宏任务队列

      • 此时, Event Table中有 [2(后面还有其他的内容)]
      • 微任务队列为[6, 8]6会在8之前,因为6是微任务队列5里的微任务
      • 宏任务队列为[7]
    2. 依次执行完微任务队列中的任务,然后再执行宏任务队列的任务。输出顺序为6,8,7
  5. 10ms后,满足触发条件,进入宏任务队列,此时,宏任务队列和微任务队列中都没有任务,所以直接执行。输出2,3进入微任务队列,输出3


async, await

async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then相同。
async函数在await之前的代码都是同步执行的,await之后的代码则是属于微任务(类似于Promise) await的表达式还是属于同步任务

下面就继续练手

console.log(1)

async function async1() {
  console.log(2)
  await async2()
  console.log(3)
}
async1()

async function async2() {
  console.log(4)

  await new Promise((resolve, reject) => {
    console.log(5)
    resolve(6)
  }).then((value) => {
    console.log(value)
  })

  console.log(7)
}

console.log(8)

输出顺序为:1, 2, 4, 5, 8, 6, 7, 3


分析:

  1. 先输出1,调用 async1函数,因为 await之前包括 await的表达式都是同步任务,所以,输出2后,进入到 async2函数中
  2. 输出4await一个 Promise也是同理,输出5,6进入微任务队列,因为await之后的代码则是属于微任务(不包括await的表达式),所以7进入微任务队列

    • 此时,微任务队列为[6, 7]
  3. 执行完 async2函数后,回到 async1函数中,之后的3进入微任务队列

    • 此时,微任务队列为[6, 7, 3]
  4. 输出8,执行微任务队列中的任务,输出6, 7, 3


参考链接:

JavaScript之彻底理解EventLoop

10分钟理解JS引擎的执行机制

目录
相关文章
|
4月前
|
前端开发 JavaScript UED
深入理解JavaScript中的事件循环机制
JavaScript中的事件循环机制是其异步编程的核心,深入理解该机制对于开发高效、流畅的前端应用至关重要。本文将介绍事件循环的工作原理、常见的事件循环模型,以及如何利用这些知识解决前端开发中的常见问题。
|
28天前
|
JavaScript 前端开发 算法
js 内存回收机制
【8月更文挑战第23天】js 内存回收机制
30 3
|
28天前
|
存储 JavaScript 前端开发
学习JavaScript 内存机制
【8月更文挑战第23天】学习JavaScript 内存机制
22 3
|
20天前
|
JavaScript 中间件 开发者
深入浅出Node.js中间件机制
【8月更文挑战第31天】本文将带你领略Node.js中间件的奥秘,通过直观的案例分析,揭示其背后的设计哲学。你将学会如何运用中间件构建强大而灵活的后端应用,以及在面对复杂业务逻辑时如何保持代码的清晰与高效。
|
20天前
|
设计模式 JavaScript 中间件
深入浅出Node.js中间件机制
【8月更文挑战第31天】在Node.js的世界里,中间件如同魔法般存在,它让复杂的请求处理变得井然有序。本文将带你领略中间件的奥秘,从原理到实战,一步步揭开它的神秘面纱。你将学会如何运用中间件来构建强大而灵活的后端应用,就像拼乐高一样有趣。
|
3月前
|
设计模式 JavaScript 前端开发
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
JavaScript的继承机制基于原型链,它定义了对象属性和方法的查找规则。每个对象都有一个原型,通过原型链,对象能访问到构造函数原型上的方法。例如`Animal.prototype`上的`speak`方法可被`Animal`实例访问。原型链的尽头是`Object.prototype`,其`[[Prototype]]`为`null`。继承方式包括原型链继承(通过`Object.create`)、构造函数继承(使用`call`或`apply`)和组合继承(结合两者)。ES6的`class`语法是语法糖,但底层仍基于原型。继承选择应根据需求,理解原型链原理对JavaScript面向对象编程至关重要
76 7
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
|
2月前
|
JavaScript 前端开发 API
js 运行机制(含异步机制、同步任务、异步任务、宏任务、微任务、Event Loop)
js 运行机制(含异步机制、同步任务、异步任务、宏任务、微任务、Event Loop)
29 0
|
4月前
|
缓存 移动开发 JavaScript
WKWebView对网页和js,css,png等资源文件的缓存机制及如何刷新缓存
WKWebView对网页和js,css,png等资源文件的缓存机制及如何刷新缓存
156 1
|
3月前
|
JavaScript 前端开发
深入解析JavaScript中的面向对象编程,包括对象的基本概念、创建对象的方法、继承机制以及面向对象编程的优势
【6月更文挑战第12天】本文探讨JavaScript中的面向对象编程,解释了对象的基本概念,如属性和方法,以及基于原型的结构。介绍了创建对象的四种方法:字面量、构造函数、Object.create()和ES6的class关键字。还阐述了继承机制,包括原型链和ES6的class继承,并强调了面向对象编程的代码复用和模块化优势。
41 0