普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月3日掘金 前端

VS Code 终端崩溃问题分析与解决方案

作者 凯哥1970
2026年1月3日 10:35

VS Code 终端崩溃问题分析与解决方案

错误代码:-2147023895 (0x800703E9)

显示如下

终端进程“C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe”已终止,退出代码: -2147023895。

问题描述

在 VS Code 中打开终端时,PowerShell 进程异常退出,返回错误代码 -2147023895。该错误会导致终端无法正常启动或使用,影响开发效率。


错误原因分析

错误代码 -2147023895 对应十六进制值 0x800703E9,是一个标准的 HRESULT 错误码,其结构解析如下:

  • 严重性位(Bit 31) :1,表示失败。
  • 设备代码(Bits 16-26) :7(FACILITY_WIN32),表示错误源自 Windows API 调用。
  • 低位代码(Bits 0-15)0x03E9(十进制 1001)。
可能的原因:
  1. 栈溢出(Stack Overflow)
    PowerShell 启动时脚本陷入无限递归,耗尽线程栈空间,触发系统异常。
  2. 文件完整性校验失败(Invalid Image Hash)
    Windows 代码完整性机制或安全软件(如 AppLocker)检测到脚本文件签名无效、文件损坏或哈希不匹配,导致加载被拒绝。
  3. 环境变量冲突
    脚本执行过程中展开的环境变量(如 PATH)过长,引发内存或栈溢出。

根本原因定位

多数情况下,该错误与 VS Code 终端 Shell 集成脚本 shellIntegration.ps1 有关。该脚本在终端启动时被自动加载,若文件损坏或与用户配置冲突,即会触发上述错误。


解决方案:手动替换脚本文件(治本)

无需禁用终端功能,直接替换损坏的脚本文件即可根治问题。

操作步骤:
  1. 定位脚本文件
    根据 VS Code 安装方式,找到目标目录:

    • 用户安装
      %LOCALAPPDATA%\Programs<EditorName>\resources\app\out\vs\workbench\contrib\terminal\common\scripts
    • 系统安装
      C:\Program Files\Microsoft VS Code\resources\app\out\vs\workbench\contrib\terminal\common\scripts
  2. 备份原文件
    将目录中的 shellIntegration.ps1 重命名为 shellIntegration.ps1.bak,作为备份。

  3. 下载官方脚本
    访问 VS Code 官方 GitHub 仓库,下载最新版本的脚本文件:
    raw.githubusercontent.com/microsoft/v…

  4. 替换文件
    将下载的 shellIntegration.ps1 复制到步骤 1 的目录中,确保当前用户有读取权限。

  5. 重启验证
    完全关闭 VS Code(包括后台进程),重新启动并打开终端,检查是否恢复正常。


方案原理

通过替换为官方完好的脚本文件,确保:

  • PowerShell 解析器能正常解析语法,避免因文件损坏导致的崩溃。
  • 脚本与用户环境兼容,避免递归冲突或安全校验失败。
  • 保留完整的终端 Shell 集成功能(如命令装饰、状态提示等)。

注意事项

  • 若问题仍然存在,可检查用户 PowerShell 配置文件($PROFILE)中是否存在与 Shell 集成冲突的自定义代码。
  • 建议定期更新 VS Code,以获取官方修复的脚本版本。

通过以上步骤,可从根本上解决终端崩溃问题,无需临时禁用功能或修改启动命令,确保开发环境稳定可用。

使用 json-server 快速创建一个完整的 REST API

2026年1月3日 01:12

json-server 使用教程

什么是 json-server

json-server 是一个基于 Node.js 的工具,可以快速创建一个完整的 REST API,使用 JSON 文件作为数据源。非常适合前端开发、原型设计和测试。

安装

全局安装

npm install -g json-server

本地安装

npm install json-server --save-dev

使用 npx(推荐)

npx json-server

快速开始

1. 创建数据文件

创建一个 db.json 文件:

{
  "posts": [
    {
      "id": 1,
      "title": "第一篇文章",
      "author": "张三"
    },
    {
      "id": 2,
      "title": "第二篇文章",
      "author": "李四"
    }
  ],
  "comments": [
    {
      "id": 1,
      "body": "很好的文章",
      "postId": 1
    }
  ],
  "profile": {
    "name": "typicode"
  }
}

2. 启动服务器

# 默认端口 3000
json-server db.json

# 指定端口
json-server db.json --port 4000

# 指定主机
json-server db.json --host 0.0.0.0

# 静默模式
json-server db.json --quiet

# 监视文件变化
json-server db.json --watch

3. 访问 API

启动后,服务器会提供以下端点:

# 资源端点
GET    /posts
GET    /posts/1
POST   /posts
PUT    /posts/1
PATCH  /posts/1
DELETE /posts/1

# 其他资源
GET    /comments
GET    /profile

REST API 操作

GET - 获取数据

# 获取所有文章
curl http://localhost:3000/posts

# 获取指定 ID 的文章
curl http://localhost:3000/posts/1

# 获取个人资料
curl http://localhost:3000/profile

POST - 创建数据

# 创建新文章
curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "新文章",
    "author": "王五"
  }'

# 使用 JavaScript fetch
fetch('http://localhost:3000/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: '新文章',
    author: '王五'
  })
})

PUT - 完整更新

# 完整更新文章(必须包含所有字段)
curl -X PUT http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{
    "id": 1,
    "title": "更新后的标题",
    "author": "张三"
  }'

PATCH - 部分更新

# 部分更新文章(只更新指定字段)
curl -X PATCH http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "只更新标题"
  }'

DELETE - 删除数据

# 删除文章
curl -X DELETE http://localhost:3000/posts/1

高级查询

过滤

# 按字段过滤
GET /posts?author=张三

# 多条件过滤
GET /posts?author=张三&id=1

# 嵌套属性过滤
GET /comments?postId=1

分页

# 分页查询
GET /posts?_page=1&_limit=10

# 获取总数(在响应头 X-Total-Count 中)

排序

# 升序排序
GET /posts?_sort=id&_order=asc

# 降序排序
GET /posts?_sort=id&_order=desc

# 多字段排序
GET /posts?_sort=author,id&_order=asc,desc

切片

# 范围查询
GET /posts?_start=0&_end=10

# 限制数量
GET /posts?_limit=5

# 跳过数量
GET /posts?_start=5

操作符

# 大于
GET /posts?id_gte=1

# 小于
GET /posts?id_lte=10

# 不等于
GET /posts?id_ne=1

# 包含
GET /posts?q=关键词

# 正则表达式
GET /posts?id_like=1

全文搜索

# 搜索包含关键词的记录
GET /posts?q=文章

# 搜索会在所有字段中查找

关联查询

# 获取文章及其评论
GET /posts/1?_embed=comments

# 获取评论及其所属文章
GET /comments?_expand=post

# 嵌套多个关系
GET /posts/1?_embed=comments&_embed=likes

配置选项

命令行选项

# 完整命令示例
json-server db.json \
  --port 4000 \
  --host 0.0.0.0 \
  --watch \
  --routes routes.json \
  --middlewares middleware.js \
  --static ./public \
  --read-only \
  --no-cors

选项说明

选项 说明
--port 指定端口号(默认 3000)
--host 指定主机地址(默认 localhost)
--watch 监视文件变化
--routes 自定义路由文件
--middlewares 中间件文件
--static 静态文件目录
--read-only 只读模式(禁用 POST/PUT/DELETE)
--no-cors 禁用 CORS
--quiet 静默模式

自定义路由

创建路由文件

创建 routes.json

{
  "/api/*": "/$1",
  "/:resource/:id/show": "/:resource/:id",
  "/posts/:category": "/posts?category=:category",
  "/articles\\?id=:id": "/posts/:id"
}

使用自定义路由

json-server db.json --routes routes.json

路由示例

# 原始路由
GET /posts/1

# 自定义路由
GET /api/posts/1        # 映射到 /posts/1
GET /posts/1/show       # 映射到 /posts/1
GET /posts/tech         # 映射到 /posts?category=tech

中间件

创建中间件

创建 middleware.js

module.exports = (req, res, next) => {
  // 添加响应头
  res.header('X-Custom-Header', 'Custom Value')
  
  // 记录请求
  console.log(`${req.method} ${req.url}`)
  
  // 模拟延迟
  if (req.url.includes('delay')) {
    setTimeout(next, 2000)
  } else {
    next()
  }
}

使用中间件

json-server db.json --middlewares middleware.js

高级中间件示例

module.exports = (req, res, next) => {
  // 身份验证模拟
  if (req.method === 'POST' && !req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  
  // 请求日志
  const timestamp = new Date().toISOString()
  console.log(`[${timestamp}] ${req.method} ${req.url}`)
  
  // 修改响应
  const originalSend = res.send
  res.send = function (data) {
    const modifiedData = JSON.parse(data)
    modifiedData.timestamp = timestamp
    originalSend.call(this, JSON.stringify(modifiedData))
  }
  
  next()
}

数据生成

使用 Faker.js 生成测试数据

const faker = require('faker')
const fs = require('fs')

const generateData = () => {
  const users = []
  for (let i = 1; i <= 100; i++) {
    users.push({
      id: i,
      name: faker.name.findName(),
      email: faker.internet.email(),
      phone: faker.phone.phoneNumber(),
      address: {
        street: faker.address.streetAddress(),
        city: faker.address.city(),
        country: faker.address.country()
      },
      company: faker.company.companyName()
    })
  }
  
  const db = { users }
  fs.writeFileSync('db.json', JSON.stringify(db, null, 2))
}

generateData()

运行生成器

node generate-data.js
json-server db.json

与前端框架集成

React 示例

import axios from 'axios'

const API_URL = 'http://localhost:3000'

// 获取所有文章
export const getPosts = async () => {
  const response = await axios.get(`${API_URL}/posts`)
  return response.data
}

// 创建文章
export const createPost = async (post) => {
  const response = await axios.post(`${API_URL}/posts`, post)
  return response.data
}

// 更新文章
export const updatePost = async (id, post) => {
  const response = await axios.put(`${API_URL}/posts/${id}`, post)
  return response.data
}

// 删除文章
export const deletePost = async (id) => {
  const response = await axios.delete(`${API_URL}/posts/${id}`)
  return response.data
}

Vue 示例

import axios from 'axios'

const api = axios.create({
  baseURL: 'http://localhost:3000'
})

export default {
  async getPosts() {
    const response = await api.get('/posts')
    return response.data
  },
  
  async createPost(post) {
    const response = await api.post('/posts', post)
    return response.data
  },
  
  async updatePost(id, post) {
    const response = await api.put(`/posts/${id}`, post)
    return response.data
  },
  
  async deletePost(id) {
    const response = await api.delete(`/posts/${id}`)
    return response.data
  }
}

package.json 配置

添加脚本

{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "json-server": "json-server --watch db.json --port 4000",
    "dev": "npm run json-server"
  },
  "devDependencies": {
    "json-server": "^0.17.3"
  }
}

运行脚本

npm run dev

常见问题

1. 端口被占用

# 使用其他端口
json-server db.json --port 4000

2. CORS 问题

json-server 默认启用 CORS,如果遇到问题:

# 确保没有禁用 CORS
json-server db.json

3. 数据持久化

json-server 会将修改保存到 db.json 文件中,确保文件有写入权限。

4. 重置数据

# 删除 db.json 或恢复备份
rm db.json
cp db.json.backup db.json

最佳实践

1. 版本控制

# 添加到 .gitignore
echo "db.json" >> .gitignore

# 创建示例数据文件
cp db.json db.example.json

2. 环境配置

// config.js
module.exports = {
  port: process.env.JSON_SERVER_PORT || 3000,
  host: process.env.JSON_SERVER_HOST || 'localhost',
  watch: process.env.NODE_ENV !== 'production'
}

3. 数据验证

// middleware.js
module.exports = (req, res, next) => {
  if (req.method === 'POST' || req.method === 'PUT') {
    const body = req.body
    
    // 验证必填字段
    if (!body.title) {
      return res.status(400).json({ error: 'Title is required' })
    }
  }
  next()
}

4. 性能优化

# 使用静态文件服务
json-server db.json --static ./public

# 禁用日志(生产环境)
json-server db.json --quiet

总结

json-server 是一个强大且易于使用的工具,适合:

  • ✅ 前端开发和原型设计
  • ✅ API 测试和调试
  • ✅ 快速搭建演示项目
  • ✅ 学习 REST API 概念

通过本教程,你应该能够:

  • 安装和启动 json-server
  • 执行 CRUD 操作
  • 使用高级查询功能
  • 自定义路由和中间件
  • 与前端框架集成

开始使用 json-server,快速构建你的 REST API 吧!

Vue 原生渲染真要来了?Lynx引擎首次跑通Vue

作者 小小荧
2026年1月2日 23:44

Vue 原生渲染真要来了?Lynx引擎首次跑通Vue

“这一次,Vue 终于能在移动端跑出原生性能了。”

最新动态:一位前端工程师在48小时内,成功将Vue 3的响应式系统与字节跳动的Lynx.js引擎对接,实现了首个Vue自定义渲染器原型。这标志着近200万Vue开发者有望直接使用熟悉的ref<SFC>等语法,驱动iOS/Android的原生控件,告别WebView的性能束缚。

一、突破性进展:Vue 已在 Lynx 上跑通

近日,前端工程师 @Shenqingchuan 在社交平台展示了他的成果:一个在Lynx引擎上运行的Vue 3计数器Demo。

这项原型验证了技术可行性,他也公开邀请对 “Vue Lynx” 感兴趣且熟悉Vue核心代码(尤其是runtime-core)的开发者加入共建。

二、什么是 Lynx.js?

Lynx 是字节跳动于今年3月开源的一款高性能双线程原生渲染框架,其核心架构优势在于:

  • UI线程:使用自研PrimJS配合基于Rust的Rspack(Rspeedy),实现毫秒级首帧直出。
  • 后台线程:独立运行业务逻辑、网络请求等,避免复杂计算阻塞界面。
  • 原生渲染:直接调用平台原生控件,其渲染性能与Flutter属于同一梯队。
  • 实战验证:已广泛应用于TikTok搜索、直播等亿级月活业务场景。

Lynx框架本身保持中立,其团队曾公开表示欢迎Vue等框架接入,这为此次“Vue-Lynx”的原型诞生提供了土壤。

三、官方与社区的积极信号

此次尝试迅速获得了来自双方核心人物的关注:

  • Lynx架构师 @Huxpro 转发并帮助招募合作者。
  • Vue作者 @youyuxi 的转发,相当于给予了项目“官方默许”的认证。

此外,在最近的React Advanced大会上,@Huxpro预告了lynx-ui组件库将于12月开源,这将为上层框架提供丰富的原生UI物料,进一步夯实生态基础。

四、核心优势:为什么这次可能成了?

相比历史上的类似尝试(如Weex),此次“Vue + Lynx”的组合在多个层面具备了更坚实的基础:

维度 Vue + Lynx 方案 传统方案的典型痛点
渲染性能 双线程原生控件,无WebView层级 WebView易掉帧、卡顿
开发体验 完整Vue 3组合式API,对接现代构建工具(Vite/Rspeedy) 需学习新语法,或构建速度慢
调试支持 拥有Lynx DevTool,支持真机断点调试 调试依赖日志,体验差
技术验证 底层引擎已在10亿+DAU产品中验证 多为实验室级原型,缺乏大规模验证

五、代码一瞥:Vue Lynx 初体验

一个简单的Vue组件在Lynx环境下可能这样编写:

<!-- HelloLynx.vue -->
<script setup>
import logo from './assets/lynx-logo.png'
import { ref } from 'vue'

const count = ref(0)
setInterval(() => count.value++, 1800)
</script>

<template>
  <view class="container">
    <image :src="logo" class="logo" />
    <text class="h1">Hello Vue-Lynx</text>
    <text class="p">双线程原生渲染,首帧直出!</text>
    <button class="btn" @click="count++">点我:{{ count }}</button>
  </view>
</template>

其中的 <view><text><image> 等标签将被编译并映射为平台原生组件,而开发者使用的仍然是百分之百标准的Vue语法。

六、技术实现路径展望

要实现生产可用的“Vue Lynx”,还需攻克几个关键节点:

  1. 编译链路适配:需要开发新的插件(如vue-loader-rs),将Vue SFC编译为Lynx双线程可识别的代码包,并严格区分UI线程与后台线程的职责。
  2. 定制运行时:在Vue核心库中新增一个vue/runtime-lynx包,实现与PrimJS API对接的节点操作、调度器和事件系统。
  3. 线程边界管理:可能通过扩展SFC语法(如引入<script main>标签),或在编译时进行静态分析,来明确代码的运行线程,确保开发者既能畅快编码又不违反架构约束。

七、Vue Native 生态路线图

目前,让Vue开发移动原生应用的方案并非唯一,开发者可根据需求选择:

路线 渲染方式 性能 开发体验 适用场景
NativeScript-Vue 3 原生控件 ★★★★ Vite + Tailwind,成熟 追求100%原生UI,无需WebView
Ionic Vue + Capacitor WebView ★★★ 最接近Web开发,PWA友好 一套代码覆盖Web/App,重开发效率
uni-app / uni-appx WebView → 原生渲染 ★★★☆ 中文生态完善,工具链强 需同时发布国内多端(小程序+App)
Vue + Lynx 双线程原生 ★★★★☆ 早期,需配置,潜力大 追求极致性能,愿参与生态共建

简单决策参考

  • “我现在就要用” → 选择 NativeScript-Vue 或 uni-app。
  • “我要最像Web的开发体验” → 选择 Ionic Vue。
  • “我看重未来性能和前沿技术” → 密切关注并尝试参与 Vue + Lynx

八、结语:这一次,不再缺席?

Vue社区对“原生渲染”的期待由来已久。如今,多条技术路径正在并行发展:

  • NativeScript-Vue 3 已趋成熟。
  • uni-appx 持续拓展多端能力。
  • 而最具颠覆性的 Vue + Lynx 路径,正以开源共建的模式吸引开发者。

或许在不久的将来,我们只需一条命令:

npm create vue-native@latest

便可从多个生产就绪的Vue原生渲染模板中任选其一。

Weex时代的遗憾,或许真的能在2025年被彻底填补。Vue Native,这一次可能真的要启动了。

保持关注

  • Lynx 项目:github.com/lynx-family/lynx
  • 生态动态:可关注 @Huxpro 等核心开发者的最新消息。

Vue3 应用实例创建及页面渲染底层原理

2026年1月2日 23:13

整体流程

完整的创建与渲染流程可以分成这些阶段:

  1. 创建 App 实例
  2. 创建根组件实例
  3. 设置响应式状态
  4. 创建渲染器(Renderer)
  5. 挂载 Mount
  6. vnode -> DOM 渲染
  7. 数据变更触发更新
  8. 重新渲染 / diff / patch

流程图大致如下:

createApp() ───> app.mount('#app')
         │                 │
         ▼                 ▼
   createRootComponent    createRenderer
         │                 │
         ▼                 ▼
 setup() / render()   render(vnode) -> patch
         │                 │
         ▼                 ▼
   effect(fn) ────> scheduler -> patch updates

1、createApp 初始化

Vue 应用的入口通常是:

createApp(App).mount('#app')

