阅读视图

发现新文章,点击刷新页面。

JavaScript 运行机制详解:再谈 Event Loop

同步更新至个人站点: JavaScript 运行机制详解:再谈 Event Loop

005.avif

本文从经典的 Promise 与 setTimeout 执行顺序问题入手,深入浅出地剖析了 JavaScript 的单线程模型、事件循环(Event Loop)机制。通过辨析宏任务与微任务的区别与优先级,帮助你彻底理解 JS 异步执行的底层原理,看懂页面卡顿的真相。

我常常在各种场合被问到类似下面代码的输出顺序。

console.log("start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(function () {
  console.log("promise");
});

console.log("end");

如果你能毫不犹豫地答出 start, end, promise, setTimeout,并解释其原因,那么你对 JS 的异步机制已经有了不错的理解。如果你还有一丝困惑,希望本文能帮助你彻底梳理清楚。

这个问题的背后,是整个 JavaScript 的运行模型(runtime model),也就是我们常说的“事件循环”(Event Loop)。理解它,是前端工程师进阶的必经之路。

为什么 JavaScript 是单线程?

首先,我们必须记住一个基本事实:JavaScript 语言是一门单线程语言。

这意味着,在任何一个时刻,JS 引擎只能执行一段代码。为什么这么设计?这与它的初衷有关。JavaScript 最初是为浏览器设计的,用于处理用户的交互,比如鼠标点击、键盘输入,以及操作 DOM。

试想一下,如果 JavaScript 是多线程的,会发生什么?一个线程要在一个 DOM 节点上增加内容,另一个线程要删除这个节点。那么浏览器应该听谁的?这会带来极其复杂的同步问题。为了避免这种复杂性,JavaScript 从诞生起就选择了单线程。

这既是它的优点,也是它的缺点。优点是简单,没有多线程的竞态、死锁等问题。缺点是,如果一个任务耗时很长,整个程序就会被“卡住”,无法响应其他操作。

浏览器:一个多进程的“操作系统”

“JS 是单线程的”这个说法其实不完全准确。准确来说,执行 JavaScript 代码的那个主线程是单线程的

现代浏览器(以 Chrome 为例)本身是一个非常复杂的程序,它采用了多进程架构来保证稳定性和安全性。你可以打开 Chrome 的任务管理器(“更多工具” > “任务管理器”)看看,通常会看到好几个进程:

  • 浏览器进程(Browser Process):负责浏览器界面的“外壳”,比如地址栏、书签、前进后退按钮,以及协调其他进程。
  • 渲染进程(Renderer Process):核心部分,负责将 HTML、CSS 和 JavaScript 转换成用户可以看到的网页。我们写的 JS 代码,主要就在这个进程的主线程(Main Thread)上运行。每个标签页通常会有一个独立的渲染进程。
  • 网络进程(Network Process):负责处理网络请求,比如 fetch
  • GPU 进程(GPU Process):负责处理 GPU 相关的任务,加速 3D 绘图和页面渲染。

这种设计的好处是隔离。一个标签页(渲染进程)崩溃了,不会影响到整个浏览器。

任务队列(Task Queue)和事件循环(Event Loop)

我们回到渲染进程的主线程。这个线程非常繁忙,它要做的事情包括:

  • 执行 JavaScript 代码
  • 渲染页面布局(Layout)
  • 绘制页面(Paint)
  • 响应用户交互(Click, Scroll)

如果所有任务都排队等着,一个耗时长的 JS 计算就会阻塞页面渲染和用户响应,这就是“假死”现象。

// 一个会让页面卡住的例子
document.getElementById("myButton").addEventListener("click", function () {
  // 假装这是一个非常耗时的计算
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // 这5秒内,页面完全无法响应
  }
  console.log("计算完成!");
});

为了解决这个问题,浏览器引入了异步(asynchronous)执行模型。当遇到一些耗时操作(比如网络请求、定时器)时,主线程不会傻等,而是把这些任务“外包”给浏览器的其他线程(比如网络线程、定时器线程)。

这些“外包”任务完成后,会把一个“回调函数”(callback)放进一个叫做**任务队列(Task Queue)**的地方。主线程则继续执行自己手头的同步代码。

等到主线程的同步代码全部执行完毕,它就会去任务队列里看看,有没有需要执行的回调函数。如果有,就取出一个来执行。这个“主线程不断从任务队列里读取并执行任务”的过程,就叫做事件循环(Event Loop)

这个模型可以用一张经典的图来表示:

019.jpg

微任务(Microtask)和宏任务(Macrotask)

事情还没完。任务队列其实不止一个。根据 WHATWG 规范,任务被分为两种类型:

  1. 宏任务(Macrotask,规范中称为 Task)

    • setTimeout, setInterval
    • script(整体代码块)
    • I/O 操作, UI 渲染
    • 用户交互事件(如 click, scroll
  2. 微任务(Microtask)

    • Promise.then(), Promise.catch(), Promise.finally()
    • queueMicrotask()
    • MutationObserver

事件循环的规则是,优先级更高的是微任务。主线程在执行完一个宏任务后,并不是立刻去执行下一个宏任务,而是会检查微任务队列。

完整的事件循环流程如下:

  1. 从宏任务队列中取出一个任务(通常是 script 脚本本身)并执行。
  2. 执行完毕后,检查微任务队列。
  3. 循环执行微任务队列中的所有任务,直到队列清空。
  4. 执行浏览器 UI 渲染(这一步不一定每次都会发生)。
  5. 回到第一步,从宏任务队列中取出下一个任务。

这个“执行一个宏任务 -> 清空所有微任务 -> 再取下一个宏任务”的循环,是理解所有异步执行顺序的关键。

回到最初的问题

现在,我们用这个模型来分析开头的代码:

console.log("start"); // 1

setTimeout(function () {
  // 4
  console.log("setTimeout");
}, 0);

Promise.resolve().then(function () {
  // 3
  console.log("promise");
});

console.log("end"); // 2
  1. 第一轮宏任务(script 脚本)开始执行。

    • 遇到 console.log('start'),直接执行。输出 start
    • 遇到 setTimeout,它是一个宏任务。浏览器定时器线程接管,0ms 后将其回调函数推入宏任务队列
    • 遇到 Promise.resolve().then().then() 的回调是一个微任务。它被推入微任务队列
    • 遇到 console.log('end'),直接执行。输出 end
  2. 第一个宏任务(script)执行完毕。

    • 现在,事件循环会检查微任务队列。发现里面有一个任务(打印 promise)。
    • 取出并执行该微任务。输出 promise
    • 微任务队列现在空了。
  3. 开始下一轮宏任务。

    • 事件循环检查宏任务队列,发现 setTimeout 的回调函数在那里。
    • 取出并执行该宏任务。输出 setTimeout

至此,所有代码执行完毕。最终输出 start, end, promise, setTimeout

应用与思考

理解了事件循环,很多问题就迎刃而解了。

  • setTimeout(fn, 0) 为什么不是立即执行? 因为它只是把 fn 尽快地推入宏任务队列,但必须等到当前主线程的同步代码和所有微任务都执行完之后,才有机会被执行。

  • 页面为什么会卡顿? 通常是因为一个宏任务(比如一段 JS 计算或一个事件回调)执行时间过长,导致主线程无法脱身去处理其他宏任务(如 UI 渲染、用户点击)。

  • 如何处理耗时计算? 对于真正 CPU 密集的计算,应该使用 Web Worker。它允许你在一个完全独立的后台线程中运行脚本,不会阻塞主线程。

参考链接

希望读完本文,你对 JavaScript 的运行机制有了更深入的理解。

(完)

Node.js v24.6.0 新功能速览 🚀🚀🚀

前言

Node.js v24.6.0 发布了,让我们一起来看看这些朴实却强大的变化!

往期精彩推荐

正文

以下是 v24.6.0 的核心更新和功能的详细介绍。

1. CLI:系统 CA 支持

Node.js v24.6.0 新增 NODE_USE_SYSTEM_CA=1 环境变量,支持使用系统 CA 证书。这简化了企业环境下的证书配置,提升兼容性。

示例

NODE_USE_SYSTEM_CA=1 node app.js

这对需要严格安全合规的场景尤其实用。

2. Crypto:支持 ML-DSA 算法

Crypto 模块新增了对 ML-DSA(Module Lattice-based Digital Signature Algorithm)的支持,包括 KeyObject 生成、签名和验证。这是后量子密码学算法,为未来安全奠定基础。

示例

const crypto = require('crypto');

// 生成 ML-DSA 密钥对
const { publicKey, privateKey } = crypto.generateKeyPairSync('ml-dsa');

// 签名
const signature = crypto.sign(null, Buffer.from('data'), privateKey);

// 验证
const isValid = crypto.verify(null, Buffer.from('data'), publicKey, signature);
console.log(isValid); // true

这为高安全需求的加密应用提供了新选择。

3. Zlib:zstdCompress 和 zstdDecompress 字典支持

Zlib 模块的 zstdCompresszstdDecompress 函数新增字典支持,通过预定义常见模式提升压缩效率。

示例

const zlib = require('zlib');
const dict = Buffer.from('common patterns'); // 自定义字典

const compressed = zlib.zstdCompressSync('data to compress', { dictionary: dict });
const decompressed = zlib.zstdDecompressSync(compressed, { dictionary: dict });
console.log(decompressed.toString()); // 'data to compress'

这优化了数据传输和存储场景。

4. HTTP:新增 keepAliveTimeoutBuffer 选项

HTTP 服务器新增 keepAliveTimeoutBuffer 选项,用于缓冲 keep-alive 超时,提升连接管理效率。

示例

const http = require('http');

const server = http.createServer((req, res) => res.end('Hello'));
server.keepAliveTimeoutBuffer = 1000; // 缓冲 1 秒
server.listen(3000);

这有助于减少网络抖动,提高服务器稳定性。

5. Lib:文档废弃 http*

内部 HTTP 模块的 _http_* 函数已被文档废弃,鼓励使用标准 API,提升代码规范性。

6. FS:移植 SonicBoom 作为 Utf8Stream

FS 模块引入了 Utf8Stream,通过移植 SonicBoom 提升文件流处理性能,适合高吞吐量场景。

7. 其他改进

  • 基准测试:优化基准脚本,提升测试效率。
  • 依赖更新:升级 ada 到 3.2.7、OpenSSL 到 3.5.2,确保安全性。
  • 文档优化:修复 Pbkdf2Params 和 x509.keyUsage 的文档问题。

最后

Node.js 新版本从 Crypto 的后量子算法到 HTTP 的连接优化,这些功能让你的项目更健壮、更高效。快来升级到 v24.6.0,体验这些实用的新特性吧!

今天的分享就这些了,感谢大家的阅读!如果文章中存在错误的地方欢迎指正!

往期精彩推荐

移动端浏览器中设置 100vh 却出现滚动条?

🎉 写在前面 你是否遇到过这样的诡异场景:明明设置了 height: 100vh,却在移动端意外触发了滚动条?本文将从底层原理到实战方案为你彻底剖析这一经典陷阱,并提供多种可靠解决方案。


以下是对问题的详细解答:

一、现象原因分析

  1. 浏览器UI元素的动态特性:移动浏览器(如Chrome、Safari)的地址栏、工具栏等界面组件会根据用户操作(如滚动页面)自动显示或隐藏。这种动态行为会导致视口(viewport)的可用高度发生变化,但 100vh 的值始终基于初始隐藏状态下的视口高度计算,而非实时变化的可见区域高度。

  2. 视口高度计算偏差:当地址栏从隐藏变为可见时,实际可用视口高度会减小,但 100vh 仍保持原值,导致内容超出可视区域,触发滚动条。

  3. 浏览器厂商差异:不同浏览器对视口高度的计算逻辑存在差异,例如 iOS Safari 更倾向于将 100vh 视为未包含地址栏的高度。


二、解决方案

✅方案1:动态计算视口高度 + CSS 变量(推荐)

  1. 核心思路:通过 JavaScript 实时获取 window.innerHeight(当前可视区域高度),将其转换为 CSS 变量,并在样式中使用该变量替代 100vh

  2. 实现步骤

    • JavaScript 部分:监听窗口大小变化事件,动态更新 CSS 变量。
      function setViewportHeight() {
        const innerHeight = window.innerHeight ; 
        document.documentElement.style.setProperty('--innerHeight', `${innerHeight}px`);
      }
      window.addEventListener('resize', setViewportHeight);
      setViewportHeight(); // 初始化
      
    • CSS 部分:使用自定义变量控制元素高度。
      .fullscreen {
        height: var(--innerHeight);
        background: pink;
        overflow: hidden; /* 避免子内容溢出 */
      }
      

    优势:
    ✔️ 完美适配各种设备状态变化
    ✔️ 兼容所有支持 CSS 变量的现代浏览器
    ✔️ 无需修改现有布局结构


✅方案2:绝对定位 + 全屏覆盖

  1. 适用场景:简单布局且需完全覆盖屏幕的元素。

  2. 实现代码

    .fullscreen {
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      background: lightblue;
    }
    

    注意:
    ⚠️ 如果父元素不是 body,需确保其父级链上的所有元素都有 height: 100%
    ⚠️ 此方法会脱离文档流,可能影响其他元素布局

    适用场景

    • 模态对话框/加载动画等临时全屏组件
    • 视频播放器等需要强制全屏的场景

✅方案3:使用动态视口单位(dvh)

  1. 实验性方案:部分现代浏览器支持 dvh(Dynamic Viewport Units),可直接响应视口变化。
    .fullscreen {
      height: 100dvh; /* 根据最新标准动态计算 */
    }
    
    现状: 📱 仅部分现代浏览器支持(Chrome 88+、Edge 88+) 🚫 iOS Safari 暂未支持 👉 适合作为渐进增强方案,需配合 fallback 使用 在这里插入图片描述

💡 经验之谈:无论采用哪种方案都能解决大多数问题,如果不行可以叠加其他方案试试,只用不断地尝试,不断优化适配策略。

TypeScript 接口入门:定义代码的契约与形态

一、什么是接口?

用于描述一个对象的结构。

// 定义一个名为 User 的接口
interface User {
    id: number;
    name: string;
    email: string;
}

function printUserInfo(user: User) {
    console.log(`ID: ${user.id}, Name: ${user.name}, Email: ${user.email}`);
}

const myUser: User = {

    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
};

printUserInfo(myUser); // OK

const invalidUser: User = {
    id: 2,
    username: 'Bob', // 属性名不匹配 编译时错误
    // 缺少 name,email 属性
};


二、接口的丰富特性

1. 可选属性(Optional Properties)

有时,对象的某些属性不是必需的。我们可以使用 ? 来标记它们。

interface UserProfile {
    id: number;
    username: string;
    bio?: string; // bio 是可选的
}

const user1: UserProfile = { id: 1, username: 'Alice' }; // OK
const user2: UserProfile = { id: 2, username: 'Bob', bio: 'Developer' }; // OK

2. 只读属性(Readonly Properties)

我们可以使用 readonly 关键字来防止对象属性在创建后被修改,这对于创建不可变数据非常有用。

interface Point {
    readonly x: number;
    readonly y: number;
}

const p1: Point = { x: 10, y: 20 };
p1.x = 5; // Error: 无法为“x”赋值,因为它是只读属性。

3. 函数类型

接口也能用来定义函数的签名(参数类型和返回值类型)。

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc = function (src: string, sub: string) {
    let result = src.search(sub);
    return result > -1;
};

console.log(mySearch('hello', 'll'));

4. 可索引类型(Indexable Types)

接口可以描述那些可以通过索引得到的类型,比如数组和对象。

interface StringArray {
    [index: number]: string; // 索引是数字,值是字符串
}

let myArray: StringArray;
myArray = ['Bob', 'Fred'];
let myStr: string = myArray[0]; // OK
console.log(myStr);


interface Dictionary {
    [key: string]: any; // 索引是字符串,值是任意类型
}

let user: Dictionary = {
    name: '张三',
    age: 18,
    sex: '男',
}

console.log(user.name);

5. 类实现(Class Implementations)

接口可以被类(Class)implements(实现),强制一个类必须遵循接口定义的契约。

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date): void;
}

