越金

52 阅读19分钟

1.浏览器和node的事件循环有什么不一样

1. 任务队列的结构与执行顺序

浏览器的事件循环是基于 HTML5 规范,而 Node.js 的事件循环是基于 libuv 库的实现。虽然它们的基本思想一致——都是“在单线程中通过事件循环和任务队列来处理异步任务”——但由于它们所处的环境和要解决的问题不同,导致在细节上有显著差异。

Node.js:
  • 宏任务队列(阶段划分) :Node.js 的事件循环分为 6 个阶段(按顺序执行),每个阶段对应一个宏任务队列:

    1. timers:执行 setTimeoutsetInterval 回调(到时间的任务)。
    2. pending callbacks:执行延迟到下一轮的 I/O 回调(如网络错误回调)。
    3. idle, prepare:内部使用(可忽略)。
    4. poll:等待新的 I/O 事件(如文件读写、网络请求),执行相关回调(核心阶段)。
    5. check:执行 setImmediate 回调。
    6. close callbacks:执行关闭回调(如 socket.on('close', ...))。
  • 微任务:包括 Promise.then/catch/finallyqueueMicrotaskprocess.nextTick(特殊,优先级最高)。

  • 执行顺序

    1. 执行一个阶段的所有宏任务(当前阶段队列清空或执行到上限)。
    2. 执行所有微任务(先执行 process.nextTick 队列,再执行其他微任务)。
    3. 进入下一个阶段,重复上述步骤。

    关键:每个阶段的宏任务执行完毕后,才会清空微任务队列,而非单个宏任务后立即执行。

浏览器:

  • 宏任务队列:在早期版本中,可以认为有一个宏任务队列。但实际上,为了优化性能(如保证用户交互的及时响应),浏览器可能会有多个不同优先级的宏任务队列(如网络请求、DOM事件、定时器等)。

    • 常见的宏任务:<script>整体代码、setTimeoutsetIntervalI/O、UI 渲染、requestAnimationFrame、用户交互事件(如点击)。
  • 微任务队列:只有一个微任务队列。

    • 常见的微任务:Promise.then/catch/finallyqueueMicrotaskMutationObserver

执行顺序(浏览器):

  1. 执行一个宏任务(通常是从<script>开始)。
  2. 执行过程中遇到微任务,就将其放入微任务队列。
  3. 当前宏任务执行完毕后,立即清空整个微任务队列
  4. 进行 UI 渲染(如果需要)。
  5. 从事件队列中取下一个宏任务,开始新一轮循环。

Node.js:

  • 阶段性的循环模型:Node.js 的事件循环分为多个阶段,每个阶段都有一个 FIFO(先进先出)的回调队列。这些阶段按顺序执行,如下图所示,执行完一个阶段的所有任务后,才会进入下一个阶段。

    • 定时器阶段:执行 setTimeout 和 setInterval 的回调。

    • 待定回调阶段:执行一些系统操作的回调,例如 TCP 的错误。

    • 闲置阶段/准备阶段:仅在内部使用。

    • 轮询阶段

      • 计算应该阻塞和轮询 I/O 的时间。
      • 执行 几乎所有的 I/O 回调(如文件读取、网络请求)。如果轮询队列不为空,则迭代执行队列中的回调,直到队列为空或达到系统限制。
      • 如果轮询队列为空,会检查是否有 setImmediate 回调,如果有则结束轮询阶段,进入检查阶段。
    • 检查阶段:执行 setImmediate 设置的回调。

    • 关闭回调阶段:执行关闭事件的回调,如 socket.on('close', ...)

  • 微任务队列:在 Node.js 中,微任务不属于事件循环的任何一个阶段,而是在每个阶段结束后、进入下一个阶段前执行。

    • 常见的微任务:Promise.then/catch/finallyqueueMicrotaskprocess.nextTick
    • 特别注意process.nextTick 拥有一个独立的队列,它的优先级高于其他所有微任务。在一个阶段切换后,会先清空 nextTick 队列,再清空其他微任务队列。

执行顺序(Node.js):

  1. 执行一个阶段(如定时器阶段)的所有宏任务。
  2. 该阶段执行完毕后,立即清空 process.nextTick 队列(如果存在)。
  3. 然后清空其他微任务队列(如 Promise)。
  4. 进入事件循环的下一个阶段。

2. 微任务的优先级
  • 浏览器:所有微任务(PromisequeueMicrotaskMutationObserver)在同一队列中,按添加顺序执行,优先级相同。
  • Node.jsprocess.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 优先级最高
特有 APIrequestAnimationFrameMutationObserversetImmediateprocess.nextTick
setImmediate不支持支持,在 Check 阶段执行
process.nextTick不支持支持,优先级最高的微任务