从源码看 createApp:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _props: rootProps,
      _container: null,
      _context: createAppContext()
    }
    const proxy = (app._instance = {
      app
    })
    // register global APIs
    // ...
    return {
      mount(container) {
        const vnode = createVNode(rootComponent, rootProps)
        app._container = container
        render(vnode, container)
      },
      unmount() { /* ... */ }
    }
  }
}

关键点:

  • createAppAPI(render) 生成 createApp 函数
  • app 内保存 _component、上下文 _context
  • app.mount 调用 render(vnode, container)

render平台渲染器 注入(在 web 下是 DOM 渲染器)。

2、createVNode 创建虚拟节点(VNode)

在 mount 前会创建一个虚拟节点:

function createVNode(type, props, children) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
    key: props && props.key
  }
  return vnode
}

vnode 是渲染的基础单元:

shapeFlag 用来快速判断 vnode 类型,是内部性能优化。

3、渲染器 Renderer 初始化

Vue3 是平台无关的(runtime-core),真正依赖 DOM 的是在 runtime-dom 中。

创建 Renderer:

export const renderer = createRenderer({
  createElement: hostCreateElement,
  patchProp: hostPatchProp,
  insert: hostInsert,
  remove: hostRemove,
  setElementText: hostSetElementText
})

createRenderer 返回了我们前面在 createApp 中使用的 render(vnode, container) 函数。

4、render & patch

核心渲染入口:

function render(vnode, container) {
  patch(null, vnode, container, null, null)
}

patch 是渲染补丁函数:

function patch(n1, n2, container, parentComponent, anchor) {
  const { type, shapeFlag } = n2
  if (shapeFlag & ShapeFlags.ELEMENT) {
    processElement()
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    processComponent(...)
  }
}

简化为:

  • 如果是 DOM 元素 vnode → 挂载/更新
  • 如果是 组件 vnode → 创建组件实例、挂载、渲染子树

5、组件实例创建

当渲染组件时:

function processComponent(n1, n2, container, parentComponent, anchor) {
  mountComponent(n2, container, parentComponent, anchor)
}
function mountComponent(vnode, container, parentComponent, anchor) {
  const instance = createComponentInstance(vnode, parentComponent)
  setupComponent(instance)
  setupRenderEffect(instance, container, anchor)
}
  • processComponent 处理组件
  • mountComponent 挂载组件
    • createComponentInstance 创建组件实例
    • setupComponent 创建组件对象

createComponentInstance:

function createComponentInstance(vnode, parent) {
  const instance = {
    vnode,
    parent,
    proxy: null,
    ctx: {},
    props: {},
    attrs: {},
    slots: {},
    setupState: {},
    isMounted: false,
    subTree: null
  }
  return instance
}

实例保存基础信息,还没运行 setup。

6、 setupComponent(初始化组件)

function setupComponent(instance) {
  initProps(instance, vnode.props)
  initSlots(instance, vnode.children)
  setupStatefulComponent(instance)
}

内部会执行:

const { setup } = Component
if (setup) {
  const setupResult = setup(props, ctx)
  handleSetupResult(instance, setupResult)
}

setup 返回值

  • 返回对象 → 作为响应式状态 state
  • 返回函数 → render 函数

最终让组件拥有 instance.render

7、创建响应式状态

Vue3 的响应式来自 reactivity 包:

const state = reactive({ count: 0 })

底层是 Proxy 拦截 getter/setter:

  • getter:收集依赖
  • setter:触发依赖更新

依赖管理核心是 effect / track / trigger

8、 setupRenderEffect 与首次渲染

创建渲染器副作用,并调度组件挂载和异步更新:

function setupRenderEffect(instance, container, anchor) {
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      const subTree = (instance.subTree = instance.render.call(proxy))
      patch(null, subTree, container, instance, anchor)
      instance.isMounted = true
    } else {
      // 更新更新逻辑
    }
  }, {
    scheduler: queueJob
  })
}

这里:

  • 创建一个 响应式 effect
  • 第一次执行 render 得到 subTree
  • patch 子树到 DOM

effect + scheduler 实现异步更新。

9、vnode-> 真实 DOM(DOM mount)

当 patch 到真正的 DOM 时,走的是 element 分支:

function processElement(...) {
  if (!n1) {
    mountElement(vnode, container)
  } else {
    patchElement(n1, n2)
  }
}

mountElement

function mountElement(vnode, container) {
  const el = (vnode.el = hostCreateElement(vnode.type))
  // props
  for (key in props) {
    hostPatchProp(el, key, null, props[key])
  }
  // children
  if (typeof children === 'string') {
    hostSetElementText(el, children)
  } else {
    children.forEach(c => patch(null, c, el))
  }
  hostInsert(el, container)
}

10、更新 & Diff 算法

当响应式状态改变:

state.count++

触发 setter → trigger

  • 将 effect 放入更新队列
  • 异步执行 scheduler
  • 调用 instance.update 再次 patch

更新阶段:

patchElement(n1, n2)

核心逻辑:

  1. props diff
  2. children diff
  3. unkeyed/keyed diff 算法(最小化移动)

具体见 patchChildrenpatchKeyedChildren

整体核心对象关系架构

App
 └─ vnode(root)
     └─ ComponentInstance
         ├─ props / slots
         ├─ setupState
         └─ render() -> subTree
             └─ vnode tree
                 └─ DOM nodes

响应式依赖结构:

reactive state
 ├─ effects[]
 └─ track -> effect
              └─ scheduler -> patch

webpack异步加载原理梳理解构

作者 sophie旭
2026年1月2日 22:03

背景:

这几天重新梳理了一下 webpack异步加载的原理,并对实现细节进行了一番拆解,再次让我感叹:真是万变不离其宗,基础知识真的是构建上层建筑的坚实底座,在此也分享给大家,希望大家可以领略到webpack实现异步加载之美

基座一:webpack模块化方案

你可能也吐槽过 webpack产物为啥这么丑?

原始源代码

// src/index.js (入口文件)
let title = require('./title.js')
console.log(title);
// src/title.js
module.exports = 'bu';

构建后产物结构概览

/******/ (() => { // webpack bootstrap 启动函数
/******/ var __webpack_modules__ = ([
/* 0 */ /* title.js 模块 */
((module) => {
module.exports = 'bu';
}),
/* 1 */ /* index.js 模块 */
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
let title = __webpack_require__(0); // 加载模块0
console.log(title);
})
/******/ ]);
/******/ /* 模块缓存 */
/******/ var __webpack_module_cache__ = {};
/******/ /* Webpack 自实现的 require 函数 */
/******/ function __webpack_require__(moduleId) {
/******/ /* 检查缓存 */
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ /* 创建新模块并加入缓存 */
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ exports: {}
/******/ };
/******/ /* 执行模块函数 */
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ /* 返回模块的 exports 对象 */
/******/ return module.exports;
/******/ }
/******/ /* 启动入口模块 */
/******/ return __webpack_require__(1); // 加载入口模块(index.js)
/******/ })()
;

✅ 核心原因:规范转换成本(模块系统适配)

浏览器原生不支持模块规范
浏览器无法直接执行 CommonJS/AMP/UMD 模块语法(如 require() / module.exports / define())。Webpack 必须将这些规范统一转换为浏览器可执行的函数包装形式

  • 整个打包产物被包裹在一个外层 IIFE 中(立即执行)

    • 意义:创建独立作用域
     // 未包裹:变量暴露全局
    var utils = {...} // 可能覆盖其他脚本的同名变量
    
    // IIFE 包裹后:
    (function() {
      var utils = {...} // 安全隔离
    })();
    
  • 每个模块被转换为标准函数(非立即执行),作为参数传递给运行时:function(module, exports...)

    • 意义模块环境隔离 -每次调用模块函数时都会创建新的:
    const module = { exports: {} };
    modules[moduleId].call(module.exports, ...);
    
  • 模块路径被替换为 数字 ID

    • 提升性能 & 减少体积
      • ✅ 大幅缩短引用路径
        './src/utils/string-format.js' → __webpack_require__(17)
      • ✅ 避免路径解析开销:浏览器无需处理文件路径逻辑
  • 原生 require/module 被替换为 Webpack 自实现的  __webpack_require__  函数

    • 规范统一:将 ESM/CommonJS/AMD 转为浏览器可执行格式
  • 生成复杂的 运行时(runtime)代码 处理模块加载/缓存

    • 避免重复执行(如多次 require 同一模块)
  • 模块字典(所有代码被打包成键值对(键:上面提到的数字ID,值:上面提到的每个模块被转换的标准函数))

    • ✅ 快速索引:通过数字 ID 实现 O(1) 复杂度的模块查找

基座二:jsonp

传统的 JSONP 流程:


sequenceDiagram

participant Client as 客户端

participant Server as 服务器

Client->>Client: 创建回调函数

Note right of Client: window.myCallback = <br/>function(data){...}

Client->>Server: 动态创建<script src="api?callback=myCallback">

Server->>Server: 准备数据

Server->>Client: 返回 myCallback({...数据...})

Client->>Client: 执行回调函数

Webpack 异步加载机制与 JSONP 的对比

  1. 脚本加载机制相同

// 两者都使用相同的基础加载方式

const script = document.createElement('script');

script.src = 'resource.js';

document.head.appendChild(script);

  1. 全局回调设计

// JSONP

window.jsonpCallback = function(data) { ... }

// Webpack

window.webpackJsonp = [];

window.webpackJsonp.push = function(data) { ... }

  1. 执行流程相似

graph TD

A[创建 script 标签] --> B[设置 src]

B --> C[添加到 DOM]

C --> D[服务器返回 JS]

D --> E[执行 JS 代码]

E --> F[触发全局回调]

那么不一样的地方在哪呢?

对了就是全局回调,异步加载的回调函数设计是非常精妙的,咱们往下看

精妙的全局回调设计-webpackJsonpCallback

好了,现在我们拿到异步组件脚本了,我们应该做什么呢?

对了,就是要接入 我们上面聊的 webpack模块化方案,只有这样webpack 才能正常加载并缓存我们的异步模块

那么问题来了,怎样才能和上面说的 webpack模块化方案 接上轨呢?

答案就在 这个jsonp的全局回调上

显然我们要在回调里 把当前模块加入到 模块字典里,及 webpack_modules 里,

我们管这一步,叫做 全局模块注册,注意这里仅仅是注册模块,并没有执行模块

按需执行 如何做到

好了,注册模块我们实现了,那么按需执行呢?

按需执行,即我们希望由我们来控制 什么时候执行该模块,那么如何实现呢?

精妙设计一:加载模块和执行模块分离--解耦请求与响应

graph LR
A[发起请求] --> B[存储控制器]
C[响应到达] --> D[取出控制器]
D --> E[触发回调]


// 步骤1: 初始化Promise
const promise = new Promise((resolve, reject) => {
  // 这个回调会立即执行!
  installedChunks[chunkId] = [[resolve, reject]];
});

// 步骤2: 文件加载完成后
function chunkLoaded() {
  const callbacks = installedChunks[chunkId];
  for (const [res] of callbacks) {
    res(); // 手动触发所有resolve
  }
  installedChunks[chunkId] = 0; // 标记为已加载
}

// 步骤3: 触发.then()
promise.then(() => {
  // 这里才会执行!
  __webpack_require__(moduleId);
});

也就是说在异步加载模块流程会封装成一个promise, 在加载模块前,我们会提前将该promise的resolve回调存储起来,存到 installedChunks;

当加载模块请求响应回来之后,我们从 installedChunks里拿到 resolve 回调执行 我们可以在resolve 里面控制何时 执行模块


sequenceDiagram

participant T as .then()调用

participant R as Runtime(运行时)

participant S as 网络请求

T->>R: __webpack_require__.e("hello_chunk")

activate R

R->>R: 创建Promise<br>installedChunks["hello_chunk"] = [[resolve, reject]]

R->>S: 发起chunk加载请求

deactivate R

S-->>R: 返回chunk内容

activate R

R->>R: 执行webpackJsonpCallback

R->>R: 找到对应resolve函数

R->>Promise: 执行resolve()

deactivate R

Promise-->>T: 触发.then()回调

到这里我们可以给出__webpack_require__.e 和 webpackJsonpCallback的代码了:

// 异步加载函数 (修正版)
  __webpack_require__.e = (chunkId) => {
    return new Promise((resolve, reject) => {
      // 检查模块是否已加载
      if (installedChunks[chunkId] === 0) {
        resolve();
        return;
      }
      
      // 检查是否已在加载中
      if (installedChunks[chunkId]) {
        installedChunks[chunkId].push([resolve, reject]);
        return;
      }
      
      // 初始化加载状态
      installedChunks[chunkId] = [[resolve, reject]];
      
      // 创建脚本标签加载 chunk
      const script = document.createElement('script');
      script.src = `${chunkId}.js`;
      script.onerror = () => {
        reject(new Error(`Failed to load chunk ${chunkId}`));
        // 清理加载状态
        if (installedChunks[chunkId]) {
          installedChunks[chunkId] = undefined;
        }
      };
      
      // 关键步骤:将脚本添加到文档头部 (之前遗漏的部分)
      document.head.appendChild(script);
    });
  };
  
function webpackJsonpCallback(data) {
    const [chunkIds, moreModules] = data;
    
    // 注册新模块
    for (const moduleId in moreModules) {
      __webpack_modules__[moduleId] = moreModules[moduleId];
    }
    
    // 处理每个 chunk 的 Promise
    for (const chunkId of chunkIds) {
      const chunkState = installedChunks[chunkId];
      if (!chunkState) continue;
      
      // 执行所有 resolve 回调
      for (const [resolve] of chunkState) {
        resolve();
      }
      
      // 标记 chunk 为已加载
      installedChunks[chunkId] = 0;
    }
  }

是不是很棒,加载由我们控制,执行也由我们控制,

精妙设计二:加载时序问题解决方案

当我们加载异步组件后,我们发现它并不是用 webpackJsonpCallback 包裹起来,而是用 webpackJsonp.push

// hello_chunk.js
webpackJsonp.push([["hello_chunk"], {"./src/hello.js": function(){...}}])

在webpack 实现里我们会看到这句话:

webpackJsonp.push = webpackJsonpCallback;

为什么 webpack 要多此一举 ?为什么不直接用 webpackJsonpCallback 包裹起来呢?

核心原因:解决异步加载的顺序问题