class Clock implements ClockInterface {
    currentTime: Date = new Date();
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) {
        this.currentTime.setHours(h);
        this.currentTime.setMinutes(m);
    }

    printTime() {
        console.log(this.currentTime.toLocaleTimeString());
    }
}


let clock = new Clock(12, 30);
clock.printTime(); //12:30:43
clock.setTime(new Date('2024-5-6 09:30:43'));
clock.printTime(); //09:30:43

三、接口的扩展与合并

1. 继承(Extends)

一个接口可以像类一样继承另一个接口,从而复用和扩展类型定义。

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

// Square 继承了 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square: Square = {
    color: 'blue',
    penWidth: 5.0,
    sideLength: 10,
};

2. 声明合并(Declaration Merging)

这是一个接口独有的、非常强大的特性。如果你在同一个作用域内定义了两个同名的接口,它们会自动合并成一个单一的接口。

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

// 合并后,Box 接口同时拥有 height, width, 和 scale 属性
const box: Box = { height: 5, width: 6, scale: 10 };

常用的用法 扩展第三方库的类型定义。例如,如果你想为 window 对象添加一个自定义属性,你可以这样做,而不会覆盖原有的定义:

// 在你的 .d.ts 文件中
declare global {
    interface Window {
        myAppConfig: object;
    }
}

// 现在你可以在代码中安全地访问它
window.myAppConfig = { version: '1.0' };

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多TypeScript开发干货

使用自定义高亮API增强用户‘/’体验

本篇依然来自于我们的 《前端周刊》 项目!

由团队成员 0bipinnata0 翻译,这位佬有技术追求、翻译风格精准细腻,还擅长挖掘原文背后的技术细节~

欢迎大家 进群 同该佬深度交流😁 以及持续追踪全球最新前端资讯!!

原文地址:Using the Custom Highlight API

生成前端周刊图.png

最近 CSS Custom Highlight API 引起了我的注意,因为 Firefox 最近开始支持它(Firefox 140,2025年6月),这使得所有主流浏览器都支持了这个 API。通过它,你可以对通过 JavaScript 中的 Range() 类获取的文本应用(某些)样式。我本来想说是你选择的文本,但这里实际上并没有涉及真正的普通选择器,这对于像我这样的 CSS 开发者来说是相当不寻常的。

