新生代总结 JavaScript 运行机制解析

简介: 新生代总结 JavaScript 运行机制解析

image.png

📢 大家好,我是小丞同学,一名准大二的前端爱好者


📢 这篇文章将带你一起学习理解 JavaScript 运行机制


📢 愿你忠于自己,热爱生活


引言

在一些面试中,我们或许会被问到这样的问题


简述一下 JavaScript 的运行机制?


还有可能会被问这样的代码

setTimeout(function () {
    console.log('定时器开始啦')
});
new Promise(function (resolve) {
    console.log('马上执行for循环啦');
    for (var i = 0; i < 10000; i++) {
        i == 99 && resolve();
    }
}).then(function () {
    console.log('执行then函数啦')
});

这些虽然看起来很深奥很复杂,但是如果你了解了 JavaScript 的运行机制,这些问题都能够一一化解  先附上本文的纲要,本文将会从这三个方面去解析 JavaScript 的运行机制

image.png

首先我们来谈谈 JavaScript 的单线程


1. 为什么是单线程?

众所周知,JavaScript 是一门单线程的语言,也因此带来了很多诟病,那么单线程如此不堪,为什么不把它设计成多线程的呢?


其实这个问题就出现在了 JavaScript 的应用场景上,我们通常采用 JavaScript 来操作 DOM 元素,这在现在来看没什么问题。但是我们想一想,如果 JavaScript 变成了一门多线程的语言,那会发生什么呢?


想象一下下面的场景


一段 JS 代码删除 DOM 元素,一段 JS 代码更改 DOM 元素样式,它们一起被执行了,这会发生什么?


先别说浏览器该怎么处理了,我都不知道该如何处理,那浏览器就会崩溃掉 …


为了避免这样的情况, JavaScript 被设计成了一门单线程的语言


单线程就意味着,一次只能执行一个任务,其他任务都需要排队等待


但是为了能有多线程的功能,有了很多的尝试


在 HTML5 中提出了 web worker 标准,它提供了一套完整的 API 去允许在主线程以外去执行另一个线程,但是这不意味着 JavaScript 从此拥有了多线程的能力,同时我们也不能用它来操作 DOM 元素。


在 JavaScript 中还有着独特执行机制,它将主线程中的任务分为同步任务和异步任务


2. 为什么需要异步?

为了能够解决单线程带来的代码阻塞等问题


JS 是单线程的,我们可以想象成有一个售票窗口,有很多人在窗口排队办理业务,而 JS 只能一个一个处理,那如果有一个客户的需求很多,办理业务的时间很长,那么这条队伍的其他人就只能干等着了,就相当于代码阻塞了,也就是浏览器假死,等待代码执行


因此有了同步任务和异步任务的概念


就是需要通过这样来区分,将那些办理业务时间长的分出来,等到其他客户处理完毕之后再统一处理


关于同步任务和异步任务是这样解释的


同步任务:是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,例如:console.log

异步任务:不进入主线程、通过事件循环机制处理,在任务队列中注册回调函数最终拿到结果,例如:setTimeout

了解了什么是同步,什么是异步,我们来一道非常简单的题目

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

结果输出 1 3 2  原因是 setTimeout 是异步任务,需要在同步代码执行之后再执行  接下来我们聊聊运行机制的核心:事件循环  3. 事件循环 首先我们用一张图来理解事件循环

image.png

它的运行机制如下:


所有同步任务在主线程上执行,形成一个执行栈,也就是上图蓝色箭头表示

主线程以外有一个异步任务队列(红色箭头),会等到异步任务返回结果后将它放入任务队列

当主线程中执行栈代码执行完毕,也就是同步任务执行完毕,就会开始读取任务队列代码,再放入执行栈中执行

不断地重复上面三步,这就是事件循环

用图形来描绘的话,就是上图中的三个黑色箭头,连成的闭环


也就是说:只要主线程执行栈空了,就会去读取任务队列,这个过程是循环不断的,这种运行机制就叫做事件循环


了解了事件循环,我们对前面那题做一个简单的升级

console.log(1);
setTimeout(()=>{console.log(2);},0)
setTimeout(()=>{console.log(3);},1000)
setTimeout(()=>{console.log(4);},0)
console.log(5);

这次输出了 1 – 5 – 2 – 4 – 3


可能会有人会对 3 的输出有疑惑,首先定时器都是异步任务,会先被放入异步任务队列当中,需要等待异步任务返回结果后,再将回调函数放入任务队列当中,等待主线程来执行,因此,2 和 4 会在 3 之前输出


4. 异步任务队列细节

常见的会放入异步任务队列的事件


DOM 事件

Promise

Ajax 请求

setTimeout 和 setlnterval

文件上传

至于加入异步任务队列的时间,是需要根据当前异步任务而定的,不是说拿到异步任务直接添加到任务队列里面,是要等到当前异步任务执行完成返回结果,才将其放到任务队列里


就拿 setTimeout 来说,是需要等待定时结束再将回调加入任务队列的


也可以结合下图理解


image.png

image.png

了解了任务队列,我们需要再谈一谈异步任务当中,又被细分出来的宏任务和微任务