加载顺序不确定性

  • 异步 chunk 可能在主 runtime 加载完成之前就加载完毕(关键:此时webpackJsonpCallback 可能还没有定义,因为 webpackJsonpCallback 是写在主 runtime里的

  • 也可能在主 runtime 加载完成之后才加载

关键设计:劫持push方法

此时就算找不到 webpackJsonpCallback,但是 webpackJsonp.push 是原生方法,肯定可以找到,这样即使 webpackJsonpCallback 未定义,也不会让 已加载的分块 丢失

也就是允许任何时间加载的分块,webpack都能处理,完全解耦加载顺序

// 关键设计:劫持push方法
webpackJsonp.push = webpackJsonpCallback;

// 处理初始化前已加载的分块
for (var i = 0; i < webpackJsonp.length; i++) {
    webpackJsonpCallback(webpackJsonp[i]);
}

webpackJsonp.length = 0; // 清空初始队列

// 将处理后的队列暴露到全局
window.webpackJsonp = webpackJsonp;

场景1:分块在主runtime之前加载完成


sequenceDiagram

participant Browser

participant Chunk as 异步分块

participant Runtime as Webpack Runtime

Browser->>Chunk: 1. 加载分块文件

Chunk->>Browser: 2. 执行分块代码

Note right of Chunk: webpackJsonp.push([[1], modules])

Browser->>Runtime: 3. 加载主runtime

Runtime->>Runtime: 4. 初始化时重写push方法

Runtime->>Runtime: 5. 处理已缓存的推送

Runtime->>Runtime: 6. 执行回调逻辑

场景2:分块在主runtime之后加载完成


sequenceDiagram

participant Browser

participant Runtime as Webpack Runtime

participant Chunk as 异步分块

Browser->>Runtime: 1. 加载主runtime

Runtime->>Runtime: 2. 初始化时重写push方法

Runtime->>Runtime: 3. 处理初始队列(空)

Browser->>Chunk: 4. 加载分块文件

Chunk->>Browser: 5. 执行分块代码

Note right of Chunk: webpackJsonp.push([[1], modules])

Browser->>Runtime: 6. 推送触发回调

总流程概览

1. 编译阶段(Build Time)

  • 语法识别:Webpack 解析 AST 时识别 import('./LazyComponent') 语法

  • 模块分离

  // 原始代码
  import('./LazyComponent');
  
  // Webpack 处理:
  1.LazyComponent 及其依赖抽离为独立 chunk(如 `src_LazyComponent_js.js`2. 生成 chunk ID(如 "chunk-lazy"3. 生成模块 ID(如 42)
- **代码转换**:
  ```javascript
  // 转换后代码
  __webpack_require__.e("chunk-lazy")
    .then(__webpack_require__.bind(__webpack_require__, 42))
  • 生成 chunk 文件
// src_LazyComponent_js.js 内容
  (window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    ["chunk-lazy"],
    {
      "./src/LazyComponent.js": (module, __webpack_exports__, __webpack_require__) => {
        // 模块实际代码
      }
    }
  ]);

2. 运行时阶段(Runtime)

  • 触发加载
// 执行编译后的代码
const promise = __webpack_require__.e("chunk-lazy");
  • 加载器执行
// __webpack_require__.e 核心逻辑
  __webpack_require__.e = (chunkId) => {
    // 检查缓存
    if (installedChunks[chunkId] === 0) return Promise.resolve();
    
    // 创建加载 Promise
    const promise = new Promise((resolve, reject) => {
      // 创建 script 标签
      const script = document.createElement('script');
      script.src = `${publicPath}${chunkId}.chunk.js`;
      
      // 错误处理
      script.onerror = () => reject(new Error(`Loading failed ${chunkId}`));
      
      // 注册全局回调
      const originalPush = webpackJsonp.push.bind(webpackJsonp);
      webpackJsonp.push = (item) => {
        webpackJsonpCallback(item);
        originalPush(item);
      };

      // 处理运行时初始化前已加载的数据
      for (let i = 0; i < webpackJsonp.length; i++) {
        webpackJsonpCallback(webpackJsonp[i]);
      }

      // 清空队列但不移除引用
      webpackJsonp.splice(0, webpackJsonp.length);


      
      // 触发加载
      document.head.appendChild(script);
    });
    
    // 标记为加载中
    installedChunks[chunkId] = [promise, resolve, reject];
    return promise;
  };

3. 模块注册阶段(Chunk Execution)

  • Chunk 脚本执行
// 浏览器加载并执行 src_LazyComponent_js.js
  window.webpackJsonp.push([
    ["chunk-lazy"], 
    { 
      "./src/LazyComponent.js": function(module, exports) {
        // 组件实现
        exports.default = function LazyComp() { ... }
      }
    }
  ]);
  • 回调触发
function webpackJsonpCallback(data) {
    const [chunkIds, modules] = data;
    
    // 1. 注册模块到全局存储
    for (const moduleId in modules) {
      __webpack_modules__[moduleId] = modules[moduleId];
    }
    
    // 2. 标记chunk为已加载
    chunkIds.forEach(chunkId => {
      installedChunks[chunkId] = 0; // 0 = 已加载
    });
    
    // 3. 执行所有等待中的resolve
    const resolves = [];
    chunkIds.forEach(chunkId => {
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][1]); // 获取resolve函数
        installedChunks[chunkId] = 0; // 清除等待状态
      }
    });
    // resolve可以控制何时执行模块
    // 比如resolve可以是() => {

    // 执行模块加载
    //const module = __webpack_require__(42);
    //return module;
    //}
    resolves.forEach(resolve => resolve());
  }

至此,webpack异步加载原理 我们已经大致清楚了,是不是还挺有意思的,同时也能给我们一些启发,尤其精妙设计那里,希望有机会用到,这样我们也是站在巨人的肩膀上了~

面试官 : “ 说一下 Vue 的 8 个生命周期钩子都做了什么 ? ”

作者 千寻girling
2026年1月2日 20:53

一、Vue3 8 个核心生命周期钩子(按执行顺序)

阶段 选项式 API 名称 组合式 API 名称 执行时机 核心作用 & 实战场景
初始化阶段 beforeCreate 无(setup 替代) 实例创建前,数据 / 方法未初始化,this 不可用 Vue2 中用于初始化非响应式数据;Vue3 中逻辑移到 setup 最顶部(无响应式操作)
初始化阶段 created 无(setup 替代) 实例创建完成,数据 / 方法已初始化,DOM 未生成 1. 发起异步请求(接口请求);2. 初始化非 DOM 相关逻辑(如数据格式化)
挂载阶段 beforeMount onBeforeMount 挂载开始前,模板编译完成,DOM 未挂载到页面($el 未生成) 1. 预操作 DOM 结构(如计算 DOM 尺寸,需结合 nextTick);2. 初始化第三方库(挂载前准备)
挂载阶段 mounted onMounted DOM 挂载完成($el 已挂载),页面可见 1. 操作真实 DOM(如初始化 ECharts / 地图);2. 发起依赖 DOM 的异步请求;3. 监听 DOM 事件
更新阶段 beforeUpdate onBeforeUpdate 数据更新后,DOM 重新渲染前 1. 获取更新前的 DOM 状态(如旧输入框值);2. 取消不必要的监听 / 定时器(避免重复执行)
更新阶段 onUpdated onUpdated DOM 重新渲染完成,页面已更新 1. 获取更新后的 DOM 状态;2. 重新计算 DOM 相关数据(如滚动位置重置)
卸载阶段 beforeUnmount onBeforeUnmount 组件卸载前(实例仍可用,DOM 未销毁) 1. 清理副作用(清除定时器 / 事件监听);2. 销毁第三方库实例(如 ECharts 销毁)
卸载阶段 unmounted onUnmounted 组件卸载完成,DOM 销毁,实例失效 1. 最终清理(如取消接口请求);2. 释放内存(清空大型数组 / 对象引用)

二、关键细节(Vue3 核心变化)

1. setup 替代 beforeCreate/created

Vue3 中 setup 执行时机 = beforeCreate + created,这两个钩子在组合式 API 中被废弃,所有初始化逻辑直接写在 setup 中:

以下是 Vue3 生命周期钩子的完整可运行代码示例,包含选项式 API 和组合式 API(<script setup> 推荐写法)  两种风格,附带详细注释和实战场景(如接口请求、DOM 操作、定时器清理等),可直接复制到 Vue3 项目中运行。

三、组合式 API 示例(<script setup> 推荐)

Vue3 生命周期演示

<template>
  <div class="life-cycle-demo">
    <h3>Vue3 生命周期演示(组合式 API)</h3>
    <!-- 绑定响应式数据,触发更新阶段 -->
    <p>当前计数:{{ count }}</p>
    <button @click="count++">点击更新计数(触发更新钩子)</button>
    <!-- 挂载 ECharts 示例 DOM -->
    <div id="chart" style="width: 300px; height: 200px; margin: 20px 0;"></div>
  </div>
</template>

<script setup>
import { 
  ref, 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated, 
  onBeforeUnmount, 
  unmounted 
} from 'vue';
// 模拟 ECharts(实际需安装:npm install echarts)
import * as echarts from 'echarts';

// 🫱🫱🫱 1. setup 本身替代 beforeCreate + created(初始化阶段)
console.log('===== setup 执行(等价于 beforeCreate + created)=====');
// 响应式数据初始化
const count = ref(0);
// 模拟接口请求(created 阶段核心场景)
const fetchData = async () => {
  try {
    console.log('发起异步接口请求(created 阶段)');
    // 模拟接口延迟
    const res = await new Promise(resolve => {
      setTimeout(() => resolve({ data: '模拟接口返回数据' }), 1000);
    });
    console.log('接口请求完成:', res.data);
  } catch (err) {
    console.error('接口请求失败:', err);
  }
};
// 执行接口请求(等价于 created 中调用)
fetchData();

// 🫱🫱🫱 2. 挂载阶段:beforeMount(DOM 未挂载)
onBeforeMount(() => {
  console.log('===== onBeforeMount 执行 =====');
  console.log('DOM 未挂载,#chart 元素:', document.getElementById('chart')); // null
  // 若需提前操作 DOM,需结合 nextTick
});

// 🫱🫱🫱 3. 挂载阶段:mounted(DOM 已挂载,核心操作 DOM 场景)
let myChart = null;
onMounted(() => {
  console.log('===== onMounted 执行 =====');
  console.log('DOM 已挂载,#chart 元素:', document.getElementById('chart')); // 存在
  // 初始化 ECharts(依赖 DOM 的第三方库)
  myChart = echarts.init(document.getElementById('chart'));
  myChart.setOption({
    title: { text: '生命周期演示图表' },
    xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
    yAxis: { type: 'value' },
    series: [{ data: [120, 200, 150], type: 'bar' }]
  });
  // 模拟定时器(需在卸载阶段清理)
  const timer = setInterval(() => {
    console.log('定时器运行中(count:', count.value, ')');
  }, 1000);
  // 把定时器存到全局,方便卸载时清理
  window.lifeCycleTimer = timer;
});

// 🫱🫱🫱 4. 更新阶段:beforeUpdate(数据更新,DOM 未重新渲染)
onBeforeUpdate(() => {
  console.log('===== onBeforeUpdate 执行 =====');
  console.log('数据已更新(count:', count.value, '),DOM 未刷新');
  // 可获取更新前的 DOM 状态(如旧的图表数据)
});

// 🫱🫱🫱 5. 更新阶段:updated(DOM 已重新渲染)
onUpdated(() => {
  console.log('===== onUpdated 执行 =====');
  console.log('DOM 已更新(count:', count.value, ')');
  // 若数据更新后需重新渲染图表
  if (myChart) {
    myChart.setOption({
      series: [{ data: [120 + count.value * 10, 200 + count.value * 10, 150 + count.value * 10] }]
    });
  }
});

// 🫱🫱🫱 6. 卸载阶段:beforeUnmount(组件即将卸载,清理副作用)
onBeforeUnmount(() => {
  console.log('===== onBeforeUnmount 执行 =====');
  // 清理定时器
  clearInterval(window.lifeCycleTimer);
  // 销毁 ECharts 实例
  if (myChart) {
    myChart.dispose();
    myChart = null;
  }
  console.log('副作用已清理(定时器、ECharts 已销毁)');
});

// 🫱🫱🫱 7. 卸载阶段:unmounted(组件已完全卸载)
unmounted(() => {
  console.log('===== unmounted 执行 =====');
  console.log('组件已卸载,DOM 已销毁,实例失效');
});
</script>

四、选项式 API 示例(兼容 Vue2 写法)

<template>
  <div class="life-cycle-demo">
    <h3>Vue3 生命周期演示(选项式 API)</h3>
    <p>当前计数:{{ count }}</p>
    <button @click="count++">点击更新计数</button>
    <div id="chart" style="width: 300px; height: 200px; margin: 20px 0;"></div>
  </div>
</template>

<script>
import * as echarts from 'echarts';

export default {
  // 响应式数据
  data() {
    return {
      count: 0,
      myChart: null,
      timer: null
    };
  },

  // 🫱🫱🫱 1. 初始化阶段:beforeCreate(实例刚创建,数据/方法未初始化)
  beforeCreate() {
    console.log('===== beforeCreate 执行 =====');
    console.log('数据未初始化:', this.count); // undefined
    console.log('方法未初始化:', this.fetchData); // undefined
  },

  // 🫱🫱🫱 2. 初始化阶段:created(数据/方法已初始化,DOM 未生成)
  created() {
    console.log('===== created 执行 =====');
    console.log('数据已初始化:', this.count); // 0
    // 发起异步请求
    this.fetchData();
  },

  // 🫱🫱🫱 3. 挂载阶段:beforeMount(模板编译完成,DOM 未挂载)
  beforeMount() {
    console.log('===== beforeMount 执行 =====');
    console.log('DOM 未挂载,#chart 元素:', document.getElementById('chart')); // null
  },

  // 🫱🫱🫱 4. 挂载阶段:mounted(DOM 已挂载,可操作真实 DOM)
  mounted() {
    console.log('===== mounted 执行 =====');
    console.log('DOM 已挂载,#chart 元素:', document.getElementById('chart')); // 存在
    // 初始化 ECharts
    this.myChart = echarts.init(document.getElementById('chart'));
    this.myChart.setOption({
      title: { text: '选项式 API 图表' },
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
      yAxis: { type: 'value' },
      series: [{ data: [120, 200, 150], type: 'bar' }]
    });
    // 启动定时器
    this.timer = setInterval(() => {
      console.log('定时器运行中(count:', this.count, ')');
    }, 1000);
  },

  // 🫱🫱🫱 5. 更新阶段:beforeUpdate(数据更新,DOM 未重新渲染)
  beforeUpdate() {
    console.log('===== beforeUpdate 执行 =====');
    console.log('数据已更新(count:', this.count, '),DOM 未刷新');
  },

  // 🫱🫱🫱 6. 更新阶段:updated(DOM 已重新渲染)
  updated() {
    console.log('===== updated 执行 =====');
    console.log('DOM 已更新(count:', this.count, ')');
    // 重新渲染图表
    if (this.myChart) {
      this.myChart.setOption({
        series: [{ data: [120 + this.count * 10, 200 + this.count * 10, 150 + this.count * 10] }]
      });
    }
  },

  // 🫱🫱🫱 7. 卸载阶段:beforeUnmount(组件即将卸载,清理副作用)
  beforeUnmount() {
    console.log('===== beforeUnmount 执行 =====');
    // 清理定时器
    clearInterval(this.timer);
    // 销毁 ECharts
    if (this.myChart) {
      this.myChart.dispose();
      this.myChart = null;
    }
  },

  // 🫱🫱🫱 8. 卸载阶段:unmounted(组件已完全卸载)
  unmounted() {
    console.log('===== unmounted 执行 =====');
    console.log('组件已卸载,资源已清理');
  },

  // 自定义方法:模拟接口请求
  methods: {
    async fetchData() {
      try {
        console.log('发起接口请求(created 阶段)');
        const res = await new Promise(resolve => {
          setTimeout(() => resolve({ data: '选项式 API 接口数据' }), 1000);
        });
        console.log('接口请求完成:', res.data);
      } catch (err) {
        console.error('接口请求失败:', err);
      }
    }
  }
};
</script>

五、测试方式(验证生命周期执行)

  1. 挂载阶段:页面加载后,控制台会依次打印 setup/beforeCreatecreatedbeforeMountmounted,同时 ECharts 图表渲染完成,定时器开始运行。

  2. 更新阶段:点击 “点击更新计数” 按钮,触发 beforeUpdateupdated,图表数据随计数更新。

  3. 卸载阶段

    • 若使用路由,跳转到其他页面(组件卸载);
    • 或手动移除组件(如用 v-if 控制),控制台会打印 beforeUnmountunmounted,定时器停止,ECharts 实例销毁。

六、核心注意点

  1. 组合式 API 无 beforeCreate/created:所有初始化逻辑直接写在 <script setup> 顶部,等价于这两个钩子。
  2. 副作用必须清理:定时器、事件监听、第三方库实例(如 ECharts)需在 onBeforeUnmount/beforeUnmount 中清理,避免内存泄漏。
  3. DOM 操作仅在 mounted/updated 中安全beforeMount 中操作 DOM 需结合 nextTick
  4. updated 中避免无限循环:不要在 updated 中直接修改响应式数据(除非加条件判断)。

通过这个示例,你可以直观看到每个生命周期钩子的执行时机和实际用途,覆盖日常开发中 90% 以上的生命周期场景

面试官 : ” 说一下 Vue 中的 setup 中的 props 和 context “

作者 千寻girling
2026年1月2日 19:53

一、props:和 Vue2 核心逻辑完全一致,仅访问方式微调

props 作为 Vue 「父传子」的核心通信方式

Vue3 中 单向数据流、类型校验、默认值 / 必传项 等核心规则和 Vue2 完全一样,唯一区别是「访问方式」:

1. 共性(Vue2/Vue3 通用)

  • 单向数据流:子组件不能直接修改 props,必须通过 emit 通知父组件修改;
  • 支持类型校验( String / Number / Array / Object 等)、默认值、自定义校验规则;
  • 父组件传的属性如果没被 props 声明,会落到 attrs 中(下文会提)。

2. 用法对比(Vue2 选项式 vs Vue3 组合式)

<!-- Vue2 选项式 API -->
<script>
export default {
  props: {
    name: { type: String, default: '默认名' },
    age: { type: Number, required: true }
  },
  mounted() {
    console.log(this.name); // 👉 通过 this 访问
  }
}
</script>

<!-- Vue3 组合式 API(setup) -->
<script>
export default {
  // 👉 props 定义规则和 Vue2 完全一样
  props: {
    name: { type: String, default: '默认名' },
    age: { type: Number, required: true }
  },
  // 👉 props 作为 setup 第一个参数传入,无需 this
  setup(props) {
    console.log(props.name); // 直接访问
    // 注意:props 是响应式的,解构会丢失响应式,需用 toRefs
    const { name } = Vue.toRefs(props);
    console.log(name.value); // ref 需 .value 访问
  }
}
</script>

二、context:Vue3 把 Vue2 的「this 上的通信属性」聚合到上下文

context 是 setup 的第二个参数(非响应式,可直接解构),核心作用是替代 Vue2 中 this 上的「非 props 相关通信能力」,你提到的几个属性对应关系精准,补充用法细节:

context 属性 Vue2 对应写法 核心用法示例
context.emit this.$emit 子传父触发事件:context.emit('change', { id: 1 })(父组件用 @change 接收)
context.slots this.$slots 访问父组件传入的插槽:context.slots.header()(Vue3 插槽是函数,需加 () 调用)
context.attrs this.$attrs 接收父组件未被 props 声明的属性:父传 class="box" 且 props 未声明 → context.attrs.class
context.expose() 无(Vue2 无此能力) 主动暴露子组件内部属性给父组件:context.expose({ fn: () => console.log('暴露的方法') })

核心示例(context 解构使用,更简洁)

<script>
export default {
  setup(props, { emit, slots, attrs, expose }) {
    // 1. 子传父:触发自定义事件
    const handleClick = () => emit('submit', '子组件数据');

    // 2. 访问具名插槽
    console.log(slots.footer()); // 获取父组件传入的 footer 插槽内容

    // 3. 访问未声明的属性
    console.log(attrs['data-id']); // 父传 data-id="123" 且未被 props 声明

    // 4. 暴露内部方法给父组件(父通过 ref 仅能访问暴露的内容)
    const internalFn = () => '内部逻辑';
    expose({ internalFn }); // 父组件 ref.value.internalFn() 可调用

    return { handleClick };
  }
}
</script>

1. 子组件(Child.vue):核心逻辑详解

<template>
  <!-- 点击按钮触发子传父事件 -->
  <button @click="handleClick">点击触发submit事件</button>

  <!-- 渲染父组件传入的footer具名插槽 -->
  <div class="slot-container">
    <slot name="footer"></slot>
  </div>

  <!-- 把attrs中的data-id透传给内部div(演示attrs用法) -->
  <div :data-id="attrs['data-id']">透传父组件未声明的data-id属性</div>
</template>

<script>
// 导入vue的核心方法(按需导入,Vue3组合式API规范)
import { toRefs } from 'vue';

export default {
  // 第一步:声明props(仅声明name,未声明data-id,所以data-id会落到attrs中)
  props: {
    name: {
      type: String,
      default: '默认名称'
    }
  },

  // setup第二个参数解构出:emit(子传父)、slots(插槽)、attrs(透传属性)、expose(暴露内容)
  setup(props, { emit, slots, attrs, expose }) {
    // 👉 1. 子传父:触发自定义事件(核心用法)
    const handleClick = () => {
      // 第一个参数:事件名(父组件用@submit接收);第二个参数:传递给父组件的数据
      emit('submit', {
        msg: '子组件传递的数据',
        name: props.name // 结合props使用,把props数据也传给父组件
      });
    };

    // 👉 2. 访问具名插槽(控制台打印插槽内容,验证是否传入)
    console.log('===== 访问footer插槽 =====');
    // Vue3中slots的每个插槽都是函数,调用后返回VNode数组(插槽的DOM结构)
    if (slots.footer) { // 先判断父组件是否传入了footer插槽,避免报错
      const footerSlotContent = slots.footer();
      console.log('footer插槽的VNode内容:', footerSlotContent);
    } else {
      console.log('父组件未传入footer插槽');
    }

    // 👉 3. 访问父组件未被props声明的属性(attrs)
    console.log('===== 访问attrs =====');
    console.log('父组件传入的data-id:', attrs['data-id']); // 父传的data-id未被props声明,所以在attrs中
    console.log('父组件传入的class(若有):', attrs.class); // class/style会自动透传,也会在attrs中
    // 注意:attrs是非响应式的,若需要响应式,可结合toRefs(但一般attrs无需响应式)

    // 👉 4. 暴露子组件内部方法/属性给父组件(父组件通过ref访问)
    // 定义子组件内部方法(未return也能通过expose暴露)
    const internalFn = () => {
      return `内部方法执行成功!props.name的值是:${props.name}`;
    };
    // 定义内部属性(仅暴露给父组件,模板中无法直接使用,除非return)
    const internalData = '子组件内部私有数据';

    // 主动暴露指定内容(只有这里声明的,父组件才能通过ref访问)
    expose({
      internalFn, // 暴露内部方法
      internalData, // 暴露内部属性
      // 也可以暴露props(方便父组件直接获取props值)
      getPropsName: () => props.name
    });

    // 👉 5. 补充:props的响应式使用(可选)
    // 解构props并保留响应式(若需要单独使用props中的属性)
    const { name } = toRefs(props);
    console.log('===== props使用 =====');
    console.log('props.name的值(响应式):', name.value);

    // 把需要在模板中使用的方法return出去(handleClick在模板中绑定点击事件,必须return)
    return {
      handleClick,
      attrs // 把attrsreturn出去,方便模板中使用(如上面模板中的:data-id="attrs['data-id']")
    };
  }
};
</script>

2. 父组件(Parent.vue):调用子组件并配合使用

<template>
  <div class="parent-container">
    <h3>父组件</h3>
    <!-- 第二步:使用子组件,完成以下操作:
         1. 传props:name="测试名称"
         2. 传未声明的属性:data-id="10086"(会落到子组件attrs中)
         3. 绑定子组件的自定义事件:@submit="handleChildSubmit"
         4. 传入具名插槽:<template #footer>...</template>
         5. 给子组件加ref:childRef(用于访问子组件暴露的内容)
    -->
    <Child
      ref="childRef"
      name="测试名称"
      data-id="10086"
      class="child-component"
      @submit="handleChildSubmit"
    >
      <!-- 传入footer具名插槽(子组件会访问这个插槽) -->
      <template #footer>
        <p>这是父组件传给子组件的footer插槽内容</p>
      </template>
    </Child>

    <!-- 显示子组件传递的数据 -->
    <div class="child-data" v-if="childSubmitData">
      <h4>子组件传递的数据:</h4>
      <p>msg:{{ childSubmitData.msg }}</p>
      <p>name:{{ childSubmitData.name }}</p>
    </div>

    <!-- 点击按钮访问子组件暴露的方法/属性 -->
    <button @click="accessChildExpose">访问子组件暴露的内容</button>
  </div>
</template>

<script>
// 导入子组件
import Child from './Child.vue';
// 导入vue的ref(用于创建子组件的引用)和onMounted(生命周期)
import { ref, onMounted } from 'vue';

export default {
  // 注册子组件
  components: {
    Child
  },

  setup() {
    // 👉 1. 创建子组件的ref引用(用于访问子组件暴露的内容)
    const childRef = ref(null);

    // 👉 2. 接收子组件的自定义事件数据
    const childSubmitData = ref(null);
    const handleChildSubmit = (data) => {
      console.log('父组件接收到子组件的submit事件数据:', data);
      childSubmitData.value = data; // 把数据存到响应式变量中,模板中显示
    };

    // 👉 3. 访问子组件通过expose暴露的内容
    const accessChildExpose = () => {
      // 确保子组件已挂载(避免初始时childRef.value为null)
      if (childRef.value) {
        // 调用子组件暴露的internalFn方法
        const fnResult = childRef.value.internalFn();
        console.log('调用子组件暴露的internalFn结果:', fnResult);

        // 获取子组件暴露的internalData属性
        const internalData = childRef.value.internalData;
        console.log('获取子组件暴露的internalData:', internalData);

        // 调用子组件暴露的getPropsName方法(获取子组件的props.name)
        const propsName = childRef.value.getPropsName();
        console.log('子组件的props.name:', propsName);

        // 注意:子组件未暴露的内容,父组件无法访问(比如子组件的handleClick)
        console.log('访问子组件未暴露的handleClick:', childRef.value.handleClick); // undefined
      }
    };

    // 👉 4. 生命周期:组件挂载后,也可以主动访问子组件暴露的内容
    onMounted(() => {
      console.log('===== 组件挂载后访问子组件暴露内容 =====');
      if (childRef.value) {
        console.log('挂载后获取internalData:', childRef.value.internalData);
      }
    });

    // return需要在模板中使用的变量/方法
    return {
      childRef,
      childSubmitData,
      handleChildSubmit,
      accessChildExpose
    };
  }
};
</script>

三、关键总结

  1. props规则完全继承 Vue2,仅在 Vue3 setup 中需通过第一个参数访问,注意响应式解构(用 toRefs);
  2. context替代 Vue2 中 this 上的通信属性,把 $emit/$slots/$attrs 聚合到 上下文( context ) ,新增 expose() 增强组件封装性(Vue2 父组件通过 ref 能访问子组件所有内容,Vue3 需主动暴露才可见);
  3. 简化记忆:setup(props, context) → 第一个参数管「父传子的 props」,第二个参数管「子传父、插槽、透传属性、暴露内容」。

这种设计既保留了 Vue2 的使用习惯,又让组合式 API 脱离了 this 的束缚,逻辑更聚合,是 Vue3 兼顾「易用性」和「灵活性」的核心设计。

赫蹏(hètí):为中文网页内容赋予优雅排版的开源利器

作者 修己xj
2026年1月2日 23:42

fHJ9cZeOp.jpg

fHJ9cZeOp.jpg

在当今信息爆炸的时代,内容呈现的形式往往决定了阅读体验的优劣。对于中文网站来说,一个长期存在的挑战是如何实现符合传统中文排版美学的网页展示。尽管现代CSS技术已经十分强大,但针对中文特点的排版优化仍然不够完善。今天,我们将介绍一个专门为解决这一问题而生的开源项目——赫蹏(hètí)。

什么是赫蹏?

赫蹏是一个专为中文内容展示设计的排版样式增强库,名称取自古代对纸张的雅称。这个项目由开发者Sivan创建,基于通行的中文排版规范,旨在为网站的读者提供更加舒适、专业的文章阅读体验。

简单来说,赫蹏让中文网页内容“自动变好看”

该项目在github 已有6.6k star

_20260102_222650.png

_20260102_222650.png

_20260102_223923.png

_20260102_223923.png

核心特性一览

🎯 贴合网格的精准排版

赫蹏实现了基于网格系统的中文排版,确保文字、段落和间距都遵循严谨的视觉规律,让页面呈现出整齐划一的专业感。

📝 全标签样式美化

项目不仅仅针对段落文本,而是对整个HTML文档中的各类标签(标题、列表、表格、代码块等)都进行了细致的美化,形成统一而和谐的整体视觉风格。

🏮 传统版式支持

赫蹏贴心地预置了古文、诗词样式,并提供了多种传统排版样式支持:

  • 行间注(类似于古籍中的双行小注)
  • 多栏排版
  • 竖排文字
  • 为需要展示传统文学内容的网站提供了极大便利

🌗 智能适配设计

  • 自适应黑暗模式:跟随系统设置自动切换明暗主题
  • 移动端优先:在各种屏幕尺寸上都有良好表现
  • 简繁中文支持:满足不同地区用户的需求

✨ 智能排版增强(基于JavaScript)

这是赫蹏的“黑科技”部分:

  • 中西文混排美化:自动在中英文之间添加适当间距,再也不用手动敲空格
  • 全角标点挤压:智能调整标点符号的间距,避免出现难看的空白

🎨 字体优化

提供多种预设的中文字体族选择(桌面端),可根据不同内容风格搭配最合适的字体组合。

极简的安装与使用

赫蹏的设计哲学是“最小化侵入”,使用起来异常简单:

基础使用(仅CSS)

<!-- 1. 引入样式 -->
<link rel="stylesheet" href="//unpkg.com/heti/umd/heti.min.css">

<!-- 2. 添加类名 -->
<article class="heti">
  <h1>文章标题</h1>
  <p>这里是你所有的中文内容...</p>
</article>

只需这两步,你的内容就会立刻获得专业级的中文排版效果。

增强功能(添加JavaScript)

<script src="//unpkg.com/heti/umd/heti-addon.min.js"></script>
<script>
  const heti = new Heti('.heti');
  heti.autoSpacing(); // 启用智能中西文混排和标点挤压
</script>

实际效果展示

我比较喜欢的是竖排排版的样式,我们在markdown中也可以直接使用,如下使用

<link rel="stylesheet" href="//unpkg.com/heti/umd/heti.min.css">

<div class="card__vertical-container">
<section class="heti--vertical heti--ancient">
<h1>出師表</h1>
<p class="heti-small">作者:<abbr title="字孔明">諸葛亮</abbr>(181年-234年10月8日)</p>
<p>先帝創業未半,而中道崩殂;今天下三分,益州疲弊,此誠危急存亡之秋也﹗然侍衞之臣,不懈於內;忠志之士,忘身於外者,蓋追先帝之殊遇,欲報之於陛下也。</p>
<p>誠宜開張聖聽,以光先帝遺德,恢弘志士之氣﹔不宜妄自菲薄,引喻失義,以塞忠諫之路也。</p>
<p>宮中、府中,俱為一體;陟罰臧否,不宜異同。若有作姦、犯科,及為忠善者,宜付有司,論其刑賞,以昭陛下平明之治;不宜偏私,使內外異法也。</p>
<p>侍中、侍郎郭攸之、費禕、董允等,此皆良實,志慮忠純,是以先帝簡拔以遺陛下。愚以為宮中之事,事無大小,悉以咨之,然後施行,必能裨補闕漏,有所廣益。將軍向寵,性行淑均,曉暢軍事,試用於昔日,先帝稱之曰「能」,是以眾議舉寵為督。愚以為營中之事,悉以咨之,必能使行陣和睦,優劣得所。</p>
<p>親賢臣,遠小人,此先漢所以興隆也﹔親小人,遠賢臣,此後漢所以傾頹也。先帝在時,每與臣論此事,未嘗不歎息痛恨於桓、靈也!侍中、尚書、長史、參軍,此悉貞良死節之臣,願陛下親之、信之,則漢室之隆,可計日而待也。</p>
<p>臣本布衣,躬耕於南陽,苟全性命於亂世,不求聞達於諸侯。先帝不以臣卑鄙,猥自枉屈,三顧臣於草廬之中,諮臣以當世之事;由是感激,遂許先帝以驅馳。後值傾覆,受任於敗軍之際,奉命於危難之間,爾來二十有一年矣。先帝知臣謹慎,故臨崩寄臣以大事也。受命以來,夙夜憂歎,恐託付不效,以傷先帝之明。故五月渡瀘,深入不毛。今南方已定,兵甲已足,當獎率三軍,北定中原,庶竭駑鈍,攘除姦凶,興復漢室,還於舊都。此臣所以報先帝而忠陛下之職分也。至於斟酌損益,進盡忠言,則攸之、禕、允之任也。</p>
<p>願陛下託臣以討賊興復之效;不效,則治臣之罪,以告先帝之靈。若無興德之言,則責攸之、禕、允等之慢,以彰其咎。陛下亦宜自謀,以諮諏善道,察納雅言,深追先帝遺詔。臣不勝受恩感激。今當遠離,臨表涕零,不知所言!</p>
</section>
</div>

效果如下:

_20260102_224539.png

_20260102_224539.png

我使用ai 基于赫蹏(hètí)做了一个竖版排版的网站,感兴趣的家人们也可以使用下

博主竖版排版网址: h5.xiuji.mynatapp.cc/heti/

_20260102_232901.png

_20260102_232901.png

_.png

_.png

结语:让内容回归本质

在追求炫酷交互和复杂动画的今天,赫蹏提醒我们一个基本事实:对于内容型网站,优秀的可读性才是最重要的用户体验。这个项目以极低的成本,为中文网页带来了显著的品质提升。

如果你正在经营一个以中文内容为主的网站,或者只是想在个人博客上获得更好的排版效果,不妨尝试一下赫蹏。正如项目README中所说的:“总之,用上就会变好看。”

赫蹏不仅是一个工具,更是对中文数字阅读体验的一次优雅致敬。在这个注意力稀缺的时代,为读者提供一个舒适的阅读环境,或许就是最好的内容策略。

昨天 — 2026年1月2日掘金 前端

面试官 : “ Vue 选项式api 和 组合式api 什么区别? “

作者 千寻girling
2026年1月2日 18:12

Vue 的选项式 API (Options API)  和组合式 API (Composition API)  是两种核心的代码组织方式,前者侧重 “按选项分类”,后者侧重 “按逻辑分类”,核心差异体现在设计理念、代码结构、复用性等维度,以下是全面对比和实用解读:

一、核心区别对比表

维度 选项式 API (Options API) 组合式 API (Composition API)
设计理念 按 “选项类型” 组织代码(data、methods、computed 等) 按 “业务逻辑” 组织代码(同一逻辑的代码聚合)
代码结构 分散式:同一逻辑的代码分散在不同选项中 聚合式:同一逻辑的代码集中在 setup 或 <script setup>
适用场景 小型组件、简单业务(快速上手) 中大型组件、复杂业务(逻辑复用 / 维护)
逻辑复用 依赖 mixin(易命名冲突、来源不清晰) 依赖组合式函数(Composables,纯函数复用,清晰可控)
类型推导 对 TypeScript 支持弱(需手动标注) 天然适配 TS,类型推导更完善
响应式写法 声明式:data 返回对象,Vue 自动响应式 手动式:用 ref/reactive 创建响应式数据
生命周期 直接声明钩子(created、mounted 等) setup 中通过 onMounted 等函数调用(无 beforeCreate/created)
代码可读性 简单场景清晰,复杂场景 “碎片化” 复杂场景逻辑聚合,可读性更高
上手成本 低(符合传统前端思维) 稍高(需理解 ref/reactive/setup 等概念)

二、代码示例直观对比

1. 选项式 API(Vue 2/3 兼容)

<template>
  <div>{{ count }} <button @click="add">+1</button></div>
</template>

<script>
export default {
  // 数据(响应式)
  data() {
    return {
      count: 0
    };
  },
  // 方法
  methods: {
    add() {
      this.count++;
    }
  },
  // 生命周期
  mounted() {
    console.log('组件挂载:', this.count);
  }
};
</script>

特点:代码按 data/methods/mounted 等选项拆分,同一 “计数逻辑” 分散在不同区块。

2. 组合式 API(Vue 3 推荐,<script setup> 语法糖)

<template>
  <div>{{ count }} <button @click="add">+1</button></div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 响应式数据(ref 用于基本类型)
const count = ref(0);

// 方法(与数据聚合)
const add = () => {
  count.value++; // ref 需通过 .value 访问
};

// 生命周期
onMounted(() => {
  console.log('组件挂载:', count.value);
});
</script>

特点:“计数逻辑” 的数据、方法、生命周期全部聚合在一处,逻辑边界清晰。

三、核心差异深度解读

1. 逻辑复用:从 mixin 到 Composables(组合式函数)

  • 选项式 API 的痛点(mixin) :复用逻辑需写 mixin 文件,多个 mixin 易出现命名冲突,且无法清晰知道属性来源:

    // mixin/countMixin.js
    export default {
      data() { return { count: 0 }; },
      methods: { add() { this.count++; } }
    };
    // 组件中使用
    export default {
      mixins: [countMixin], // 引入后,count/add 混入组件,但来源不直观
    };
    
  • 组合式 API 的优势(Composables) :复用逻辑封装为纯函数,按需导入,属性来源清晰,无命名冲突:

    // composables/useCount.js
    import { ref } from 'vue';
    export const useCount = () => {
      const count = ref(0);
      const add = () => count.value++;
      return { count, add };
    };
    // 组件中使用
    <script setup>
    import { useCount } from './composables/useCount';
    const { count, add } = useCount(); // 明确导入,来源清晰
    </script>
    

2. 响应式原理:声明式 vs 手动式

  • 选项式 API:data 返回的对象会被 Vue 递归劫持(Object.defineProperty/Proxy),自动变成响应式,直接通过 this.xxx 访问;
  • 组合式 API:需手动用 ref(基本类型)/reactive(引用类型)创建响应式数据,ref 需通过 .value 访问(模板中自动解包),更灵活且可控。

3. 大型项目适配性

  • 选项式 API:组件复杂度提升后,同一业务逻辑的代码会分散在 data/methods/computed/watch 等多个选项中,形成 “面条代码”,维护成本高;
  • 组合式 API:可将复杂业务拆分为多个 Composables(如 useUser/useCart/useOrder),每个函数负责一个逻辑模块,代码结构清晰,便于多人协作和维护。

四、选型建议

场景 推荐 API
小型组件 / 快速原型 选项式 API
中大型项目 / 复杂逻辑 组合式 API
需兼容 Vue 2 + Vue 3 选项式 API(过渡)
用 TypeScript 开发 组合式 API

总结

  • 选项式 API 是 “面向选项” 的思维,适合入门和简单场景,符合传统前端的代码组织习惯;
  • 组合式 API 是 “面向逻辑” 的思维,解决了选项式 API 在复杂场景下的复用、维护痛点,是 Vue 3 的核心升级,也是大型项目的最佳实践。

两者并非互斥,Vue 3 完全兼容选项式 API,可根据项目规模和团队习惯灵活选择(甚至同一项目中混合使用)。

🔥3 kB 换 120 ms 阻塞? Axios 还是 fetch?

2026年1月2日 17:50

0. 先抛结论,再吵不迟

指标 Axios 1.7 fetch (原生)
gzip 体积 ≈ 3.1 kB 0 kB
阻塞时间(M3/4G) 120 ms 0 ms
内存峰值(1000 并发) 17 MB 11 MB
生产 P1 故障(过去一年) 2 次(拦截器顺序 bug) 0 次
开发体验(DX) 10 分 7 分

结论:

  • 极致性能/SSG/Edge → fetch 已足够;
  • 企业级、需要全局拦截、上传进度 → Axios 仍值得;
  • 二者可共存:核心链路与首页用 fetch,管理后台用 Axios。

1. 3 kB 到底贵不贵?

2026 年 1 月,HTTP Archive 最新采样(Chrome 桌面版)显示:

  • 中位 JS 体积 580 kB,3 kB 似乎“九牛一毛”;
  • 但放到首屏预算 100 kB 的站点(TikTok 推荐值),3 kB ≈ 3 % 预算,再加 120 ms 阻塞,LCP 直接从 1.5 s 飙到 1.62 s,SEO 评级掉一档。

“ bundle 每 +1 kB,4G 下 FCP +8 ms”——Lighthouse 2025 白皮书。


2. 把代码拍桌上:差异只剩这几行

下面 4 个高频场景,全部给出“可直接复制跑”的片段,差异一目了然。

2.1 自动 JSON + 错误码

// Axios:零样板
const {data} = await axios.post('/api/login', {user, pwd});

// fetch:两行样板
const res = await fetch('/api/login', {
  method:'POST',
  headers:{'Content-Type':'application/json'},
  body:JSON.stringify({user, pwd})
});
if (!res.ok) throw new Error(res.status);
const data = await res.json();

争议

  • Axios 党:少写两行,全年少写 3000 行。
  • fetch 党:gzip 后 3 kB 换两行?ESLint 模板一把就补全。

2.2 超时 + 取消

// Axios:内置
const source = axios.CancelToken.source();
setTimeout(() => source.cancel('timeout'), 5000);
await axios.get('/api/big', {cancelToken: source.token});

// fetch:原生 AbortController
const ctl = new AbortController();
setTimeout(() => ctl.abort(), 5000);
await fetch('/api/big', {signal: ctl.signal});

2025 之后 Edge/Node 22 已全支持,AbortSignal.timeout(5000) 一行搞定:

await fetch('/api/big', {signal: AbortSignal.timeout(5000)});

结论:语法差距已抹平。

2.3 上传进度条

// Axios:progress 事件
await axios.post('/upload', form, {
  onUploadProgress: e => setProgress(e.loaded / e.total)
});

// fetch:借助 `xhr` 或 `ReadableStream`
// 2026 仍无原生简易方案,需要封装 `xhr` 才能拿到 `progress`。

结论:大文件上传场景 Axios 仍吊打 fetch。

2.4 拦截器(token、日志)

// Axios:全局拦截
axios.interceptors.request.use(cfg => {
  cfg.headers.Authorization = `Bearer ${getToken()}`;
  return cfg;
});

// fetch:三行封装
export const $get = (url, opts = {}) => fetch(url, {
  ...opts,
  headers: {...opts.headers, Authorization: `Bearer ${getToken()}`}
});

经验:拦截器一旦>2 个,Axios 顺序地狱频发;fetch 手动链式更直观。


3. 实测!同一个项目,两套 bundle

测试场景

  • React 18 + Vite 5,仅替换 HTTP 层;
  • 构建目标:es2020 + gzip + brotli;
  • 网络:模拟 4G(RTT 150 ms);
  • 采样 10 次取中位。
指标 Axios fetch
gzip bundle 46.7 kB 43.6 kB
首屏阻塞时间 120 ms 0 ms
Lighthouse TTI 2.1 s 1.95 s
内存峰值(1000 并发请求) 17 MB 11 MB
生产报错(过去一年) 2 次拦截器顺序错乱 0

数据来自 rebrowser 2025 基准 ;阻塞时间差异与 51CTO 独立测试吻合 。


4. 什么时候一定要 Axios?

  1. 需要上传进度(onUploadProgress)且不想回退 xhr;
  2. 需要请求/响应拦截链 >3 层,且团队对“黑盒”可接受;
  3. 需要兼容 IE11(2026 年政务/银行仍存);
  4. 需要Node 16 以下老版本(fetch 需 18+)。

5. 共存方案:把 3 kB 花在刀刃上

// core/http.js
export const isSSR = typeof window === 'undefined';
export const HTTP = isSSR || navigator.connection?.effectiveType === '4g'
  ? { get: (u,o) => fetch(u,{...o, signal: AbortSignal.timeout(5000)}) }
  : await import('axios');   // 动态 import,只在非 4G 或管理后台加载

结果:

  • 首屏 0 kB;
  • 管理后台仍享受 Axios 拦截器;
  • 整体 bundle 下降 7 %,LCP −120 ms。

6. 一句话收尸

2026 年的浏览器,fetch 已把“缺的课”补完:取消、超时、Node 原生、TypeScript 完美。
3 kB 的 Axios 不再是“默认”,而是“按需”。
上传进度、深链拦截、老浏览器——用 Axios;
其余场景,让首页飞一把,把 120 ms 还给用户。

在ios上动态插入元素的列表使用:last-child样式可能不能及时生效

作者 tjswk2008
2026年1月2日 17:39

这是一个在 iOS Safari(Webkit 引擎)中比较经典的渲染 Bug。当通过 JavaScript 动态向容器末尾添加元素时,Webkit 有时未能正确触发重绘(Repaint)或样式重新计算(Style Recalculation),导致 :last-child 伪类仍然停留在之前的元素上。

以下是解决这个问题的几种常用方案,按推荐程度排序:

1. 强制触发重绘 (Force Reflow)

这是最简单直接的“黑科技”。在插入元素的代码后面,读取一次容器或新元素的某个布局属性(如 offsetHeight),强制浏览器同步计算样式。

JavaScript

const container = document.getElementById('container');
const newItem = document.createElement('div');
container.appendChild(newItem);

// 强制触发重绘
container.offsetHeight; 

2. 使用 Flex/Grid 的 gap 属性 (推荐)

如果你的 :last-child 主要是为了处理间距(例如 margin-bottom: 0),那么弃用 :last-child 改用 gap 是最现代且根本的解决方案。gap 会自动处理元素间的间距,不需要判断谁是最后一个。

CSS

.container {
  display: flex;
  flex-direction: column;
  gap: 10px; /* 元素之间自动产生 10px 间距,最后一个元素后方不会有间距 */
}

3. 反向思路:使用 :not(:last-child)

有时候 Webkit 对“谁是最后一个”反应迟钝,但对“谁不是最后一个”反应较快。尝试给所有非末尾元素设置样式:

CSS

/* 不推荐 */
/* .item { margin-bottom: 10px; } */
/* .item:last-child { margin-bottom: 0; } */

/* 推荐 */
.item:not(:last-child) {
  margin-bottom: 10px;
}

4. 手动切换 Class

如果逻辑比较复杂,伪类失效频繁,建议放弃 CSS 伪类,改用 JavaScript 在插入时手动维护一个 .is-last 类。

JavaScript

// 插入逻辑
const items = container.querySelectorAll('.item');
items.forEach(el => el.classList.remove('is-last'));
const lastItem = items[items.length - 1];
lastItem.classList.add('is-last');

为什么会发生这种情况?

Webkit 引擎为了性能优化,会尽量减少样式重新计算的频率。当 DOM 树发生变化时,它本应标记该容器为 "dirty" 并重新检查伪类状态,但在某些复杂的嵌套布局或特定的 iOS 版本中,这个触发机制会漏掉对 :last-child 的检查。

建议: 如果你的项目环境允许(iOS 14.1+),优先使用 Flexbox/Grid 的 gap。它不仅性能更好,还能彻底规避此类由于动态插入导致的伪类失效问题。

Vue 组件通信的 8 种最佳实践,你知道几种?

作者 刘大华
2026年1月2日 17:11

经常写 Vue 的朋友应该很熟悉,在 Vue 的应用中,组件化开发可以让我们的代码更容易维护,而组件之间的数据传递事件通信也是我们必须要解决的问题。

经过多个项目的实践,我逐渐摸清了Vue3中8种组件通信方式和适用场景。

下面来给大家分享一下。

1. Props / Emits:最基础的父子传值

这是 Vue 的官方推荐通信方式,遵循单向数据流原则,数据只能从上往下流,事件从下往上传。

Props:父传子的单向数据流

适用场景:当你需要把配置、用户信息、状态等数据从父组件传递给子组件时。

<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <!-- 传递静态和动态数据 -->
    <ChildComponent 
      title="用户信息" 
      :user="userData"
      :count="clickCount"
    />
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'

const userData = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
})

const clickCount = ref(0)
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <h3>{{ title }}</h3>
    <div class="user-card">
      <p>姓名:{{ user.name }}</p>
      <p>年龄:{{ user.age }}</p>
      <p>邮箱:{{ user.email }}</p>
    </div>
    <p>点击次数:{{ count }}</p>
  </div>
</template>

<script setup>
// 方式1:简单定义
// defineProps(['title', 'user', 'count'])

// 方式2:带类型验证(推荐)
defineProps({
  title: {
    type: String,
    required: true
  },
  user: {
    type: Object,
    default: () => ({})
  },
  count: {
    type: Number,
    default: 0
  }
})

// 方式3:使用 TypeScript(最佳实践)
interface Props {
  title: string
  user: {
    name: string
    age: number
    email: string
  }
  count?: number
}

defineProps<Props>()
</script>

为什么推荐带验证?

它能提前发现传参错误,比如把字符串传给了 count,Vue 会在控制台报错,避免线上bug。


Emits:子传父的事件机制

适用场景:子组件需要通知父组件有事发生,比如表单提交、按钮点击、输入变化等。

<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <button @click="handleButtonClick">通知父组件</button>
    <input 
      :value="inputValue" 
      @input="handleInputChange"
      placeholder="输入内容..."
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 定义可触发的事件
const emit = defineEmits(['button-clicked', 'input-changed', 'update:modelValue'])

const inputValue = ref('')

const handleButtonClick = () => {
  // 触发事件并传递数据
  emit('button-clicked', {
    message: '按钮被点击了!',
    timestamp: new Date().toISOString()
  })
}

const handleInputChange = (event) => {
  inputValue.value = event.target.value
  emit('input-changed', inputValue.value)
  
  // 支持 v-model 的更新方式
  emit('update:modelValue', inputValue.value)
}
</script>
<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <ChildComponent 
      @button-clicked="handleChildButtonClick"
      @input-changed="handleChildInputChange"
    />
    
    <div v-if="lastEvent">
      <p>最后收到的事件:{{ lastEvent.type }}</p>
      <p>数据:{{ lastEvent.data }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const lastEvent = ref(null)

const handleChildButtonClick = (data) => {
  lastEvent.value = {
    type: 'button-clicked',
    data: data
  }
  console.log('收到子组件消息:', data)
}

const handleChildInputChange = (value) => {
  lastEvent.value = {
    type: 'input-changed',
    data: value
  }
  console.log('输入内容:', value)
}
</script>

关键点:

  • 子组件不直接修改父组件数据,而是发出请求,由父组件决定如何处理。
  • 这种解耦设计让组件更可复用、更易测试。

2. v-model:双向绑定的语法糖

v-model 在 Vue3 中变得更加强大,支持多个 v-model 绑定。

基础用法

<!-- 父组件 -->
<template>
  <div>
    <CustomInput v-model="username" />
    <p>当前用户名:{{ username }}</p>
    
    <!-- 多个 v-model -->
    <UserForm
      v-model:name="userName"
      v-model:email="userEmail"
      v-model:age="userAge"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const username = ref('')
const userName = ref('')
const userEmail = ref('')
const userAge = ref(0)
</script>
<!-- 子组件 CustomInput.vue -->
<template>
  <div class="custom-input">
    <label>用户名:</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="input-field"
    />
  </div>
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 子组件 UserForm.vue -->
<template>
  <div class="user-form">
    <div class="form-group">
      <label>姓名:</label>
      <input
        :value="name"
        @input="$emit('update:name', $event.target.value)"
      />
    </div>
    <div class="form-group">
      <label>邮箱:</label>
      <input
        :value="email"
        @input="$emit('update:email', $event.target.value)"
        type="email"
      />
    </div>
    <div class="form-group">
      <label>年龄:</label>
      <input
        :value="age"
        @input="$emit('update:age', parseInt($event.target.value) || 0)"
        type="number"
      />
    </div>
  </div>
</template>

<script setup>
defineProps({
  name: String,
  email: String,
  age: Number
})

defineEmits(['update:name', 'update:email', 'update:age'])
</script>

v-model的核心优势:

  • 语法简洁,减少样板代码
  • 符合双向绑定的直觉
  • 支持多个v-model绑定
  • 类型安全(配合TypeScript)

适用场景:自定义表单控件(如日期选择器、富文本编辑器)需要双向绑定。


3. Ref / 模板引用:直接操作组件

当需要直接访问子组件或 DOM 元素时,模板引用是最佳选择。

<!-- 父组件 -->
<template>
  <div class="parent">
    <ChildComponent ref="childRef" />
    <CustomForm ref="formRef" />
    <video ref="videoRef" controls>
      <source src="./movie.mp4" type="video/mp4">
    </video>
    
    <div class="controls">
      <button @click="focusInput">聚焦输入框</button>
      <button @click="getChildData">获取子组件数据</button>
      <button @click="playVideo">播放视频</button>
      <button @click="validateForm">验证表单</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue'

// 创建引用
const childRef = ref(null)
const formRef = ref(null)
const videoRef = ref(null)

// 确保 DOM 更新后访问
const focusInput = async () => {
  await nextTick()
  childRef.value?.focusInput()
}

const getChildData = () => {
  if (childRef.value) {
    const data = childRef.value.getData()
    console.log('子组件数据:', data)
  }
}

const playVideo = () => {
  videoRef.value?.play()
}

const validateForm = () => {
  formRef.value?.validate()
}

// 组件挂载后访问
onMounted(() => {
  console.log('子组件实例:', childRef.value)
})
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <input ref="inputEl" type="text" placeholder="请输入..." />
    <p>内部数据:{{ internalData }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const inputEl = ref(null)
const internalData = ref('这是内部数据')

// 暴露给父组件的方法和数据
defineExpose({
  focusInput: () => {
    inputEl.value?.focus()
  },
  getData: () => {
    return {
      internalData: internalData.value,
      timestamp: new Date().toISOString()
    }
  },
  internalData
})
</script>

适用场景:需要调用子组件方法(如弹窗打开)、聚焦输入框、操作原生元素(如 video 播放)。

4. Provide / Inject:跨层级数据传递

解决"prop 逐级传递"问题,实现祖先与后代组件的直接通信。

<!-- 根组件 App.vue -->
<template>
  <div id="app">
    <Header />
    <div class="main-content">
      <Sidebar />
      <ContentArea />
    </div>
    <Footer />
  </div>
</template>

<script setup>
import { provide, ref, reactive, computed } from 'vue'

// 提供用户信息
const currentUser = ref({
  id: 1,
  name: '张三',
  role: 'admin',
  permissions: ['read', 'write', 'delete']
})

// 提供应用配置
const appConfig = reactive({
  theme: 'dark',
  language: 'zh-CN',
  apiBaseUrl: import.meta.env.VITE_API_URL
})

// 提供方法
const updateUser = (newUserData) => {
  currentUser.value = { ...currentUser.value, ...newUserData }
}

const updateConfig = (key, value) => {
  appConfig[key] = value
}

// 计算属性
const userPermissions = computed(() => currentUser.value.permissions)

// 提供数据和方法
provide('currentUser', currentUser)
provide('appConfig', appConfig)
provide('updateUser', updateUser)
provide('updateConfig', updateConfig)
provide('userPermissions', userPermissions)
</script>
<!-- 深层嵌套的组件 ContentArea.vue -->
<template>
  <div class="content-area">
    <UserProfile />
    <ArticleList />
  </div>
</template>

<script setup>
// 这个组件不需要处理 props,直接渲染子组件
</script>
<!-- 使用注入的组件 UserProfile.vue -->
<template>
  <div class="user-profile">
    <h3>用户信息</h3>
    <div class="profile-card">
      <p>姓名:{{ currentUser.name }}</p>
      <p>角色:{{ currentUser.role }}</p>
      <p>权限:{{ userPermissions.join(', ') }}</p>
      <p>主题:{{ appConfig.theme }}</p>
    </div>
    <button @click="handleUpdateProfile">更新资料</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 注入数据和方法
const currentUser = inject('currentUser')
const appConfig = inject('appConfig')
const userPermissions = inject('userPermissions')
const updateUser = inject('updateUser')

const handleUpdateProfile = () => {
  updateUser({
    name: '李四',
    role: 'user'
  })
}
</script>

Provide/Inject的优势

  • 避免Props逐层传递的繁琐
  • 实现跨层级组件通信
  • 提供全局状态和方法的统一管理
  • 提高代码的可维护性

适用场景:当数据需要从顶层组件传递到底层组件,中间隔了好几层(比如主题、用户信息、语言设置)。


5. Pinia:现代化状态管理

对于复杂应用,Pinia 提供了更优秀的状态管理方案。

创建 Store

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    isLoggedIn: false,
    token: '',
    permissions: []
  }),
  
  getters: {
    userName: (state) => state.user?.name || '未登录用户',
    isAdmin: (state) => state.user?.role === 'admin',
    hasPermission: (state) => (permission) => 
      state.permissions.includes(permission)
  },
  
  actions: {
    async login(credentials) {
      try {
        // 模拟 API 调用
        const response = await mockLoginApi(credentials)
        
        this.user = response.user
        this.token = response.token
        this.isLoggedIn = true
        this.permissions = response.permissions
        
        // 保存到 localStorage
        localStorage.setItem('token', this.token)
        
        return { success: true }
      } catch (error) {
        console.error('登录失败:', error)
        return { success: false, error: error.message }
      }
    },
    
    logout() {
      this.user = null
      this.token = ''
      this.isLoggedIn = false
      this.permissions = []
      
      localStorage.removeItem('token')
    },
    
    async updateProfile(userData) {
      if (!this.isLoggedIn) {
        throw new Error('请先登录')
      }
      
      this.user = { ...this.user, ...userData }
      // 这里可以调用 API 更新后端数据
    }
  }
})

// 模拟登录 API
const mockLoginApi = (credentials) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        user: {
          id: 1,
          name: credentials.username,
          role: 'admin'
        },
        token: 'mock-jwt-token',
        permissions: ['read', 'write', 'delete']
      })
    }, 1000)
  })
}

在组件中使用 Store

<!-- UserProfile.vue -->
<template>
  <div class="user-profile">
    <div v-if="userStore.isLoggedIn" class="logged-in">
      <h3>欢迎回来,{{ userStore.userName }}!</h3>
      <div class="user-info">
        <p>角色:{{ userStore.user.role }}</p>
        <p>权限:{{ userStore.permissions.join(', ') }}</p>
      </div>
      
      <div class="actions">
        <button 
          @click="updateName" 
          :disabled="!userStore.hasPermission('write')"
        >
          更新姓名
        </button>
        <button @click="userStore.logout" class="logout-btn">
          退出登录
        </button>
      </div>
    </div>
    
    <div v-else class="logged-out">
      <LoginForm />
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import LoginForm from './LoginForm.vue'

const userStore = useUserStore()

const updateName = () => {
  userStore.updateProfile({
    name: `用户${Math.random().toString(36).substr(2, 5)}`
  })
}
</script>
<!-- LoginForm.vue -->
<template>
  <div class="login-form">
    <h3>用户登录</h3>
    <form @submit.prevent="handleLogin">
      <div class="form-group">
        <input 
          v-model="credentials.username" 
          placeholder="用户名"
          required
        />
      </div>
      <div class="form-group">
        <input 
          v-model="credentials.password" 
          type="password" 
          placeholder="密码"
          required
        />
      </div>
      <button type="submit" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </form>
    
    <div v-if="message" class="message" :class="messageType">
      {{ message }}
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const credentials = reactive({
  username: '',
  password: ''
})

const loading = ref(false)
const message = ref('')
const messageType = ref('')

const handleLogin = async () => {
  loading.value = true
  message.value = ''
  
  const result = await userStore.login(credentials)
  
  if (result.success) {
    message.value = '登录成功!'
    messageType.value = 'success'
  } else {
    message.value = `登录失败:${result.error}`
    messageType.value = 'error'
  }
  
  loading.value = false
}
</script>

Pinia 优势:

  • 无 mutations,直接修改 state
  • 完美支持 TypeScript
  • DevTools 调试友好
  • 模块化设计,易于拆分

适用场景:中大型应用,多个组件需要共享复杂状态(如用户登录态、购物车、全局配置)。

6. 事件总线:轻量级全局通信

Vue3 移除了实例上的 onon、off 方法,不再支持这种模式,但我们可以使用 mitt 库实现。

// utils/eventBus.js
import mitt from 'mitt'

// 创建全局事件总线
const eventBus = mitt()

// 定义事件类型
export const EVENTS = {
  USER_LOGIN: 'user:login',
  USER_LOGOUT: 'user:logout',
  NOTIFICATION_SHOW: 'notification:show',
  MODAL_OPEN: 'modal:open',
  THEME_CHANGE: 'theme:change'
}

export default eventBus
<!-- 发布事件的组件 -->
<template>
  <div class="publisher">
    <h3>事件发布者</h3>
    <div class="buttons">
      <button @click="sendNotification">发送通知</button>
      <button @click="openModal">打开模态框</button>
      <button @click="changeTheme">切换主题</button>
    </div>
  </div>
</template>

<script setup>
import eventBus, { EVENTS } from '@/utils/eventBus'

const sendNotification = () => {
  eventBus.emit(EVENTS.NOTIFICATION_SHOW, {
    type: 'success',
    title: '操作成功',
    message: '这是一个来自事件总线的通知',
    duration: 3000
  })
}

const openModal = () => {
  eventBus.emit(EVENTS.MODAL_OPEN, {
    component: 'UserForm',
    props: { userId: 123 },
    title: '用户表单'
  })
}

const changeTheme = () => {
  const themes = ['light', 'dark', 'blue']
  const randomTheme = themes[Math.floor(Math.random() * themes.length)]
  
  eventBus.emit(EVENTS.THEME_CHANGE, {
    theme: randomTheme,
    timestamp: new Date().toISOString()
  })
}
</script>
<!-- 监听事件的组件 -->
<template>
  <div class="listener">
    <h3>事件监听者</h3>
    <div class="events-log">
      <div 
        v-for="(event, index) in events" 
        :key="index"
        class="event-item"
      >
        <strong>{{ event.type }}</strong>
        <span>{{ event.data }}</span>
        <small>{{ event.timestamp }}</small>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus, { EVENTS } from '@/utils/eventBus'

const events = ref([])

// 事件处理函数
const handleNotification = (data) => {
  events.value.unshift({
    type: EVENTS.NOTIFICATION_SHOW,
    data: `通知: ${data.title} - ${data.message}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

const handleModalOpen = (data) => {
  events.value.unshift({
    type: EVENTS.MODAL_OPEN,
    data: `打开模态框: ${data.component}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

const handleThemeChange = (data) => {
  events.value.unshift({
    type: EVENTS.THEME_CHANGE,
    data: `主题切换为: ${data.theme}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

// 注册事件监听
onMounted(() => {
  eventBus.on(EVENTS.NOTIFICATION_SHOW, handleNotification)
  eventBus.on(EVENTS.MODAL_OPEN, handleModalOpen)
  eventBus.on(EVENTS.THEME_CHANGE, handleThemeChange)
})

// 组件卸载时移除监听
onUnmounted(() => {
  eventBus.off(EVENTS.NOTIFICATION_SHOW, handleNotification)
  eventBus.off(EVENTS.MODAL_OPEN, handleModalOpen)
  eventBus.off(EVENTS.THEME_CHANGE, handleThemeChange)
})
</script>

不太推荐使用。为什么?

  • 数据流向不透明,难以追踪
  • 容易忘记 off 导致内存泄漏
  • 大型项目维护困难
  • 建议:优先用 Pinia 或 provide/inject

适用场景:小型项目中,两个无关联组件需要临时通信(如通知弹窗、模态框控制)。


7. 属性透传($attrs)和边界处理

当你封装一个组件,并希望把未声明的属性自动传递给内部元素时,就用 $attrs。

<!-- 基础组件 BaseButton.vue -->
<template>
  <button 
    v-bind="filteredAttrs"
    class="base-button"
    @click="handleClick"
  >
    <slot></slot>
  </button>
</template>

<script setup>
import { computed, useAttrs } from 'vue'

const attrs = useAttrs()

// 过滤掉不需要透传的属性
const filteredAttrs = computed(() => {
  const { class: className, style, ...rest } = attrs
  return rest
})

const emit = defineEmits(['click'])

const handleClick = (event) => {
  emit('click', event)
}

// 也可以选择性地暴露 attrs
defineExpose({
  attrs
})
</script>
</style>
<!-- 使用基础组件 -->
<template>
  <div>
    <!-- 透传 class、style、data-* 等属性 -->
    <BaseButton
      class="custom-btn"
      style="color: red;"
      data-testid="submit-button"
      title="提交按钮"
      @click="handleSubmit"
    >
      提交表单
    </BaseButton>
    
    <!-- 多个按钮使用相同的基组件 -->
    <BaseButton
      class="secondary-btn"
      data-testid="cancel-button"
      @click="handleCancel"
    >
      取消
    </BaseButton>
  </div>
</template>

<script setup>
const handleSubmit = () => {
  console.log('提交表单')
}

const handleCancel = () => {
  console.log('取消操作')
}
</script>

<style>
.custom-btn {
  background: blue;
  color: white;
}

.secondary-btn {
  background: gray;
  color: white;
}
</style>

特性

  • 用户传的 class 和 style 会和组件内部的样式合并(Vue 自动处理)。
  • 所有 data-、title、aria- 等原生 HTML 属性都能正常生效。
  • 你不用提前知道用户会传什么,也能支持!

适用场景:封装通用组件(如按钮、输入框),希望保留原生 HTML 属性(class、style、data-* 等)。

8. 组合式函数:逻辑复用

对于复杂的通信逻辑,可以使用组合式函数封装。

// composables/useCommunication.js
import { ref, onUnmounted } from 'vue'

export function useCommunication() {
  const messages = ref([])
  const listeners = new Map()

  const sendMessage = (type, data) => {
    messages.value.unshift({
      type,
      data,
      timestamp: new Date().toISOString()
    })
    
    // 通知监听者
    if (listeners.has(type)) {
      listeners.get(type).forEach(callback => {
        callback(data)
      })
    }
  }

  const onMessage = (type, callback) => {
    if (!listeners.has(type)) {
      listeners.set(type, new Set())
    }
    listeners.get(type).add(callback)
  }

  const offMessage = (type, callback) => {
    if (listeners.has(type)) {
      listeners.get(type).delete(callback)
    }
  }

  // 清理函数
  const cleanup = () => {
    listeners.clear()
  }

  onUnmounted(cleanup)

  return {
    messages,
    sendMessage,
    onMessage,
    offMessage,
    cleanup
  }
}
<!-- 使用组合式函数 -->
<template>
  <div class="communication-demo">
    <div class="senders">
      <MessageSender />
      <EventSender />
    </div>
    <div class="receivers">
      <MessageReceiver />
      <EventReceiver />
    </div>
    <div class="message-log">
      <h4>消息日志</h4>
      <div 
        v-for="(msg, index) in messages" 
        :key="index"
        class="log-entry"
      >
        [{{ formatTime(msg.timestamp) }}] {{ msg.type }}: {{ msg.data }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { useCommunication } from '@/composables/useCommunication'
import MessageSender from './MessageSender.vue'
import MessageReceiver from './MessageReceiver.vue'
import EventSender from './EventSender.vue'
import EventReceiver from './EventReceiver.vue'

const { messages } = useCommunication()

const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString()
}
</script>

优势

  • 逻辑高度复用
  • 类型安全(配合 TS)
  • 易于单元测试

适用场景:将复杂的通信逻辑抽象成可复用的函数,比如 WebSocket 连接、本地存储同步等。


避坑指南

1. Props 设计原则

// 好的 Props 设计
defineProps({
  // 必需属性
  title: { type: String, required: true },
  
  // 可选属性带默认值
  size: { type: String, default: 'medium' },
  
  // 复杂对象
  user: { 
    type: Object, 
    default: () => ({ name: '', age: 0 }) 
  },
  
  // 验证函数
  count: {
    type: Number,
    validator: (value) => value >= 0 && value <= 100
  }
})

2. 事件命名规范

// 使用 kebab-case 事件名
defineEmits(['update:title', 'search-change', 'form-submit'])

// 避免使用驼峰命名
// defineEmits(['updateTitle']) // 不推荐

3. Provide/Inject 的响应性

// 保持响应性
const data = ref({})
provide('data', readonly(data))

// 提供修改方法
const updateData = (newData) => {
  data.value = { ...data.value, ...newData }
}
provide('updateData', updateData)

4. 内存泄漏预防

// 及时清理事件监听
onUnmounted(() => {
  eventBus.off('some-event', handler)
})

// 清理定时器
const timer = setInterval(() => {}, 1000)
onUnmounted(() => clearInterval(timer))

总结

经过上面的详细讲解,相信大家对 Vue3 的组件通信有了更深入的理解。让我最后做个总结:

  • 核心原则:根据组件关系选择合适方案
  • 父子组件:优先使用 Props/Emits,简单直接
  • 表单控件:v-model是最佳选择,语法优雅
  • 深层嵌套:Provide/Inject 避免 prop 透传地狱
  • 全局状态:Pinia 专业强大,适合复杂应用
  • 临时通信:事件总线可用但需谨慎
  • 组件封装:属性透传提供更好用户体验
  • 逻辑复用:组合式函数提升代码质量

在实际开发中,可以这样:

  1. 先从 Props/Emits 开始,这是基础
  2. 熟练掌握 v-model 的表单处理
  3. 在需要时引入 Pinia,不要过度设计
  4. 保持代码的可读性和可维护性

简单的需求用简单的方案,复杂的需求才需要复杂的工具。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计》

《代码里全是 new 对象,真的很 Low 吗?我认真想了一晚》

《这 5 个冷门 HTML 标签,让我直接删了100 行 JS 代码》

《这 10 个 Vue3 性能优化技巧很实用,但很多项目都没用上》

CSS 写 SQL 查询?后端慌了!

作者 小小荧
2026年1月2日 17:09

CSS 写 SQL 查询?后端慌了!

cover_image

初次接触到这个项目时,我的第一反应只有四个字

这也行?

最近在 X 上大火的一个叫 TailwindSQL 的项目,引发了广泛讨论。

其核心玩法非常简单——通过 CSS 的 className 来实现 SQL 查询功能。

前端发展到这个地步了吗?

让我们先看一个示例:

<DB className="db-users-name-where-id-1" />

如果你是前端开发者,可能会下意识地认为这是在定义样式

但如果你是后端开发者,估计已经开始皱眉了。

然而实际上,这段代码执行的是:

SELECT name FROM users WHERE id = 1;

看到这里,我确实愣了一下。

TailwindSQL 的本质

简而言之,它将 SQL 语句拆解为一个个「类名」片段。

这种做法类似于 TailwindCSSCSS 的处理方式:

db-users
db-users-name
db-users-name-where-id-1
db-products-orderby-price-desc

这些 className 最终会被解析为 SQL 语句,并在 React Server Components 中直接执行。

你甚至无需编写 API 接口,也无需使用 ORM 框架。

这个方案可靠吗?

从工程实践的角度来看,答案其实很明确:

并不可靠。

SQL 的复杂性,从来不是语法层面的问题。

真正的挑战在于:

  • 表关系管理

  • 复杂 JOIN 操作

  • 嵌套子查询

  • 事务控制

  • 权限验证

  • 边界条件处理

一旦查询逻辑稍显复杂,className 就会变得越来越冗长,最终形成一串难以维护的代码片段。

说实话,我很难想象在实际项目中,会有开发者认真地写出这样的代码:

className="db-orders-user-products-joinwhere-user-age-gt-18and-order-status-paidgroupby-user-id"

这已经不再是 DSL(领域特定语言)了,而是一种折磨。

我认为 TailwindSQL 很难在生产环境中得到应用,它更像是 vibe coding(氛围编程)的产物。

是否使用?可以了解一下,然后继续编写你熟悉的 SQL 吧。

  • TailwindSQL 官网https://tailwindsql.com/

前端面试题整理(方便自己看的)

2026年1月2日 16:46

JavaScript题

1.JavaScript中的数据类型?

JavaScript中,分两种类型:

  • 基本类型
  • 引用类型

基本类型主要有以下6种:Number、String、Boolean、Undefined、null、symbol。 引用类型主要有Object、Array、Function。其它的有Date、RegExp、Map等

2.DOM

文档对象模型(DOM)HTMLXML文档的编程接口。 日常开发离不开DOM的操作,对节点的增删改查等操作。在以前,使用Jquery,zepto等库来操作DOM,之后在vue,Angular,React等框架出现后,通过操作数据来控制DOM(多数情况下),越来越少的直接去操作DOM

3.BOM

3.1 BOM是什么?

BOM(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。其作用就是跟浏览器做一些交互效果,比如:进行页面的后退、前进、刷新、浏览器窗口发生变化,滚动条滚动等。

3.2 window

Bom的核心对象是window,它表示浏览器的一个实例。 在浏览器中,window即是浏览器窗口的一个接口,又是全局对象。

4 == 和 === 区别,分别在什么情况使用

image.png

等于操作符用俩个等于号(==)表示,如果操作数相等,则会返回true。 等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等。

全等操作符由3个等于号(===)表示,只有俩个操作数在不转换的前提下相等才返回true,即类型相同,值也相同。

区别:等于操作符(==)会做类型转换再进行值的比较,全等操作符不会做类型转换。 nullundefined 比较,相等操作符为true 全等为false

5 typeof 和 instanceof 的区别

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

instanceof 运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

区别:

  • typeof 会返回一个变量的基本类型,instanceof 返回的是一个Boolean.
  • instanceof 可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型。
  • 如果需要通用检测数据类型,可以通过Object.prototype.toString,调用该方法,统一返回格式 [object XXX]的字符串。

6 JavaScript 原型,原型链?有什么特点?

原型

JavaScript常被描述为一种基于原型的语言---每个对象拥有一个原型对象。访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或达到原型链的末尾。

原型链

原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

在对象实例和它的构造器之间建立一个链接(它是_proto_属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

  • 一切对象都是继承自Object对象,object对象直接继承根源对象null
  • 一切的函数对象(包括object对象),都是继承自Function对象
  • Object 对象直接继承自 Function 对象
  • Function 对象的 _proto_ 会指向自己的原型对象,最终还是继承自 Object 对象

7.对作用域链的理解

作用域,即变量和函数生效的区域或集合,作用域决定了代码区块中变量和其他资源的可见性。

  • 全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在任意位置访问。
  • 函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的,它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
  • 块级作用域:ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号外面不能访问这些变量。

作用域链

当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。

8. 谈谈对this对象的理解

8.1定义

函数的this关键字在JavaScript中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在绝大数情况下,函数的调用方式决定了this的值。 this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。

8.2 new绑定

通过构造函数new 关键字生成一个实例对象,此时this指向这个实例对象。

apply()、call()、bind()、是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这个第一个参数。

8.3 箭头函数

在ES6的语法中,提供了箭头函数法,让我们在代码书写时就能确定this的指向。

9.new操作符具体干了什么

  • 创建一个新的对象
  • 将对象与构建函数通过原型链链接起来
  • 将构建函数中的this绑定到新建的对象上
  • 根据构建函数返回类型做判断,如果原始值则被忽略,如果是返回对象,需要正常处理。

10.bind、call、apply区别?

bindcallapply、作用是改变函数执行时的上下文,改变函数运行时的this指向。

区别:

  • 三者都可以改变函数的this指向
  • 三者第一个参数都是this要指向的对象,如果没有这个参数或者参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分多次传入
  • bind是返回绑定this之后的函数,applycall则是立即执行

11.闭包的理解?闭包使用场景?

11.1 闭包是什么?

一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让你可以在一个内层函数中访问到其外层函数的作用域。

11.2 闭包使用场景

  • 创建私有变量
  • 延长变量的生命周期

11.3 柯里化函数

柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。

11.4 闭包的缺点

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能有负面影响。

12.深拷贝浅拷贝的区别?实现一个深拷贝?

12.1 浅拷贝

Object.assignArray.prototype.slice()Array.prototype.concat()拓展运算符实现复制。

var obj = {
    name: 'xxx',
    age: 17
}
var newObj = Object.assign({}, obj);
const a = [1,2,3];
const b = a.slice(0);
b[1] = 4;
console.log(a, b);// [1,2,3] [1,4,3]
const a = [1,2,3];
const b = [...a];
b[1] = 4;
console.log(a, b);// [1,2,3] [1,4,3]

12.2 深拷贝

常见深拷贝方式:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归
const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

// 有缺点 会忽略undefined、symbol、函数
const obj2=JSON.parse(JSON.stringify(obj1));

循环递归

function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return obj; //null或者undefined就不拷贝
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    // 可能是对象或者普通的值 如果是函数的话不拷贝
    if (typeof obj !== "object") return obj;
    // 是对象的话就要进行深拷贝
    if (hash.get(obj)) return hash.get(obj);
    let cloneObj = new obj.constructor();
    hash.set(obj, cloneObj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 实现一个递归拷贝
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
    return cloneObj;
}

12.3 区别

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存,修改对象属性会影响原对象。

深拷贝会另外创建一个一模一样的对象,不共享内存,不影响原对象。

13. JavaScript字符串的常用方法

let stringValue = "hello world"; 
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
// 删除前、后或者前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
// 接收一个整数参数,将字符串复制多少次,返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na
  • toUpperCase()、toLowerCase() 大小写转化
  • indexOf() 从字符串开头去搜索传入的字符串,并返回位置(没找到返回-1)
  • includes() 字符串是否包含传入的字符串
  • split() 把字符串按照指定分隔符,拆分成数组
  • replace() 接收俩个参数,第一个参数为匹配的内容,第二个参数为替换的元素

14.数组常用方法

  • push() 添加到数组末尾
  • unshift() 在数组开头添加
  • splice() 传入3个参数,开始位置、0(要删除的元素数量)、插入的元素
  • concat() 合并数组,返回一个新数组

  • pop() 删除数组最后一项,返回被删除的项。
  • shift() 删除数组的第一项,返回被删除的项。
  • splice()传入两个参数,开始位置,删除元素的数量,返回包含删除元素的数组。
  • slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组。

  • indexOf() 返回要查找元素在数组中的位置,如果没找到则返回 -1.
  • includes() 返回查找元素是否在数组中,有返回true,否则false.
  • find() 返回第一个匹配的元素。

排序方法

  • reverse() 将数组元素方向反转
  • sort() 接受一个比较函数,用于判断那个值在前面
function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15

转换方法

join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。

循环方法

some() 和 every() 方法一样

对数组每一项都运行传入的测试函数,如果至少有一个元素返回true,则这个方法返回true.

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true
forEach()

对数组每一项都运行传入的函数,没有返回值

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    //执行操作
});
filter()

函数返回true 的项会组成数组之后返回。

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
map()

返回由每次函数调用的结果构成的数组。

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2

15.事件循环的理解?

事件循环

JavaScript是一门单线程的语言,实现单线程非阻塞的方法就是事件循环。 在JavaScript中,所有的任务都可以分为:

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程执行。
  • 异步任务:异步的比如ajax网络请求,setTimeout定时函数等。

image.png

同步任务进入主线程,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程不断重复就是事件循环

宏任务与微任务
console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)
  • 遇到 console.log(1),直接打印1
  • 遇到定时器,属于新的宏任务,留着后面执行
  • 遇到 new Promise,这个是直接执行的,打印'newPromise
  • .then 属于微任务,放入微任务队列,后面再执行
  • 遇到 console.log(3)直接打印 3
  • 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现.then 的回调,执行它打印'then'
  • 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

结果是:1=>'new Promise'=> 3 => 'then' => 2

异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取。

微任务

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • process.nextTice(node.js)
宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合.

常见的宏任务有:

  • script(可以理解为外层同步代码)
  • setTimeout/setInterval
  • Ul rendering/Ul事件
  • postMessage、MessageChannel
  • setlmmediate、1/0(Node.is) 这时候,事件循环,宏任务,微任务的关系如图所示

image.png

它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
async 与 await

async就是用来声明一个异步方法,await是用来等待异步方法执行。

async函数返回一个promise对象,下面代码是等效的:

function f() {
    return Promise.resolve('TEST');
}
async function asyncF() {
    return 'TEST';
}

正常情况下, await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async function f() {
    // 等同于 return 123
    return await 123
}
f().then(i => console.log(i)) // 123

不管 await 后面跟着的是什么,await 都会阻塞后面的代码。

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}
async function fn2 (){
    console.log('fn2')
}
fn1()
console.log(3)

await 会阻塞下面的代码(即加入微任务队列),上面的例子中,先执行 async 外面的同步代码同步代码执行完,再回到 async函数中,再执行之前阻塞的代码

输出:1,fn2,3,2

async function async1() {
    console.log('1')
    await async2()
    console.log('2')
}
async function async2() {
    console.log('3')
}
console.log('4')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('5')
    resolve()
}).then(function () {
    console.log('6')
})
console.log('7');
// 输出结果: 4 1 3 5 7 2 6 settimeout

分析过程:

  • 1.执行整段代码,遇到 console.log('4')直接打印结果,输出 4;
  • 2.遇到定时器了,它是宏任务,先放着不执行;
  • 3.遇到 async1(),执行 async1 函数,先打印 1 ,下面遇到 await 怎么办?先执行 async2,打印 3,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  • 4.跳到 new Promise 这里,直接执行,打印 5,下面遇到 .then(),它是微任务,放到微任务列表等待执行;
  • 5.最后一行直接打印 7 ,现在同步代码执行完了,开始执行微任务,即 await 下面的代码,打印 2;
  • 6.继续执行下一个微任务,即执行 then 的回调,6;
  • 7.上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout所以最后的结果是: 4 1 3 5 7 2 6 settimeout

16.JavaScript本地存储方式有哪些?区别及应用场景?

16.1 方式

javaScript 本地缓存的方法主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB
16.1.1.cookie

Cookie ,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题。 作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie 有效期、安全性、使用范围的可选属性组成。

但是 cookie 在每次请求中都会被发送,如果不使用 HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险。

16.1.2 localStorage
  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
  • 存储的信息在同一域中是共享的。
  • 当本页操作(新增、修改、删除)了 localStorage 的时候,本页面不会触发 storage 事件,但是别的页面会触发 storage 事件。
  • 大小:5M(跟浏览器厂商有关系)。
  • localstorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。
  • 受同源策略的限制。
localStorage.setItem('username','你的名字');
localStorage.getItem('username');
localStorage.key(0) // 获取第一个键名
localStorage.removeItem('username');
localStorage.clear(); // 清空localStorage
16.1.3 sessionStorage

sessionStoragelocalstorage 使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据。

16.1.4 indexedDB

indexedDB 是一种低级AP,用于客户端存储大量结构化数据(包括,文件/blobs)。该API使用索引来 实现对该数据的高性能搜索。

虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。

优点:

  • 储存量理论上没有上限
  • 所有操作都是异步的,相比LocalStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持储存 JS 的对象
  • 是个正经的数据库,意味着数据库能干的事它都能干

缺点:

  • 操作非常繁琐
  • 本身有一定门槛
区别
  • 存储大小: cookie 数据大小不能超过 4ksessionStorage 和 localStorage 虽然也有存储大小的限制,但比cookie 大得多,可以达到5M或更大。
  • 有效时间: localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage 数据在当前浏览器窗口关闭后自动删除; cookie 设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭。
  • 数据与服务器之间的交互方式,cookie 的数据会自动的传递到服务器,服务器端也可以写 cookie 到客户端;sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存

17.Ajax 原理是什么?如何实现?

Ajax 的原理简单来说通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用 JavaScript 来操作 DOM 而更新页面。

简单封装一个ajax请求:

function ajax(options) {
    //创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest();
    //初始化参数的内容
    options = options || {};
    options.type = (options.type || 'GET').toUpperCase();
    options.dataType = options.dataType 'json';
    const params = options.data;

    // 发送请求
    if (options.type === 'GET') {
        xhr.open('GET', options.url + '?' + params, true) xhr.send(null)
    } else if (options.type === 'POST') {
        xhr.open('POST', options.url, true) xhr.send(params)
        // 接收请求
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                let status = xhr.status;
                if (status >= 200 && status < 300) {
                    options.success && options.success(xhr.responseText, xhr.responseXML)
                } else {
                    options.fail && options.fail(status)
                }
            }
        }
    }
}