我认为这里需要一个基本的文字说明,因为当我第一次开始研究它时,这样的说明肯定会对我有帮助:

  1. 你需要一个 textNode(例如 document.querySelector("p").firstChild

  2. 然后你需要一个 Range(),在其上执行 setStartsetEnd,这意味着范围现在在这两个整数之间。

  3. 然后你在该 Range 上调用 CSS.highlights.set(),给它一个名称。

  4. 然后你在 CSS 中使用 ::highlight(),传入你刚才使用的名称。

如果我们在页面上有一个 <p> 文本,整个过程看起来是这样的:

const WORD_TO_HIGHLIGHT = "wisdom";
const NAME_OF_HIGHLIGHT = "our-highlight";

const textNode = document.querySelector("p").firstChild;
const textContent = textNode.textContent;

const startIndex = textContent.indexOf(WORD_TO_HIGHLIGHT);
const endIndex = startIndex + WORD_TO_HIGHLIGHT.length;

const range = new Range();
range.setStart(textNode, startIndex);
range.setEnd(textNode, endIndex);

const highlight = new Highlight(range);
CSS.highlights.set(NAME_OF_HIGHLIGHT, highlight); 

在开发者工具中看到这个效果很有趣,单词 "wisdom" 明显应用了自定义 CSS 样式,但在该单词周围没有你通常认为应用这些样式所必需的元素。

image.png

这很可能就是浏览器本身在需要仅对文本的某些部分应用样式时所做的事情,比如当你使用浏览器内置的查找功能时。

image.png

这是演示:

codepen.io/editor/anon…

为什么这很有用?

  • 能够在完全不需要操作 DOM 的情况下定位和样式化文本是很有趣的。有时,DOM API 被批评为缓慢,所以能够避免这种情况可能是有利的,特别是如果你需要大量这样做的话。

  • 添加和删除 <span> 元素,除了可能"缓慢"之外,还会影响 DOM 结构,从而可能影响其他处理 DOM 的 CSS 和 JavaScript。

  • DOM 复杂度可能是网页性能的一个问题。过多的 DOM 节点,重新计算可能非常"昂贵",页面上的用户体验可能会受到影响,比如动画和滚动变慢。

这是一个只有 17 个更改文件的 GitHub PR 页面。该页面已经有超过 4,500 个 span 元素,用于诸如代码差异着色和语法高亮等功能。这已经相当重了,而且肯定会变得更糟。

![image.png](使用自定义高亮API增强用户‘+’体验+54149756-b7ed-41c7-9c27-b0ec61235095/image 2.png)

我确信这个 API 存在的原因还有很多,但这些只是我立即想到的几个原因。

做更多事情(搜索示例)

创建一个 new Highlight() 可以接受多个 Range。这意味着 CSS 中的单个 ::highlight() 可以应用于许多文本范围。如果我们在页面上构建自己的搜索功能,这将很有用。如果搜索是你正在构建的 Web 应用程序的关键功能,我可以很容易地想象为它构建自己的 UI,而不是依赖内置的浏览器功能。

这次,让我们让要在文本中查找的单词来自用户:

<label>
  Search the text below
  <input type="search" value="oven" id="searchTerm">
</label>  

然后我们监听变化:

window.searchTerm.addEventListener("input", (e) => {
  doSearch(e.target.value.toLowerCase());
}); 

注意我们将输入的值传递给一个函数,并在传递时将其转换为小写,因为搜索在不区分大小写时通常最有用。

我们的 doSearch 函数然后将接受该搜索词并在所有文本上运行正则表达式:

const regex = new RegExp(searchTerm, "gi"); 

我们需要的是一个包含所有找到的文本实例索引的数组。这是一段有点冗长的代码,但就是这样:

const indexes = [...theTextContent.matchAll(new RegExp(searchTerm, 'gi'))].map(a => a.index); 

有了这个索引数组,我们可以循环遍历它们创建 Range,然后将所有 Range 发送到新的 Highlight。

const arrayOfRanges = [];

indexes.forEach(matchIndex => {
  // 从索引值创建一个 "Range"。
  const searchRange = new Range();
  searchRange.setStart(par, matchIndex);
  searchRange.setEnd(par, matchIndex + searchTerm.length);

  arrayOfRanges.push(searchRange);
})

const ourHighlight = new Highlight(...arrayOfRanges);
CSS.highlights.set("search-results", ourHighlight); 

总的来说,它创建了一个功能完整的搜索体验:

codepen.io/editor/anon…

用于语法高亮

感觉语法高亮代码是这个 API 的一个很好的用例。André Ruffert 已经采用了这个想法并付诸实践,制作了一个 [<syntax-highlight> Web Component](https://andreruffert.github.io/syntax-highlight-element/),它使用 Lea Verou 的 Prism.js 来解析代码,但然后不像开箱即用的 Prism 那样应用 <span>,而是使用这个自定义高亮 API。

示例:

codepen.io/editor/anon…

我认为这很棒,但值得注意的是,这个 API 只能在客户端使用。对于语法高亮这样的功能,这可能意味着在看到代码和语法高亮"生效"之间会有延迟。我承认在可能的情况下,我更喜欢服务器端渲染的语法高亮。这意味着如果你可以从服务器提供一堆像这样的 <span>(并且不会严重影响性能或可访问性),那可能会更好。

我也承认我仍然对内置语法高亮的字体有些着迷,这感觉像是字体厂商可以进入的未开发领域。

我们让 JSON.stringify 的速度提升了两倍以上

本篇依然来自于我们的 《前端周刊》 项目!

由团队成员 掘金安东尼 翻译,欢迎大家 进群 持续追踪全球最新前端资讯!!

原文地址:v8.dev/blog/json-s…

生成前端周刊图.png

译者小结

JSON.stringify 提速的核心为以下6点:

  1. 快速路径:避开一大堆通用检查(节省 CPU 时间)
  2. 专用版本:按字符串类型分开编译(减少分支判断)
  3. 批量扫描:一次看多字符(降低循环次数)
  4. 缓存形状:重复对象直接批量处理(跳过重复工作)
  5. 更快算法:数字转字符串的计算加速(核心耗时优化)
  6. 分段缓冲:内存分配更聪明(避免大搬家)

原文

JSON.stringify 是 JavaScript 中用于序列化数据的核心函数。它的性能直接影响着 Web 上的常见操作——从为网络请求序列化数据,到将数据保存到 localStorage。更快的 JSON.stringify 意味着页面交互更迅速、应用响应更灵敏。这就是为什么我们很高兴地分享:最近的一次工程改进,使得 V8 中的 JSON.stringify 性能提升了两倍以上。本文将拆解实现这一提升的技术优化。

无副作用的快速路径

此次优化的基础是一条新的快速路径,建立在一个简单的前提上:如果我们能够保证序列化对象时不会触发任何副作用,就可以使用更快的专用实现。这里的“副作用”指的是任何会打破对象简单、顺序遍历的情况。

这不仅包括明显的情况,比如在序列化过程中执行用户定义的代码,还包括一些更隐蔽的内部操作,比如可能触发垃圾回收的过程。有关哪些情况会导致副作用,以及如何避免它们的更多细节,请参见 Limitations

只要 V8 能确定序列化过程不会出现这些情况,就可以一直停留在高度优化的路径上。这使它能够绕过通用序列化器中许多昂贵的检查和防御逻辑,从而在处理最常见的、代表纯数据的 JavaScript 对象时获得显著加速。

此外,这条新快速路径是迭代式的,而不是像通用序列化器那样递归。这一架构选择不仅免去了栈溢出检查,并允许我们在编码改变后快速恢复,还能让开发者序列化比以前更深层嵌套的对象图。

处理不同的字符串表示

在 V8 中,字符串可以用单字节或双字节字符表示。如果一个字符串只包含 ASCII 字符,它会被存储为单字节字符串,每个字符占 1 个字节。但如果字符串中有一个字符超出 ASCII 范围,那么整个字符串都会使用双字节表示,内存占用翻倍。

为了避免统一实现中不断分支和类型检查的开销,整个字符串序列化器现在基于字符类型进行模板化。这意味着我们会编译两个独立的、专门优化的版本:一个完全针对单字节字符串优化,另一个针对双字节字符串优化。这确实会影响二进制大小,但我们认为性能提升绝对值得。

该实现还能高效处理混合编码。在序列化过程中,我们必须检查每个字符串的实例类型,以检测无法在快速路径处理的表示形式(比如 ConsString,它在扁平化时可能触发 GC),这些会回退到慢路径。这个检查同时也能知道字符串是单字节还是双字节编码。

因此,从乐观的单字节序列化器切换到双字节版本几乎是零成本的。当检查发现双字节字符串时,就会新建一个双字节序列化器,并继承当前状态。最后,只需将初始单字节序列化器的输出与双字节版本的输出拼接即可。这种策略确保了在常见情况下保持高度优化的路径,同时转向处理双字节字符的开销很小且高效。

使用 SIMD 优化字符串序列化

在 JavaScript 中,任意字符串在序列化为 JSON 时都可能包含需要转义的字符(例如 " 或 ``)。传统的逐字符循环查找这些字符速度很慢。

为了加速这一过程,我们基于字符串长度采用了两级策略:

  • 长字符串:使用专用的硬件 SIMD 指令(例如 ARM64 Neon)。这样可以将字符串的大块内容加载到宽 SIMD 寄存器中,并在几条指令内同时检查多个字节是否存在需要转义的字符。
  • 短字符串:使用 SWAR(寄存器内 SIMD)技术。该方法通过在标准通用寄存器上进行巧妙的按位逻辑运算,以极低开销一次处理多个字符。

无论采用哪种方法,流程都很高效:按块快速扫描字符串。如果某个块中没有特殊字符(这是常见情况),就可以直接复制整个字符串。

快速路径上的“快速通道”

即使在主快速路径中,我们也找到了进一步加速的机会。默认情况下,快速路径仍需遍历对象的每个属性,并对每个键执行一系列检查:确认不是 Symbol、确保可枚举、扫描字符串是否包含需要转义的字符(例如 " 或 ``)。

为消除这些步骤,我们在对象的隐藏类上引入了一个标志。一旦我们序列化了对象的所有属性,就会将其隐藏类标记为 fast-json-iterable,前提是属性键都不是 Symbol、全部可枚举、且不包含需要转义的字符。

当我们序列化另一个具有相同隐藏类的对象(这种情况很常见,比如一组形状相同的对象数组)并且它是 fast-json-iterable 时,我们可以直接将所有键复制到字符串缓冲区,而无需进一步检查。

我们还将这种优化应用到了 JSON.parse,当解析数组时,如果数组中的对象通常有相同的隐藏类,就可以用它来进行快速键比较。

更快的数字转字符串算法

将数字转换为字符串是一个出乎意料的复杂且性能关键的任务。在 JSON.stringify 的优化中,我们发现可以显著加速这一过程,于是升级了核心的 DoubleToString 算法。我们用 Dragonbox 替换了长期使用的 Grisu3 算法,用于最短长度的数字转字符串转换。

虽然这一优化是为了 JSON.stringify,但新的 Dragonbox 实现会惠及 V8 中所有 Number.prototype.toString() 的调用。这意味着任何数字转字符串的代码,不仅仅是 JSON 序列化,都会自动获得这一性能提升。

优化底层临时缓冲区

任何字符串构建操作中的一个主要开销是内存管理。之前,我们的序列化器会在 C++ 堆上构建一个单一的连续缓冲区。虽然简单,但这种方式有一个显著缺点:一旦缓冲区空间耗尽,就必须分配更大的缓冲区,并将全部现有内容复制过去。对于大型 JSON 对象,这种反复分配和复制的过程会造成很大的性能损耗。

关键洞察是,强制这个临时缓冲区保持连续并没有真正的好处,因为最终结果只会在最后一步组装成一个字符串。

基于此,我们将旧系统替换为分段缓冲区。不再是一个大的、不断增长的内存块,而是使用 V8 的 Zone 内存分配一组较小的缓冲段。当一个段写满时,我们直接分配一个新的段继续写,完全消除了昂贵的复制操作。

限制

新的快速路径通过专门优化常见、简单的情况来实现速度提升。如果被序列化的数据不满足这些条件,V8 会回退到通用序列化器以确保正确性。要获得全部性能提升,JSON.stringify 调用需要满足以下条件:

  • 无 replacer 或 space 参数:提供 replacer 函数或 space/gap 参数(用于美化输出)会使其进入通用路径。快速路径仅支持紧凑的、未转换的序列化。
  • 纯数据对象和数组:被序列化的对象应是简单的数据容器,即它们及其原型不能有自定义的 .toJSON() 方法。快速路径假设标准原型(如 Object.prototype、Array.prototype),且无自定义序列化逻辑。
  • 对象无索引属性:快速路径针对具有常规字符串键的对象进行优化。如果对象包含类数组的索引属性(如 '0'、'1'…),则会使用较慢的通用序列化器。
  • 简单字符串类型:某些内部 V8 字符串表示(如 ConsString)在序列化前需要分配内存进行扁平化。快速路径避免执行可能触发这种分配的操作,最适合处理简单的顺序字符串。作为 Web 开发者,这一点难以直接控制,但大多数情况下都能正常工作。

对于绝大多数使用场景(如为 API 响应序列化数据、缓存配置对象),这些条件都是自然满足的,开发者可以自动享受到性能提升。

结论

通过从高层逻辑到底层内存与字符处理的全方位重构,我们在 JetStream2 的 json-stringify-inspector 基准测试中实现了超过 2 倍的性能提升。下图展示了在不同平台上的结果。这些优化从 V8 版本 13.8(Chrome 138)开始可用。

image.png

《会聊天的文件筐:用 Next.js 打造“图音双绝”的上传组件》

开场三句话

  1. 用户说:“发张图。”
  2. 用户说:“发段语音。”
  3. 你说:“稍等,我让浏览器先开个 AI 小灶。”

今天,我们要写一个聊天 UI 的上传组件,它既能识图又能辨音,还要保持界面优雅,像一位会魔法的管家。
(配图:一只端着托盘的小机器人,托盘上躺着一张猫咪照片和一只麦克风)


一、需求拆解:到底要上传什么?

类型 浏览器能做什么 我们要做什么
图片 <input type="file" accept="image/*"> 预览、压缩、OCR/打标签
音频 <input type="file" accept="audio/*"> or MediaRecorder 波形预览、转文字、情绪分析

一句话:浏览器负责“拿”,我们负责“看/听”


二、技术地图:从点击到 AI 的大脑

┌────────────┐     ┌──────────────┐     ┌──────────┐
│ 用户点击   │──→──│ 前端预览     │──→──│ 后端识别  │
│ input file │     │ canvas /    │     │ OCR /    │
└────────────┘     │ Web Audio   │     │ Whisper  │
                   └──────────────┘     └──────────┘

三、前端实现:React + TypeScript(Next.js 亦可)

3.1 组件骨架:一个 Hook 统治所有上传

// hooks/useUploader.ts
import { useState, useCallback } from 'react';

type FileType = 'image' | 'audio';

export function useUploader() {
  const [file, setFile] = useState<File | null>(null);
  const [preview, setPreview] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const handleChange = useCallback(
    (type: FileType) => (e: React.ChangeEvent<HTMLInputElement>) => {
      const f = e.target.files?.[0];
      if (!f) return;
      setFile(f);
      setPreview(URL.createObjectURL(f));
      setLoading(true);
      // ⭐ 交给识别函数
      recognize(type, f).then((result) => {
        console.log('识别结果', result);
        setLoading(false);
      });
    },
    []
  );

  return { file, preview, loading, handleChange };
}

3.2 图片识别:浏览器端就能 OCR(tesseract.js)

// utils/recognize.ts
import Tesseract from 'tesseract.js';

export async function recognize(type: 'image' | 'audio', file: File) {
  if (type === 'image') {
    const { data: { text } } = await Tesseract.recognize(file, 'eng+chi_sim');
    return { text };
  }
  if (type === 'audio') {
    // 音频先上传,后端 Whisper 转文字,下文细讲
    const form = new FormData();
    form.append('audio', file);
    const res = await fetch('/api/transcribe', { method: 'POST', body: form });
    return res.json();
  }
}

浏览器里跑 OCR 就像让小学生在操场上背圆周率——能背,但跑不快。
所以我们只在小图离线场景用 tesseract.js,大图还是走后端 GPU。


3.3 音频录制:边录边传,体验拉满

// components/AudioRecorder.tsx
import { useState } from 'react';

export default function AudioRecorder({ onDone }: { onDone: (f: File) => void }) {
  const [recording, setRecording] = useState(false);
  const mediaRef = useRef<MediaRecorder | null>(null);

  const start = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const mr = new MediaRecorder(stream, { mimeType: 'audio/webm' });
    const chunks: BlobPart[] = [];
    mr.ondataavailable = (e) => chunks.push(e.data);
    mr.onstop = () => {
      const blob = new Blob(chunks, { type: 'audio/webm' });
      onDone(new File([blob], 'speech.webm'));
    };
    mr.start();
    mediaRef.current = mr;
    setRecording(true);
  };

  const stop = () => {
    mediaRef.current?.stop();
    setRecording(false);
  };

  return (
    <>
      <button onClick={recording ? stop : start}>
        {recording ? '⏹️ 停止' : '🎤 录音'}
      </button>
    </>
  );
}

浏览器录音使用的是 MediaDevices.getUserMedia → MediaRecorder → Blob 这条“黄金管道”。
数据在内存里是 PCM 原始波形,压缩成 webm/opus 后才上传,节省 90% 流量。


四、后端识别:GPU 才是第一生产力

4.1 图片:OCR + 打标签(Python 示例,Next.js API Route 可调用)

# api/ocr.py  (FastAPI 伪代码)
from fastapi import UploadFile
import pytesseract, torch, timm

@app.post("/ocr")
async def ocr(file: UploadFile):
    img = await file.read()
    text = pytesseract.image_to_string(img, lang='eng+chi_sim')
    labels = model(img)  # timm 预训练 ResNet
    return {"text": text, "labels": labels}

4.2 音频:用 Whisper 转文字(OpenAI 开源版)

# api/transcribe.py
import whisper, tempfile, os

model = whisper.load_model("base")

@app.post("/transcribe")
async def transcribe(file: UploadFile):
    with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as tmp:
        tmp.write(await file.read())
        tmp.flush()
        result = model.transcribe(tmp.name, language='zh')
        os.unlink(tmp.name)
        return {"text": result["text"]}

Whisper 的「魔法」:把 30 秒音频切成 mel 频谱 → Transformer 编码 → 解码文字。
在 A100 上,转 30 秒音频只需 100 ms,比你泡咖啡还快。


五、前端 UI:让文件像聊天泡泡一样优雅

┌────────────────────────────┐
│  用户 A                   │
│  [猫咪照片预览]           │
│  🖼️ 识别:一只橘猫在打盹 │
└────────────────────────────┘

实现思路:

  1. 上传成功 → 本地先渲染占位泡泡(带 spinner)。
  2. 后端返回结果 → 更新泡泡内容(图片 + 文字 / 语音 + 文字)。
  3. 失败 → 泡泡变红色,重试按钮出现。

六、性能 & 体验小贴士

问题 解法
大图片 10 MB+ 浏览器 canvas.toBlob(file, 'image/jpeg', 0.8) 压缩
音频长 5 min+ 分片上传 + 后端流式转写
弱网 上传前存 IndexedDB,网络恢复后重试
隐私 敏感图片走本地 OCR,不上传

七、彩蛋:一行代码让上传支持拖拽

<div
  onDrop={(e) => {
    e.preventDefault();
    const f = e.dataTransfer.files[0];
    // 复用前面 useUploader 的逻辑
  }}
  onDragOver={(e) => e.preventDefault()}
  className="border-2 border-dashed border-gray-400 rounded p-8"
>
  📂 把文件扔进来
</div>

八、结语:上传的尽头,是理解

当 AI 把猫咪照片识别成“一只橘猫在打盹”,把语音转成“今晚吃什么?”时,
上传组件就不再是冷冰冰的 <input>,而是人类与算法握手言欢的桥梁

愿你写的每一个上传按钮,都能把比特变成诗。
祝你编码愉快,文件永不 413!

前端跨域全解析:从 CORS 到 postMessage,再到 WebSocket

小白友好版,跨域不再迷路


1️⃣ 什么是跨域?

跨域,简单理解就是:

“浏览器:哎呀,这请求要跑到别人家去拿数据,我敢不敢让它去呢?”

严格说法:当浏览器从一个源(Origin)请求另一个源(Origin)的资源时,如果两者不一样,就触发同源策略(Same-Origin Policy),这就是跨域。

源(Origin)组成:

源 = 协议(Protocol) + 域名(Host) + 端口(Port)

三者任意一项不同,就算跨域。

为什么有同源策略?

  • 防止 CSRF(跨站请求伪造)
  • 防止 XSS(跨站脚本攻击)
  • 防止隐私泄露(Cookie、账户信息)

举个例子:

http://example.com:80/page1.html  →  http://api.example.com:80/data

域名不同 → 跨域


2️⃣ CORS(跨域资源共享)

CORS 就像浏览器和服务器的“通行证”,谁允许你进,谁说了算。

浏览器:我想拿数据!
服务器:你是安全的,我允许你。
浏览器:好,我拿走数据!

2.1 两种 CORS 请求

🔹 简单请求(Simple Request)

条件(全部满足):

  1. 方法:GET / POST / HEAD
  2. 请求头:只能是浏览器安全集合
  3. 请求体:安全格式(纯文本/表单)
// 简单请求示例
fetch('https://api.example.com/data', {
  method: 'GET',
})
  .then(res => res.json())
  .then(data => console.log(data));

服务器只需允许跨域即可:

Access-Control-Allow-Origin: *

🔹 预检请求(Preflight Request)

触发条件:

  • 方法不是 GET/POST/HEAD
  • 自定义请求头(如 X-Token
  • Content-Type 非简单类型

流程示意:

浏览器:我想PUT数据,可以吗?(OPTIONS预检)
服务器:可以,你的来源、方法、头都允许
浏览器:好,发真正请求

代码示例:

fetch('https://api.example.com/data', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-Token': '123456'
  },
  body: JSON.stringify({ name: '跨域小白' })
})
.then(res => res.json())
.then(data => console.log(data));

面试小技巧:能区分“简单请求 vs 预检请求”,CORS 面试题轻松过。


3️⃣ JSONP

JSONP 是老派跨域方式,利用 <script> 标签天生跨域的特性。

<script>
  function handleData(data) {
    console.log('拿到跨域数据啦!', data);
  }
</script>
<script src="https://api.example.com/data?callback=handleData"></script>

优点:兼容老浏览器、实现简单
缺点:只能 GET、XSS 风险、错误处理麻烦

现代项目基本不用 JSONP,直接 CORS + Fetch 就好。


4️⃣ postMessage

当你和不同源的 iframe/窗口要聊聊时,postMessage 就像安全的对讲机。

<!-- 父页面 -->
<iframe id="child" src="https://other.com"></iframe>
<script>
  const iframe = document.getElementById('child');
  iframe.contentWindow.postMessage({ msg: 'Hi小伙伴' }, 'https://other.com');

  window.addEventListener('message', (event) => {
    if(event.origin !== 'https://other.com') return; // 安全检查
    console.log('收到子页面消息:', event.data);
  });
</script>

记住:安全第一,一定要检查 event.origin


5️⃣ WebSocket

WebSocket 就是“前端的即时聊天神器”,浏览器和服务器可以随时互发消息。

const ws = new WebSocket('wss://example.com/socket');

ws.onopen = () => ws.send('hello server!');

ws.onmessage = (msg) => console.log('收到服务器消息:', msg.data);

ws.onclose = () => console.log('连接关闭');

特点:

  • 全双工
  • 单连接
  • 跨域天然支持

6️⃣ 跨域常见应用场景

场景 示例 解决方案 代码示例
前端调用后端 API 开发 localhost → 远端 API CORS / 反向代理 fetch('https://api.example.com')
第三方接口 高德地图、支付 CORS / JSONP fetch('https://maps.com/api')
跨域 iframe 通信 支付 iframe postMessage iframe.contentWindow.postMessage(...)
多窗口/标签页 登录状态同步 postMessage + window.open window.opener.postMessage(...)
Web Worker 跨域 Worker 加载脚本 postMessage + CORS worker.postMessage(...)
静态资源跨域 CDN JS/CSS/图片 允许跨域 <script src="https://cdn.com/lib.js"></script>

7️⃣ 面试问答专栏:跨域篇

1️⃣ 面试官问:什么是跨域?

回答示例

“跨域就是浏览器发现你要去访问别人家的资源,它会先问一句:我敢不敢让它去?
严格来说,就是源(协议 + 域名 + 端口)不同,就触发同源策略。”


2️⃣ 面试官问:什么是 CORS?

回答示例

“CORS 就是浏览器和服务器的通行证,服务器在响应头声明允许的源、方法、头,浏览器通过才交数据给前端。
简单请求直接发,复杂请求会先发 OPTIONS 预检。”

代码示例:

fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log(data));

3️⃣ 面试官问:JSONP 和 CORS 有什么区别?

回答示例

“JSONP 是老派方案,靠 <script> 标签跨域,只能 GET,有 XSS 风险。
CORS 是现代方案,更安全灵活,支持 POST/PUT/DELETE。”


4️⃣ 面试官问:postMessage 是什么?

回答示例

“父页面和 iframe 或者窗口与 Worker 需要互相通信时,用 postMessage 传消息。安全重点是检查 event.origin。”

代码示例:

iframe.contentWindow.postMessage({ msg: 'hello' }, 'https://other.com');

5️⃣ 面试官问:WebSocket 跨域吗?

回答示例

“WebSocket 建立的是 TCP 长连接,一旦连接建立就天然跨域,可以双向通信。”


8️⃣ 总结

跨域知识点其实就像“安检关卡”,理解它,你就能安全访问资源,而且面试题轻松拿分。

技术 适用场景 优点 注意点
CORS 前后端接口跨域 简单灵活 后端需支持
JSONP 老 GET 请求 兼容老浏览器 只能 GET、有 XSS 风险
postMessage iframe/窗口/Worker 通信 安全灵活 检查 origin
WebSocket 实时通信 高效全双工 服务端支持

小结:

  • CORS → 现代前端首选
  • JSONP → 老项目遗留
  • postMessage → 窗口/iframe/Worker 通信
  • WebSocket → 实时双向通信

跨域学会了,你就是前端安全小能手!


第二章 虎牢关前初试Composition,吕布持v-model搦战

上回说到桃园三英,以reactive义旗初燃响应之火。未及旬日,消息传遍十八路诸侯:董卓挟旧框架jQuery,虎踞虎牢关,其义子吕布手持方天画戟,戟上双锋刻着“v-model”二字,号称“双向绑定第一猛将”,凡与之交锋者,模板与数据顷刻错乱,士卒(开发者)叫苦不迭。诸侯震惧,聚于酸枣大营,共议破敌之策。

玄德谓关、张曰:“吕布之势,在于旧双向绑定蛮横:一处改值,处处牵连,调试如坠五里雾。吾等新得Vue 3利器,可趁此关试其锋芒。”于是三人随公孙瓒军,星夜抵虎牢关下。

翌日黎明,鼓角齐鸣。吕布匹马出阵,戟指诸侯大喝:“谁敢与我斗绑定!”诸侯阵中,一将应声而出,乃江东孙坚,旧用Angular.js,被吕布一戟挑落马下,数据流当场断链。诸侯失色。

玄德回顾云长:“二弟,汝可出战?”云长丹凤眼微睁,提刀而出,却非青龙偃月,而是一柄新铸长刀,名曰<script setup>。刀背暗藏三大新纹:

  1. 纹一:ref()——化普通值为响应利刃;
  2. 纹二:computed()——凝衍生数据为刀罡;
  3. 纹三:watchEffect()——布追踪暗劲,敌一动则我即知。

云长横刀立马,朗声道:“吕布小儿,敢接我Composition刀法否?”

吕布大笑,挥戟直取。云长举刀迎敌,只见刀光闪处,代码如诗:

<!-- 虎牢关·关云长挑战牌.vue -->
<script setup lang="ts">
import { ref, computed, watchEffect } from 'vue'

const luBuHP = ref(100)          // 吕布血条
const guanYuATK = ref(30)        // 关羽攻击
const isCritical = computed(() => luBuHP.value < 50)
watchEffect(() => {
  if (isCritical.value) console.warn('吕布进入红血,狂暴模式开启!')
})
</script>

<template>
  <div>
    <p>吕布剩余血量:{{ luBuHP }}</p>
    <button @click="luBuHP -= guanYuATK">青龙斩</button>
  </div>
</template>

刀戟相交,吕布顿觉旧法迟钝:每次v-model改动,需层层触发$digest,而云长刀法轻灵,仅在必要处精准更新,DOM重绘大减。三十合后,吕布戟法渐乱。

翼德见云长占上风,大吼一声,挺矛跃马而出。其矛名Teleport,一矛刺出,竟将“

”元素瞬息移至关外战场中央,避开层层嵌套之DOM重围,令吕布措手不及。

玄德亦催马上前,袖中飞出一面小旗,旗上书“Suspense”。旗展处,诸侯军阵忽现异步大军:先遣async setup()轻骑诱敌,随后<Suspense>大军稳压阵脚,待数据从远域(API)归来,三军齐发,吕布旧部顿时崩溃。

吕布见势不妙,拨马欲走。云长刀锋一转,大喝:“留下v-model!”刀落处,吕布戟上“双向绑定”四字应声而碎,化作漫天光屑。诸侯军乘势掩杀,虎牢关大门轰然洞开。

战后,酸枣大营庆功。曹操把盏谓玄德曰:“今日之战,方知Vue 3新特性之利:
<script setup>简军书(样板代码减半);
ref & reactive分兵权(原始与对象各得其所);
computedwatchEffect如伏兵暗哨,料敌先机;
Teleport奇兵突袭,解定位之困;
Suspense则整饬异步之乱,令军容肃整。

有此五利,何愁旧框架不破?”

玄德谦逊答曰:“皆赖众志。然董卓未灭,jQuery余孽犹在,吾等当整军向西,直驱长安。”

众人齐应。夜色下,营火映照着一面新旗——旗上赫然是Vue 3的Logo,而下方绣着一行小字:

“Composition API · 破釜沉舟”

(第二章完)

——下回《凤仪亭密谋自定义ref,貂蝉夜探shallowReactive》

第一章 桃园灯火初燃,响应义旗始揭

却说中平元年,黄巾大乱,页面失序,交互崩坏。时有涿郡涿县义士刘备字玄德,胸怀仁道,常叹 DOM 操作之繁;关羽字云长,力能扛鼎,恨 reflow 之劳形;张飞字翼德,声若巨雷,苦 repaint 之伤神。三人心念苍生,俱欲收拾旧山河,重整乾坤之渲染。

是夜,桃园深处,月色如银。三人焚香再拜,誓以 Vue 3 为号,举响应式义旗。刘备执defineReactive为剑,关羽握ref作刀,张飞抡reactive成矛。金兰结义,共立宏愿:
“自此以后,凡我兄弟,同写单文件组件,同守 Composition API,同赴前端沙场,生死与共,不可背弃!”

誓毕,刘备展卷,出一物示二人,乃《setup()》秘策一卷。卷首云:
“夫响应之道,先立 state,后衍 effect;state 者,民生之本,effect 者,治世之干。”
关羽、张飞拜受,顿首再拜。于是三英于桃园之中,点燃第一簇数据之火——

// 桃园结义·state.ts
import { reactive } from 'vue'

export const peachGarden = reactive({
  brothers: ['刘备', '关羽', '张飞'],
  oath: '上报国家,下安黎庶,同生共死,永不背义'
})

火光照处,页面微颤,旧日静态之 HTML 忽生涟漪。张飞惊曰:“异哉!我但改一字,视图即随动,莫非天命?”
刘备笑曰:“非天命也,Proxy 之力耳。凡入reactive者,皆录于 WeakMap,牵一发而动全身,此即‘响应’二字真谛!”

关羽抚髯而思:“既有响应之兵,尚缺调度之帅。来日当筑effect营寨,使数据之兵随帅旗而行,无令散乱。”

三人言谈未尽,忽闻远处鼓角之声——黄巾残党jQuery余孽,正聚众欲复辟直接操作 DOM 之旧制。刘备拔剑而起:“兄弟,随我出村,初试响应锋芒!”

于是桃园灯火未灭,三骑已扬尘而去。前端乱世,自此拉开序幕。后人有诗赞曰:

桃园一火照前端,
Proxy 初开响应天。
自此 DOM 随令转,
三分代码见真源。

(第一章完)

——下回《虎牢关前初试Composition,吕布持双向绑定搦战》

npm发包自己的组件并安装更新版本应该如何做?

npm发包组件

发布一个属于自己的npm包吧!接下来我们便使用Vue封装组件并发布到npm仓库

封装NPM组件-验证码

预览

npm-fabao1.png

1、创建账号注册登录

👉注册申请以及登录账号

官网

https://www.npmjs.com/

正常申请注册即可,选择 sign up 进入账户注册页面

npm-fabao2.png

2、创建vue3项目Tbcode

👉搭建项目

yarn create vite NexusCode --template vue

// 安装依赖
yarn 

👉创建组件

<template>
  <div class="necode">
    <div 
      class="codebox"
      :style="{
        'background': codeback,
        'width': width + 'px',
        'height': height + 'px'
      }"
      @click="getCode(length)"
    >
      {{ codevalue }}
    </div>
  </div>