5. 宏任务和微任务

宏任务队列可以有多个,微任务队列只有一个


那么什么是宏任务,什么是微任务呢?


宏任务有:HTML解析、鼠标事件、键盘事件、网络请求、执行主线程js代码和定时器

微任务有:promise.then,DOM 渲染,async,process.nextTick

那它是怎么被执行的呢?


当执行栈中的同步任务执行完毕后,先执行微任务


微任务队列执行完毕后,会读取宏任务


执行宏任务的过程中,遇到微任务,再加入微任务队列


宏任务执行完后,再次读取微任务队列,依次循环


画个图来辅助理解一下


用一句简单的话来总结:微任务永远在宏任务执行之前被执行完毕


image.png

image.png

特别注意的是:由于代码的入口就是一个 script 标签。因此,全局任务属于宏任务  6. 实战 在了解了这么多后,我们来看一到经典的面试题

console.log("1");
setTimeout(function () {
    console.log("2");
    new Promise(function (resolve) {
        console.log("3");
        resolve();
    }).then(function () {
        console.log("4");
    });
});
new Promise(function (resolve) {
    console.log("5");
    resolve();
}).then(function () {
    console.log("6");
});
setTimeout(function () {
    console.log("7");
});
setTimeout(function () {
    console.log("8");
    new Promise(function (resolve) {
        console.log("9");
        resolve();
    }).then(function () {
        console.log("10");
    });
});
new Promise(function (resolve) {
    console.log("11");
    resolve();
}).then(function () {
    console.log("12");
});
console.log("13");

答案是:1 – 5 – 11 – 13 – 6 – 12 – 2 – 3 – 4 – 7 – 8 – 9 – 10


第一轮循环


从全局任务入口,首先打印日志 1

遇到宏任务 setTimeout ,交给异步处理模块,记为setTimeout1

再遇到 promise 对象,打印日志 5 ,将 promise.then 加入微任务队列,记做 p1

又遇到 setTimeout 交给异步处理模块,记为 setTimeout2

又遇到 setTimeout 交给异步处理模块,记为 setTimeout3

遇到 promise 对象,打印日志 11 ,将 promise.then 加入微任务队列,记做 p2

遇到打印语句,直接打印日志 13

本轮循环共打印:1 – 5 – 11 – 13


当前循环结果


image.png

第二轮循环


首先执行微任务队列 p1 和 p2 ,先进先出,先打印 6 再打印 12

微任务事件处理完毕,开始执行宏任务 setTimeout1

遇到打印语句,直接打印日志 2

又遇到 promise 对象,打印日志 3,将 promise.then 加入微任务队列,记做 p3

第二轮循环结束


当前运行图为

image.png

第三轮循环  首先执行微任务队列,打印日志 4 微任务处理完毕,执行宏任务 setTimeout2 遇到打印语句,直接输出 7 本轮循环结束

image.png

第四轮循环  微任务队列为空,执行宏任务 setTimeout3 遇到打印语句,打印日志 8 遇到 promise 对象,执行打印语句,打印 9 将 promise.then 加入微任务队列 记做 p4

image.png

第五轮循环  首先清空微任务队列,执行打印语句,打印 10 执行完毕 以上就是关于 JavaScript 运行机制的全部内容,希望能有所收获


相关文章
|
2月前
|
JavaScript 前端开发
Vue 应用 main.js 里的源代码解析
Vue 应用 main.js 里的源代码解析
26 0
|
1天前
|
JavaScript 前端开发 UED
深入解析JavaScript原生操作DOM技术
【4月更文挑战第22天】本文深入探讨JavaScript原生DOM操作技术,包括使用`getElement*`方法和CSS选择器获取元素,借助`createElement`与`appendChild`动态创建及插入元素,修改元素内容、属性和样式,以及删除元素。通过掌握这些技术,开发者能实现页面动态交互,但应注意避免过度操作DOM以优化性能和用户体验。
|
1天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
1月前
|
自然语言处理 JavaScript 前端开发
Vue.js 深度解析:模板编译原理与过程
Vue.js 深度解析:模板编译原理与过程
|
1月前
|
缓存 JavaScript 网络架构
Vue.js 进阶技巧:keep-alive 缓存组件解析
Vue.js 进阶技巧:keep-alive 缓存组件解析
|
1月前
|
JavaScript 前端开发 API
Vue.js 深度解析:nextTick 原理与应用
Vue.js 深度解析:nextTick 原理与应用
|
1月前
|
JavaScript 前端开发 IDE
JavaScript 中的 structuredClone():详尽解析
您是否知道,现在 JavaScript 中有一种原生的方式可以深拷贝对象?
|
2月前
|
前端开发 JavaScript BI
JavaScript的过滤大师:深度解析Filter用法
JavaScript的过滤大师:深度解析Filter用法
65 0
|
2月前
|
JSON 前端开发 JavaScript
掌握现代JavaScript:ES7到ES12的新特性全解析!
ES2016(ES7)中新增了如下特性👇 Array.prototype.includes Exponentiation Operator
|
3月前
|
JavaScript
js生成二维码和解析二维码
js生成二维码和解析二维码
25 0

推荐镜像

更多