1.浏览器和node的事件循环有什么不一样
1. 任务队列的结构与执行顺序
浏览器的事件循环是基于 HTML5 规范,而 Node.js 的事件循环是基于 libuv 库的实现。虽然它们的基本思想一致——都是“在单线程中通过事件循环和任务队列来处理异步任务”——但由于它们所处的环境和要解决的问题不同,导致在细节上有显著差异。
Node.js:
-
宏任务队列(阶段划分) :Node.js 的事件循环分为 6 个阶段(按顺序执行),每个阶段对应一个宏任务队列:
timers:执行setTimeout、setInterval回调(到时间的任务)。pending callbacks:执行延迟到下一轮的 I/O 回调(如网络错误回调)。idle, prepare:内部使用(可忽略)。poll:等待新的 I/O 事件(如文件读写、网络请求),执行相关回调(核心阶段)。check:执行setImmediate回调。close callbacks:执行关闭回调(如socket.on('close', ...))。
-
微任务:包括
Promise.then/catch/finally、queueMicrotask、process.nextTick(特殊,优先级最高)。 -
执行顺序:
- 执行一个阶段的所有宏任务(当前阶段队列清空或执行到上限)。
- 执行所有微任务(先执行
process.nextTick队列,再执行其他微任务)。 - 进入下一个阶段,重复上述步骤。
关键:每个阶段的宏任务执行完毕后,才会清空微任务队列,而非单个宏任务后立即执行。
浏览器:
-
宏任务队列:在早期版本中,可以认为有一个宏任务队列。但实际上,为了优化性能(如保证用户交互的及时响应),浏览器可能会有多个不同优先级的宏任务队列(如网络请求、DOM事件、定时器等)。
- 常见的宏任务:
<script>整体代码、setTimeout、setInterval、I/O、UI 渲染、requestAnimationFrame、用户交互事件(如点击)。
- 常见的宏任务:
-
微任务队列:只有一个微任务队列。
- 常见的微任务:
Promise.then/catch/finally、queueMicrotask、MutationObserver。
- 常见的微任务:
执行顺序(浏览器):
- 执行一个宏任务(通常是从
<script>开始)。 - 执行过程中遇到微任务,就将其放入微任务队列。
- 当前宏任务执行完毕后,立即清空整个微任务队列。
- 进行 UI 渲染(如果需要)。
- 从事件队列中取下一个宏任务,开始新一轮循环。
Node.js:
-
阶段性的循环模型:Node.js 的事件循环分为多个阶段,每个阶段都有一个 FIFO(先进先出)的回调队列。这些阶段按顺序执行,如下图所示,执行完一个阶段的所有任务后,才会进入下一个阶段。
-
定时器阶段:执行
setTimeout和setInterval的回调。 -
待定回调阶段:执行一些系统操作的回调,例如 TCP 的错误。
-
闲置阶段/准备阶段:仅在内部使用。
-
轮询阶段:
- 计算应该阻塞和轮询 I/O 的时间。
- 执行 几乎所有的 I/O 回调(如文件读取、网络请求)。如果轮询队列不为空,则迭代执行队列中的回调,直到队列为空或达到系统限制。
- 如果轮询队列为空,会检查是否有
setImmediate回调,如果有则结束轮询阶段,进入检查阶段。
-
检查阶段:执行
setImmediate设置的回调。 -
关闭回调阶段:执行关闭事件的回调,如
socket.on('close', ...)。
-
-
微任务队列:在 Node.js 中,微任务不属于事件循环的任何一个阶段,而是在每个阶段结束后、进入下一个阶段前执行。
- 常见的微任务:
Promise.then/catch/finally、queueMicrotask、process.nextTick。 - 特别注意:
process.nextTick拥有一个独立的队列,它的优先级高于其他所有微任务。在一个阶段切换后,会先清空nextTick队列,再清空其他微任务队列。
- 常见的微任务:
执行顺序(Node.js):
- 执行一个阶段(如定时器阶段)的所有宏任务。
- 该阶段执行完毕后,立即清空
process.nextTick队列(如果存在)。 - 然后清空其他微任务队列(如
Promise)。 - 进入事件循环的下一个阶段。
2. 微任务的优先级
- 浏览器:所有微任务(
Promise,queueMicrotask,MutationObserver)在同一队列中,按添加顺序执行,优先级相同。 - Node.js:
process.nextTick的优先级高于Promise等其它微任务。这意味着在同一个“阶段切换点”,nextTick的回调总会先于Promise的回调执行。
javascript
复制下载
// Node.js 示例
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// 输出:
// nextTick
// Promise
3. 特有的 API
-
浏览器特有:
requestAnimationFrame:在每次重绘前执行,用于流畅的动画。MutationObserver:监听 DOM 变化。
-
Node.js 特有:
setImmediate:设计为在当前轮询阶段完成后执行。process.nextTick:不属于事件循环的任何一个阶段,它在当前操作完成后立即执行,优先级极高。
setImmediate vs setTimeout(fn, 0)
- 在主模块(顶层代码)中,它们的执行顺序是不确定的,受进程性能影响。
- 在 I/O 回调内部,
setImmediate总是先于setTimeout被执行,因为它是在轮询阶段之后的检查阶段执行。
javascript
复制下载
// 在 I/O 回调内部
const fs = require('fs');
fs.readFile('test.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 输出:
// immediate
// timeout
总结表格
| 特性 | 浏览器 | Node.js |
|---|---|---|
| 规范/实现 | HTML5 规范 | libuv 库 |
| 结构模型 | 宏任务/微任务队列 | 多阶段循环(Timers, Poll, Check等) |
| 微任务执行时机 | 每一个宏任务之后 | 事件循环的每一个阶段之后 |
| 微任务优先级 | 所有微任务平等 | process.nextTick 优先级最高 |
| 特有 API | requestAnimationFrame, MutationObserver | setImmediate, process.nextTick |
setImmediate | 不支持 | 支持,在 Check 阶段执行 |
process.nextTick | 不支持 | 支持,优先级最高的微任务 |
实践建议
- 避免递归调用
process.nextTick:因为这会导致 I/O 饥饿(I/O Starvation),因为事件循环一直卡在清空nextTick队列,无法进入轮询阶段执行 I/O 任务。 - 在 Node.js 中,推荐使用
setImmediate来延迟执行,而不是setTimeout(fn, 0),因为前者效率更高,并且在 I/O 周期内顺序可预测。 - 理解差异是关键:在编写同构代码或需要精确控制异步流程时,必须清楚你的代码是运行在浏览器还是 Node.js 环境中。
2.跟vue不一样的框架Svelte??预编译有什么不一样?
Svelte(一个与 Vue 不同的前端框架,以 “无虚拟 DOM、预编译” 为核心特点)。如果是 Svelte,它与 Vue 在预编译(或编译时处理)方面的差异主要体现在以下几个核心维度:
1. 预编译的核心目标不同
-
Vue:Vue 的预编译(如通过
vue-loader处理单文件组件.vue)主要目标是 “优化运行时” :- 将模板字符串编译为渲染函数(生成虚拟 DOM 的创建逻辑),避免运行时解析模板的性能开销。
- 对模板进行静态分析(如标记静态节点、静态提升),优化虚拟 DOM 的 diff 过程(减少比对次数)。
- 但最终仍然依赖 运行时的虚拟 DOM 机制 来处理状态更新和 DOM 操作。
-
Svelte:Svelte 的预编译是 “消除运行时” :
- 编译阶段直接将组件代码转换为 原生 DOM 操作代码(无需虚拟 DOM 或框架运行时核心)。
- 状态与 DOM 的关联通过编译时分析,生成精确的更新逻辑(例如,某个状态变化时,直接修改对应的 DOM 节点,而非重新计算整个虚拟 DOM 树)。
2. 对 “响应式” 的编译处理不同
-
Vue:Vue 的响应式依赖 运行时的 Proxy/Object.defineProperty:
- 预编译阶段仅处理模板与逻辑的关联(如将模板中的
{{ count }}绑定到组件实例的count属性)。 - 状态变化的追踪、依赖收集、更新触发均在运行时完成(通过响应式系统监听数据变化,再通知虚拟 DOM 重新渲染)。
- 预编译阶段仅处理模板与逻辑的关联(如将模板中的
-
Svelte:Svelte 的响应式是 编译时静态分析的结果:
- 编译阶段直接分析代码中哪些状态(如
let count = 0)被用于 DOM 渲染,以及在哪些地方被修改(如count += 1)。 - 生成专门的更新函数,当状态变化时,直接执行对应的 DOM 操作(例如,修改某个
<span>的textContent),无需运行时的依赖追踪机制。
- 编译阶段直接分析代码中哪些状态(如
3. 输出产物的差异
- Vue:编译后仍需要 Vue 运行时库(
vue.runtime.esm.js等)的支持,包含虚拟 DOM、响应式系统、组件生命周期等核心逻辑。产物体积 = 业务代码 + 框架运行时(约 30KB+,视版本和功能而定)。 - Svelte:编译后 几乎不依赖框架运行时(仅极少数辅助函数),产物是直接操作 DOM 的原生 JavaScript 代码。产物体积 = 业务代码 + 极小的编译残留(无虚拟 DOM、无响应式核心,体积通常更小,尤其对简单组件)。
4. 对 “组件” 的编译处理不同
- Vue:组件编译后会被转换为 包含
render函数的组件选项对象,运行时通过createComponent函数实例化,依赖组件树的层级渲染和生命周期管理。父子组件通信、插槽等功能依赖运行时的组件实例机制。 - Svelte:组件编译后被转换为 独立的函数,通过编译时分析直接处理组件间的数据流(如
export let props直接编译为参数传递逻辑)。插槽、事件等功能通过静态分析生成对应的 DOM 拼接逻辑,无需运行时的组件实例代理。
总结:核心差异对比
| 维度 | Vue 预编译 | Svelte 预编译 |
|---|---|---|
| 运行时依赖 | 依赖虚拟 DOM 和响应式运行时 | 几乎无运行时依赖,直接生成 DOM 操作 |
| 状态更新机制 | 运行时通过虚拟 DOM diff 触发更新 | 编译时生成精确的 DOM 更新逻辑 |
| 产物体积 | 包含框架运行时,体积较大 | 无冗余运行时,体积更小 |
| 核心理念 | “优化运行时”(通过预编译提升性能) | “消除运行时”(通过预编译替代运行时) |
简单来说,Vue 是 “编译时辅助,运行时主导”,而 Svelte 是 “编译时主导,运行时最小化”。这种差异导致 Svelte 在性能(尤其内存占用和更新效率)和体积上有优势,但 Vue 在灵活性(如动态模板)和生态成熟度上更胜一筹。
3.vue在编译时和运行时有什么不一样
1. 编译时
定义:编译时是一个 “预处理” 阶段,它的主要工作是将开发者编写的 模板(Template) 编译成 渲染函数(Render Function) 。这个阶段通常发生在 构建过程 中(例如使用 vue-loader 或 @vue/compiler-sfc)。
输入:模板字符串(例如来自 .vue 文件中的 <template> 标签或内联模板)。
输出:渲染函数(JavaScript 代码)。
编译时的具体工作(三步走):
-
解析
- 将模板字符串解析成一个 抽象语法树。
- 就像解析 HTML 标签一样,识别出哪些是元素、哪些是属性、哪些是文本、哪些是指令。
-
转换
- 对 AST 进行各种优化和转换。
- 最重要的优化:静态标记。Vue 的编译器会标记出哪些节点是静态的(永远不会改变的),哪些是动态的(可能随数据变化而改变)。这样在运行时,就可以直接复用静态节点,跳过对比过程,极大提升性能。
-
代码生成
- 将优化后的 AST 转换为字符串形式的 JavaScript 代码,这个代码就是 渲染函数。
- 渲染函数被执行时,会返回一个 虚拟 DOM 节点。
示例:
假设你有以下模板:
html
复制下载运行
<div id="app">{{ message }}</div>
经过编译后,它可能会变成这样的渲染函数(简化):
javascript
复制下载
function render() {
return h('div', { id: 'app' }, this.message);
}
小结:编译时是 "从模板到渲染函数" 的过程,它利用构建时的计算能力做优化,为运行时减轻负担。
2. 运行时
定义:运行时是 Vue 库的核心,它在 浏览器中执行。它的工作是使用编译时产生的渲染函数,根据应用程序的状态变化,动态地渲染和更新用户界面。
输入:渲染函数、响应式数据。
输出:真实 DOM。
运行时的具体工作:
-
执行渲染函数
- 当组件的响应式数据发生变化时,Vue 会触发渲染函数的重新执行。
- 渲染函数执行后,并不会直接操作 DOM,而是返回一个新的 虚拟 DOM 树。
-
Diff & Patch
- 虚拟 DOM Diff:Vue 会将新生成的虚拟 DOM 树与上一次的旧树进行对比(Diff 算法),精确找出发生变化的最小单位。
- Patch(打补丁) :根据 Diff 的结果,高效地更新真实 DOM,只修改那些需要改变的部分,而不是重新渲染整个页面。
小结:运行时是 "从渲染函数到真实视图" 的过程,它处理动态逻辑、响应数据变化并高效更新 UI。
两种构建版本:runtime-only 和 runtime-compiler
理解了编译时和运行时的区别,就能明白 Vue 为什么提供两种不同的构建版本。
| 特性 | runtime-only | runtime + compiler |
|---|---|---|
| 组成 | 只包含 运行时 | 包含 运行时 和 编译器 |
| 体积 | 更小(轻约 30%) | 更大 |
| 工作原理 | 只能使用渲染函数或由构建工具预编译的 .vue 单文件组件。 | 可以直接在浏览器中编译模板字符串,例如通过 template 选项。 |
| 使用方式 | 推荐用于生产环境。 | 通常用于需要动态编译模板的特殊场景,或初学者在学习时。 |
示例:
javascript
复制下载
// 需要 runtime + compiler 的写法
new Vue({
template: '<div>{{ message }}</div>' // 需要在客户端编译这个字符串
})
// 使用 runtime-only 的写法 (推荐)
new Vue({
render(h) {
return h('div', this.message)
}
})
// 或者更常见的是:使用 .vue 文件,由 vue-loader 在构建时完成编译
总结
| 维度 | 编译时 | 运行时 |
|---|---|---|
| 发生时机 | 构建时 | 浏览器中 |
| 输入 | 模板字符串 | 渲染函数、响应式数据 |
| 输出 | 渲染函数 | 真实 DOM |
| 核心工作 | 模板解析、静态优化、代码生成 | 执行渲染函数、虚拟 DOM Diff、Patch 更新 |
| 类比 | 厨师备菜:洗菜、切菜、准备好半成品。 | 厨师炒菜:根据客人的点单(数据变化),用准备好的食材(渲染函数)快速炒出菜肴。 |
最佳实践:在现代前端开发中,我们几乎总是使用 runtime-only 版本,并配合 Webpack、Vite 等构建工具,在项目构建阶段就完成所有模板的编译工作。这样做的好处是:
- 更小的体积:用户浏览器不需要下载编译器代码。
- 更好的性能:避免了在用户客户端进行编译的开销。
- 更好的开发体验:可以使用
.vue单文件组件。
4.js查找一段文字里面字数最多的
场景 1:统计单个字符(含标点、空格)
如果需要统计每个字符(包括字母、数字、标点、空格等)的出现次数,并找出次数最多的字符:
javascript
运行
function findMostFrequentChar(text) {
if (!text) return null; // 处理空字符串
// 1. 统计每个字符的出现次数
const charCount = {};
for (const char of text) {
charCount[char] = (charCount[char] || 0) + 1;
}
// 2. 找出次数最多的字符
let maxCount = 0;
let mostFrequent = [];
for (const [char, count] of Object.entries(charCount)) {
if (count > maxCount) {
maxCount = count;
mostFrequent = [char]; // 重置,记录当前最多的字符
} else if (count === maxCount) {
mostFrequent.push(char); // 若次数相同,一起记录
}
}
return {
chars: mostFrequent,
count: maxCount
};
}
// 示例用法
const text = "hello world! hello javascript";
const result = findMostFrequentChar(text);
console.log(`出现次数最多的字符:${result.chars},共出现 ${result.count} 次`);
// 输出:出现次数最多的字符:l,共出现 5 次(注:实际计算需看具体字符)
场景 2:统计词语(以空格分割,忽略标点)
如果需要按 “词语” 统计(忽略标点,以空格分割),可以先处理文本再统计:
javascript
运行
function findMostFrequentWord(text) {
if (!text) return null;
// 1. 预处理:移除标点,转为小写(统一大小写),按空格分割为词语
const words = text
.replace(/[^\w\s]/g, '') // 移除所有非字母、数字、空格的字符(标点)
.toLowerCase() // 转为小写,避免大小写差异(如 "Hello" 和 "hello" 视为同一词)
.split(/\s+/) // 按空格分割(支持多个空格)
.filter(word => word); // 过滤空字符串(避免连续空格导致的空元素)
// 2. 统计每个词语的出现次数
const wordCount = {};
for (const word of words) {
wordCount[word] = (wordCount[word] || 0) + 1;
}
// 3. 找出次数最多的词语
let maxCount = 0;
let mostFrequent = [];
for (const [word, count] of Object.entries(wordCount)) {
if (count > maxCount) {
maxCount = count;
mostFrequent = [word];
} else if (count === maxCount) {
mostFrequent.push(word);
}
}
return {
words: mostFrequent,
count: maxCount
};
}
// 示例用法
const text = "Hello world! Hello javascript. World is great, hello again!";
const result = findMostFrequentWord(text);
console.log(`出现次数最多的词语:${result.words},共出现 ${result.count} 次`);
// 输出:出现次数最多的词语:hello,共出现 3 次
核心逻辑说明:
- 统计阶段:用对象(
{ key: 次数 })记录每个字符 / 词语的出现次数。 - 查找阶段:遍历统计结果,跟踪最大次数及对应的字符 / 词语(支持多个并列最多的情况)。
- 预处理(词语场景) :通过正则移除标点、统一大小写,确保统计准确性。
根据需求选择对应的场景即可,如需更复杂的分词规则(如中文分词),可引入专门的分词库(如 nodejieba)。
5.如何做登录,前端如何判断登录过期
登录功能的实现涉及前端与后端的协作,而登录过期判断则是保障系统安全的重要环节。以下是完整的实现思路和技术方案:
一、登录功能的基本实现(前端 + 后端协作)
登录的核心是身份验证,通常流程如下:
1. 前端发起登录请求
用户输入账号密码后,前端通过 HTTP 请求将凭证发送给后端:
javascript
运行
// 登录请求示例(使用 fetch 或 axios)
async function login(username, password) {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
// 登录成功:保存后端返回的身份凭证(如 Token)
const { token, expiresIn } = data;
// 存储 Token(通常用 localStorage 或 sessionStorage,或 httpOnly Cookie)
localStorage.setItem('token', token);
// 可选:记录过期时间(用于前端主动判断)
const expireTime = Date.now() + expiresIn * 1000; // expiresIn 单位通常是秒
localStorage.setItem('tokenExpireTime', expireTime);
return true;
} else {
// 登录失败(如账号密码错误)
alert(data.message || '登录失败');
return false;
}
} catch (error) {
console.error('登录请求失败', error);
return false;
}
}
2. 后端验证与返回凭证
后端验证账号密码后,生成身份凭证(如 JWT Token)并返回给前端,同时包含凭证的过期时间。
-
凭证类型:
- JWT Token:无状态,前端存储在
localStorage/sessionStorage,每次请求通过Authorization头携带。 - Session + Cookie:后端维护 Session,前端通过 Cookie 自动携带(推荐用
httpOnly + secureCookie,更安全)。
- JWT Token:无状态,前端存储在
3. 前端携带凭证发起后续请求
登录后,前端需在后续请求中携带凭证,证明身份:
javascript
运行
// 请求拦截器(以 axios 为例)
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`; // JWT 通常用 Bearer 前缀
}
return config;
});
二、前端判断登录过期的两种方式
登录过期的本质是 “身份凭证失效”,前端判断方式分为主动检查和被动接收:
方式 1:被动接收后端的过期提示(推荐)
后端在验证凭证时,若发现已过期,会返回特定的 HTTP 状态码(如 401 Unauthorized 或 403 Forbidden),前端通过拦截器统一处理:
javascript
运行
// 响应拦截器(处理过期)
axios.interceptors.response.use(
response => response,
error => {
const { response } = error;
if (response && response.status === 401) {
// 401 通常表示 Token 过期或无效
handleLoginExpire(); // 处理登录过期逻辑
}
return Promise.reject(error);
}
);
// 登录过期处理函数
function handleLoginExpire() {
// 1. 清除本地存储的过期凭证
localStorage.removeItem('token');
localStorage.removeItem('tokenExpireTime');
// 2. 提示用户并跳转登录页
alert('登录已过期,请重新登录');
window.location.href = '/login'; // 跳转到登录页
}
优势:完全依赖后端的权威判断,避免前端时间与后端不同步导致的误差(如用户修改本地时间)。适用场景:所有需要验证身份的请求(如接口调用)。
方式 2:前端主动检查凭证过期时间(辅助)
前端可记录凭证的过期时间,在发起请求前或页面跳转时主动检查:
javascript
运行
// 检查登录是否过期
function isLoginExpired() {
const expireTime = localStorage.getItem('tokenExpireTime');
if (!expireTime) return true; // 无过期时间,视为已过期
return Date.now() > Number(expireTime); // 当前时间 > 过期时间 → 已过期
}
// 路由跳转前检查(以 Vue Router 为例)
router.beforeEach((to, from, next) => {
// 非登录页且已过期 → 跳登录页
if (to.path !== '/login' && isLoginExpired()) {
next('/login');
} else {
next();
}
});
// 发起请求前检查(在请求拦截器中)
axios.interceptors.request.use(config => {
if (isLoginExpired()) {
handleLoginExpire();
return Promise.reject(new Error('登录已过期'));
}
// 携带 Token 逻辑...
return config;
});
注意:
- 需后端返回
expiresIn(过期秒数),前端计算过期时间 = 当前时间 + expiresIn * 1000并存储。 - 仅作为辅助手段,不能替代后端判断(因用户可篡改本地存储的过期时间)。
三、进阶:处理 Token 自动刷新(避免频繁登录)
为提升用户体验,可实现 “Token 即将过期时自动刷新”:
- 后端返回两个 Token:
accessToken(短期,如 2 小时)和refreshToken(长期,如 7 天)。 - 前端检测到
accessToken即将过期(如剩余 10 分钟),用refreshToken调用后端刷新接口,获取新的accessToken。
javascript
运行
// 检查是否需要刷新 Token(剩余时间 < 10 分钟)
function needRefreshToken() {
const expireTime = localStorage.getItem('tokenExpireTime');
if (!expireTime) return false;
const remainingTime = Number(expireTime) - Date.now();
return remainingTime > 0 && remainingTime < 10 * 60 * 1000; // 剩余时间 < 10 分钟
}
// 请求拦截器中添加自动刷新逻辑
axios.interceptors.request.use(async config => {
if (needRefreshToken()) {
// 调用刷新 Token 接口
const refreshToken = localStorage.getItem('refreshToken');
const { data } = await axios.post('/api/refresh-token', { refreshToken });
// 更新本地 Token 和过期时间
localStorage.setItem('token', data.newAccessToken);
localStorage.setItem('tokenExpireTime', Date.now() + data.newExpiresIn * 1000);
}
// 携带新 Token...
return config;
});
总结
-
登录实现:前端发送凭证 → 后端验证并返回 Token → 前端存储 Token 并在后续请求中携带。
-
过期判断:
- 核心:依赖后端返回的
401状态码(被动处理)。 - 辅助:前端记录过期时间,主动检查(如路由跳转前)。
- 核心:依赖后端返回的
-
体验优化:通过
refreshToken实现 Token 自动刷新,减少用户重复登录。
这种方案兼顾安全性和用户体验,是前后端分离架构中最常用的登录与过期处理模式。