</template>

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

// 接收父组件传递的 props
defineProps({
  value: {
    type: String,
    required: false,
  },
  length: {
    type: Number,
    default: 4,
    required: false,
  },
  back: {
    type: String,
    required: false,
  },
  width: {
    type: Number,
    default: 120,  // 默认宽度为120px
  },
  height: {
    type: Number,
    default: 40,   // 默认高度为40px
  }
});


const codelength = ref(4);

// 响应式变量
const codevalue = ref(''); // 验证码值
const codeback = ref('');  // 验证码背景色

// onMounted 是 Vue 3 的生命周期钩子,类似于 Vue 2 的 created
onMounted(() => {
  codelength.value=length?length:codelength.value;
  getCode(codelength.value); // 获取验证码
});

// 新增试用码-前端随机生成方法
const getCode = (row) => {
  // 随机背景颜色
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  const rgb = `rgb(${r},${g},${b})`;
  codeback.value = rgb;

  const arrall = [
    'A', 'B', 'C', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
  ];
  let str = '';
  for (let i = 0; i < row; i++) {
    str += arrall[Math.floor(Math.random() * arrall.length)];
  }
  codevalue.value = str;
};
</script>

<style scoped>
.codebox {
  text-align: center;
  font-weight: 800;
  line-height: 40px;
  display: inline-block;
  float: left;
  cursor: pointer;
  font-size: 24px;
  color: #fff;
  border-radius: 4px;
}
</style>