// 调用
ajax({
    type: 'post',
    dataType: 'json',
    data: {},
    url: 'https://xxxx',
    success: function(valse, xml){ 
        console.log(valse)
    },
    fail: function(status){ 
        console.log(status)
    }
})

18. 防抖和节流?区别?如何实现?

  • 节流: n秒内只运行一次,若在n秒内重复触发,只有一次生效。
  • 防抖: n 秒后在执行该事件,若在n秒内被重复触发,则重新计时

应用场景:

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小 resize 。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

节流

function throttled(fn, delay) {
    let timer = null;
    let starttime = Date.now();
    return function () {
        let curTime = Date.now(); // 当前时间
        let remaining = delay - (curTime - starttime); // 从上一次到现在,还剩下多少多余事件
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        clearTimeout(timer);
        if (remaining <= 0) {
            fn.apply(context, args);
            starttime = Date.now();
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}

防抖

function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait);
    }
}

如果需要立即执行防抖,可加入第三个参数

function debounce(func, wait, immediate) {
    let timeout;
    return function() {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象
        if (timeout) clearTimeout(timeout); // timeout 不为 null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会触发
            timeout = setTimeout(function() {
                timeout = null;
            },
            wait);
            if (callNow) {
                func.apply(context, args)
            }
        } else {
            timeout = setTimeout(function() {
                func.apply(context, args)
            },
            wait);
        }
    }
}

