JavaScript 事件循环(Event Loop)理解
JavaScript 是一种单线程语言,所谓单线程,就是一次只做一件事,按顺序执行。而要在单线程的世界里实现流畅的异步操作,必须理解事件循环(Event Loop)机制。
事件循环机制并非 JavaScript 引擎的一部分,而是宿主环境(浏览器、Node.js 等)提供的,其本质是一个持续运行的进程,负责:协调执行栈(Call Stack)、任务队列(Task Queue)和Web APIs的工作。
对于同步代码,会进入执行栈中直接执行。遇到异步任务时(setTimeout/setInterval、网络请求等),异步任务会被交给对应的Web APIs处理,避免阻塞主进程。异步任务完成后,其回调函数也不会立即执行,而是放入任务队列中排队等待执行。事件循环的主要工作就是:在执行栈为空时,从任务队列中取出一个任务,压入执行栈在执行。
任务队列中的任务分为宏任务(Macrotask) 和 微任务(Microtask)。
- 宏任务:
script标签本身、setTimeout、setInterval、I/O操作、UI渲染等 - 微任务:
Promise.then/catch/finally、MetationObserver 以及 Node.js 中的process.nextTick等
事件循环中,一个完整的事件循环tick遵循以下流程:
首先执行宏任务(如加载script脚本),当这个宏任务执行完毕后,执行栈变为空时,并不立即取下一个宏任务,而是先检查微任务队列,若微任务队列不为空,则一次性不间断的将作用的微任务执行完,直到队列为空,微任务产生的新的微任务会添加到队尾,在当前轮次中一起执行完成。只有当微任务彻底清空之后,浏览器才会进行必要的UI渲染,然后开启下一轮循环,从宏任务队列中取下一个宏任务。
也就是说,微任务拥有更高的优先级,可以“插队”。这使得我们能在当前的宏任务之后、下一次渲染和下一次宏任务之前,完成一些关键的、需要立即生效的逻辑。如将Promise的状态尽快通知到后续的处理当中,对必要的异步任务使用微任务形式可以确保其回调函数比setTimeout这类宏任务更早执行,从而保证状态更新和及时性、一致性。
对闭包的理解,其应用场景以及如何规避可能的内存泄漏
闭包(Closure):从技术上讲,当一个函数能够“记住”并持续访问其被创建时的词法作用域(lexical scope)中的变量时,即使该函数在其词法作用域之外被执行,闭包也就产生了。通俗来说,闭包就是一个函数与其周围状态(词法环境)的捆绑。
闭包的形成通常发生在一个函数内部定义了另一个函数,并将内部函数作为返回值或传递到其他地方。这个内部函数就携带了对外部函数作用域的引用,就像“记忆”一样。使用场景如下:
- 数据封装与私有变量:这是闭包最经典的应用。创建一个模块,其内部状态对外部是不可见的,只通过暴露特定的函数接口操作这些状态。避免全局变量的污染,是早期模块化实现的基础。
- 高阶函数的应用:函数防抖(Debounce)和节流(Throttle)的实现严重依赖闭包来保存定时器 ID 和时间戳状态。函数柯里化(Currying)同样利用闭包来“缓存”参数,生成新的函数。
- React Hooks 的基石:
useState、useEffect等 React Hooks 的底层魔法正是闭包。每个组件实例的状态之所以能在多次渲染之间保持,就是因为 Hooks 函数形成一个闭包,捕捉了特定与该组件实例的状态变量。
然而,闭包可以维持这样的功能,也容易出现其引用的外部变量无法被垃圾回收机制(GC)正常回收,如果滥用可能会导致内存泄漏。特别酸当闭包中引用 DOM 元素,而该 DOM 元素后续被移除时,如果闭包的引用依然存在,内存就无法释放。解决方法是:在不被需要是,手动接触引用。如在组件卸载时,清除定时器、将不再使用的外部变量设置为null,从而打破引用链,让 GC 可以正常工作。
proto、prototype和constructor之间有什么关系?描述 JavaScript 的原型链继承机制
原型链的三个重要部分:proto、prototype、constructor。
prototype:是函数独有的属性。当一个函数被定义时,它会自动获得一个prototype属性,该属性指向一个对象,被称为“原型对象”,这个原型对象的作用是为所有通过该函数作为构造函数创建的实例提供共享的属性和方法。proto:是每个对象都拥有的内部属性。当通过new关键字创建一个实例时,这个实例的proto属性会被自动设置为其构造函数的prototype对象,也就是实例.__proto__ === 构造函数.prototype。constructor存在于原型对象上(prototype.constructor),默认指向拥有该prototype的构造函数本身。
三者共同编织了原型链。当试图访问一个对象的属性时,JavaScript 引擎会首先在对象自身上查找。如果找不到,它就会沿着该对象的proto指针,去其原型对象上查找。如果原型对象上依然没有,则返回undefined。这种通过proto链接起来的、自下而上的查找路径,就是原型链基础的核心机制。
Webpack 的核心工作流程是什么
Webpack 的核心工作流程可以看作一个高度自动化的模块化打包流水线。其核心目的是将各种类型的资源(如 JavaScript、CSS、图片、字体等)转换、组合成浏览器能够高效加载的静态文件。
整个过程可以概括为以下几个核心步骤:
-
初始化(启动构建)
- 读取并合并用户在配置文件
webpack.config.js中设置的参数和命令行传入的参数。 - 初始化 Compiler 对象,这个对象是 Webpack 的“大脑”,负责整个构建生命周期的调度。
- 加载所有配置的 Plugin,并调用它们的
apply方法,让插件可以监听后续的事件钩子。
- 读取并合并用户在配置文件
-
开始编译(
compile)- 确定了入口(Entry)。Webpack 从配置的入口文件开始。
- Compiler 对象发出
run或compile等事件,标志着编译正式开始。
-
编译模块(
make)- 寻找依赖:从入口文件开始,调用对应的 Loader 对模块的源代码进行转换(例如,将 TypeScript 转换成 JavaScript,将 SASS 转换成 CSS)。
- 构建依赖图(Dependency Graph):在转换过程中,Webpack 会解析文件中的
import/require等语句,找出该模块依赖的其他模块。 - 递归处理:然后,Webpack 会递归地进入这些依赖模块,重复上述“转换 → 找依赖”的过程,直到项目中所有被用到的模块都被处理完毕。最终形成一个以入口为起点的、包含所有模块及其依赖关系的“依赖图”。
-
完成模块编译(
seal)- 模块编译阶段结束。此时,所有模块的转换已经完成,模块之间的依赖关系也已经确定。
-
输出资源(
emit)- 封装(Chunk):根据入口点和代码分割(Code Splitting)配置,将依赖图中的模块分组到不同的 Chunk 中。一个 Chunk 通常对应一个输出文件(Bundle)。
- 生成文件:Webpack 会根据每个 Chunk 的内容,生成最终的输出文件。在这个过程中,会进行代码优化(如 Tree Shaking)、作用域提升(Scope Hoisting)等操作。
- 写入文件系统:确定好输出内容后,根据配置的输出路径(Output)和文件名,将文件内容写入到硬盘的指定位置。
-
完成(
done)- 整个构建过程结束,Compiler 对象发出
done事件。
- 整个构建过程结束,Compiler 对象发出
简单总结流程:初始化配置 → 找到入口 → 用 Loader 编译模块 → 构建依赖图 → 封装成 Chunk → 输出到文件系统。
Loader 和 Plugin 的区别是什么
Loader(模块加载器/转换器)
-
核心职责:转换资源。
- Loader 是一个函数(或模块),它的工作目标非常单一:将一种类型的文件内容转换成另一种类型。
- 它充当了“翻译官”的角色,让 Webpack 能够处理非 JavaScript 文件(如
.css,.scss,.jpg,.vue,.ts)。 - 例如:
css-loader:将 CSS 文件转换成 Webpack 能理解的 JavaScript 模块(通常是字符串)。babel-loader:使用 Babel 将 ES6+ 代码转换成 ES5 代码。file-loader:将文件资源(如图片)复制到输出目录,并返回一个公共 URL。
-
执行时机:在模块编译阶段执行。
- Loader 工作在“单个文件级别”,在将模块添加到依赖图之前,对模块的源代码进行转换。
-
配置方式:在
module.rules中配置,它是一个数组,定义了针对不同文件类型使用哪些 Loader。
module.exports = {
module: {
rules: [
{
test: /\.css$/i, // 匹配 .css 文件
use: ['style-loader', 'css-loader'], // 使用这两个 loader
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
],
},
};- 本质:一个函数,接收源文件内容作为参数,返回转换后的内容。
Plugin(插件)
-
核心职责:执行更广泛的任务。
- Plugin 的功能比 Loader 强大和灵活得多。它可以介入到 Webpack 构建过程的每一个环节中。
- 它通过监听 Webpack 生命周期中广播出来的各种事件(hooks),在合适的时机执行自定义的逻辑,从而扩展 Webpack 的功能。
- 例如:
HtmlWebpackPlugin:在输出目录自动生成一个 HTML 文件,并自动注入打包好的 JS 和 CSS 文件链接。CleanWebpackPlugin:在每次打包前,自动清理输出目录。MiniCssExtractPlugin:将 CSS 从 JS 中提取出来,成为一个独立的 CSS 文件。DefinePlugin:允许在编译时创建配置的全局常量。
-
执行时机:贯穿整个 Webpack 构建周期。
- 从初始化到输出完成,Webpack 在不同阶段会触发不同的事件。Plugin 可以监听这些事件,并在任意时刻执行。
-
配置方式:在
plugins数组中配置,通常需要通过new来创建它的实例。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ... 其他配置
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
};- 本质:一个具有
apply方法的 JavaScript 类(或构造函数)。这个apply方法会在插件安装时被 Webpack compiler 调用。
总结对比表
| 特性 | Loader | Plugin |
|---|---|---|
| 核心职责 | 转换模块源码 | 扩展功能,介入构建流程 |
| 工作级别 | 单个文件级别 | 整个构建流程/项目级别 |
| 执行时机 | 模块编译阶段(早期) | 整个构建生命周期(多个时机) |
| 配置方式 | 在 module.rules 中配置 | 在 plugins 数组中 new 实例 |
| 本质 | 一个转换函数 | 一个具有 apply 方法的类 |
一个比喻:
- Loader 就像是工厂流水线上的 “操作工”,他们只负责对流水线上的 “单个零件”(文件)进行特定的加工(转换),比如打磨、喷漆。
- Plugin 就像是工厂的 “项目经理” 或 “自动化系统”,它不直接加工零件,而是管理整个生产线,负责在特定时间点做全局性的事情,比如在流水线开始时清理工作台、在所有零件加工完成后打包发货、或者优化整个生产流程。
Vite 为什么比 Webpack 在开发环境下快很多?其原理是什么
Vite 在开发环境下之所以能实现远超 Webpack 的“秒级”启动和热更新速度,其秘诀在于它颠覆了传统 bundler 的工作模式,充分利用了现代浏览器原生支持的ESModules(ESM)。
传统的 WebpackDeyServer,在启动时必须先遍历所有模块,构建完整的依赖图,然后将整个应用打包到内存中。项目越庞大,这个初始的打包过程就越耗时。
Vite 则完全不同。它启动时,几乎不做任何打包工作。它直接以项目源码作为服务根目录,当浏览器发起请求时,例如请求 main.js,ViteDevServer 会直接返回这个文件。浏览器会解析文件中的 import 语句,然后按需发起对其他模块(如 import App from'./App.vue')的 HTTP 请求。Vite 会拦截这些请求,即时(Just-in-Time)地对被请求的模块进行编译转换(如将 vue 文件编译成 JavaScript),然后以原生 ESM 的格式返回给浏览器。
这种按需编译的模式,意味着只有当代码实际被请求时,Vite 才会去处理它。应用的启动时间不再与项目的大小成正比。而在热更新(HMR)时,Vite 只需让被修改的模块失效,并精确地通知浏览器重新请求这一个模块即可,无需重新构建整个 bundle。这种极致的效率,为前端开发带来了前所未有的流畅体验。当然,在生产环境中,Vite 还是会使用 Rollup 进行打包,以获得最佳的性能和兼容性。
[Vite 与 Rollup 的关系](/01-developer/frontend/js_rollup### Vite 与 Rollup 的关系)
JavaScript 和 TypeScript
TypeScript 和 JavaScript 是两种常用的编程语言,它们的主要区别和优点如下:
| 对比项 | JavaScript | TypeScript |
|---|---|---|
| 类型系统 | 动态类型语言,变量在运行时确定 | 静态类型语言,支持类型注解,类型检查在编译时进行。 |
| 编译 | 直接由浏览器或 Node.js 执行,无需编译。 | 需要编译为 JavaScript 后才能运行。 |
| 工具支持 | 工具支持较少,尤其在大型项目中。 | 提供更好的开发工具支持,如代码补全、类型检查、重构等。 |
| 兼容性 | 所有 JavaScript 代码都可在 TypeScript 中运行。 | 编译后的代码与 JavaScript 完全兼容。 |
| 学习曲线 | 学习曲线较平缓,适合初学者。 | 需要掌握类型系统等额外概念,学习曲线稍陡。 |
| 社区生态 | 社区庞大,资源丰富。 | 社区增长迅速,尤其在大型项目中应用广泛。 |
| 适用场景 | 适合小型项目或快速原型开发。 | 适合大型项目,尤其是需要长期维护的复杂应用。 |
| 优点 | 无需编译,开发流程简单。 学习门槛低,适合初学者。 社区资源丰富,生态成熟。 | 静态类型检查减少运行时错误。 更好的工具支持提升开发效率。 增强代码可读性和可维护性。 支持最新的 JavaScript 特性。 |
| 总结 | 适合大型项目,提供更强的类型检查和工具支持。 | 适合小型项目或快速开发,学习成本低。 |
Vue2 和 Vue3 区别
性能优化
- Vue 3:通过重写虚拟 DOM 和优化编译器,性能显著提升,渲染速度更快,内存占用更少。
- Vue 2:性能较好,但不如 Vue 3。
Composition API
- Vue 3:引入了 Composition API,允许开发者按逻辑组织代码,提升复杂组件的可维护性。
- Vue 2:主要使用 Options API,代码组织方式相对固定。
响应式系统
- Vue 3:使用
Proxy实现响应式系统,支持更多数据类型,性能更好。 - Vue 2:使用
Object.defineProperty,存在一些局限性,如无法检测数组和对象的变化。
TypeScript 支持
- Vue 3:内置 TypeScript 支持,类型推断更完善。
- Vue 2:对 TypeScript 的支持较弱,类型推断不够完善。
Fragment 和 Teleport
- Vue 3:支持 Fragment(多根节点组件)和 Teleport(将组件渲染到 DOM 其他位置)。
- Vue 2:不支持这些特性。
全局 API 更改
- Vue 3:全局 API 改为按需导入,减少打包体积。
- Vue 2:全局 API 通过
Vue对象访问。
生命周期钩子
- Vue 3:部分生命周期钩子更名(如
beforeDestroy改为beforeUnmount),并新增了setup函数。 - Vue 2:使用传统的生命周期钩子。
自定义渲染器
- Vue 3:支持自定义渲染器,适用于非 DOM 环境(如小程序、Canvas)。
- Vue 2:不支持自定义渲染器。
Suspense
- Vue 3:支持 Suspense,用于处理异步组件加载。
- Vue 2:不支持 Suspense。
打包体积
- Vue 3:通过 Tree-shaking 优化,打包体积更小。
- Vue 2:打包体积相对较大。
Vue 3 在性能、开发体验和灵活性上都有显著提升,尤其是 Composition API 和响应式系统的改进。对于新项目,推荐使用 Vue 3;对于现有 Vue 2 项目,可以根据需求逐步迁移。
从输入 URL 到页面加载完成,整个过程发生了什么?请尽可能详细地描述,并说明其中哪些环节可以进行性能优化。
当用户在浏览器地址栏输入 URL 并按下回车,一场跨越网络和计算机内部的复杂协作便拉开了序幕。从输入 URL 到页面加载完成发生了什么
旅程始于 DNS 查询,浏览器需要将用户输入的域名(如 google.com)解析为服务器的 IP 地址。这个过程会依次查询浏览器缓存、系统缓存、路由器缓存,直至向 DNS 服务器发起请求。拿到 IP 地址后,浏览器会通过三次握手与服务器建立一条可靠的 TCP 连接。如果网站是 HTTPS 的,还需要在此之上进行一次 TLS 握手,以建立加密信道。
连接建立后,浏览器便可以发送 HTTP 请求报文。服务器接收到请求后,进行处理(可能涉及数据库查询、业务逻辑计算等),然后返回一个 HTTP 响应报文,其中包含了状态码(如 2OOOK)和响应体(通常是 HTML 内容)。
浏览器接收到 HTML 后,渲染引擎开始工作,进入我们前面讨论的关键渲染路径。它会解析 HTML 构建 DOM,解析 CSS 构建 CSSOM。在解析过程中,如果遇到其他资源引用(如 JS、图片、字体文件)浏览器会为这些资源再次发起 HTTP 请求。这些后续请求可能会复用已建立的 TCP 连接(得益于 HTTP 持久连接或 HTTP/2 的多路复用),从而提高效率。最终,页面被渲染出来,并在后续的”水合”过程中绑定交互事件。
几乎每个环节都存在优化空间:DNS 预解析、TCP 预连接、利用 CDN 加速内容分发、启用 HTTP/2 或 HTTP/3、对资源进行压缩和缓存、优化关键渲染路径等等,这些共同决定了最终的用户体验。