👉配置package.json

私人化配置

"private": true,
这就代表私人的包

公共包配置(这里我们使用这个)

{
  "name": "tbcode",
  "version": "0.0.2",
  "files": [
    "dist"
  ],
  "module": "./dist/tbcode.es.js",
  "main": "./dist/tbcode.umd.js",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/tbcode.es.ts",
      "require": "./dist/tbcode.umd.ts"
    },
    "./dist/style.css": {
      "import": "./dist/tbcode.css",
      "require": "./dist/tbcode.css"
    }
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.5.18"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^6.0.1",
    "vite": "^7.1.2"
  }
}

3、发布组件

👉版本名字

🍎名称规则

包名使用了@开头,一般使用@符号开头的包都是私有包,npm需要收费

加上--access public可直接发布组件,当成公有包,不需要支付费用

name:发布的包名,默认是上级文件夹名。不得与现在npm中的包名重复。包名不能有大写字母/空格/下滑线!

👉版本登录发布

Major version:大版本,代表破坏性变更。
Minor version:小版本,代表向后兼容的新功能。
Patch version:修订版本,代表 bug 修复和小改进。
🍎 版本发布
//查看当前用户是否登录
npm whoami 

//登陆
npm login 

// 或  npm addUser  

//部署
npm publish

👉发布名称冲突报错

下面就是名字被占用了

npm notice
npm notice package: Necode@0.0.2
npm notice Tarball Contents
npm notice 385B README.md
npm notice 169B dist/Necode.css
npm notice 2.0kB dist/necode.es.js
npm notice 1.3kB dist/necode.umd.js
npm notice 613B package.json
npm notice Tarball Details
npm notice name: Necode
npm notice version: 0.0.2
npm notice filename: Necode-0.0.2.tgz
npm notice package size: 2.1 kB
npm notice unpacked size: 4.4 kB
npm notice shasum: 6534df3352b5d299457dbff0b6e363b1b6ab6d4f
npm notice integrity: sha512-UZ1QGtymSFl73[...]rc1luwb77w/4Q==
npm notice total files: 5
npm notice
npm notice Publishing to 
  https://registry.npmjs.org/ with tag latest and default access
npm error code E400
npm error 400 Bad Request - PUT https://registry.npmjs.org/Necode - 
"Necode" is invalid for new packages
npm error A complete log of this run can be found in: 
C:\Users\admin\AppData\Local\npm-cache\_logs\2025-08-15T08_24_42_644Z-debug-0.log

👉更新名字,再次发布

npm publish提示信息如下

npm notice
npm notice package: tbcode@0.0.2
npm notice Tarball Contents
npm notice 385B README.md
npm notice 169B dist/Necode.css
npm notice 2.0kB dist/necode.es.js
npm notice 1.3kB dist/necode.umd.js
npm notice 613B package.json
npm notice Tarball Details
npm notice name: tbcode
npm notice version: 0.0.2
npm notice filename: tbcode-0.0.2.tgz
npm notice package size: 2.1 kB
npm notice unpacked size: 4.4 kB
npm notice shasum: 97d3dc40035c0e3fcbcb590704e7c0d531d9d16e
npm notice integrity: sha512-M4EQ/J8XRHZNT[...]CC0OwXVluzH8Q==
npm notice total files: 5
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access
+ tbcode@0.0.2

搜索已经可以发现我们的npm包了

tbcode

👉更新版本

接下来我们传第二个版本包

添加一些关键词

// 发布一个小的补丁号版本
npm version patch
npm publish

这个时候已经可以看到我们的关键词和版本信息了

Keywords
vuereacttbcodelintaibai林太白
npm version patch -m "xx"
【
    patch增加一位补丁号,
    minor增加一位小版本号,
    major增加一位大版本号
 】

👉取消版本

// 舍弃某个版本的模块  24小时使用这个即可(使用)
npm unpublish tbcode@1.0.0

// 舍弃某个版本的模块
npm deprecate my-npm@"< 1.0.8" "critical bug fixed in v1.0.2"

// 如何撤销发布
npm --force unpublish my_npm

4、使用

发布到 npm 后,安装使用自己封装的组件

安装依赖

npm i tbcode

//或者
yarn add tbcode

项目引入使用

import tbCode from 'tbCode'
import 'tbcode/dist/style.css'

app.component('Tbcode', tbcode) 


<Tbcode/>

问题以及处理

👉组件样式不生效,

问题就出现在下面两点

(1)组件的导出方式配置不对

(2)使用时候引入没有

// 导出方式配置
"exports": {
    ".": {
      "import": "./dist/tbcode.es.js",
      "require": "./dist/tbcode.umd.js"
    },
    "./dist/style.css": {
      "import": "./dist/tbcode.css",
      "require": "./dist/tbcode.css"
    }
},


// 引入使用
import 'tbcode/dist/style.css'

告别jQuery:2025年原生DOM操作最佳实践

随着现代浏览器对ECMAScript标准的全面支持,原生JavaScript已能高效替代jQuery的绝大多数功能。本文将深入探讨2025年DOM操作的核心优化策略,助你构建高性能前端应用。

一、选择器性能优化:querySelector陷阱与getElementById的抉择

querySelector的隐藏代价
虽然querySelectorquerySelectorAll提供了类似jQuery的CSS选择器语法,但其性能表现与选择器复杂度直接相关:

// 简单ID选择器
const element = document.querySelector('#myId'); 

// 复杂嵌套选择器
const nestedItems = document.querySelectorAll('div.container > ul.list > li:first-child');

当解析复杂选择器时,浏览器需遍历DOM树进行模式匹配,消耗时间与DOM规模成正比。尤其在万级节点中频繁调用时,可能成为性能瓶颈。

getElementById的极致优化
专为ID查找设计的API具备显著优势:

// 直接通过哈希映射定位元素
const element = document.getElementById('myId');

浏览器内部维护全局ID索引,使得时间复杂度稳定为O(1)。测试表明,其执行速度比querySelector('#id')快约15-30%。

决策矩阵:何时选用何种API

场景 推荐API 性能依据
单元素ID查找 getElementById 直接访问哈希索引,零解析开销
简单类选择(单个元素) querySelector 仅需解析单类选择器
复杂组合选择 querySelectorAll 牺牲部分性能换取开发效率
动态元素集合 getElementsByClassName 返回实时HTMLCollection,响应DOM变化

实践建议:在循环或动画中优先使用getElementByIdgetElementsByClassName;复杂静态元素组可缓存querySelectorAll结果避免重复查询。

二、高效批量操作:DocumentFragmentwill-change的协同

DocumentFragment:离线DOM的原子化操作
作为轻量级虚拟容器,其核心优势在于:

const fragment = document.createDocumentFragment();

// 批量创建节点(不触发重排)
for(let i=0; i<1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}

// 单次插入(仅1次重排)
document.getElementById('list').appendChild(fragment);

通过脱离文档流的特性,使中间操作完全避开渲染管线,将N次重排压缩为1次。实测显示,万级节点插入耗时从12s降至350ms。

will-change:GPU加速的预优化
当需要对现有元素进行连续动画时,CSS提示可触发硬件加速:

.animated-element {
  will-change: transform, opacity; 
  transition: transform 0.3s ease-out;
}

此声明通知浏览器预先将元素提升至独立合成层,避免后续transform/opacity变化引发重排。但需注意过度使用会导致内存暴涨。

双剑合璧技术方案

  1. 静态节点批量插入
    DocumentFragment创建 → 填充内容 → 单次挂载DOM
    (适用:列表初始化、大块模板渲染)

  2. 动态元素连续动画
    添加will-change提示 → 使用transform/opacity驱动动画 → 动画结束移除提示
    (适用:拖拽、滚动特效、渐变过渡)

关键警示will-change应作为最终优化手段,而非预防性添加。过度使用将导致层爆炸(Layer Explosion),移动设备内存开销可超300MB。

三、虚拟滚动:万级列表渲染的核心实现

虚拟滚动通过动态可视区域渲染破解性能困局,核心流程:

1. 布局引擎(Layout Engine)