区别

相同点

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout 实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次。

19. web常见的攻击方式有哪些?如何防御?

常见的有:

  • XSS 跨站脚本攻击
  • CSRF 跨站请求伪造
  • SQL 注入攻击

防止csrf常用方案如下:

  • 阻止不明外域的访问,同源检测,Samesite Coolkie
  • 提交时要求附加本域才能获取信息 CSRF Token, 双重Cookie验证

预防SQL如下:

  • 严格检查输入变量的类型和格式
  • 过滤和转义特殊字符
  • 对访问数据库的web应用程序采用web应用防火墙

20.JavaScript内存泄露的几种情况?

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。

Javascript 具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。

常见的内存泄露情况:

  • 意外的全局变量。a='我是未声明的变量'.
  • 定时器

21. JavaScript数字精度丢失的问题?如何解决?

0.1 + 0.2 === 0.3; // false

可以使用parseFloat解决

CSS题型整理

1.盒模型

盒模型:由4个部分组成,content,padding,border,margin.

2.BFC的理解

BFC:即块级格式化上下文。

常见页面情况有:

  • 元素高度没了
  • 俩栏布局没法自适应
  • 元素间距奇怪
2.1清除内部浮动

元素添加overflow: hidden;

3.元素水平垂直居中的方法有哪些?