实践建议

  1. 避免递归调用 process.nextTick:因为这会导致 I/O 饥饿(I/O Starvation),因为事件循环一直卡在清空 nextTick 队列,无法进入轮询阶段执行 I/O 任务。
  2. 在 Node.js 中,推荐使用 setImmediate 来延迟执行,而不是 setTimeout(fn, 0),因为前者效率更高,并且在 I/O 周期内顺序可预测。
  3. 理解差异是关键:在编写同构代码或需要精确控制异步流程时,必须清楚你的代码是运行在浏览器还是 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在编译时和运行时有什么不一样

image.png

1. 编译时

定义:编译时是一个  “预处理”  阶段,它的主要工作是将开发者编写的 模板(Template)  编译成 渲染函数(Render Function) 。这个阶段通常发生在 构建过程 中(例如使用 vue-loader 或 @vue/compiler-sfc)。

输入:模板字符串(例如来自 .vue 文件中的 <template> 标签或内联模板)。
输出:渲染函数(JavaScript 代码)。

编译时的具体工作(三步走):

  1. 解析

    • 将模板字符串解析成一个 抽象语法树
    • 就像解析 HTML 标签一样,识别出哪些是元素、哪些是属性、哪些是文本、哪些是指令。
  2. 转换

    • 对 AST 进行各种优化和转换。
    • 最重要的优化静态标记。Vue 的编译器会标记出哪些节点是静态的(永远不会改变的),哪些是动态的(可能随数据变化而改变)。这样在运行时,就可以直接复用静态节点,跳过对比过程,极大提升性能。
  3. 代码生成

    • 将优化后的 AST 转换为字符串形式的 JavaScript 代码,这个代码就是 渲染函数
    • 渲染函数被执行时,会返回一个 虚拟 DOM 节点

示例:

假设你有以下模板:

html

复制下载运行

<div id="app">{{ message }}</div>

经过编译后,它可能会变成这样的渲染函数(简化):

javascript

复制下载

function render() {
  return h('div', { id: 'app' }, this.message);
}

小结:编译时是  "从模板到渲染函数"  的过程,它利用构建时的计算能力做优化,为运行时减轻负担。


2. 运行时

定义:运行时是 Vue 库的核心,它在 浏览器中执行。它的工作是使用编译时产生的渲染函数,根据应用程序的状态变化,动态地渲染和更新用户界面。

输入:渲染函数、响应式数据。
输出:真实 DOM。

运行时的具体工作:

  1. 执行渲染函数

    • 当组件的响应式数据发生变化时,Vue 会触发渲染函数的重新执行。
    • 渲染函数执行后,并不会直接操作 DOM,而是返回一个新的 虚拟 DOM 树
  2. Diff & Patch

    • 虚拟 DOM Diff:Vue 会将新生成的虚拟 DOM 树与上一次的旧树进行对比(Diff 算法),精确找出发生变化的最小单位。
    • Patch(打补丁) :根据 Diff 的结果,高效地更新真实 DOM,只修改那些需要改变的部分,而不是重新渲染整个页面。

小结:运行时是  "从渲染函数到真实视图"  的过程,它处理动态逻辑、响应数据变化并高效更新 UI。


两种构建版本:runtime-only 和 runtime-compiler

理解了编译时和运行时的区别,就能明白 Vue 为什么提供两种不同的构建版本。

特性runtime-onlyruntime + 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 等构建工具,在项目构建阶段就完成所有模板的编译工作。这样做的好处是:

  1. 更小的体积:用户浏览器不需要下载编译器代码。
  2. 更好的性能:避免了在用户客户端进行编译的开销。
  3. 更好的开发体验:可以使用 .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 次

核心逻辑说明:

  1. 统计阶段:用对象({ key: 次数 })记录每个字符 / 词语的出现次数。
  2. 查找阶段:遍历统计结果,跟踪最大次数及对应的字符 / 词语(支持多个并列最多的情况)。
  3. 预处理(词语场景) :通过正则移除标点、统一大小写,确保统计准确性。

根据需求选择对应的场景即可,如需更复杂的分词规则(如中文分词),可引入专门的分词库(如 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 + secure Cookie,更安全)。
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 即将过期时自动刷新”:

  1. 后端返回两个 Token:accessToken(短期,如 2 小时)和 refreshToken(长期,如 7 天)。
  2. 前端检测到 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;
});

总结

  1. 登录实现:前端发送凭证 → 后端验证并返回 Token → 前端存储 Token 并在后续请求中携带。

  2. 过期判断

    • 核心:依赖后端返回的 401 状态码(被动处理)。
    • 辅助:前端记录过期时间,主动检查(如路由跳转前)。
  3. 体验优化:通过 refreshToken 实现 Token 自动刷新,减少用户重复登录。

这种方案兼顾安全性和用户体验,是前后端分离架构中最常用的登录与过期处理模式。