const container = {
  clientHeight: 800,   // 可视区域高度
  itemHeight: 50,      // 单项预估高度
  bufferSize: 5,       // 渲染缓冲项数
};

// 计算可见项索引
const startIdx = Math.floor(scrollTop / itemHeight);
const endIdx = startIdx + Math.ceil(clientHeight / itemHeight) + bufferSize;

2. 动态渲染(Dynamic Rendering)

<div class="viewport" style="height:800px; overflow-y: auto">
  <!-- 撑开总高度的占位符 -->
  <div class="scroll-holder" style="height:${totalItems * itemHeight}px"></div> 
  
  <!-- 仅渲染可视项 -->
  <div class="visible-items" style="position:relative; top:${startIdx * itemHeight}px">
    ${visibleItems.map(item => `<div class="item">${item.text}</div>`)}
  </div>
</div>

3. 滚动优化(Scroll Optimization)

  • 使用requestAnimationFrame节流滚动事件
  • 持久化已渲染节点避免重复创建
  • 异步加载非可视区数据

性能对比(渲染10,000项)

方案 初始化时间 滚动帧率 内存占用
传统全量渲染 4200ms 8fps 850MB
虚拟滚动 380ms 60fps 95MB

进阶技巧:结合IntersectionObserver实现懒加载,使用ResizeObserver处理动态项高,并采用渐进渲染策略避免跳帧。

四、架构启示:现代DOM操作核心原则

  1. 选择性优化
    仅对滚动容器动画高频区等关键路径实施虚拟化,避免过度工程化

  2. 读写分离
    集中执行样式修改后统一读取布局属性,防止强制同步布局(Forced Synchronous Layout)

    // 错误示范(读写交错)
    elements.forEach(el => {
      el.style.width = (el.offsetWidth + 10) + 'px'; // 触发重排
    });
    
    // 正确做法(批量写 → 批量读)
    elements.forEach(el => el.style.width = '110%');
    const newWidths = elements.map(el => el.offsetWidth);
    
  3. 分层渲染策略

    // 首次加载 → 数据量>500 → 虚拟滚动+骨架屏 → 滚动时增量加载
    // 首次加载 → 数据量<500 → 全量渲染 → 常规交互
    

2025年最佳实践组合

  • 选择器getElementById + 缓存querySelectorAll
  • 批量操作DocumentFragment + CSS contain:content
  • 长列表:虚拟滚动 + IntersectionObserver
  • 动画will-change + transform硬件加速

原生DOM操作并非简单替换jQuery语法,而是重新理解浏览器渲染管线。通过精准选择API、利用硬件加速、动态加载策略,即使处理十万级DOM也能保持60fps流畅体验。在前端框架盛行的今天,掌握原生能力仍是性能优化的终极底牌。

Next.js 嵌套路由与中间件:数据与逻辑的前哨站

在现代 Web 应用的世界里,路由是城市道路,中间件是守在路口的警察,确保一切交通有序、安全。
Next.js 则是那位既懂交通规则、又能修路铺桥的工程师——你不仅可以在它的路网上自由嵌套路线,还可以让中间件在用户抵达目的地前对他们的身份、行李、甚至心情(如果你愿意)做检查。


一、嵌套路由的本质

在 Next.js 中,文件即路由的哲学让你少了很多配置文件的负担,但当你需要结构化复杂页面时,嵌套路由就派上了用场。

比如,你有一个博客系统:

/app
  /blog
    /page.js
    /[slug]
      /page.js
  • /blog → 博客列表页
  • /blog/[slug] → 某篇博客详情页

底层原理:

  • Next.js 会遍历 app 目录下的文件夹结构。
  • 目录名映射为 URL 路径,[param] 形式表示动态路由。
  • 嵌套文件夹会形成嵌套路由,父级路由可以包含 Layout,用来统一头部、底部、导航栏。

Layout 嵌套机制

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div>
      <header>Blog Header</header>
      <main>{children}</main>
    </div>
  );
}

这样 /blog/blog/[slug] 都会共享这个 BlogLayout,底层是组件树递归渲染,Next.js 会为每一层 Layout 建立独立 React 节点,从而实现父子关系。


二、中间件(Middleware)的使命

想象一下你有一个高档餐厅(网站),中间件就是门口的保安——

  • 检查身份证(鉴权)
  • 检查预订记录(权限控制)
  • 检查是否穿正装(条件跳转)
  • 甚至可以把迟到的人送去别的餐厅(重定向)

中间件的运行时机

  • 请求到达页面组件之前
  • 运行在 Edge Runtime(轻量、低延迟,全球分布)。
  • 可以读取和修改请求、响应。

底层机制

  • 你在项目根目录(或子目录)下放置一个 middleware.js 文件。
  • Next.js 会在构建时将它编译为 Edge Function。
  • 每次请求进入匹配的路径时,都会先经过中间件逻辑。

三、实战:嵌套路由 + 中间件

假设你有一个 /dashboard 路由和它的嵌套页面 /dashboard/settings,你想在用户进入这些页面前检查是否已登录。

目录结构:

/app
  /dashboard
    /page.js
    /settings
      /page.js
/middleware.js

中间件示例:

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(req) {
  const token = req.cookies.get('token');
  
  if (!token) {
    // 未登录则跳转到登录页
    return NextResponse.redirect(new URL('/login', req.url));
  }
  
  // 已登录则放行
  return NextResponse.next();
}

// 限制中间件只匹配 dashboard 路由
export const config = {
  matcher: ['/dashboard/:path*']
};

四、嵌套路由与中间件的协作

嵌套路由提供结构化的页面层级,而中间件提供请求入口的守卫
就像机场一样:

  • 嵌套路由 → 航站楼结构(国际、国内、贵宾厅等分区)
  • 中间件 → 安检口(拦截违禁品、核对身份、放行)

好处:

  1. 安全:中间件阻挡未授权用户。
  2. 体验:减少无意义的页面渲染。
  3. 性能:Edge Runtime 在边缘节点直接处理,不必每次回到主服务器。

五、最佳实践建议

  1. 中间件逻辑要精简

    • 它运行在边缘节点,不适合做大量计算。
    • 适合做快速判断、重定向、设置 cookie。
  2. 嵌套路由中 Layout 复用 UI

    • 避免重复代码,让不同子页面共享样式和结构。
  3. 分层控制

    • 根目录 middleware.js 管全局规则。
    • 子目录 middleware.js 处理局部规则(Next.js 13+ 支持子目录中间件)。

六、幽默的尾声

嵌套路由像一座大厦的楼层结构,
中间件是大门口的保安,
而 Next.js 是那位能帮你造大厦、请保安、装电梯的承包商。

有人会问:
“那如果我没中间件,直接让所有人进来会怎样?”
——那就像把你家 Wi-Fi 密码贴在电梯里,很快就会发现隔壁邻居比你还熟悉你的路由结构

AI UI 数据展示:Chart.js / Recharts + AI 总结文本的艺术

在现代 Web 应用的世界里,数据展示早已不再是枯燥的表格,而是一场视觉盛宴。
就像数据是食材,AI 是大厨,Chart.js / Recharts 是精致的餐具——最终的 UI 是那道端上用户桌面的米其林级菜肴

本篇文章,我们将从底层原理到代码实践,一起探讨如何用 Chart.js / Recharts 绘制出优雅的数据图表,并用 AI 自动生成人类可读的总结文本


一、为什么 Chart.js 和 Recharts 是好搭档?

在前端图表界,Chart.js 和 Recharts 有点像两个性格不同的朋友:

  • Chart.js

    • 优势:轻量级,原生 Canvas 渲染,动画丝滑。
    • 适合场景:需要快速渲染高性能、交互不太复杂的图表。
    • 底层机制:直接操作 <canvas>,用 2D 渲染上下文绘制像素。
    • 缺点:配置复杂时需要更多手动调整。
  • Recharts

    • 优势:基于 React 组件化开发,易维护,语义化强。
    • 适合场景:React 项目里快速搭建交互性强的图表。
    • 底层机制:基于 D3.js 的计算和 SVG 渲染(矢量图,缩放不失真)。
    • 缺点:在大量数据点时性能可能逊色于 Canvas。

一句话总结

Chart.js 是“性能小钢炮”,Recharts 是“优雅绅士”,你可以根据业务场景选择或混用。


二、AI 在数据展示中的角色

如果 Chart.js 和 Recharts 是负责画画的,那 AI 就是旁白解说员

为什么需要 AI 文本总结?

  • 人眼对趋势敏感,但 AI 可以直接用自然语言告诉你结论
  • 当用户面对一堆数据曲线时,AI 可以说:“看!这个月的销售额比上月增长了 35%,并且主要得益于东南亚市场的爆发式增长。”

AI 的底层工作逻辑:

  1. 获取数据(JSON / API)。
  2. 特征提取:计算平均值、最大值、趋势变化率等。
  3. 语言生成:将这些特征喂给 AI 模型(如 GPT-4、Claude),让它用自然语言总结。
  4. 输出优化:控制字数、调整语气、加上商业或技术背景。

三、数据流的底层原理

一个典型的 AI UI 数据展示系统,数据流是这样的:

[ 数据源 API ][ 前端获取数据 fetch() ][ 数据处理:统计、归一化 ][ Chart.js / Recharts 渲染 ][ AI 调用接口生成总结文本 ][ 页面展示:图表 + 文本 ]

在底层实现里,Chart.js 会直接操作 Canvas 的像素点,而 Recharts 会在 DOM 中生成 <svg> 标签,并通过 D3.js 计算坐标和路径。

AI 部分则通常通过 HTTP 请求调用 LLM API,比如:

const summary = await fetch('/api/ai-summary', {
  method: 'POST',
  body: JSON.stringify({ data }),
});

在服务器上,你可能用 OpenAI API:

import OpenAI from 'openai';
const openai = new OpenAI();

const aiText = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [
    { role: "system", content: "你是数据分析师,帮我总结趋势" },
    { role: "user", content: JSON.stringify(data) }
  ]
});

四、实战示例:Chart.js + AI 总结

假设我们有一组销售额数据(按月份),我们先用 Chart.js 画出来,再调用 AI 给出文字总结。

import { Chart } from 'chart.js';

// 模拟数据
const salesData = [120, 140, 180, 160, 200, 250, 300];
const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];

// 1. 绘制图表
new Chart(document.getElementById('salesChart'), {
  type: 'line',
  data: {
    labels,
    datasets: [{
      label: 'Monthly Sales',
      data: salesData,
      borderColor: '#4CAF50',
      fill: false
    }]
  }
});

// 2. 请求 AI 总结
async function getAISummary(data) {
  const res = await fetch('/api/ai-summary', {
    method: 'POST',
    body: JSON.stringify({ salesData: data })
  });
  const { summary } = await res.json();
  document.getElementById('summary').innerText = summary;
}

getAISummary(salesData);

五、Recharts + AI 总结(React 版本)

import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';

const data = [
  { month: 'Jan', sales: 120 },
  { month: 'Feb', sales: 140 },
  { month: 'Mar', sales: 180 },
  { month: 'Apr', sales: 160 },
  { month: 'May', sales: 200 },
  { month: 'Jun', sales: 250 },
  { month: 'Jul', sales: 300 }
];

export default function SalesChart() {
  return (
    <>
      <LineChart width={500} height={300} data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <Tooltip />
        <Line type="monotone" dataKey="sales" stroke="#4CAF50" />
      </LineChart>
      <div id="summary">AI 正在生成总结...</div>
    </>
  );
}

在 React 中,可以用 useEffect 触发 AI 总结的 API 调用,将数据传过去,再更新到 summary 状态中。


六、幽默的收尾

传统的数据展示是“看图说话”,
AI + 图表的组合是“看图不用说话,AI 替你说完”。

当 Chart.js 像年轻的涂鸦艺术家用画笔在 Canvas 上狂飙,
Recharts 则是那位戴着圆框眼镜、温文尔雅的 SVG 绘图师。
而 AI,就像后台的那位戏精,随时准备为你的数据配上旁白——
甚至会夸张地说你是下一个商业传奇。

Next.js 入门实战:从零构建你的第一个 SSR 应用

序言

在当今前端开发中,Next.js 已成为构建高性能、SEO 友好应用的必备框架。今天我将带大家从零开始创建一个 Next.js 项目,并深入解析其服务器端渲染(SSR)机制的优势。

创建 Next.js 项目

Next.js 提供了便捷的脚手架工具,让我们可以快速初始化项目:

npx create-next-app@latest my-todo

这里使用 npx 命令的优势在于:

  • 无痕使用:无需全局安装依赖,避免污染全局环境
  • 即用即走:非常适合快速尝试新技术
  • 版本控制:始终使用最新版本的 create-next-app

当然,你也可以选择全局安装:

npm i -g create-next-app@latest

Next.js 与传统 React 应用的区别

特性 React (CSR) Next.js (SSR)
渲染位置 客户端浏览器 服务器端
初始加载速度 较慢(需下载所有JS) 较快(服务器返回完整HTML)
SEO 友好度 较差(爬虫难以解析) 优秀(直接返回完整内容)
适用场景 后台管理系统 内容型网站、企业站

理解 SSR 的核心优势

1. SEO 优化

传统 React 应用(CSR)在浏览器端渲染时,初始 HTML 只有一个空容器:

<div id="root"></div>

搜索引擎爬虫抓取时,只能看到一个空页面,严重影响 SEO。而 Next.js 的 SSR 在服务器端就完成了渲染,返回的是完整的 HTML 内容:

<h1>首页</h1>
<div>我在秋招,我去字节</div>

2. 性能提升

用户无需等待所有 JavaScript 加载完成就能看到内容,大大提升了首屏加载速度。

实战:创建你的第一个页面

在 Next.js 项目中,页面组件位于 app 目录下。我们创建一个简单的首页:

// app/page.tsx
import Image from "next/image";

export default function Home() {
  return (
    <>
      <h1>首页</h1>
      <div>我在秋招,我去字节</div>
    </>
  );
}

这个组件会在服务器端被渲染成 HTML,然后发送到客户端。注意我们使用了 Next.js 内置的 Image 组件,它可以自动优化图片性能。

运行你的 Next.js 应用

在项目目录下执行:

npm run dev

访问 http://localhost:3000,你将看到服务器渲染的页面。

如何验证 SSR 效果?

  1. 在浏览器中右键点击"查看页面源代码"
  2. 你将看到完整的 HTML 内容,而非空容器
  3. 这意味着搜索引擎爬虫可以直接抓取到页面内容

使用场景推荐

Next.js 特别适合以下场景:

  • 内容型网站:博客、新闻站点(SEO 关键)
  • 电商平台:商品列表页需要被搜索引擎收录
  • 企业官网:需要良好的搜索引擎排名
  • 掘金等技术社区:内容需要被广泛传播和搜索

总结

Next.js 通过 SSR 解决了传统 React 应用的两大痛点:

  1. SEO 不友好:服务器直接返回完整 HTML
  2. 首屏加载慢:用户立即看到内容而非空白页

deepseek_mermaid_20250815_05091b.png

CSS居中布局:从基础到进阶全解析

在前端开发中,居中布局是高频需求,也是面试常考点。今天我们将系统梳理各类居中场景的实现方案,涵盖水平居中、垂直居中及水平垂直居中,并深入分析其原理与适用场景。

一、水平居中:文本与块级元素

1.1 文本水平居中:text-align

适用于行内元素(inline/inline-block)或文本:

.container { 
  text-align: center; /* 子元素继承居中 */ 
} 

特性

  • 作用于父容器,子元素自动继承
  • 仅对行内内容生效(如<span><img>

1.2 块级元素水平居中:margin: auto

适用于固定宽度的块级元素:

.box { 
  width: 200px; /* 必须定义宽度 */ 
  margin: 0 auto; /* 左右外边距自适应 */ 
} 

原理:浏览器自动分配左右剩余空间

二、垂直居中:单行文本的解决方案

2.1 line-height方案

当元素高度确定时:

.container { 
  height: 100px; 
  line-height: 100px; /* 等于容器高度 */ 
} 

限制

  • 仅适用于单行文本
  • 内容高度不能超过容器

2.2 padding方案

通过内边距挤压内容:

.container { 
  padding: 40px 0; /* 上下内边距相等 */ 
} 

优势:无需计算行高,适应多行文本

三、固定宽高元素的水平垂直居中

3.1 绝对定位 + 负边距(经典方案)

.parent { position: relative; } 
.child { 
  position: absolute; 
  width: 300px; 
  height: 200px; 
  top: 50%; 
  left: 50%; 
  margin-top: -100px; /* height/2 */ 
  margin-left: -150px; /* width/2 */ 
} 

缺点

  • 需精确知道元素尺寸
  • 调整尺寸需同步修改边距

3.2 绝对定位 + margin: auto(推荐方案)

.child { 
  position: absolute; 
  width: 300px; 
  height: 200px; 
  top: 0; 
  left: 0; 
  right: 0; 
  bottom: 0; 
  margin: auto; /* 自动填充剩余空间 */ 
} 

优势

  • 代码简洁,易于维护
  • 兼容性好(IE8+)

3.3 绝对定位 + calc()

.child { 
  position: absolute; 
  top: calc(50% - 100px); /* 50% - height/2 */ 
  left: calc(50% - 150px); /* 50% - width/2 */ 
} 

缺点

  • 计算性能较差(频繁重绘时影响渲染)
  • 可读性低

四、未知宽高元素的水平垂直居中

4.1 绝对定位 + transform(现代方案)

.child { 
  position: absolute; 
  top: 50%; 
  left: 50%; 
  transform: translate(-50%, -50%); /* 反向位移自身50% */ 
} 

原理

  1. top/left定位到父容器中心点
  2. translate将元素向左上移动自身宽高的50%
    优势:自适应任意尺寸,无需知道宽高

4.2 line-height + vertical-align

利用文本属性实现:

.parent { 
  line-height: 300px; /* 等于容器高度 */ 
  text-align: center; 
} 
.child { 
  display: inline-block; 
  vertical-align: middle; /* 垂直中线对齐 */ 
  line-height: initial; /* 重置子元素行高 */ 
} 

适用场景:需要兼容旧浏览器的项目

4.3 writing-mode技巧

改变文本流向实现垂直居中:

.parent { 
  writing-mode: vertical-lr; /* 改为垂直流向 */ 
  text-align: center; 
} 
.child { 
  writing-mode: horizontal-tb; /* 改回水平流向 */ 
  display: inline-block; 
} 

注意:此方案会改变文本布局方向,需谨慎使用

4.4 table-cell布局

模拟表格单元格行为:

.parent { 
  display: table-cell; 
  width: 100vw; 
  height: 100vh; 
  vertical-align: middle; /* 垂直居中 */ 
  text-align: center; /* 水平居中 */ 
} 
.child { 
  display: inline-block; 
} 

缺点:父元素需定义明确宽高


五、Flexbox:终极居中方案

.parent { 
  display: flex; 
  justify-content: center; /* 主轴居中 */ 
  align-items: center; /* 交叉轴居中 */ 
} 

优势

  • 三行代码解决所有居中问题
  • 完美支持响应式布局
  • 无需计算尺寸

扩展技巧:多元素居中

.parent { 
  display: flex; 
  flex-direction: column; /* 改为垂直排列 */ 
  justify-content: center; 
} 

六、Grid布局:二维居中控制

.parent { 
  display: grid; 
  place-items: center; /* 行列同时居中 */ 
} 

等价写法

.parent { 
  display: grid; 
  justify-content: center; 
  align-content: center; 
} 

适用场景:复杂网格系统中的居中需求


七、方案对比与选择指南

方案 适用场景 兼容性 灵活性
text-align 行内元素水平居中 所有浏览器 ★★☆
负边距 已知尺寸元素 IE6+ ★☆☆
transform 未知尺寸元素 IE10+ ★★★
Flexbox 现代布局 IE11+ ★★★
Grid 二维复杂布局 IE11+ ★★★
table-cell 兼容旧浏览器 IE8+ ★★☆

选择原则:

  1. 已知宽高:优先使用absolute + margin: auto(性能最佳)

  2. 未知宽高

    • 现代项目:Flexbox
    • 需兼容旧浏览器:transformtable-cell
  3. 文本内容line-heightpadding

总结与思考

居中布局的核心在于理解坐标系定位基准

  1. 水平居中本质是左右空间均等分配
  2. 垂直居中依赖行高控制定位偏移
  3. 绝对定位方案需建立位置参照系(父元素position: relative

现代CSS已大幅简化居中实现:

  • 单元素居中首选transform
  • 多元素排列必用Flexbox
  • 避免滥用calc(),性能敏感场景慎用

看似简单的居中背后,是CSS视觉格式化模型的深刻体现。掌握每种方案的底层原理,方能灵活应对复杂场景。当然也能让你在面试官面前眼前一亮

面试题深度解析:父子组件通信与生命周期执行顺序

在现代前端框架(如 Vue 和 React)的面试中,“父子组件如何通信?生命周期的执行顺序是怎样的?” 是一道经典且高频的综合题。它不仅考察你对框架 API 的掌握,更深入检验你对组件化思想、数据流、渲染机制和副作用处理的理解。

本文将以 Vue 3 和 React 为例,从基础到原理,全面剖析父子组件通信方式与生命周期执行顺序,助你在面试中脱颖而出。


一、父子组件通信的五大方式

组件通信的核心是数据流的传递与同步。在单向数据流(Unidirectional Data Flow)理念下,父组件通过 props 向下传递数据,子组件通过 事件(Event)向上通信

1. Props Down:父 → 子(数据传递)

Vue 3 示例

<!-- Parent.vue -->
<template>
  <Child :msg="message" :user="userInfo" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const message = ref('Hello from Parent')
const userInfo = { name: 'Alice', age: 25 }
</script>

<!-- Child.vue -->
<script setup>
// 接收 props
const props = defineProps({
  msg: String,
  user: Object
})

console.log(props.msg) // 'Hello from Parent'
</script>

React 示例

// Parent.js
function Parent() {
  const [message, setMessage] = useState('Hello from Parent');
  const userInfo = { name: 'Alice', age: 25 };

  return <Child msg={message} user={userInfo} />;
}

// Child.js
function Child({ msg, user }) {
  console.log(msg); // 'Hello from Parent'
  return <div>{msg}</div>;
}

关键点

  • Props 是只读的,子组件不应直接修改。
  • 传递引用类型(对象、数组)时,子组件修改其内部属性会影响父组件(浅共享),需避免。

2. Events Up:子 → 父(事件通知)

Vue 3:emit 事件

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['update', 'close'])

function handleClick() {
  emit('update', 'New Value')
  emit('close')
}
</script>

<!-- Parent.vue -->
<template>
  <Child @update="handleUpdate" @close=" handleClose" />
</template>

<script setup>
function handleUpdate(value) {
  console.log('Received:', value)
}
function handleClose() {
  console.log('Child closed')
}
</script>

React:回调函数(Callback)

// Parent.js
function Parent() {
  const handleUpdate = (value) => {
    console.log('Received:', value);
  };
  const handleClose = () => {
    console.log('Child closed');
  };

  return <Child onUpdate={handleUpdate} onClose={handleClose} />;
}

// Child.js
function Child({ onUpdate, onClose }) {
  return (
    <button onClick={() => {
      onUpdate('New Value');
      onClose();
    }}>
      Click
    </button>
  );
}

关键点:这是最标准、最安全的子 → 父通信方式。


3. v-model / v-model:value(双向绑定)

Vue 3 的语法糖,本质是 :modelValue + @update:modelValue

<!-- Parent.vue -->
<Child v-model="message" />

<!-- 等价于 -->
<Child 
  :modelValue="message" 
  @update:modelValue="value => message = value" 
/>

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function handleChange(e) {
  emit('update:modelValue', e.target.value)
}
</script>

适用场景:表单组件(如 Input, Select)。


4. $refs / ref(直接访问子组件实例)

Vue 3

<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
</template>

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

const childRef = ref(null)

onMounted(() => {
  childRef.value.someMethod() // 调用子组件方法
})
</script>

<!-- Child.vue -->
<script setup>
import { defineExpose } from 'vue'

function someMethod() {
  console.log('Called from parent')
}

// 暴露给父组件
defineExpose({ someMethod })
</script>

React:useRef + forwardRef

// Child.js
const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    someMethod() {
      console.log('Called from parent');
    }
  }));

  return <div>Child</div>;
});

// Parent.js
function Parent() {
  const childRef = useRef();

  useEffect(() => {
    childRef.current.someMethod();
  }, []);

  return <Child ref={childRef} />;
}