实现方式如下:

  • 利用定位+margin:auto
  • 利用定位+margin: 负值
  • 利用定位+transform
  • flex布局等

4.实现两栏布局,右侧自适应?三栏布局中间自适应?

两栏布局的话:

  • 使用float左浮动布局
  • 右边模块使用margin-left 撑出内容块做内容展示
  • 为父级元素添加BFC,防止下方元素跟上方内容重叠。

flex布局:

  • 简单易用代码少

三栏布局:

  • 两边用float,中间用margin
  • 两边用absolute,中间用margin
  • display: table
  • flex
  • grid网格布局

5.css中,有哪些方式隐藏页面元素?

例如:

  • display: none; 最常用,页面彻底消失,会导致浏览器重排和重绘
  • visibility: hidden; dom存在,不会重排,但是会重绘
  • opacity: 0; 元素透明 元素不可见,可以响应点击事件
  • position: absolute; 将元素移出可视区域,不影响页面布局

6.如何实现单行/多行文本溢出的省略样式

单行:

<style>
p {
    overflow: hidden;
    line-height: 40px;
    width:400px;
    height:40px;
    border:1px solid red;
    text-overflow: ellipsis;
    white-space: nowrap;
}

</style>
<p>文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本</p>

多行

<style>
.demo {
    position: relative;
    line-height: 20px;
    height: 40px;
    overflow: hidden;
}
.demo::after {
    content: "...";
    position: absolute;
    bottom: 0;
    right: 0;
    padding: 0 20px 0 10px;
}
</style>
<body>
    <div class="demo">文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本文本</div>