⚠️ 注意:应尽量避免使用 ref,破坏了组件的封装性,仅用于 DOM 操作或特定方法调用。


5. Provide / Inject(跨层级通信)

适用于祖孙组件通信,避免“props 逐层透传”。

<!-- App.vue (祖先) -->
<script setup>
import { provide } from 'vue'

provide('theme', 'dark')
provide('user', { name: 'Admin' })
</script>

<!-- AnyChild.vue (任意后代) -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light') // 第二个参数是默认值
const user = inject('user')
</script>

优点:解耦层级依赖。 ❌ 缺点:数据流不清晰,调试困难。


二、父子组件生命周期执行顺序深度解析

理解生命周期顺序,是避免“子组件未挂载就访问”、“卸载时内存泄漏”等 bug 的关键。

Vue 3 执行顺序(Composition API)

1. 首次挂载(Mount)

// Parent
setup()           // 1
onBeforeMount()   // 3
onMounted()       // 5

// Child
setup()           // 2
onBeforeMount()   // 4
onMounted()       // 6

🔍 流程

  1. 父组件 setup 执行(准备数据和逻辑)。
  2. 子组件 setup 执行。
  3. 父组件进入 onBeforeMount(DOM 未生成)。
  4. 子组件进入 onBeforeMount
  5. 子组件 onMounted 触发(子组件 DOM 已挂载)。
  6. 父组件 onMounted 触发(父组件等待所有子组件挂载完成才算自己挂载完成)。

结论onMounted 的完成顺序是 子 → 父

2. 更新(Update)

// Parent
onBeforeUpdate()  // 1
onUpdated()       // 3

// Child
onBeforeUpdate()  // 2
onUpdated()       // 3

🔍 流程

  1. 父组件触发 onBeforeUpdate
  2. 子组件触发 onBeforeUpdate
  3. 子组件 onUpdated
  4. 父组件 onUpdated

结论:更新也是 子先完成,父后完成

3. 卸载(Unmount)

// Parent
onBeforeUnmount() // 1
onUnmounted()     // 3

// Child
onBeforeUnmount() // 2
onUnmounted()     // 3

结论:卸载顺序 子 → 父 完成。


React 执行顺序(函数组件 + useEffect)

1. 首次渲染

// Parent
render()          // 1
useEffect(() => { /* mount */ }) // 3

// Child
render()          // 2
useEffect(() => { /* mount */ }) // 4

🔍 流程

  1. 父组件 render(生成虚拟 DOM)。
  2. 子组件 render
  3. DOM 提交到页面
  4. useEffect 异步执行:父 → 子。

结论useEffect 执行顺序是 父 → 子

2. 更新

顺序与首次渲染一致:父 render → 子 render → DOM 提交 → 父 useEffect → 子 useEffect

3. 卸载

// cleanup 执行顺序
Parent cleanup   // 1
Child cleanup    // 2

结论useEffect 的 cleanup 函数执行顺序是 父 → 子


三、通信与生命周期的协同应用

场景:父组件等待子组件初始化完成

<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

onMounted(() => {
  // 此时 childRef.value 已存在,且子组件已挂载
  childRef.value.initialize()
})
</script>

依据onMounted 触发时,所有子组件已挂载完毕。


四、总结:一张表看懂核心要点

维度 Vue 3 React
父 → 子通信 props props
子 → 父通信 emit 事件 回调函数(Callback)
双向绑定 v-model 受控组件 + onChange
直接访问 ref + defineExpose useRef + forwardRef + useImperativeHandle
跨层级通信 provide / inject Context API
挂载完成顺序 子 → 父 (onMounted) 父 → 子 (useEffect)
更新完成顺序 子 → 父 (onUpdated) 父 → 子 (useEffect)
卸载顺序 子 → 父 (onUnmounted) 父 → 子 (cleanup)

面试加分回答

“父子组件通信应遵循单向数据流原则:父传 props,子发 eventv-modelref 是语法糖和特殊手段,应谨慎使用。生命周期顺序的核心是渲染从父到子,完成从子到父(Vue),而 React 的 useEffect 是在渲染后统一执行。理解这一点,能帮助我们正确处理 DOM 操作、事件绑定、资源清理和异步初始化,避免因时机错误导致的 bug。”

掌握这些,你不仅能回答面试题,更能设计出健壮、可维护的组件体系。

高德地图关键字查询和输入提示后查询踩坑记录

最近在接入高德地图1.4.15版本,其中有地图关键字查询输入提示后查询的功能,中间踩了很多坑,一度怀疑是高德的bug,最终发现是自己代码的问题,好在最终解决了,在此记录一下。

地图关键字查询

踩坑1:频繁的创建new AMap.PlaceSearch实例。我一开始将创建new AMap.PlaceSearch实例写在了handleSearchChange事件中,这样会导致placeSearch.search的分页查询不准确,比如我一开始搜索北京,在panel中展示出来的是北京的列表;之后我又搜索济南,一开始在panel中展示的是济南的列表,点击分页页码后,在panel中展示出来的是北京的列表结果。

错误代码如下:
<script>
export default {
  data() {
    return {
      amap: null,
      apanel: null,
      searchValue: "",
    };
  },
  methods: {
    loadScript(url, callback) {
     //省略
    },
    initMap() {
      // 省略
    },
    handleSearchChange() { 
      const placeSearch = new AMap.PlaceSearch({ // 这里这么写是错误的
        pageSize: 5, // 单页显示结果条数
        pageIndex: 1, // 页码
        map: this.amap,
        panel: "panel",
      }); //构造地点查询类
      this.placeSearch = placeSearch;
      this.placeSearch.search(this.searchValue, (status, result) => {
        console.log("status", status);
        console.log("result", result);
      });
    },
  },
  mounted() {
    this.loadScript(
      "https://webapi.amap.com/maps?v=1.4.15&key=你的key值&plugin=AMap.Scale,AMap.OverView,AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch",
      this.initMap
    );
  },
};
</script>

错误截图
  1. 第一步:先搜索北京 image.png
  2. 第二步:搜索其他地点,比如济南,出现搜索结果后点击分页 image.png 3.可以看到搜索结果是错误的!!!一切都是因为在input框的change事件里频繁的创建new AMap.PlaceSearch实例。
关键字查询的正确的实现代码如下:

在初始化中建立new AMap.PlaceSearch实例

<template>
  <div style="width: 100%; height: 100vh">
    <div ref="amap" id="container" style="height: 100%" />

    <div ref="apanel" id="panel" class="panel" />
    <div class="search-bar">
      <input
        placeholder="地图检索"
        prefix-icon="el-icon-search"
        v-model="searchValue"
        @change="handleSearchChange"
      />
    </div>
  </div>
</template>
<style>
.panel {
  position: absolute;
  background-color: white;
  max-height: 90%;
  overflow-y: auto;
  top: 10px;
  left: 100px;
  width: 280px;
}
.search-bar {
  position: fixed;
  width: 250px;
  padding: 20px 0;
  right: 20px;
  top: 20px;
  z-index: 10000;
  display: flex;
}

.search-bar .el-input {
  background: rgba(255, 255, 255, 0.2);
}
</style>
<script>
export default {
  data() {
    return {
      amap: null,
      apanel: null,
      searchValue: "",
    };
  },
  methods: {
    loadScript(url, callback) {
      // 加载高德地图js
      let script = document.createElement("script");

      script.type = "text/javascript";

      script.src = url;

      document.getElementsByTagName("head")[0].appendChild(script);

      script.onload = () => {
        callback();
      };
    },
    initMap() {
      let scale = new AMap.Scale({
        visible: true,
      });
      let toolBar = new AMap.ToolBar({
        visible: true,
      });
      let overView = new AMap.OverView({
        visible: true,
      });

      console.log([this.longitude, this.latitude]);
      this.amap = new AMap.Map("container", {
        //center: [longitude, latitude], //地图中心点
        zoom: 15, //地图级别
        mapStyle: "amap://styles/dark", //设置地图的显示样式
        viewMode: "2D", //设置地图模式
        lang: "zh_cn", //设置地图语言类型
        resizeEnable: true,
      });
      this.amap.addControl(scale);
      this.amap.addControl(toolBar);
      this.amap.addControl(overView);
      const placeSearch = new AMap.PlaceSearch({ //在初始化中建立new AMap.PlaceSearch实例
        pageSize: 5, // 单页显示结果条数
        pageIndex: 1, // 页码
        map: this.amap,
        panel: "panel",
      }); //构造地点查询类
      this.placeSearch = placeSearch;
    },
    handleSearchChange() {
      this.placeSearch.search(this.searchValue, (status, result) => {
        console.log("status", status);
        console.log("result", result);
      });
    },
  },
  mounted() {
    this.loadScript(
      "https://webapi.amap.com/maps?v=1.4.15&key=你的key值&plugin=AMap.Scale,AMap.OverView,AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch",
      this.initMap
    );
  },
};
</script>

输入提示后查询

踩坑1:创建new AMap.PlaceSearch实例时:
1. 错误写法:将传入的map参数写为存放map的dom,即this.$refs.amap。
2. 正确写法:这里的map参数应该写为创建的new AMap.Map实例。

输入提示后查询的正确的实现代码如下:
<template>
  <div style="width: 100%; height: 100vh">
    <div ref="amap" id="container" style="height: 100%" />

    <div ref="apanel" id="panel" class="panel" />
    <div class="search-bar">
      <input
        placeholder="地图检索"
        prefix-icon="el-icon-search"
        v-model="searchValue"
        @change="handleSearchChange"
        id="tipinput"
      />
    </div>
  </div>
</template>
<style>
.panel {
  position: absolute;
  background-color: white;
  max-height: 90%;
  overflow-y: auto;
  top: 10px;
  left: 100px;
  width: 280px;
}
.search-bar {
  position: fixed;
  width: 250px;
  padding: 20px 0;
  right: 20px;
  top: 20px;
  z-index: 10000;
  display: flex;
}

.search-bar .el-input {
  background: rgba(255, 255, 255, 0.2);
}
</style>
<script>
export default {
  data() {
    return {
      amap: null,
      apanel: null,
      searchValue: "",
    };
  },
  methods: {
    loadScript(url, callback) {
      // 加载高德地图js
      let script = document.createElement("script");
      script.type = "text/javascript";
      script.src = url;
      document.getElementsByTagName("head")[0].appendChild(script);
      script.onload = () => {
        callback();
      };
    },
    initMap() {
      let scale = new AMap.Scale({
        visible: true,
      });
      let toolBar = new AMap.ToolBar({
        visible: true,
      });
      let overView = new AMap.OverView({
        visible: true,
      });

      this.amap = new AMap.Map("container", {
        //center: [longitude, latitude], //地图中心点
        zoom: 15, //地图级别
        mapStyle: "amap://styles/dark", //设置地图的显示样式
        viewMode: "2D", //设置地图模式
        lang: "zh_cn", //设置地图语言类型
        resizeEnable: true,
      });
      this.amap.addControl(scale);
      this.amap.addControl(toolBar);
      this.amap.addControl(overView);
      const autoOptions = {
        input: "tipinput",
      };
      const auto = new AMap.Autocomplete(autoOptions);
      const placeSearch = new AMap.PlaceSearch({
        pageSize: 5, // 单页显示结果条数
        pageIndex: 1, // 页码
        map: this.amap, // 这里填写new AMap.Map的实例
        panel: "panel",
      }); //构造地点查询类
      AMap.event.addListener(auto, "select", select); //注册监听,当选中某条记录时会触发
      function select(e) {
        placeSearch.search(e.poi.name, (status, result) => {
          console.log("status", status);
          console.log("result", result);
        });
      }
      this.placeSearch = placeSearch;
    },
    handleSearchChange() {
      this.placeSearch.search(this.searchValue, (status, result) => {
        console.log("status", status);
        console.log("result", result);
      });
    },
  },
  mounted() {
    this.loadScript(
      "https://webapi.amap.com/maps?v=1.4.15&key=095f388e7a22189c7cb0095485e1ca59&plugin=AMap.Scale,AMap.OverView,AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch",
      this.initMap
    );
  },
};
</script>
最终实现截图

image.png

监听设备网络状态

概述 在前端应用中监听网络状态是一个常见的需求,可以用于优化离线体验、提示用户网络变化等。 场景 移动端项目,有大量的图片需要访问,在4g或者wifi的网络状态下做不同的优化 在后台系统访问,有时候会
❌