</body>

css实现

<style>
p {
    width: 400px;
    border-radius: 1px solid red;
    -webkit-line-clamp: 2;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
}
</styl

7.CSS3新增了哪些新特性?

选择器:

  • nth-child(n)
  • nth-last-child(n)
  • last-child

新样式:

  • border-radius; 创建圆角边框
  • box-shadow; 为元素添加阴影
  • border-image; 图片绘制边框
  • background-clip; 确定背景画区
  • background-size; 调整背景图大小

文字:

  • word-wrap: normal|break-word; 使用浏览器默认的换行 | 允许在单词内换行;
  • text-overflow; clip | ellipsis; 修剪文本 | 显示省略符号来代表被修剪的文本;
  • text-decoration; text-fill-color| text-stroke-color | text-stroke-width;

transition 过渡、transform 转换、animatin动画、渐变、等

8.CSS提高性能的方法有哪些?

如下:

  • 内联首屏关键css
  • 异步加载css
  • 资源压缩(webpack/gulp/grunt)压缩代码
  • 合理的使用选择器
  • 不要使用@import
  • icon图片合成等

ES6

1.var,let, const的区别?

  • var 声明的变量会提升为全局变量,多次生成,会覆盖。
  • let let声明的变量只在代码块内有效。
  • const 声明一个只读常量,常量的值不能改变。

区别:

  • 变量提升,var会提升变量到全局。let, const直接报错
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量

2.ES6中数组新增了哪些扩展?

  • 扩展运算符...
  • 构造函数新增的方法 Array.from(),Array.of()
  • 数组实例新增方法有:copyWithin(),find(),findIndex(),fill(),includes(),keys(),values()等

3.对象新增了哪些扩展

对象名跟对应值名相等的时候,可以简写。 const a = {foo: foo} == const a = {foo}

属性的遍历:

  • for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
  • Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)的键名
  • Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名
  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性的键名----- Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举.

对象新增的方法

  • Object.is();
  • Object.assign();
  • Object.getOwnPropertyDescriptors() ;
  • Object.keys(), Object.values(),Object.entries();
  • Object.fromEntries();

4.理解ES6中Promise的?

优点:

  • 链式操作减低了编码难度
  • 代码可读性增强

promise对象仅有三种状态,pending(进行中),fulfilled(已成功),rejected(已失败)。一旦状态改变(从 pending变为 fulfilled和从 pending变为 rejected),就不会再变,任何时候都可以得到这个结果。

使用方法

const promise = new Promise(function(resolve, reject) {});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从"未完成"变为"成功"
  • reject函数的作用是,将Promise对象的状态从"未完成"变为"失败"

实例方法:

  • then() 是实例状态发生改变时的回调函数。
  • catch() 指定发生错误时的回调函数。
  • finally() 不管Prosime对象最后状态如何,都会执行。

构造函数方法 Promise构造函数存在以下方法:

  • all() 将多个Promise实例包装成一个新的Promise实例。
  • race() 将多个Promise实例包装成一个新的Promise实例。
  • allSettled() 接受一组Promise实例作为参数,只有等所有这些参数返回结果,实例才会结束。
  • resolve() 将现有对象转为Promise对象。
  • reject() 返回一个新的Promise实例,状态为rejected。

Vue2面试题

1.生命周期?

beforeCreate -> created -> beforeMount -> mounted -> beforeUpdate -> updated -> beforeDestroy -> destroyed

1.4 数据请求在created和mouted的区别

created 是在组件实例一旦创建完成的时候立刻调用,这时候页面 dom 节点并未生成; mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,

两者的相同点:都能拿到实例对象的属性和方法。讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在 created 生命周期当中。

2.双向数据绑定是什么?

释义:当js代码更新Model时,view也会自动更新,用户更新view,Model的数据也会自动被更新,就是双向绑定。

3.Vue组件之间的通信方式有哪些?

  • 1.通过props传递 (父给子组件传递)
  • 2.通过$emit触发自定义事件 (子传父)
  • 3.使用ref (父组件使用子组件的时候)
    1. EventBus (兄弟组件传值)
    1. attrs 与 listeners (祖先传递给子孙)
    1. Provide 与 Inject (在祖先组件定义provide)返回传递的值,在后代组件通过inject 接收组件传递过来的值。
    1. Vuex (复杂关系组件数据传递,存放共享变量)

4.v-if和v-for的优先级是什么?

v-for的优先级比v-if的高

注意

不能把v-if 和 v-for 同时在同一个元素上,带来性能方面的浪费。必须使用的话可以在外层套一个template

5. 未完待续。。。

防抖(Debounce)实战解析:如何用闭包优化频繁 AJAX 请求,提升用户体验

2026年1月2日 16:16

在现代 Web 开发中,用户交互越来越丰富,但随之而来的性能问题也日益突出。一个典型场景是:搜索框实时建议功能。当用户在输入框中快速打字时,如果每按一次键就立即向服务器发送一次 AJAX 请求,不仅会造成大量无效网络开销,还可能导致页面卡顿、响应错乱,甚至压垮后端服务。本文将以“百度搜索建议”为例,通过对比未防抖防抖两种实现方式,深入浅出地讲解防抖技术的原理、实现及其带来的显著优势。


一、问题引入:不防抖的“蛮力请求”有多糟糕?

假设我们正在开发一个类似百度搜索的自动补全功能。用户在输入框中输入关键词,前端实时将内容发送到服务器,获取匹配建议并展示。

❌ 不防抖的实现(反面教材)

const input = document.getElementById('search');
input.addEventListener('input', function(e) {
    ajax(e.target.value); // 每次输入都立刻发请求
});

function ajax(query) {
    console.log('发送请求:', query);
    // 实际项目中这里是 fetch 或 XMLHttpRequest
}

用户输入 “javascript” 的过程:

表格

输入步骤 触发次数 发送的请求
j 1 "j"
ja 2 "ja"
jav 3 "jav"
java 4 "java"
javascript 10 "javascript"

后果分析:

  • 资源浪费:前9次请求几乎无意义(用户还没输完),却消耗了带宽、CPU 和服务器连接。
  • 响应错乱:如果“j”的响应比“javascript”晚到,页面会先显示“j”的结果,再跳变到最终结果,体验极差。
  • 页面卡顿:高频 DOM 操作 + 网络回调,容易导致主线程阻塞,输入框变得“卡手”。

这就是典型的“执行太密集、任务太复杂”问题——事件触发频率远高于实际需求


二、解决方案:用防抖(Debounce)优雅降频

✅ 什么是防抖?

防抖(Debounce)  是一种函数优化技术:在事件被频繁触发时,仅在最后一次触发后等待指定时间,才真正执行函数。

通俗理解:

用户打字时,我不急着查;等他停手500毫秒,我才认为他“打完了”,这时才发请求。

🔧 防抖的核心实现(基于闭包)

function debounce(fn, delay) {
    let timer; // 闭包变量:保存定时器ID
    return function(...args) {
        const context = this;
        clearTimeout(timer); // 清除上一次的定时器
        timer = setTimeout(() => {
            fn.apply(context, args); // 延迟执行,并保持this和参数
        }, delay);
    };
}

关键点解析:

  • 闭包作用timer 被内部函数引用,不会被垃圾回收,可跨多次调用共享。
  • 清除旧定时器:每次触发都重置倒计时,确保只执行“最后一次”。
  • 保留上下文:通过 apply 保证原函数的 this 和参数正确传递。

✅ 防抖后的使用

const debouncedAjax = debounce(ajax, 500);
input.addEventListener('input', function(e) {
    debouncedAjax(e.target.value);
});

用户输入 “javascript” 的效果:

  • 快速打完10个字母 → 只触发1次请求(“javascript”)
  • 中途停顿超过500ms → 触发当前值的请求(如打到“java”停住)

三、对比实验:防抖 vs 不防抖

我们在 HTML 中放置两个输入框:

<input id="undebounce" placeholder="不防抖(危险!)">
<input id="debounce" placeholder="防抖(推荐)">

绑定不同逻辑:

// 不防抖:每输入一个字符就请求
undebounce.addEventListener('input', e => ajax(e.target.value));

// 防抖:500ms 内只执行最后一次
debounce.addEventListener('input', e => debouncedAjax(e.target.value));

打开浏览器控制台,分别快速输入 “react”:

  • 不防抖输入框:控制台瞬间打印 5 条日志(r, re, rea, reac, react)
  • 防抖输入框:控制台仅在你停止输入后 0.5 秒打印 1 条日志(react)

用户体验差异:

  • 不防抖:页面可能闪烁、卡顿,建议列表频繁跳动。
  • 防抖:输入流畅,结果稳定,资源消耗降低 80% 以上。

四、为什么防抖能解决性能问题?

  1. 减少无效请求
    用户输入过程中产生的中间状态(如“j”、“ja”)通常无需处理,防抖直接忽略它们。
  2. 避免竞态条件(Race Condition)
    后发的请求覆盖先发的结果,确保 UI 始终显示最新、最完整的查询结果。
  3. 降低服务器压力
    假设每天有 10 万用户使用搜索,平均每人输入 10 次,不防抖产生 100 万请求;防抖后可能仅 10 万请求,节省 90% 计算资源。
  4. 提升前端性能
    减少 JavaScript 执行、DOM 更新和网络回调的频率,主线程更“轻盈”,页面更流畅。

五、防抖的适用场景

表格

场景 说明
搜索框建议 用户输入时延迟请求,等输入稳定后再查
窗口 resize 防止调整窗口大小时频繁触发布局计算
表单提交 防止用户狂点“提交”按钮导致重复提交
按钮点击 如“点赞”功能,避免快速连点

⚠️ 注意:滚动加载(scroll)更适合用节流(Throttle) ,因为用户持续滚动时仍需定期触发(如每 200ms 检查是否到底部),而防抖会在滚动结束才触发,可能错过加载时机。


六、总结:防抖是前端性能优化的基石

通过本文的对比实验,我们可以清晰看到:不加控制的事件监听是性能杀手,而防抖则是优雅的“减速阀” 。它利用闭包保存状态,通过定时器智能合并高频操作,在不牺牲用户体验的前提下,大幅降低系统开销。

在实际项目中,建议:

  • 对 inputkeyupresize 等高频事件默认使用防抖或节流
  • 使用成熟的工具库(如 Lodash 的 _.debounce)避免手写 bug
  • 根据业务调整延迟时间(搜索建议常用 300–500ms)

记住:好的前端工程师,不仅要让功能跑起来,更要让它跑得稳、跑得快、跑得省。  而防抖,正是你工具箱中不可或缺的一把利器。

🌟 小提示:下次当你看到百度搜索框在你打字时不急不躁、等你停手才给出建议时,就知道——背后一定有防抖在默默守护性能!

cloudflare使用express实现api防止跨域cors

作者 1024小神
2026年1月2日 15:49

大家好,我是1024小神,想进 技术群 / 私活群 / 股票群 或 交朋友都可以私信我,如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~

在 Cloudflare Workers 上,必须自己处理 CORS,Express 默认的 cors 中间件 并不会自动生效。

在中间件中写一个cors.ts文件,里面的代码如下:

import { Request, Response, NextFunction } from 'express';

export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
// ⚠️ in production, write the specific domain
res.setHeader('Access-Control-Allow-Origin', '*');

res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');

// handle preflight request
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}

// next middleware
next();
}

然后配置中间件在所有的路由前面:

然后重启项目,再次发送请求就没事了:

如果你有好的想法或需求,都可以私信我,我这里有很多程序员朋友喜欢用代码来创造丰富多彩的计算机世界

从定时器管理出发,彻底搞懂防抖与节流的实现逻辑

作者 烟袅破辰
2026年1月2日 15:01

在前端开发中,高频事件(如输入、滚动、窗口缩放)若不加控制,极易引发性能问题。为应对这一挑战,防抖(debounce)节流(throttle) 成为必备工具。


一、防抖:每次触发都重置定时器

假设我们要实现一个功能:用户在输入框打字时,只有当他停止输入超过 1 秒,才发送请求。

第一步:我需要延迟执行

显然,要用 setTimeout

setTimeout(() => {
    ajax(value);
}, 1000);

第二步:但如果用户继续输入,之前的请求就不该发

→ 所以必须取消之前的定时器,再建一个新的。

这就要求我们保存定时器 ID

let timerId;
// 每次触发时:
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
    ajax(value);
}, 1000);

第三步:处理 this 和参数

因为 ajax 可能依赖上下文或多个参数,不能直接写死。我们需要在触发时捕获当前的 thisarguments

function debounce(fn, delay) {
    let timerId;
    return function(...args) {
        const context = this;
        if (timerId) clearTimeout(timerId);
        timerId = setTimeout(() => {
            fn.apply(context, args);
        }, delay);
    };
}

到此,防抖完成。它的全部逻辑就源于一句话: “每次触发,先删旧定时器,再建新定时器。”
所谓“停手后执行”,只是这种操作的自然结果。


二、节流:控制执行频率,必要时预约补发

现在需求变了:不管用户多快输入,每 1 秒最多只发一次请求,且最后一次输入不能丢

第一步:我能立即执行吗?

用时间戳判断是否已过 delay

const now = Date.now();
if (now - last >= delay) {
    fn(); 
    last = now; // 记录执行时间
}

这能保证最小间隔,但有个致命缺陷:如果用户快速输入后立刻停止,最后一次可能永远不会执行。

第二步:如何不丢尾?

→ 在冷却期内,预约一次未来的执行。这就要用到 setTimeout

于是逻辑分裂为两条路径:

  • 路径 A(可立即执行) :时间到了,马上执行,更新 last
  • 路径 B(还在冷却) :清除之前的预约,重新预约一次执行

第三步:管理预约定时器

我们需要一个变量 deferTimer 来保存预约任务的 ID:

let last = 0;
let deferTimer = null;

当处于冷却期时:

clearTimeout(deferTimer); // 清除旧预约
deferTimer = setTimeout(() => {
    last = Date.now(); // 关键:这次执行也要记录时间!
    fn.apply(this, args);
}, delay - (now - last)); // 精确计算剩余等待时间

第四步:整合逻辑

function throttle(fn, delay) {
    let last = 0;
    let deferTimer = null;

    return function(...args) {
        const context = this;
        const now = Date.now();

        if (now - last >= delay) {
            // 路径 A:立即执行
           
            last = now;
            fn.apply(context, args);
        } else {
            // 路径 B:预约执行
            if (deferTimer) clearTimeout(deferTimer);
            deferTimer = setTimeout(() => {
                last = Date.now(); // 必须更新!
               
                fn.apply(context, args);
            }, delay - (now - last));
        }
    };
}

节流的核心,是两种执行方式的协同

  • 立即执行靠时间戳判断
  • 补发执行靠 setTimeout 预约
    而两者共享同一个 last 状态,确保整体节奏不乱。

三、对比总结:防抖 vs 节流的机制差异

维度 防抖(Debounce) 节流(Throttle)
核心操作 每次触发都 clearTimeout + setTimeout 冷却期内 clearTimeout + setTimeout,否则立即执行
状态变量 仅需 timerId last(时间) + deferTimer(预约ID)
执行特点 只执行最后一次 固定间隔执行,且不丢尾
适用场景 搜索建议、表单校验 滚动加载、按钮限频、实时位置上报

前端 Token 刷新机制实战:基于 Axios 的 accessToken 自动续期方案

2026年1月2日 15:00

一、背景

在前后端分离的项目中,前端通常通过 accessToken 来访问业务接口。 但由于 accessToken 有有效期限制,过期后的处理方式,直接影响系统的安全性用户体验

本文将介绍一种 基于 Axios 响应拦截器 + 请求队列 的 Token 自动刷新方案,适用于实际生产环境。

二、整体设计思路

核心目标只有三个:

  1. accessToken 过期时自动刷新
  2. refreshToken 只请求一次
  3. 刷新完成后自动重试过期前的请求

整体流程如下:

token_FlowChart.png

三、基于业务 code 的统一错误抛出

项目中后端返回统一的数据结构:

{
  code:number;
  msg:string;
  data:any
}

在 Axios 的响应拦截器的 成功回调 中:

if (code === ApiCodeEnum.SUCCESS) {
  return data;
}

// 业务错误,主动抛出
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
  • 成功流只处理 真正成功的数据
  • 所有业务异常 统一进入 error 分支
  • 后续逻辑更清晰、集中

四、在 error 分支中统一处理 Token 异常

在 Axios 响应拦截器的 error 回调中:

async (error) => {
  const { response, config } = error;

  if (!response) {
    ElMessage.error("网络连接失败");
    return Promise.reject(error);
  }

  const { code, msg } = response.data;

  switch (code) {
    case ApiCodeEnum.ACCESS_TOKEN_INVALID:
      return refreshTokenAndRetry(config, service);

    case ApiCodeEnum.REFRESH_TOKEN_INVALID:
      await redirectToLogin("登录已过期");
      return Promise.reject(error);

    default:
      ElMessage.error(msg || "系统出错");
      return Promise.reject(error);
  }
};

设计要点

  • 只在一个地方判断 Token 失效
  • 不在业务代码中关心 Token 状态

五、Token 刷新的难点:并发请求问题

如果多个接口同时返回 ACCESS_TOKEN_INVALID

  • ❌ 会触发多次 refreshToken 请求
  • ❌ 后端压力大
  • ❌ Token 状态混乱

解决方案:请求队列 + 刷新锁

六、基于闭包的 Token 刷新队列实现

通过组合式函数 useTokenRefresh 实现:

核心状态

let isRefreshingToken = false;
const pendingRequests = [];

刷新 Token 并重试请求

async function refreshTokenAndRetry(config, httpRequest) {
  return new Promise((resolve, reject) => {
    const retryRequest = () => {
      const newToken = AuthStorage.getAccessToken();
      config.headers.Authorization = `Bearer ${newToken}`;
      httpRequest(config).then(resolve).catch(reject);
    };

    pendingRequests.push({ resolve, reject, retryRequest });

    if (!isRefreshingToken) {
      isRefreshingToken = true;

      useUserStoreHook()
        .refreshToken()
        .then(() => {
          pendingRequests.forEach(req => req.retryRequest());
          pendingRequests.length = 0;
        })
        .catch(async () => {
          pendingRequests.forEach(req =>
            req.reject(new Error("Token refresh failed"))
          );
          pendingRequests.length = 0;
          await redirectToLogin("登录已失效");
        })
        .finally(() => {
          isRefreshingToken = false;
        });
    }
  });
}

七、为什么要提前初始化刷新函数?

在创建 Axios 函数中要提前初始化刷新函数

const { refreshTokenAndRetry } = useTokenRefresh();

原因

  • 利用 闭包 保存刷新状态

  • 确保所有请求共享:

    • isRefreshingToken
    • pendingRequests
  • 防止重复刷新

完整代码示例

import type { InternalAxiosRequestConfig } from "axios";
import { useUserStoreHook } from "@/store/modules/user.store";
import { AuthStorage, redirectToLogin } from "@/utils/auth";

/**
 * 等待请求的类型接口
 */
type PendingRequest = {
  resolve: (_value: any) => void;
  reject: (_reason?: any) => void;
  retryRequest: () => void;
};

/**
 * Token刷新组合式函数
 */
export function useTokenRefresh() {
  // Token 刷新相关状态s
  let isRefreshingToken = false;
  const pendingRequests: PendingRequest[] = [];

  /**
   * 刷新 Token 并重试请求
   */
  async function refreshTokenAndRetry(
    config: InternalAxiosRequestConfig,
    httpRequest: any
  ): Promise<any> {
    return new Promise((resolve, reject) => {
      // 封装需要重试的请求
      const retryRequest = () => {
        const newToken = AuthStorage.getAccessToken();
        if (newToken && config.headers) {
          config.headers.Authorization = `Bearer ${newToken}`;
        }
        httpRequest(config).then(resolve).catch(reject);
      };

      // 将请求加入等待队列
      pendingRequests.push({ resolve, reject, retryRequest });

      // 如果没有正在刷新,则开始刷新流程
      if (!isRefreshingToken) {
        isRefreshingToken = true;

        useUserStoreHook()
          .refreshToken()
          .then(() => {
            // 刷新成功,重试所有等待的请求
            pendingRequests.forEach((request) => {
              try {
                request.retryRequest();
              } catch (error) {
                console.error("Retry request error:", error);
                request.reject(error);
              }
            });
            // 清空队列
            pendingRequests.length = 0;
          })
          .catch(async (error) => {
            console.error("Token refresh failed:", error);
            // 刷新失败,先 reject 所有等待的请求,再清空队列
            const failedRequests = [...pendingRequests];
            pendingRequests.length = 0;

            // 拒绝所有等待的请求
            failedRequests.forEach((request) => {
              request.reject(new Error("Token refresh failed"));
            });

            // 跳转登录页
            await redirectToLogin("登录状态已失效,请重新登录");
          })
          .finally(() => {
            isRefreshingToken = false;
          });
      }
    });
  }

  return {
    refreshTokenAndRetry,
  };
}

吃透 JS 事件委托:从原理到实战,解锁高性能事件处理方案

2026年1月2日 14:56

事件委托(Event Delegation)是 JavaScript 中最核心的事件处理技巧之一,也是前端面试的高频考点。它基于事件冒泡机制,能大幅减少事件绑定数量、解决动态元素事件失效问题,同时降低内存占用、提升页面性能。本文将从原理拆解、实战场景、性能优化到避坑指南,全方位带你吃透事件委托。

一、为什么需要事件委托?先看痛点

在未使用事件委托的场景中,我们通常会给每个元素单独绑定事件,比如一个列表的所有项:

// 传统方式:给每个li绑定点击事件
const items = document.querySelectorAll('.list-item');
items.forEach(item => {
  item.addEventListener('click', () => {
    console.log('点击了列表项:', item.textContent);
  });
});

这种写法会暴露三个核心问题:

  1. 性能损耗:如果列表有 1000 个项,就会创建 1000 个事件处理函数,占用大量内存;
  2. 动态元素失效:新增的列表项(如通过 JS 动态添加)不会自动绑定事件,需要重新执行绑定逻辑;
  3. 代码冗余:重复的事件绑定逻辑,增加维护成本。

而事件委托能一次性解决这些问题 —— 只给父元素绑定一次事件,就能处理所有子元素的事件触发。

二、事件委托的核心原理:事件流

要理解事件委托,必须先掌握 DOM 事件流的三个阶段:

  1. 捕获阶段:事件从 window 向下传播到目标元素(从外到内);
  2. 目标阶段:事件到达目标元素本身;
  3. 冒泡阶段:事件从目标元素向上传播回 window(从内到外)。

事件委托的核心逻辑是:利用事件冒泡,将子元素的事件绑定到父元素(甚至根元素)上,通过判断事件源(target)来区分具体触发的子元素

举个直观的例子:点击列表中的<li>,事件会先触发<li>的 click 事件,然后冒泡到<ul><div>,直到documentwindow。我们只需要在<ul>上绑定一次事件,就能捕获所有<li>的点击行为。

三、基础实战:实现一个列表的事件委托

1. 核心实现代码

<ul id="list" class="item-list">
  <li class="list-item" data-id="1">列表项1</li>
  <li class="list-item" data-id="2">列表项2</li>
  <li class="list-item" data-id="3">列表项3</li>
</ul>
<button id="addItem">新增列表项</button>

<script>
// 父元素绑定事件(只绑定一次)
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
  // 核心:判断触发事件的目标元素
  const target = e.target;
  // 确认点击的是列表项(避免点击ul空白处触发)
  if (target.classList.contains('list-item')) {
    const id = target.dataset.id;
    console.log(`点击了列表项${id}:`, target.textContent);
  }
});

// 动态新增列表项(无需重新绑定事件)
const addItem = document.getElementById('addItem');
let index = 4;
addItem.addEventListener('click', () => {
  const li = document.createElement('li');
  li.className = 'list-item';
  li.dataset.id = index;
  li.textContent = `列表项${index}`;
  list.appendChild(li);
  index++;
});
</script>

2. 关键知识点解析

  • e.target:触发事件的原始元素(比如点击的<li>);
  • e.currentTarget:绑定事件的元素(这里是<ul>);
  • 类名 / 属性判断:通过classListdataset等方式精准匹配目标元素,避免非目标元素触发逻辑;
  • 动态元素兼容:新增的<li>无需重新绑定事件,因为事件委托在父元素上,天然支持动态元素。

四、进阶场景:精细化事件委托

实际开发中,事件委托的场景往往更复杂,比如多层嵌套、多类型事件、需要阻止冒泡等,以下是高频进阶用法:

1. 多层嵌套元素的委托

当目标元素嵌套在其他元素中(比如<li>里有<span><button>),需要通过closest找到最外层的目标元素:

<ul id="list">
  <li class="list-item" data-id="1">
    <span>列表项1</span>
    <button class="delete-btn">删除</button>
  </li>
</ul>

<script>
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
  // 找到最近的list-item(解决点击子元素触发的问题)
  const item = e.target.closest('.list-item');
  if (item) {
    // 区分点击的是列表项还是删除按钮
    if (e.target.classList.contains('delete-btn')) {
      console.log(`删除列表项${item.dataset.id}`);
      item.remove();
    } else {
      console.log(`点击列表项${item.dataset.id}`);
    }
  }
});
</script>

closest方法会从当前元素向上查找,返回匹配选择器的第一个祖先元素(包括自身),是处理嵌套元素的最佳方案。

2. 多类型事件的统一委托

可以在父元素上绑定多个事件类型,或通过一个处理函数区分不同事件:

// 一个处理函数处理多个事件类型
list.addEventListener('click', handleItemEvent);
list.addEventListener('mouseenter', handleItemEvent);
list.addEventListener('mouseleave', handleItemEvent);

function handleItemEvent(e) {
  const item = e.target.closest('.list-item');
  if (!item) return;

  switch(e.type) {
    case 'click':
      console.log('点击:', item.dataset.id);
      break;
    case 'mouseenter':
      item.style.backgroundColor = '#f5f5f5';
      break;
    case 'mouseleave':
      item.style.backgroundColor = '';
      break;
  }
}

3. 委托到 document/body(全局委托)

对于全局范围内的动态元素(如弹窗、动态按钮),可以将事件委托到documentbody

// 全局委托:处理所有动态生成的按钮
document.addEventListener('click', (e) => {
  if (e.target.classList.contains('dynamic-btn')) {
    console.log('点击了动态按钮:', e.target.textContent);
  }
});

// 动态创建按钮
setTimeout(() => {
  const btn = document.createElement('button');
  btn.className = 'dynamic-btn';
  btn.textContent = '动态按钮';
  document.body.appendChild(btn);
}, 1000);

⚠️ 注意:全局委托虽方便,但不要滥用 ——document上的事件会监听整个页面的点击,过多的全局委托会增加事件处理的耗时,建议优先委托到最近的父元素。

五、性能优化:让事件委托更高效

事件委托本身是高性能方案,但不当使用仍会产生性能问题,以下是优化技巧:

1. 选择最近的父元素

尽量避免直接委托到document/body,而是选择离目标元素最近的固定父元素。比如列表的事件委托到<ul>,而非document,减少事件传播的层级和处理函数的触发次数。

2. 节流 / 防抖处理高频事件

如果委托的是scrollresizemousemove等高频事件,必须结合节流 / 防抖:

// 节流函数
function throttle(fn, delay = 100) {
  let timer = null;
  return (...args) => {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

// 委托scroll事件(节流处理)
document.addEventListener('scroll', throttle((e) => {
  // 处理滚动逻辑
  console.log('滚动了');
}, 200));

3. 及时移除无用的委托事件

如果委托的父元素被销毁(比如弹窗关闭),要及时移除事件监听,避免内存泄漏:

const modal = document.getElementById('modal');
const handleModalClick = (e) => {
  // 弹窗内的事件逻辑
};

// 绑定事件
modal.addEventListener('click', handleModalClick);

// 弹窗关闭时移除事件
function closeModal() {
  modal.removeEventListener('click', handleModalClick);
  modal.remove();
}

六、避坑指南:事件委托的常见问题

1. 事件被阻止冒泡

如果子元素的事件处理函数中调用了e.stopPropagation(),会导致事件无法冒泡到父元素,委托失效:

// 错误示例:子元素阻止冒泡,委托失效
document.querySelector('.list-item').addEventListener('click', (e) => {
  e.stopPropagation(); // 阻止冒泡
  console.log('子元素点击');
});

// 父元素的委托事件不会触发
list.addEventListener('click', (e) => {
  console.log('委托事件'); // 不会执行
});

✅ 解决方案:避免在子元素中随意阻止冒泡,若必须阻止,需确保不影响委托逻辑。

2. 目标元素是不可冒泡的事件

部分事件不支持冒泡(如focusblurmouseentermouseleave),直接委托会失效:

// 错误示例:mouseenter不冒泡,委托失效
list.addEventListener('mouseenter', (e) => {
  console.log('鼠标进入列表项'); // 不会触发
});

✅ 解决方案:使用事件捕获模式(第三个参数设为true):

// 捕获模式处理不冒泡的事件
list.addEventListener('mouseenter', (e) => {
  const item = e.target.closest('.list-item');
  if (item) {
    console.log('鼠标进入列表项');
  }
}, true); // 开启捕获模式

3. 动态修改元素的类名 / 属性

如果目标元素的类名、dataset等用于判断的属性被动态修改,可能导致委托逻辑失效:

// 动态修改类名后,委托无法匹配
const item = document.querySelector('.list-item');
item.classList.remove('list-item'); // 移除类名
// 此时点击该元素,委托逻辑不会触发

✅ 解决方案:尽量使用稳定的标识(如固定的data-*属性),而非易变的类名。

七、框架中的事件委托(Vue/React)

现代前端框架虽封装了事件处理,但底层仍基于事件委托,且有专属的使用方式:

1. Vue3 中的事件委托

Vue 的v-on@)指令默认会利用事件委托(绑定到组件根元素),也可手动实现精细化委托:

<template>
  <ul @click="handleListClick">
    <li v-for="item in list" :key="item.id" :data-id="item.id">
      {{ item.name }}
      <button class="delete-btn">删除</button>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue';
const list = ref([{ id: 1, name: '列表项1' }, { id: 2, name: '列表项2' }]);

const handleListClick = (e) => {
  const item = e.target.closest('[data-id]');
  if (item) {
    const id = item.dataset.id;
    if (e.target.classList.contains('delete-btn')) {
      list.value = list.value.filter(item => item.id !== Number(id));
    } else {
      console.log(`点击列表项${id}`);
    }
  }
};
</script>

2. React 中的事件委托

React 的合成事件系统本身就是基于事件委托(所有事件绑定到document),无需手动实现,但可通过e.target判断目标元素:

import { useState } from 'react';

function List() {
  const [list, setList] = useState([{ id: 1, name: '列表项1' }]);

  const handleListClick = (e) => {
    const item = e.target.closest('[data-id]');
    if (item) {
      const id = item.dataset.id;
      console.log(`点击列表项${id}`);
    }
  };

  return (
    <ul onClick={handleListClick}>
      {list.map(item => (
        <li key={item.id} data-id={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

八、总结

事件委托是前端开发中 “四两拨千斤” 的技巧,核心是利用事件冒泡,将多个子元素的事件绑定到父元素,通过目标元素判断执行逻辑。它的优势在于:

  • 减少事件绑定数量,降低内存占用;
  • 天然支持动态元素,无需重复绑定;
  • 简化代码逻辑,提升可维护性。

使用时需注意:

  1. 优先委托到最近的父元素,避免全局委托;
  2. 处理嵌套元素用closest,处理不冒泡事件用捕获模式;
  3. 高频事件结合节流 / 防抖,及时移除无用事件;
  4. 避免随意阻止冒泡,防止委托失效。

掌握事件委托,不仅能写出更高效的代码,更能深入理解 DOM 事件流的本质 —— 这也是从 “初级前端” 到 “中高级前端” 的必经之路。

❌
❌