普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月6日技术

四叉树:二维空间的 “智能分区管理员”

作者 LeonGao
2025年7月6日 11:29

想象一下,你手里有一张巨大的城市地图,上面密密麻麻地分布着十万个路灯。现在老板突然让你找出某条小巷里的三个路灯 —— 如果像翻字典一样逐个排查,恐怕下班前都完不成任务。但如果这张地图早被划分成了街道片区,每个片区又细分出街区,街区再分成小巷,你就能像剥洋葱一样层层定位,这就是四叉树的核心智慧。

从像素格子到数学魔法

在计算机图形学的世界里,二维空间就像一块等待切割的披萨。四叉树这位 “披萨大师” 有个怪癖:每次都要把当前区域切成大小相等的四份 —— 左上、右上、左下、右下,如同给正方形蛋糕划十字刀。这种划分不是随机的,而是遵循着简单又精妙的规则:当某个区域里的 “居民”(可以是点、图形或像素)数量超过阈值,就必须分家。

让我们用坐标系理解这个过程。假设初始空间是一个从 x0 到 x1、y0 到 y1 的正方形,就像一个边长为 100 的方盒子。当里面的点超过 4 个时,四叉树会找出这个正方形的中心 ——x 中点是(x0 加 x1)除以 2,y 中点同理。这两条中线如同魔术师的魔杖,瞬间把一个大盒子变成四个小盒子,每个孩子盒子的边长都是原来的一半。

这种划分会递归进行,直到每个小盒子里的 “居民” 数量都小于等于设定的阈值。就像俄罗斯套娃,每个盒子里可能藏着更小的盒子,也可能直接住着几个 “居民”。

JavaScript 中的四叉树实现

让我们用代码给这位 “分区管理员” 编写工作手册。下面的 JavaScript 类就像四叉树的身份证,记录着它的管辖范围和家庭成员:

class Quadtree {
  // 构造函数:初始化一个区域
  constructor(x, y, width, height, capacity) {
    this.x = x;         // 区域左上角x坐标
    this.y = y;         // 区域左上角y坐标
    this.width = width; // 区域宽度
    this.height = height; // 区域高度
    this.capacity = capacity; // 最大容纳数量
    this.points = [];   // 当前区域的居民
    this.children = null; // 四个子区域(初始为空)
  }
  // 划分区域:生四个"孩子"
  subdivide() {
    const halfW = this.width / 2;
    const halfH = this.height / 2;
    // 左上孩子
    this.children = [
      new Quadtree(this.x, this.y, halfW, halfH, this.capacity),
      // 右上孩子
      new Quadtree(this.x + halfW, this.y, halfW, halfH, this.capacity),
      // 左下孩子
      new Quadtree(this.x, this.y + halfH, halfW, halfH, this.capacity),
      // 右下孩子
      new Quadtree(this.x + halfW, this.y + halfH, halfW, halfH, this.capacity)
    ];
  }
  // 插入新居民
  insert(point) {
    // 如果点不在当前区域,直接拒绝
    if (!this.contains(point)) return false;
    // 还有空位且没生孩子,直接入住
    if (this.points.length < this.capacity && !this.children) {
      this.points.push(point);
      return true;
    }
    // 人满为患,赶紧分家
    if (!this.children) this.subdivide();
    // 让四个孩子决定谁接收这个新居民
    for (let child of this.children) {
      if (child.insert(point)) return true;
    }
    return false; // 理论上不会走到这步
  }
  // 检查点是否在当前区域内
  contains(point) {
    return (point.x >= this.x &&
            point.x <= this.x + this.width &&
            point.y >= this.y &&
            point.y <= this.y + this.height);
  }
  // 查找区域内的所有居民
  query(range, found) {
    // 如果当前区域和查询范围不搭界,直接返回
    if (!this.intersects(range)) return found;
    // 收集当前区域里的居民
    for (let p of this.points) {
      if (range.contains(p)) {
        found.push(p);
      }
    }
    // 让孩子们也交出符合条件的居民
    if (this.children) {
      for (let child of this.children) {
        child.query(range, found);
      }
    }
    return found;
  }
  // 检查两个区域是否重叠
  intersects(range) {
    return !(this.x > range.x + range.width ||
             this.x + this.width < range.x ||
             this.y > range.y + range.height ||
             this.y + this.height < range.y);
  }
}

像侦探一样高效搜索

假设我们要在游戏地图中检测碰撞 —— 比如找出玩家角色周围 50 像素内的所有敌人。如果遍历整个地图的 1000 个角色,每次检测都要做 1000 次计算;而有了四叉树,我们只需:

  1. 找到玩家所在的最小区域
  1. 检查相邻的几个兄弟区域
  1. 最多只需查询几十个角色

这种效率提升在图形渲染中更明显。当你缩放地图时,远处的细节不需要渲染 —— 四叉树会告诉你:“这个区域太小了,里面的东西合并成一个点就行”,就像地图上远处的城市只用一个圆点表示。

生活中的四叉树哲学

其实四叉树的智慧早就渗透在生活里:图书馆的书架先按学科分类,再分大类,最后到具体书目;快递网点先按城市分区,再到街道,最后到小区。这种 “分而治之” 的思想,让计算机在处理海量空间数据时,从 “愚公移山” 变成了 “庖丁解牛”。

下次当你在地图软件上缩放查看路况时,不妨想想背后可能有一棵四叉树正在默默工作 —— 它可能正把你当前视野里的车辆、行人、红绿灯,都妥善地放进不同的 “小盒子” 里,等待你的每一次点击查询。

Three.js 深度冲突:当像素在 Z 轴上玩起 "挤地铁" 游戏

作者 LeonGao
2025年7月6日 11:28

想象一下,在地铁站台,两列列车同时到站,车门完美对齐,乘客们挤在同一平面想下车 —— 这混乱的场景,正是 Three.js 里深度冲突(Z-fighting)的真实写照。作为计算机图形学里最让人头疼的 "小摩擦" 之一,深度冲突就像两个过于亲密的舞者,在 Z 轴上争抢同一个舞台位置,最终让我们的 3D 画面变得支离破碎。

深度冲突的本质:像素级的 "领土争端"

要理解深度冲突,我们得从图形渲染的底层逻辑说起。当 Three.js 绘制 3D 场景时,每个像素都需要回答一个关键问题:"在当前视角下,我应该显示哪个物体的颜色?" 这个判断依靠的是深度缓冲区(depth buffer),它就像一本记录着每个像素 "海拔高度" 的账本。

深度缓冲区里的每个值都代表着对应像素在 Z 轴上的距离,范围通常是 0 到 1(近平面到远平面)。当两个物体的表面在 Z 轴上靠得太近,近到它们的 Z 值差异小于深度缓冲区的精度时,麻烦就来了 —— 缓冲区无法分辨谁前谁后,只能随机选择一个显示。这就像用毫米尺去测量两张叠在一起的薄纸,根本分不清哪张在前哪张在后。

在 Three.js 中,这种精度限制源于浮点数的存储特性。深度缓冲区通常是 16 位、24 位或 32 位的,位数越高精度越高,但即便是 32 位,在处理远距离场景时也会力不从心。比如当远平面设置得很远时,近距离内的微小 Z 值差异就会被 "淹没" 在浮点精度的误差里。

如何检测深度冲突:那些 "闪烁" 的警示灯

深度冲突最明显的特征是画面上出现不规则的闪烁斑块,就像老式电视机信号不良时的雪花点。这些闪烁区域其实是两个重叠表面在每一帧渲染中 "轮流掌权" 的结果 —— 上一帧显示 A 物体,这一帧显示 B 物体,肉眼看起来就成了闪烁。

在 Three.js 中,我们可以用一个简单的测试场景来复现这种现象:

// 创建两个几乎重叠的平面
const geometry1 = new THREE.PlaneGeometry(10, 10);
const geometry2 = new THREE.PlaneGeometry(10, 10);
const material = new THREE.MeshBasicMaterial({ 
  color: 0xff0000, 
  side: THREE.DoubleSide 
});
const plane1 = new THREE.Mesh(geometry1, material);
const plane2 = new THREE.Mesh(geometry2, material);
// 让它们在Z轴上非常接近但不重合
plane1.position.z = 0;
plane2.position.z = 0.000001; // 微小的距离
scene.add(plane1);
scene.add(plane2);

运行这段代码,你会看到红色平面上出现诡异的闪烁区域 —— 这就是典型的深度冲突。两个平面距离太近,超出了深度缓冲区的分辨能力,导致像素所有权在每一帧随机切换。

解决深度冲突:给像素们 "划清界限"

解决深度冲突的核心思路很简单:让原本挤在一起的表面保持适当距离,或者增强系统的 "分辨能力"。在 Three.js 中,我们有多种实用技巧可以采用:

1. 增加物理距离:给舞者们更多舞台空间

最直接的方法是增大两个表面之间的实际距离,就像在拥挤的地铁里喊一声 "请大家散开一点"。调整物体的 position 属性,让它们在 Z 轴上保持足够间隙:

// 从0.000001增加到0.01,提供足够的深度差异
plane2.position.z = 0.01;

这个值需要根据场景尺度调整,原则是既要明显大于深度缓冲区的精度误差,又要小到不影响视觉效果。

2. 调整相机参数:优化深度缓冲区的 "视力"

相机的近平面(near)和远平面(far)设置对深度缓冲区精度影响巨大。就像望远镜的焦距,调得合适才能看得更清楚。理想情况下,近平面应尽可能远,远平面应尽可能近,形成一个 "紧凑" 的观察范围:

// 不好的设置:近平面太近,远平面太远
camera.near = 0.0001;
camera.far = 10000;
// 更好的设置:根据实际场景调整范围
camera.near = 0.1;
camera.far = 100;
camera.updateProjectionMatrix(); // 重要:更新相机投影矩阵

这个技巧的原理是,深度缓冲区的精度在近平面附近最高,随着距离增加而降低。缩小观察范围能让有限的精度分布在更有用的区间内。

3. 使用多边形偏移:给表面添加 "虚拟垫片"

Three.js 提供了 polygonOffset(多边形偏移)功能,就像给其中一个表面垫上看不见的垫片,在不改变实际位置的情况下解决冲突。这特别适合处理网格线、阴影等必须与表面贴合但又不能重叠的元素:

// 创建带偏移的材质
const materialWithOffset = new THREE.MeshBasicMaterial({
  color: 0x00ff00,
  polygonOffset: true,
  polygonOffsetFactor: 1,    // 偏移因子
  polygonOffsetUnits: 1      // 偏移单位
});
const plane2 = new THREE.Mesh(geometry2, materialWithOffset);

polygonOffsetFactor 控制基于多边形斜率的偏移量,polygonOffsetUnits 控制基于屏幕像素的偏移量。通常从 (1,1) 开始尝试,逐渐调整到合适值。

4. 启用 logarithmicDepthBuffer:给远距离场景配 "老花镜"

对于大型场景(如室外建筑、地形),可以启用相机的对数深度缓冲区,它能在远距离保持更高的深度精度,就像老花镜帮助看清远处物体:

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.logarithmicDepthBuffer = true; // 启用对数深度缓冲

注意这个属性需要 WebGL 2.0 支持,并且会略微增加性能消耗。

深度冲突的哲学思考:精度与效率的永恒平衡

深度冲突本质上是计算机图形学中 "精度有限" 这一根本矛盾的体现。我们永远在精度和性能之间寻找平衡 —— 更高位的深度缓冲区(如 32 位)能减少冲突,但会消耗更多显存;更大的物体间距能避免冲突,但会影响视觉真实性。

作为 Three.js 开发者,理解深度冲突背后的原理能帮助我们写出更健壮的代码。记住,当你的场景出现神秘的闪烁和条纹时,不妨先检查那些看似亲密无间的表面 —— 也许只是需要给它们一点呼吸的空间。

就像现实世界中解决拥挤问题的方法永远是合理规划空间和流量,在 Three.js 的 3D 世界里,优雅解决深度冲突的关键也在于:理解你的场景尺度,优化相机参数,给每个表面合适的 "生存空间"。

深入理解JavaScript闭包:从入门到精通的实战指南

2025年7月6日 11:10

碎碎念

你是不是也有过这样的困扰?面试官问起闭包,你能说出个大概,但总感觉说不到点子上?或者在实际开发中,明明知道要用闭包解决问题,但写出来的代码总是有各种奇怪的bug?

别担心,今天我们就来彻底搞懂JavaScript中这个既神秘又实用的概念——闭包。通过实际代码示例和踩坑经验,让你从"知其然"到"知其所以然"。

叠个甲:本文基于阮一峰老师的经典教程,结合实际开发经验,用最接地气的方式带你理解闭包。如果你觉得某些地方讲得不够深入,欢迎在评论区讨论!

一、作用域:理解闭包的基石

1.1 作用域链的奥秘

在深入闭包之前,我们先来回顾一下JavaScript的作用域机制。看看这段代码:

// 全局作用域
var n = 999;

function f1() {
    // 没有使用var声明,变成了全局变量
    b = 123;
    // 函数作用域
    {
        // 块级作用域
        let a = 1;
    }
    console.log(n); // 可以访问全局变量
}

f1();
console.log(b); // 123,意外的全局变量

关键点解析:

  • 作用域链:内部作用域可以访问外部作用域的变量,这就是作用域链的核心
  • 意外的全局变量:不使用varletconst声明的变量会意外成为全局变量,这是JavaScript的一个"坏零件"(The Bad Parts)
  • 块级作用域:ES6的letconst引入了块级作用域概念

1.2 函数外部无法读取内部变量的困境

正常情况下,函数外部是无法访问函数内部变量的:

function f1() {
    var n = 999; // 局部变量
}

console.log(n); // ReferenceError: n is not defined

那么问题来了:如果我们确实需要在函数外部访问函数内部的变量怎么办?

这就是闭包要解决的核心问题!

二、闭包的本质:连接内外的桥梁

2.1 什么是闭包?

闭包就是将函数内部和函数外部连接起来的桥梁

用更技术化的语言描述:闭包是指有权访问另一个函数作用域中变量的函数

让我们看一个最简单的闭包例子:

// 让局部变量可以在全局访问
function f1() {
    // 局部变量
    var n = 999; // 自由变量
    function f2() {
        // 自由变量
        console.log(n);
    }
    return f2;
}

var result = f1();
result(); // 999

代码解析:

  1. f2函数定义在f1函数内部
  2. f2函数访问了f1函数的局部变量n
  3. f1函数返回了f2函数
  4. 在全局作用域中,我们通过result变量保存了f2函数的引用
  5. 调用result()时,依然能够访问到n变量

这就是闭包的神奇之处:即使f1函数已经执行完毕,但n变量并没有被销毁,而是被"保存"在了闭包中

2.2 自由变量的生命周期

你可能会好奇:为什么n变量没有被垃圾回收机制回收?

答案是:引用计数

function f1() {
    var n = 999;
    nAdd = function () {
        n += 1;
    }
    function f2() {
        console.log(n);
    }
    return f2;
}

var result = f1();
result(); // 999
nAdd();
result(); // 1000

关键观察:

  • 第一次调用result()输出999
  • 调用nAdd()修改了n的值
  • 第二次调用result()输出1000

这说明什么?n变量一直存活在内存中,没有被销毁!

这就是闭包的核心特性:让变量的值始终保持在内存中

三、闭包的经典应用场景

3.1 解决this指向问题

在实际开发中,闭包最常见的应用场景之一就是解决this指向问题:

var name = "The Window";
var object = {
    name: "My Object",
    getNameFunc: function () {
        var that = this; // 保存this引用
        return function(){
            return that.name; // 通过闭包访问外部的this
        }
    }
}

console.log(object.getNameFunc()()); // "My Object"

为什么需要这样做?

如果直接返回function(){ return this.name; },那么this会指向全局对象(在浏览器中是window),而不是object对象。

通过闭包,我们巧妙地"捕获"了正确的this引用。

3.2 模块化编程

闭包还可以用来实现模块化编程,创建私有变量:

var module = (function(){
    var privateVar = 0;
    
    return {
        increment: function(){
            privateVar++;
        },
        getCount: function(){
            return privateVar;
        }
    };
})();

module.increment();
module.increment();
console.log(module.getCount()); // 2
console.log(module.privateVar); // undefined,无法直接访问

这种模式在ES6模块化普及之前,是JavaScript实现模块化的主要方式。

四、闭包的陷阱与注意事项

4.1 内存泄漏的隐患

闭包虽然强大,但也带来了内存管理的挑战:

function createClosure() {
    var largeData = new Array(1000000).fill('data');
    
    return function() {
        console.log('闭包函数被调用');
        // 即使不使用largeData,它也不会被回收
    };
}

var closure = createClosure();
// largeData数组会一直占用内存

解决方案:

function createClosure() {
    var largeData = new Array(1000000).fill('data');
    
    return function() {
        console.log('闭包函数被调用');
    };
}

var closure = createClosure();
// 使用完毕后,手动清理
closure = null; // 这样largeData才能被回收

4.2 循环中的闭包陷阱

这是一个经典的面试题:

// 错误的写法
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出三次3
    }, 100);
}

为什么输出三次3?

因为setTimeout中的回调函数形成了闭包,它们都引用了同一个变量i。当定时器执行时,循环已经结束,i的值已经变成了3。

解决方案1:使用立即执行函数

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j); // 输出0, 1, 2
        }, 100);
    })(i);
}

解决方案2:使用let声明

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出0, 1, 2
    }, 100);
}

五、闭包的性能考量

5.1 内存消耗

闭包会导致额外的内存消耗,因为:

  1. 外部函数的变量不能被垃圾回收
  2. 闭包函数本身也占用内存
  3. 如果创建大量闭包,可能导致内存压力

5.2 最佳实践

  1. 及时清理不需要的闭包引用
var closure = createClosure();
// 使用完毕后
closure = null;
  1. 避免在循环中创建大量闭包
// 不好的做法
for (let i = 0; i < 10000; i++) {
    element.addEventListener('click', function() {
        // 创建了10000个闭包
    });
}

// 更好的做法
function handleClick() {
    // 只创建一个函数
}
for (let i = 0; i < 10000; i++) {
    element.addEventListener('click', handleClick);
}
  1. 在退出函数之前,将不使用的局部变量设为null
function createClosure() {
    var largeObject = {};
    var smallData = 'needed';
    
    // 使用完largeObject后
    largeObject = null;
    
    return function() {
        return smallData;
    };
}

六、现代JavaScript中的闭包

6.1 箭头函数与闭包

const createCounter = () => {
    let count = 0;
    return () => ++count;
};

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

箭头函数同样可以形成闭包,而且语法更加简洁。

6.2 Promise与闭包

function createAsyncCounter() {
    let count = 0;
    
    return function() {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(++count); // 闭包访问count
            }, 1000);
        });
    };
}

const asyncCounter = createAsyncCounter();
asyncCounter().then(console.log); // 1
asyncCounter().then(console.log); // 2

七、总结与思考

7.1 闭包的核心价值

  1. 数据封装:创建私有变量,实现信息隐藏
  2. 状态保持:让变量在函数执行完毕后依然存活
  3. 回调函数:在异步编程中保持上下文
  4. 模块化:在ES6之前实现模块化编程的重要手段

7.2 使用闭包的原则

  1. 明确目的:确实需要保持状态或封装数据时才使用
  2. 注意内存:及时清理不需要的闭包引用
  3. 性能考虑:避免在性能敏感的场景中过度使用
  4. 代码可读性:确保团队成员都能理解闭包的使用意图

7.3 闭包与现代前端开发

在现代前端开发中,虽然有了ES6模块、React Hooks、Vue Composition API等新特性,但闭包依然是理解这些概念的基础。比如:

  • React的useState本质上就是利用闭包来保持状态
  • Vue的响应式系统也大量使用了闭包
  • 各种状态管理库都离不开闭包的概念

小贴士

闭包不仅仅是一个技术概念,更是JavaScript语言设计哲学的体现。它体现了"函数是一等公民"的理念,让我们能够以更加灵活和强大的方式组织代码。

虽然闭包可能带来一些性能和内存方面的考虑,但只要我们理解其原理,合理使用,它就是我们手中的利器。

记住:闭包的自由是有代价的,这个代价就是生命周期的延长和内存的占用。但正是这种"不确定性"的自由,给了JavaScript无限的可能性。

希望这篇文章能帮你彻底理解闭包,在面试和实际开发中都能游刃有余。如果你有任何问题或者想法,欢迎在评论区讨论!


参考资料:

  • 阮一峰《JavaScript教程》
  • 《JavaScript语言精粹》
  • MDN Web Docs

Vue.js 入门指南:从零开始构建你的第一个应用

作者 markyankee101
2025年7月6日 11:02

Vue.js 入门指南:从零开始构建你的第一个应用

什么是 Vue.js?

Vue.js(通常简称为 Vue)是一个渐进式 JavaScript 框架,用于构建用户界面。由前 Google 工程师尤雨溪于 2014 年创建,如今已成为全球最流行的前端框架之一。Vue 的核心特点包括:

  • 渐进式架构:可以逐步采用,从增强静态页面到构建复杂单页应用
  • 响应式系统:自动追踪数据变化并更新视图
  • 组件化开发:将 UI 拆分为独立可复用的组件
  • 易学易用:基于标准 HTML、CSS 和 JavaScript,学习曲线平缓
  • 轻量高效:运行时仅 20KB 左右,性能优异

为什么选择 Vue?

  1. 简单易上手:对初学者友好,文档完善
  2. 灵活性强:可根据需求选择使用方式
  3. 社区活跃:拥有庞大开发者社区和丰富生态
  4. 性能出色:虚拟 DOM 和智能优化策略
  5. 现代工具链:Vite 构建工具提供极速开发体验

搭建第一个 Vue 应用

安装 Vue

方法一:CDN 引入(最简单方式)

<!DOCTYPE html>
<html>
<head>
  <title>第一个 Vue 应用</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    {{ message }}
  </div>

  <script>
    const { createApp } = Vue
  
    createApp({
      data() {
        return {
          message: '你好,Vue!'
        }
      }
    }).mount('#app')
  </script>
</body>
</html>

方法二:使用 Vite 创建项目(推荐)

npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev

方法三:使用 Vue CLI 创建项目

npm install -g @vue/cli
# 或
yarn global add @vue/clinpm 

vue --version  # 验证安装是否成功
vue create my-project # 创建项目
cd my-project
npm run serve

Vue 应用基本结构

// main.js
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
<!-- App.vue -->
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>计数器: {{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '我的第一个Vue应用',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<style scoped>
h1 {
  color: #42b983;
}
</style>

Vue 核心概念详解

1. 模板语法

Vue 使用基于 HTML 的模板语法,允许你声明式地将数据绑定到 DOM:

<template>
  <!-- 文本插值 -->
  <p>{{ message }}</p>
  
  <!-- 原始 HTML -->
  <p v-html="rawHtml"></p>
  
  <!-- 属性绑定 -->
  <a :href="url">链接</a>
  
  <!-- 条件渲染 -->
  <div v-if="isVisible">可见内容</div>
  <div v-else>其他内容</div>
  
  <!-- 列表渲染 -->
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index }}. {{ item.name }}
    </li>
  </ul>
</template>

2. 响应式数据

Vue 自动跟踪 JavaScript 对象的变化并更新 DOM:

export default {
  data() {
    return {
      message: 'Hello Vue!',
      counter: 0,
      user: {
        name: '张三',
        age: 30
      },
      todos: [
        { id: 1, text: '学习Vue' },
        { id: 2, text: '构建应用' }
      ]
    }
  },
  methods: {
    updateUser() {
      // Vue 会自动检测这些变化并更新视图
      this.user.age = 31
      this.todos.push({ id: 3, text: '部署项目' })
    }
  }
}

3. 指令系统

指令是带有 v- 前缀的特殊属性:

指令 说明 示例
v-bind 动态绑定属性 :class="{ active: isActive }
v-on 绑定事件监听器 @click="handleClick"
v-model 表单输入双向绑定 <input v-model="message">
v-if/v-show 条件渲染 <p v-if="isVisible">显示</p>
v-for 列表渲染 <li v-for="item in items">
v-html 输出原始 HTML <div v-html="rawHtml">

4. 组件基础

组件是 Vue 的核心概念,允许你将 UI 拆分为独立可复用的部分:

<!-- 父组件 ParentComponent.vue -->
<template>
  <div>
    <child-component 
      :title="parentTitle"
      @child-event="handleChildEvent"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentTitle: '来自父组件的消息'
    }
  },
  methods: {
    handleChildEvent(data) {
      console.log('收到子组件事件:', data)
    }
  }
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="sendToParent">通知父组件</button>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    }
  },
  emits: ['child-event'],
  methods: {
    sendToParent() {
      this.$emit('child-event', { message: '来自子组件的数据' })
    }
  }
}
</script>

5. 计算属性和侦听器

计算属性:基于响应式数据派生值 侦听器:响应数据变化执行操作

<template>
  <div>
    <p>原始价格: {{ price }} 元</p>
    <p>折扣后价格: {{ discountedPrice }} 元</p>
    <input v-model="discountPercent" placeholder="输入折扣百分比">
  </div>
</template>

<script>
export default {
  data() {
    return {
      price: 100,
      discountPercent: 10
    }
  },
  computed: {
    // 计算属性 - 自动缓存结果
    discountedPrice() {
      return this.price * (1 - this.discountPercent / 100)
    }
  },
  watch: {
    // 侦听器 - 响应特定数据变化
    discountPercent(newVal, oldVal) {
      if (newVal > 50) {
        alert('折扣不能超过50%!')
        this.discountPercent = 50
      }
    }
  }
}
</script>

6. 生命周期钩子

Vue 组件有多个生命周期阶段,你可以在特定阶段执行代码:

export default {
  data() {
    return {
      data: null
    }
  },
  // 组件初始化后,数据观测已建立
  created() {
    console.log('组件已创建')
    this.fetchData()
  },
  // 组件挂载到 DOM 后
  mounted() {
    console.log('组件已挂载')
    this.setupEventListeners()
  },
  // 组件更新后
  updated() {
    console.log('组件已更新')
  },
  // 组件卸载前
  beforeUnmount() {
    console.log('组件即将卸载')
    this.cleanup()
  },
  methods: {
    fetchData() { /* 获取数据 */ },
    setupEventListeners() { /* 设置事件监听 */ },
    cleanup() { /* 清理操作 */ }
  }
}

实战:构建待办事项应用

<template>
  <div class="todo-app">
    <h1>待办事项清单</h1>
  
    <!-- 添加新任务 -->
    <div class="add-todo">
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo"
        placeholder="添加新任务..."
      >
      <button @click="addTodo">添加</button>
    </div>
  
    <!-- 过滤选项 -->
    <div class="filters">
      <button 
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
      >
        全部
      </button>
      <button 
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
      >
        未完成
      </button>
      <button 
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
      >
        已完成
      </button>
    </div>
  
    <!-- 任务列表 -->
    <ul class="todo-list">
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input 
          type="checkbox" 
          v-model="todo.completed"
        >
        <span :class="{ completed: todo.completed }">
          {{ todo.text }}
        </span>
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
  
    <!-- 统计信息 -->
    <div class="stats">
      <p>剩余任务: {{ activeTodosCount }} / 总任务: {{ todos.length }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newTodo: '',
      todos: [
        { id: 1, text: '学习Vue基础', completed: false },
        { id: 2, text: '构建第一个Vue应用', completed: true },
        { id: 3, text: '探索Vue高级特性', completed: false }
      ],
      filter: 'all'
    }
  },
  computed: {
    activeTodosCount() {
      return this.todos.filter(todo => !todo.completed).length
    },
    filteredTodos() {
      switch (this.filter) {
        case 'active':
          return this.todos.filter(todo => !todo.completed)
        case 'completed':
          return this.todos.filter(todo => todo.completed)
        default:
          return this.todos
      }
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim() === '') return
  
      this.todos.push({
        id: Date.now(),
        text: this.newTodo,
        completed: false
      })
  
      this.newTodo = ''
    },
    removeTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id)
    }
  }
}
</script>

<style>
.todo-app {
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.add-todo {
  display: flex;
  margin-bottom: 20px;
}

.add-todo input {
  flex-grow: 1;
  padding: 10px;
  font-size: 16px;
}

.add-todo button {
  margin-left: 10px;
  padding: 10px 15px;
  background: #42b983;
  color: white;
  border: none;
  cursor: pointer;
}

.filters {
  margin-bottom: 20px;
}

.filters button {
  padding: 5px 10px;
  margin-right: 5px;
  background: #f0f0f0;
  border: 1px solid #ddd;
  cursor: pointer;
}

.filters button.active {
  background: #42b983;
  color: white;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-list input[type="checkbox"] {
  margin-right: 10px;
}

.todo-list span.completed {
  text-decoration: line-through;
  color: #888;
}

.todo-list button {
  margin-left: auto;
  background: #ff6b6b;
  color: white;
  border: none;
  padding: 5px 10px;
  cursor: pointer;
}

.stats {
  margin-top: 20px;
  text-align: center;
  color: #666;
}
</style>

下一步学习路径

掌握了 Vue 基础后,你可以继续深入学习:

  1. Vue Router:构建单页面应用(SPA)的路由管理
  2. 状态管理:使用 Pinia 管理全局状态
  3. 组合式 API:Vue 3 的现代代码组织方式
  4. 服务端渲染:使用 Nuxt.js 提升应用性能
  5. UI 框架:Element Plus、Vuetify 等流行 UI 库
  6. 测试:使用 Vitest 和 Vue Test Utils 进行组件测试

学习资源推荐

结语

Vue.js 以其简洁的设计和强大的功能,已成为现代前端开发的首选框架之一。通过本指南,你已经掌握了 Vue 的核心概念和基本用法。现在,是时候动手实践,构建你自己的 Vue 应用了!

记住,学习编程最好的方法就是不断实践。从简单的项目开始,逐步挑战更复杂的应用,你将很快成为一名熟练的 Vue 开发者。祝你编程愉快!

🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥

作者 MrSkye
2025年7月6日 10:49

前言:为什么 JavaScript 如此“奇怪”?

如果你是从其他编程语言(比如 Python、Java 或 C++)转到 JavaScript 的,你可能会对它的某些行为感到困惑:

console.log(name); // 输出:undefined,而不是报错!
var name = "Alice";

{
  let age = 25;
  console.log(age); // 输出:25
}
console.log(age); // 报错:age is not defined
  • 为什么 var 变量可以在声明前访问?
  • 为什么 letconst 又不行?
  • 为什么 {} 块能影响变量的作用域?

这些现象背后,是 JavaScript 独特的 编译与执行机制,以及 作用域管理方式。在传统语言中,变量通常需要先声明再使用,而 JavaScript 的 var 却允许“先使用后声明”,这源于它的 变量提升(Hoisting) 特性。而 letconst 的出现,则修复了 var 的缺陷,引入了更严格的 块级作用域(Block Scope)暂时性死区(TDZ)

本章将深入剖析:

JavaScript 代码的执行流程(编译 vs. 执行阶段)

变量提升的本质var 与函数声明的特殊行为)

letconst 如何避免变量提升问题

作用域链与闭包的底层机制

无论你是 JavaScript 新手,还是想彻底理解它的运行原理,这篇文章都会让你豁然开朗!

🚀 现在,让我们开始探索 JavaScript 的独特世界!

First:JavaScript代码是如何跑起来的

想要了解这些现象的本质,我们第一个需要了解的是,JavaScript的代码是怎么跑的,如何运行的,究竟是什么神奇的妙妙♂工具能让它的规则如此灵动~

JavaScript代码运行的基本过程:

image.png

解析阶段

JavaScript代码在运行时的第一阶段,在这一阶段中浏览器的引擎会进行解析(Phrasing),在这一阶段会进行词法分析语法分析

词法分析:会将代码字符串拆分成有意义的“单词”或“符号”,称为 Token

例如:let x = 5 + 3; 会被拆分成:[let, x, =, 5, +, 3, ;]

语法分析:根据 JavaScript 的语法规则,将这些 Token 组织成一个树状结构,称为 抽象语法树 (Abstract Syntax Tree - AST)

  • AST 代表了代码的结构和逻辑关系。
  • 例如:let x = 5 + 3; 的 AST 会表示:声明一个变量 x,它的值是一个加法表达式(操作数是 5 和 3)

此阶段的重点

  • 检查代码是否有语法错误 (Syntax Errors)。如果代码写得不合语法规则(比如缺少括号、错误的关键字),解析阶段就会失败并报错。
  • 只关心代码的结构和形式不关心变量具体代表什么值、函数具体做什么操作。
  • 输出:AST (抽象语法树) 。这是代码的“结构化蓝图”。

编译阶段

编译阶段会 静态分析 作用域关系(但不创建运行时词法环境):

  • 确定变量和函数的作用域归属

    • 识别全局作用域、函数作用域、块级作用域(let/const)。
    • 标记变量声明(varletconst)和函数声明(function)的作用域。
  • 处理变量提升(Hoisting)

    • var 和 function 声明会被记录到作用域顶部(但未赋值)。
    • let/const 也会被记录,但不会提前绑定(形成暂时性死区)。
  • 建立作用域链的静态结构

    • 确定嵌套作用域的引用关系(闭包的基础)。

什么是作用域?

看到上面编译阶段的小伙伴们可能会有疑问:什么叫作用域啊?作用域链是什么东西嘞?

作用域,作用域,就是变量能发挥作用的区域,变量能在某个区域耀武扬威,但是到了别的地方,就得喝肾宝咯~

比如以下例子:

var a = 1;

function add(a1,a2){
        var c = 2;
        return a1+a2+c;
}

{
    let f = 1;
    const g = 2;
    var h = 3;
}

这个例子的作用域就是这么组成的:

image.png

对应的变量只会在相应的作用域中发挥作用,在其他的作用域中不可发挥作用,图中,c,a1,a2仅仅能在add函数中被使用,f,g仅能在块级作用域中被使用,而a,h处于全局作用域,可以在任何地方被使用。

这个时候就有人要说了:“主播主播,h不是在块级作用域中被定义的吗?怎么跑到了全局作用域了呢?”

34B6FBAEB148C53C17D1F4A465E1E00D.gif

答案其实就是提升(hoistings)

提升(Hoisting)

提升是JavaScript中一个比较重要的特性,它决定了我们利用定义的变量和普通函数后的,它们的特殊行为:即无论在哪里定义变量(var)和函数,都会在编译阶段提升到当前顶层

通俗一点讲,就是一个人在刷抖音,他的抖音里有各种各样的内容,他会总览一下,把所有看到的美女都先收藏一下,就算不知道她们的ID,也要收藏一下,毕竟感觉来了谁会管她ID是啥呢0v0.

而在代码编译阶段,所有变量和函数就是美女,让编译器离不开眼,把她们全部拉到顶层了,不管它们是什么值,不管他们有没有值,全部拉到顶部!

就像下面这个例子:

console.log(a);  // 输出 undefined
var a = 3;
var c = 114514;
add(1,3);
function add(a,b){
    console.log(a,b); // 输出 1,3
    console.log(c);   // 输出 undefined
    var c = 4;
}

这一段代码在编译后就相当于以下内容:

var a; // 全局作用域内变量提升至顶部
var c;
function add(a,b){  // 函数提升到顶部
    var c;  // 函数作用域内变量提升
    console.log(a,b); // 输出 1,3
    console.log(c);   // 输出 undefined
    c = 4;
}
console.log(a);
a = 3;
c = 114514;
add(1,3);

在编译器的法眼下,所有var变量和函数都被标记出来了,提升到了对应的顶部,这也就是为什么这一串看似反人类的代码可以执行而不报错的原因,就在于提升(Hoisting)

!!!!提醒:

能做到提升的变量只有 var ,但是如果var存在于函数中,那么只能提升到函数作用域的顶部,而不会溢出到全局作用域。

let和const均不可以提升,这是由于其设计造成的,JavaScript仅仅用了一周的时间就被设计了出来,var是最早表示变量的标识,后来在ES6中为了消除这种反人类的设计才引入了let和const,用了let和const的变量拥有和其他语言一样的特性,不再有提升。

这个时候有的童鞋可能想问了:如果我在add函数中没有定义var c,那么c会输出什么呢?

这个就和作用域链有关系了

作用域链

作用域链(Scope Chain) 是 JavaScript 在执行过程中寻找变量的机制,它决定了 当前作用域 可以访问哪些变量,以及它们的查找顺序。

当访问一个变量时,JavaScript 引擎会 沿着作用域链逐级向上查找,直到找到该变量或到达全局作用域(未找到则报 ReferenceError)。

例如上方的例子中,要访问c变量时,就会先访问函数作用域中的c,如果没有,就会沿着作用域链一步步向外寻找。就像小孩子自己被困在家里,睡醒了找不到妈妈一样,先从房间找,又跑到客厅,最后跑出房子找是一样的....(大家一定要照顾好孩子233333~)

执行阶段

编译阶段的几个问题解答完了,我们来解释一下执行阶段。

目的:  真正逐行运行代码,产生程序的实际效果(计算、修改数据、操作 DOM、网络请求等)。

  • 过程:

    • JavaScript 引擎从上到下、逐行执行编译阶段准备好的代码(字节码或机器码),创建执行上下文

    • 赋值操作:  给变量赋予实际的值(覆盖掉编译阶段 var 变量的 undefined 初始值)。

    • 函数调用:

      • 遇到函数调用时,会为该函数创建一个新的函数执行上下文,同样经历编译(创建)  和 执行 阶段。
      • 执行函数体内部的代码。
      • 函数执行完毕后,其执行上下文通常会被销毁(闭包除外)。
    • 表达式求值:  计算表达式的结果(如 5 + 3x > y)。

    • 逻辑控制:  执行条件判断 (if/else)、循环 (forwhile) 等逻辑流程。

其中,创建执行上下文是最重要的部分。

执行上下文

简单来说,执行上下文就是记录了各种信息的载体,方便对应规则来运行代码。

执行上下文一般有三种:

  1. 全局执行上下文(Global Execution Context)  → 代码首次运行时创建。
  2. 函数执行上下文(Function Execution Context)  → 每次调用函数时创建。
  3. eval 执行上下文(较少用,不推荐)。

执行上下文都包含了啥呢?

image.png

V8引擎就是根据这些信息来对应着一定的规则,来运行了代码。

Second:let?const?var?有啥区别?

JavaScript 有三种变量声明方式:constletvar,它们在 作用域、提升(Hoisting)、重复声明、TDZ(暂时性死区) 等方面有显著区别。

首先来说说Var,这个是JavaScript最初设计用来表示变量的标识,它具有提升的特性,这一点与其他编程语言不同,比较反人类,所以呢,后来在ES6中就设计了let和const,用来替代var,去除这一反人类的特性。那为什么后来没有废除var呢,我猜可能是用var的代码已经太多了,如果删除掉var,那么开发者们就要掀桌子了哈哈哈哈哈哈哈哈。


constletvar 核心区别

特性 var let const
作用域 函数作用域(function-scoped 块级作用域(block-scoped 块级作用域(block-scoped
声明提升 ✅(初始化为 undefined ✅(未初始化,访问报错) ✅(未初始化,访问报错)
重复声明 ✅(可以多次声明) ❌(同一作用域禁止重复声明) ❌(同一作用域禁止重复声明)
重新赋值 ❌(常量,不能重新赋值)
TDZ(暂时性死区) ❌(无 TDZ) ✅(进入块级作用域到声明前不可访问) ✅(同 let

TDZ(Temporal Dead Zone,暂时性死区)

简单来讲呢,就是当你定义一个let或者const变量的时候,不能在没有声明的情况下提前访问它,这一点和其他编程语言一致.

TDZ 是 letconst 特有的行为

  • 在变量声明之前,如果访问它,会抛出 ReferenceError(而不是得到 undefined)。
  • TDZ 的范围:变量所在的作用域顶部 → 变量声明的位置。

示例 1:let 的 TDZ

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;     // 声明前访问会报错(TDZ 区域)

执行过程

  1. 进入作用域 → a 被提升(let a
  2. a 未被初始化(处于 TDZ
  3. 执行 console.log(a)报错
  4. 执行 a = 10 → TDZ 结束,可以访问 a

示例 2:const 的 TDZ

console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 20;   // 声明前访问会报错(TDZ 区域)

constlet 的 TDZ 行为一致。

示例 3:var 没有 TDZ

console.log(c); // undefined(不会报错)
var c = 30;     // var 提升并初始化为 undefined

var 不会进入 TDZ,因为它在编译阶段已经被初始化为 undefined


全局作用域下的绑定

  • var 声明的全局变量 → 会 绑定到 window(浏览器)或 global(Node.js)
  • letconst 声明的全局变量 → 不会绑定到 window/global

示例

// 浏览器环境
var globalVar = "var 变量";
let globalLet = "let 变量";
const globalConst = "const 变量";

console.log(window.globalVar);   // "var 变量"(绑定到 window)
console.log(window.globalLet);   // undefined(不绑定)
console.log(window.globalConst); // undefined(不绑定)

为什么 letconst 不绑定到 window

  • 历史遗留问题var 是 ES5 的写法,会污染全局对象(window)。
  • 块级作用域优化letconst 是 ES6 引入的,设计初衷是避免全局污染。

使用场景总结

声明方式 适用场景
var 旧代码兼容 / 不需要块级作用域的场景(但现代 JS 基本不用)。
let 需要重新赋值的变量(如循环计数器 for (let i = 0; ...))。
const 常量(如 API 密钥、数学常量 PI)、对象/数组引用(可修改属性但不能重新赋值)。

总结

关键点 var let const
作用域 函数作用域 块级作用域 块级作用域
提升(Hoisting) ✅ (undefined) ✅ (TDZ) ✅ (TDZ)
重复声明
重新赋值
TDZ(暂时性死区)
全局绑定 window

最佳实践

  • 默认使用 const(避免意外修改)。
  • 需要重新赋值时用 let(如循环变量)。
  • 避免使用 var(除非维护旧代码)。

总结

好了,这一篇文章就到这里吧,我们在这里讲解了JS代码运行的原理,详细介绍了作用域和执行上下文等各种概念,还是希望大家能够有一些收获,能够更好的了解JavaScript,如果你觉得我写的好,那么请给我一个免费的赞吧嘻嘻嘻》》》

军人王有胜一脸得意的笑GIF表情包_爱给网_aigei_com.gif

nest中如何对typeorm 的repo设置总的center

作者 小山不高
2025年7月6日 10:05

问题:在nestjs 框架中使用typeorm 每一个server的entity repo都要手动写一个注入,很麻烦,10个repo和10个server如果都使用就是写100遍注入

思考:如何只写一次注入,在这个注入的实体里面调用每一个repo

step1:

对typeorm有过了解的写的熟练的朋友可以知道手动实例化的方法,DataSource.getRepository 那么我们可以先定一个数组或者对象映射,方法无所谓,根本在于如何知道你需要的实例化的所有repo和其对应的名称,我这里简单用对象来表达。

const RepoEntityMap = {
  tagEntity: TagEntity,
  fileEntity: FileEntity,
};

@Injectable()
export class RepoCenter implements OnModuleInit {
  @InjectDataSource() dataSource: DataSource;

  onModuleInit(): any {
    for (const RepoEntityMapKey of Object.keys(RepoEntityMap)) {
      this[`${RepoEntityMapKey}Repo`] = this.dataSource.getRepository(RepoEntityMap[RepoEntityMapKey]);
    }
  }
}

step2:

前一步我们手动装配了每一个repo,但是对于开发还有重要的一步,我们如何知道这个center装配了哪些repo呢?参考我前一篇typescript的文章,我们来定义ts的类型,让ts提示我们

type RepoEntityMapType = {
  [K in keyof typeof RepoEntityMap as `${K}Repo`]?: Repository<InstanceType<typeof RepoEntityMap[K]>>;
};

declare module './RepoCenter' {
  interface RepoCenter extends RepoEntityMapType {}
}

如此,短短两步,很少的代码我们就可以使用center只需在每个server注入一次就可以使用所有的repo

当然了你也可以说我可以写一个center,在center中注入每一个repo,这个当然是可以了,但是难道说在TypeOrmModule.forFeature进行注册的时候也要来一遍吗?我可以使用我前面的对象映射或者定义一个数组(形式不重要,根本在于如何知道需要哪些repo)直接复用

跟着官方示例学习 @tanStack-form --- Linked Fields

2025年7月6日 09:41

🌲系列一:跟着官方示例学习 @tanStack-form --- Simple

🌲系列二:跟着官方示例学习 @tanStack-form --- Array


这篇并没有采用官方的示例,代码部分也较为简单,因此直接在文章中展示,感兴趣的小伙伴可以粘贴至自己项目运行哦🤝

🌱 基础知识:什么是 linked fields?

在使用表单库时,我们经常会遇到这样的需求:某个字段的值变化后,另一个字段需要跟着联动更新或校验。所谓 linked fields,指的是 字段 A 的值变化后,影响字段 B 的行为,常见于:

  • 输入“密码”后,“确认密码”字段需要实时校验一致性
  • 选择国家后,联动显示对应城市选项

TanStack Form 提供了内建能力来实现这些需求,主要有两种方式:

  • validators.onChangeListenTo: 用于跨字段验证
  • <form.Subscribe />: 适合更灵活的值同步或控制禁用等 UI 行为

🧪 场景一:确认密码字段验证(密码一致性)

最常见的联动场景:两个字段必须值相同。

首先我们可以看官方文档中提到的例子:

tanstack.com/form/latest…

function App() {
  const form = useForm({
    defaultValues: {
      password: '',
      confirm_password: '',
    },
    // ...
  })

  return (
    <div>
      <form.Field name="password">
        {(field) => (
          <label>
            <div>Password</div>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
          </label>
        )}
      </form.Field>
      <form.Field
        name="confirm_password"
        validators={{
          onChangeListenTo: ['password'],
          onChange: ({ value, fieldApi }) => {
            if (value !== fieldApi.form.getFieldValue('password')) {
              return 'Passwords do not match'
            }
            return undefined
          },
        }}
      >
        {(field) => (
          <div>
            <label>
              <div>Confirm Password</div>
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
              />
            </label>
            {field.state.meta.errors.map((err) => (
              <div key={err}>{err}</div>
            ))}
          </div>
        )}
      </form.Field>
    </div>
  )
}

Jul-06-2025 08-33-32.gif

🔍 要点说明: onChangeListenTo: ['password'] 表示“监听 password 字段”

使用 fieldApi.form.getFieldValue('password') 获取 password 的当前值进行对比

📘 延伸阅读:getFieldValue API 🔗

🧪 场景二:选择国家 → 自动填写城市

除了验证类联动,setFieldValue 可以在字段变化时,主动设置另一个字段的值。

比如:当用户选择国家时,自动填写该国家的首都。

<form>
  <div>
    <form.Field
      name="country"
      validators={{
        onChange: ({ value }) => {
          // 看这里 👈👈👈
          const options = citiesMap[value as "China" | "USA"] || "";
          form.setFieldValue("city", options);
        },
      }}
    >
      {(field) => (
        <label>
          <div>Select Country</div>
          <select onChange={(e) => field.handleChange(e.target.value)}>
            <option value="China">China</option>
            <option value="USA">USA</option>
          </select>
        </label>
      )}
    </form.Field>
  </div>
  <div>
    <form.Field name="city">
      {(field) => (
        <div>
          <label>
            <div>City</div>
            <input value={field.state.value} disabled />
          </label>
        </div>
      )}
    </form.Field>
  </div>
</form>;

Jul-06-2025 09-15-57.gif

📘 延伸阅读:setFieldValue API 🔗

🧪 场景三:选择国家 → 联动城市下拉框

这一场景稍微复杂一点,涉及到联动选项更新。推荐使用 <form.Subscribe /> 实现响应式更新。

<form>
  <div>
    <form.Field name="country">
      {(field) => (
        <label>
          <div>Select Country</div>
          <select onChange={(e) => field.handleChange(e.target.value)}>
            <option value="China">China</option>
            <option value="USA">USA</option>
          </select>
        </label>
      )}
    </form.Field>
  </div>
  <div>
    <form.Subscribe selector={(state) => state.values.country}>
      {(country) => (
        <form.Field name="city">
          {(field) => {
            const options = citiesMap[country as "China" | "USA"] || [];
            return (
              <div>
                <label>
                  <div>City</div>
                  <select
                    value={field.state.value}
                    onChange={(e) => field.handleChange(e.target.value)}
                  >
                    {options.map((city: string) => (
                      <option key={city} value={city}>
                        {city}
                      </option>
                    ))}
                  </select>
                </label>
              </div>
            );
          }}
        </form.Field>
      )}
    </form.Subscribe>
  </div>
</form>;

Jul-06-2025 09-33-11.gif

🔍 要点说明:

  • form.Subscribe 可以监听任意字段的变化,并触发 UI 重新渲染

  • citiesMap[country] 实时更新城市下拉列表的选项

  • 城市字段仍通过 form.Field 管理状态,表单数据仍是联动的

Playwright 中 Page 对象的常用方法详解

2025年7月6日 08:26

Page 对象是 Playwright 的核心 API 之一,代表浏览器中的一个标签页或弹出窗口。以下是 Page 对象最常用的方法及其使用示例和运行原理。

1. 导航相关方法

goto(url)

作用:导航到指定 URL
原理:等待页面加载到网络空闲状态(load 事件触发)

await page.goto("https://example.com")
# 同步方式
page.goto("https://example.com")

reload()

作用:重新加载当前页面
原理:触发页面刷新,等待新页面加载完成

await page.reload()
# 可带选项
await page.reload(timeout=5000, wait_until="networkidle")

go_back() / go_forward()

作用:前进/后退导航
原理:模拟浏览器前进后退按钮行为

await page.go_back()
await page.go_forward()

2. 元素定位与交互

locator(selector)

作用:创建元素定位器
原理:返回一个 Locator 对象,不立即查询 DOM

button = page.locator("button.submit")
# 推荐使用更语义化的定位方式
button = page.get_by_role("button", name="Submit")

click(selector)

作用:点击指定元素
原理:执行可操作性检查后触发点击事件

await page.click("#submit-btn")
# 带选项
await page.click("button", force=True, timeout=5000)

fill(selector, value)

作用:填充表单字段
原理:先清除字段再输入值,触发适当事件

await page.fill("#username", "testuser")

type(selector, text)

作用:模拟键盘输入
原理:逐个字符触发键盘事件

await page.type("#search", "Playwright", delay=100)  # 带输入延迟

3. 页面内容操作

evaluate(expression)

作用:在页面上下文中执行 JavaScript
原理:在浏览器环境中执行脚本并返回结果

title = await page.evaluate("document.title")
# 传参数
result = await page.evaluate("(arg) => window.myFunction(arg)", arg_value)

content()

作用:获取页面完整 HTML
原理:返回当前 DOM 的序列化 HTML

html = await page.content()

set_content(html)

作用:设置页面 HTML 内容
原理:替换当前文档内容,不触发网络请求

await page.set_content("<h1>Test Page</h1>")

4. 等待与断言

wait_for_selector(selector)

作用:等待元素出现
原理:轮询 DOM 直到元素存在

await page.wait_for_selector(".loading-spinner", state="hidden")

wait_for_event(event)

作用:等待特定事件
原理:监听页面事件,返回 Promise

async with page.expect_event("popup") as popup_info:
    await page.click("#popup-link")
popup = await popup_info.value

wait_for_timeout(ms)

作用:强制等待
原理:不推荐使用,除非绝对必要

await page.wait_for_timeout(1000)  # 等待1秒

5. 页面管理

screenshot()

作用:截取页面截图
原理:捕获当前视口或全屏

await page.screenshot(path="screenshot.png", full_page=True)

pdf()

作用:生成 PDF
原理:打印当前页面为 PDF(仅限 Chromium)

await page.pdf(path="output.pdf")

close()

作用:关闭页面
原理:触发页面卸载过程

await page.close()

6. 对话框处理

on(event, handler)

作用:监听对话框事件
原理:注册事件处理函数

page.on("dialog", lambda dialog: dialog.accept())

expect_dialog()

作用:等待对话框出现
原理:返回上下文管理器处理对话框

async with page.expect_dialog() as dialog_info:
    await page.click("#trigger-alert")
dialog = await dialog_info.value

7. 框架处理

frame(name)

作用:获取指定 iframe
原理:返回 Frame 对象用于操作 iframe 内容

frame = page.frame("login-frame")
await frame.fill("#username", "admin")

8. 浏览器上下文

context

作用:获取所属浏览器上下文
原理:返回创建该页面的 BrowserContext

context = page.context
cookies = await context.cookies()

运行原理总结

  1. 自动等待:大多数操作会自动等待元素可操作
  2. 事件驱动:基于浏览器事件循环响应各种页面事件
  3. 智能重试:操作失败时会自动重试直到超时
  4. 沙盒环境:每个测试运行在独立环境中,互不影响

最佳实践示例

async def test_complete_flow(page):
    # 导航
    await page.goto("https://shop.example.com")
    
    # 等待元素
    await page.wait_for_selector("#products")
    
    # 定位与交互
    await page.get_by_role("link", name="Login").click()
    await page.fill("#username", "testuser")
    await page.fill("#password", "password123")
    await page.click("#login-btn")
    
    # 断言
    await expect(page).to_have_url("https://shop.example.com/dashboard")
    await expect(page.get_by_text("Welcome, testuser")).to_be_visible()
    
    # 处理对话框
    page.on("dialog", lambda dialog: dialog.accept())
    await page.click("#logout-btn")
    
    # 截图
    await page.screenshot(path="after_login.png")

这些方法覆盖了 Page 对象 80% 以上的日常使用场景,掌握它们可以高效完成大多数 Web 自动化测试任务。

[Python3/Java/C++/Go/TypeScript] 一题一解:哈希表(清晰题解)

作者 lcbin
2025年7月6日 08:09

方法一:哈希表

我们注意到,数组 $\textit{nums1}$ 的长度不超过 ${10}^3$,数组 $\textit{nums2}$ 的长度达到 ${10}^5$,因此,如果直接暴力枚举所有下标对 $(i, j)$,计算 $\textit{nums1}[i] + \textit{nums2}[j]$ 是否等于指定值 $\textit{tot}$,那么会超出时间限制。

能否只枚举长度较短的数组 $\textit{nums1}$ 呢?答案是可以的。我们用一个哈希表 $\textit{cnt}$ 统计数组 $\textit{nums2}$ 中每个元素出现的次数,然后枚举数组 $\textit{nums1}$ 中的每个元素 $x$,计算 $\textit{cnt}[\textit{tot} - x]$ 的值之和即可。

在调用 $\text{add}$ 方法时,我们需要先将 $\textit{nums2}[index]$ 对应的值从 $\textit{cnt}$ 中减去 $1$,然后将 $\textit{nums2}[index]$ 的值加上 $\textit{val}$,最后将 $\textit{nums2}[index]$ 对应的值加上 $1$。

在调用 $\text{count}$ 方法时,我们只需要遍历数组 $\textit{nums1}$,对于每个元素 $x$,计算 $\textit{cnt}[\textit{tot} - x]$ 的值之和即可。

###python

class FindSumPairs:

    def __init__(self, nums1: List[int], nums2: List[int]):
        self.cnt = Counter(nums2)
        self.nums1 = nums1
        self.nums2 = nums2

    def add(self, index: int, val: int) -> None:
        self.cnt[self.nums2[index]] -= 1
        self.nums2[index] += val
        self.cnt[self.nums2[index]] += 1

    def count(self, tot: int) -> int:
        return sum(self.cnt[tot - x] for x in self.nums1)


# Your FindSumPairs object will be instantiated and called as such:
# obj = FindSumPairs(nums1, nums2)
# obj.add(index,val)
# param_2 = obj.count(tot)

###java

class FindSumPairs {
    private int[] nums1;
    private int[] nums2;
    private Map<Integer, Integer> cnt = new HashMap<>();

    public FindSumPairs(int[] nums1, int[] nums2) {
        this.nums1 = nums1;
        this.nums2 = nums2;
        for (int x : nums2) {
            cnt.merge(x, 1, Integer::sum);
        }
    }

    public void add(int index, int val) {
        cnt.merge(nums2[index], -1, Integer::sum);
        nums2[index] += val;
        cnt.merge(nums2[index], 1, Integer::sum);
    }

    public int count(int tot) {
        int ans = 0;
        for (int x : nums1) {
            ans += cnt.getOrDefault(tot - x, 0);
        }
        return ans;
    }
}

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * FindSumPairs obj = new FindSumPairs(nums1, nums2);
 * obj.add(index,val);
 * int param_2 = obj.count(tot);
 */

###cpp

class FindSumPairs {
public:
    FindSumPairs(vector<int>& nums1, vector<int>& nums2) {
        this->nums1 = nums1;
        this->nums2 = nums2;
        for (int x : nums2) {
            ++cnt[x];
        }
    }

    void add(int index, int val) {
        --cnt[nums2[index]];
        nums2[index] += val;
        ++cnt[nums2[index]];
    }

    int count(int tot) {
        int ans = 0;
        for (int x : nums1) {
            ans += cnt[tot - x];
        }
        return ans;
    }

private:
    vector<int> nums1;
    vector<int> nums2;
    unordered_map<int, int> cnt;
};

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * FindSumPairs* obj = new FindSumPairs(nums1, nums2);
 * obj->add(index,val);
 * int param_2 = obj->count(tot);
 */

###go

type FindSumPairs struct {
nums1 []int
nums2 []int
cnt   map[int]int
}

func Constructor(nums1 []int, nums2 []int) FindSumPairs {
cnt := map[int]int{}
for _, x := range nums2 {
cnt[x]++
}
return FindSumPairs{nums1, nums2, cnt}
}

func (this *FindSumPairs) Add(index int, val int) {
this.cnt[this.nums2[index]]--
this.nums2[index] += val
this.cnt[this.nums2[index]]++
}

func (this *FindSumPairs) Count(tot int) (ans int) {
for _, x := range this.nums1 {
ans += this.cnt[tot-x]
}
return
}

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * obj := Constructor(nums1, nums2);
 * obj.Add(index,val);
 * param_2 := obj.Count(tot);
 */

###ts

class FindSumPairs {
    private nums1: number[];
    private nums2: number[];
    private cnt: Map<number, number>;

    constructor(nums1: number[], nums2: number[]) {
        this.nums1 = nums1;
        this.nums2 = nums2;
        this.cnt = new Map();
        for (const x of nums2) {
            this.cnt.set(x, (this.cnt.get(x) || 0) + 1);
        }
    }

    add(index: number, val: number): void {
        const old = this.nums2[index];
        this.cnt.set(old, this.cnt.get(old)! - 1);
        this.nums2[index] += val;
        const now = this.nums2[index];
        this.cnt.set(now, (this.cnt.get(now) || 0) + 1);
    }

    count(tot: number): number {
        return this.nums1.reduce((acc, x) => acc + (this.cnt.get(tot - x) || 0), 0);
    }
}

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * var obj = new FindSumPairs(nums1, nums2)
 * obj.add(index,val)
 * var param_2 = obj.count(tot)
 */

###rust

use std::collections::HashMap;

struct FindSumPairs {
    nums1: Vec<i32>,
    nums2: Vec<i32>,
    cnt: HashMap<i32, i32>,
}

impl FindSumPairs {
    fn new(nums1: Vec<i32>, nums2: Vec<i32>) -> Self {
        let mut cnt = HashMap::new();
        for &x in &nums2 {
            *cnt.entry(x).or_insert(0) += 1;
        }
        Self { nums1, nums2, cnt }
    }

    fn add(&mut self, index: i32, val: i32) {
        let i = index as usize;
        let old_val = self.nums2[i];
        *self.cnt.entry(old_val).or_insert(0) -= 1;
        if self.cnt[&old_val] == 0 {
            self.cnt.remove(&old_val);
        }

        self.nums2[i] += val;
        let new_val = self.nums2[i];
        *self.cnt.entry(new_val).or_insert(0) += 1;
    }

    fn count(&self, tot: i32) -> i32 {
        let mut ans = 0;
        for &x in &self.nums1 {
            let target = tot - x;
            if let Some(&c) = self.cnt.get(&target) {
                ans += c;
            }
        }
        ans
    }
}

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * let mut obj = FindSumPairs::new(nums1, nums2);
 * obj.add(index, val);
 * let ret_2: i32 = obj.count(tot);
 */

###js

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 */
var FindSumPairs = function (nums1, nums2) {
    this.nums1 = nums1;
    this.nums2 = nums2;
    this.cnt = new Map();
    for (const x of nums2) {
        this.cnt.set(x, (this.cnt.get(x) || 0) + 1);
    }
};

/**
 * @param {number} index
 * @param {number} val
 * @return {void}
 */
FindSumPairs.prototype.add = function (index, val) {
    const old = this.nums2[index];
    this.cnt.set(old, this.cnt.get(old) - 1);
    this.nums2[index] += val;
    const now = this.nums2[index];
    this.cnt.set(now, (this.cnt.get(now) || 0) + 1);
};

/**
 * @param {number} tot
 * @return {number}
 */
FindSumPairs.prototype.count = function (tot) {
    return this.nums1.reduce((acc, x) => acc + (this.cnt.get(tot - x) || 0), 0);
};

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * var obj = new FindSumPairs(nums1, nums2)
 * obj.add(index,val)
 * var param_2 = obj.count(tot)
 */

###cs

public class FindSumPairs {
    private int[] nums1;
    private int[] nums2;
    private Dictionary<int, int> cnt = new Dictionary<int, int>();

    public FindSumPairs(int[] nums1, int[] nums2) {
        this.nums1 = nums1;
        this.nums2 = nums2;
        foreach (int x in nums2) {
            if (cnt.ContainsKey(x)) {
                cnt[x]++;
            } else {
                cnt[x] = 1;
            }
        }
    }

    public void Add(int index, int val) {
        int oldVal = nums2[index];
        if (cnt.TryGetValue(oldVal, out int oldCount)) {
            if (oldCount == 1) {
                cnt.Remove(oldVal);
            } else {
                cnt[oldVal] = oldCount - 1;
            }
        }
        nums2[index] += val;
        int newVal = nums2[index];
        if (cnt.TryGetValue(newVal, out int newCount)) {
            cnt[newVal] = newCount + 1;
        } else {
            cnt[newVal] = 1;
        }
    }

    public int Count(int tot) {
        int ans = 0;
        foreach (int x in nums1) {
            int target = tot - x;
            if (cnt.TryGetValue(target, out int count)) {
                ans += count;
            }
        }
        return ans;
    }
}

/**
 * Your FindSumPairs object will be instantiated and called as such:
 * FindSumPairs obj = new FindSumPairs(nums1, nums2);
 * obj.Add(index,val);
 * int param_2 = obj.Count(tot);
 */

时间复杂度 $O(n \times q)$,空间复杂度 $O(m)$。其中 $n$ 和 $m$ 分别是数组 $\textit{nums1}$ 和 $\textit{nums2}$ 的长度,而 $q$ 是调用 $\text{count}$ 方法的次数。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-找出和为指定值的下标对🟡

2025年7月6日 00:00

给你两个整数数组 nums1nums2 ,请你实现一个支持下述两类查询的数据结构:

  1. 累加 ,将一个正整数加到 nums2 中指定下标对应元素上。
  2. 计数 ,统计满足 nums1[i] + nums2[j] 等于指定值的下标对 (i, j) 数目(0 <= i < nums1.length0 <= j < nums2.length)。

实现 FindSumPairs 类:

  • FindSumPairs(int[] nums1, int[] nums2) 使用整数数组 nums1nums2 初始化 FindSumPairs 对象。
  • void add(int index, int val)val 加到 nums2[index] 上,即,执行 nums2[index] += val
  • int count(int tot) 返回满足 nums1[i] + nums2[j] == tot 的下标对 (i, j) 数目。

 

示例:

输入:
["FindSumPairs", "count", "add", "count", "count", "add", "add", "count"]
[[[1, 1, 2, 2, 2, 3], [1, 4, 5, 2, 5, 4]], [7], [3, 2], [8], [4], [0, 1], [1, 1], [7]]
输出:
[null, 8, null, 2, 1, null, null, 11]

解释:
FindSumPairs findSumPairs = new FindSumPairs([1, 1, 2, 2, 2, 3], [1, 4, 5, 2, 5, 4]);
findSumPairs.count(7);  // 返回 8 ; 下标对 (2,2), (3,2), (4,2), (2,4), (3,4), (4,4) 满足 2 + 5 = 7 ,下标对 (5,1), (5,5) 满足 3 + 4 = 7
findSumPairs.add(3, 2); // 此时 nums2 = [1,4,5,4,5,4]
findSumPairs.count(8);  // 返回 2 ;下标对 (5,2), (5,4) 满足 3 + 5 = 8
findSumPairs.count(4);  // 返回 1 ;下标对 (5,0) 满足 3 + 1 = 4
findSumPairs.add(0, 1); // 此时 nums2 = [2,4,5,4,5,4]
findSumPairs.add(1, 1); // 此时 nums2 = [2,5,5,4,5,4]
findSumPairs.count(7);  // 返回 11 ;下标对 (2,1), (2,2), (2,4), (3,1), (3,2), (3,4), (4,1), (4,2), (4,4) 满足 2 + 5 = 7 ,下标对 (5,3), (5,5) 满足 3 + 4 = 7

 

提示:

  • 1 <= nums1.length <= 1000
  • 1 <= nums2.length <= 105
  • 1 <= nums1[i] <= 109
  • 1 <= nums2[i] <= 105
  • 0 <= index < nums2.length
  • 1 <= val <= 105
  • 1 <= tot <= 109
  • 最多调用 addcount 函数各 1000

不依赖框架,如何用 JS 实现一个完整的前端路由系统

作者 zhanshuo
2025年7月5日 22:52

在这里插入图片描述

摘要

现在几乎所有前端项目都离不开“SPA”(单页应用)。它让整个网页加载体验更顺滑、内容切换更快。而这背后的关键技术之一,就是前端路由。本篇文章从原理出发,通过手写一个基础的前端路由类,配合实际页面,带你一步步理解前端路由的机制,并扩展到参数、404 页面等实战应用。

引言

传统的网页开发是“多页应用”(MPA):每次点击链接都会重新加载一个完整的 HTML 页面。但这会让体验变得卡顿、不连续。而 SPA 则只在第一次加载 HTML,后续切换都由 JavaScript 接管,动态渲染页面内容。

现代框架(React、Vue、Angular)都内置了前端路由功能,比如 react-routervue-router,但理解它们底层原理,不仅对学习框架有帮助,也能在遇到路由问题时更有思路。

手写一个最小可用的前端路由类

思路分析

核心任务:

  • 拦截浏览器地址栏变化(通过 pushState() 修改地址)
  • 监听前进/后退(通过 popstate 事件)
  • 根据路径匹配回调,渲染页面内容

实现路由类:代码 + 注释

class Router {
    constructor() {
        this.routes = {}; // 用于存储 path -> callback 的映射
        this.notFound = null;

        // 监听浏览器前进/后退事件
        window.addEventListener('popstate', this.handlePopState.bind(this));
    }

    // 注册路径和对应的回调函数
    addRoute(path, callback) {
        this.routes[path] = callback;
    }

    // 注册404页面
    setNotFound(callback) {
        this.notFound = callback;
    }

    // 跳转到指定路径
    navigate(path) {
        history.pushState({}, '', path); // 修改地址栏,但不刷新页面
        this.handlePopState(); // 手动触发一次路由处理
    }

    // 处理路径变化:找到并执行对应回调
    handlePopState() {
        const path = window.location.pathname;

        // 如果路径存在,则执行对应的渲染函数
        if (this.routes[path]) {
            this.routes[path]();
        } else if (this.notFound) {
            this.notFound();
        } else {
            console.warn(`路径不存在: ${path}`);
        }
    }
}

实战:页面导航和内容渲染

页面HTML结构

<div id="nav">
    <a href="/home" onclick="goTo('/home')">首页</a>
    <a href="/about" onclick="goTo('/about')">关于我们</a>
    <a href="/contact" onclick="goTo('/contact')">联系我们</a>
    <a href="/notfound" onclick="goTo('/notfound')">未知页面</a>
</div>
<hr />
<div id="content">这里是默认内容</div>

配置路由和回调

const router = new Router();

// 注册路由
router.addRoute('/home', () => {
    document.getElementById('content').innerHTML = '<h2>欢迎来到首页</h2>';
});

router.addRoute('/about', () => {
    document.getElementById('content').innerHTML = '<h2>我们是一个前端开发团队</h2>';
});

router.addRoute('/contact', () => {
    document.getElementById('content').innerHTML = '<h2>请通过邮箱联系我</h2>';
});

// 设置 404 页面
router.setNotFound(() => {
    document.getElementById('content').innerHTML = '<h2>404 页面未找到</h2>';
});

// 点击链接时跳转
function goTo(path) {
    event.preventDefault(); // 阻止 a 标签默认行为
    router.navigate(path);
}

// 页面刷新时保持当前路由内容
window.addEventListener('DOMContentLoaded', () => {
    router.handlePopState();
});

高阶功能拓展

场景一:支持路径参数(比如 /user/123

我们改造 Router 类,让它支持参数路由:

class Router {
    constructor() {
        this.routes = [];
    }

    addRoute(path, callback) {
        const paramNames = [];
        const regex = path.replace(/:([^\/]+)/g, (_, key) => {
            paramNames.push(key);
            return '([^\/]+)';
        });

        const pattern = new RegExp(`^${regex}$`);
        this.routes.push({ pattern, paramNames, callback });
    }

    navigate(path) {
        history.pushState({}, '', path);
        this.handlePopState();
    }

    handlePopState() {
        const path = window.location.pathname;

        for (const route of this.routes) {
            const match = route.pattern.exec(path);
            if (match) {
                const params = {};
                route.paramNames.forEach((key, i) => {
                    params[key] = match[i + 1];
                });
                route.callback(params);
                return;
            }
        }

        document.getElementById('content').innerHTML = '<h2>404 Not Found</h2>';
    }
}

使用方式如下:

const router = new Router();

router.addRoute('/user/:id', ({ id }) => {
    document.getElementById('content').innerHTML = `<h2>当前用户ID:${id}</h2>`;
});

现在访问 /user/123 会显示 当前用户ID:123

QA 问答环节

Q1:前端路由和后端路由有什么区别?

前端路由是在浏览器端用 JavaScript 控制地址栏和页面内容;后端路由是在服务器上解析 URL,返回不同页面或数据。

Q2:为什么刷新页面会 404?

因为浏览器发起了真实的 HTTP 请求,访问的是 /about 路径,而服务器没有设置“返回 index.html”,所以报错。你需要在服务器配置中添加“任意路径都返回 index.html”。

Q3:为什么不直接用 hash (#) 路由?

早期前端路由是靠 window.location.hash 实现的(比如 /#/home),兼容性好,但不好看也不能完全模拟真实路径。pushState 更现代、真实,还能配合服务端渲染。

总结

我们从最简单的原理出发,手写了一个可以运行的前端路由系统,并支持:

  • 多路径匹配
  • 浏览器前进/后退按钮
  • 404 页面
  • 参数化路径(如 /user/:id

这个 Router 类虽然功能简单,但已经足够支撑一个轻量级 SPA 应用的页面跳转。如果你刚入门前端,强烈建议自己动手实现一遍,加深理解。

如果你对嵌套路由、懒加载路由组件、路由守卫等更高级玩法感兴趣,也欢迎告诉我,我可以继续带你深入挖掘。

webgl2 方法解析: getContext()

2025年7月5日 22:50

在 WebGL2 中,getContext() 方法用于获取一个 WebGL2RenderingContext 对象,它是 WebGL2 的核心接口,提供了 OpenGL ES 3.0 的渲染上下文。

获取 WebGL2 上下文

要获取 WebGL2 上下文,需要在 HTML 的 <canvas> 元素上调用 getContext() 方法,并传入 "webgl2" 作为参数:

const canvas = document.getElementById("myCanvas");
const gl = canvas.getContext("webgl2");

如果浏览器不支持 WebGL2,getContext("webgl2") 将返回 null

上下文属性

在创建 WebGL2 上下文时,还可以通过第二个参数传递一个上下文属性对象来配置上下文的行为。例如:

const gl = canvas.getContext("webgl2", {
  alpha: false, // 禁用 alpha 通道
  depth: true, // 启用深度缓冲区
  antialias: true, // 启用抗锯齿
});

常见的上下文属性包括:

  • alpha:是否启用 alpha 通道。
  • depth:是否启用深度缓冲区。
  • stencil:是否启用模板缓冲区。
  • antialias:是否启用抗锯齿。
  • premultipliedAlpha:是否使用预乘 alpha。

兼容性

WebGL2 是 WebGL 的扩展版本,基于 OpenGL ES 3.0。它在 WebGL 1 的基础上增加了许多新功能,例如更复杂的着色器支持、更高效的缓冲区操作等。目前,WebGL2 在大多数现代浏览器中得到了支持,但一些旧版本的浏览器(如 IE 和部分旧版本的 Safari)可能不支持。

示例

以下是一个完整的示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebGL2 Uniform Buffer Example</title>
  <style>
    canvas {
      width: 800px;
      height: 600px;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script>
    // 获取 WebGL2 上下文
    const canvas = document.getElementById('canvas');
    const gl = canvas.getContext('webgl2');
    if (!gl) {
      throw new Error('WebGL2 is not supported by your browser.');
    }

    // 顶点着色器代码
    const vertexShaderSource = `#version 300 es
      in vec4 a_position;
      void main() {
        gl_Position = a_position;
      }
    `;

    // 片段着色器代码
    const fragmentShaderSource = `#version 300 es
      precision highp float;
      out vec4 outColor;
      void main() {
        outColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    `;

    // 创建着色器
    function createShader(gl, type, source) {
      const shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('Shader compile failed with:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
      }
      return shader;
    }

    // 创建程序
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      throw new Error('link falsed! ');
    }
    gl.useProgram(program);

    // 创建顶点缓冲区
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = new Float32Array([
      -0.5, -0.5, 0.0,
       0.5, -0.5, 0.0,
       0.0,  0.5, 0.0
    ]);
    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

    // 设置顶点属性指针
    const positionLocation = gl.getAttribLocation(program, 'a_position');
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

    // 渲染循环
    function render() {
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT);

      gl.drawArrays(gl.TRIANGLES, 0, 3);

      requestAnimationFrame(render);
    }

    render();
  </script>
</body>
</html>

智能前端新纪元:语音交互技术与安全实践全解析

2025年7月5日 22:25

引言:AI时代的人机交互革命

在人工智能技术迅猛发展的今天,语音交互正成为前端开发领域最具颠覆性的创新之一。根据Gartner最新报告,到2025年,全球将有超过50%的企业应用集成语音交互功能,这一数字较2020年增长了近300%。本文将从技术实现、安全防护到最佳实践,全面解析智能前端中的语音交互技术。

一、语音交互:下一代用户体验的核心

1.1 从文本到语音的AIGC革命

现代语音合成技术已从机械的TTS(Text-To-Speech)进化为富有表现力的AIGC(AI Generated Content)系统。以火山引擎的语音合成API为例,其支持:

  • 多情感声线:如"zh_male_sunwukong_mars_bigtts"(悟空语音)等数十种风格
  • 精细参数控制:语速(speed_ratio)、音量(volume_ratio)、音调(pitch_ratio)的精确调节
  • 专业音频编码:支持ogg_opus等高效音频格式,压缩率(compression_rate)可配置
// 现代语音合成请求体示例
const payload = {
  audio: {
    voice_type: "zh_female_emotional",
    encoding: "ogg_opus",
    rate: 48000,  // 采样率提升至48kHz
    emotion: "professional"  // 专业场景语气
  },
  request: {
    text: "尊敬的客户,您的业务已受理",
    text_type: "ssml"  // 支持语音合成标记语言
  }
};

1.2 三维交互体验设计原则

  1. 状态驱动UI:采用React状态机模式管理交互流程
const [interactionState, setState] = useState({
  phase: 'idle', // idle/listening/processing/playing
  confidence: 0.9, // 语音识别置信度
  feedback: '' // 用户反馈
});
  1. 渐进增强策略:优先核心功能,逐步加载语音模块
const loadVoiceSDK = async () => {
  if ('speechSynthesis' in window) {
    const module = await import('./voiceService');
    return module.init();
  }
  return null;
};

二、企业级安全防护体系

2.1 敏感信息全生命周期管理

安全层级 防护措施 实施示例
开发环境 .gitignore规范 排除.env、.key等53类文件
构建阶段 环境变量加密 使用vite环境变量前缀(VITE_)
运行时 动态令牌 JWT短期访问令牌(exp:3600s)

高级.gitignore配置

# 敏感文件
.env*
*.cert
*.pem

# 运行时生成
/dist
/node_modules

# IDE特定
.idea/
.vscode/

2.2 零信任架构实践

  1. 动态密钥获取
const fetchTempToken = async () => {
  const res = await fetch('/api/auth/temp-token', {
    credentials: 'omit'  // 避免携带主凭据
  });
  return res.json();
};
  1. 请求签名机制
const signRequest = (payload) => {
  const nonce = crypto.randomUUID();
  const timestamp = Date.now();
  const signature = crypto.subtle.digest(
    'SHA-256', 
    `${nonce}${timestamp}${secret}`
  );
  return { ...payload, nonce, timestamp, signature };
};

三、工程化最佳实践

3.1 性能优化矩阵

优化维度 传统方案 现代方案
音频加载 完整下载 流式传输(HTTP/3 QUIC)
语音缓存 本地存储 IndexedDB + LRU算法
降级策略 静默失败 智能回退(语音->文字)

流式音频处理

const processStream = async (response) => {
  const reader = response.body.getReader();
  const mediaSource = new MediaSource();
  audioRef.current.src = URL.createObjectURL(mediaSource);
  
  mediaSource.addEventListener('sourceopen', () => {
    const sourceBuffer = mediaSource.addSourceBuffer('audio/ogg; codecs=opus');
    const pushChunk = ({ done, value }) => {
      if (done) return mediaSource.endOfStream();
      sourceBuffer.appendBuffer(value);
      return reader.read().then(pushChunk);
    };
    reader.read().then(pushChunk);
  });
};

3.2 可观测性建设

  1. 全链路监控
const trackInteraction = (metrics) => {
  navigator.sendBeacon('/analytics', {
    ...metrics,
    deviceInfo: {
      memory: navigator.deviceMemory,
      cores: navigator.hardwareConcurrency
    }
  });
};
  1. 语音质量评估
const evaluateQuality = (audioContext) => {
  const analyser = audioContext.createAnalyser();
  const dataArray = new Uint8Array(analyser.frequencyBinCount);
  analyser.getByteFrequencyData(dataArray);
  
  return {
    clarity: calculateSNR(dataArray),
    volume: calculateRMS(dataArray)
  };
};

四、前沿趋势与商业价值

  1. 多模态交互:结合WebXR实现3D虚拟数字人对话
  2. 边缘计算:使用WebAssembly加速本地语音处理
  3. 情感计算:通过Web Audio API分析用户语音情绪

某金融科技公司案例显示,集成智能语音前端后:

  • 客户服务效率提升40%
  • 用户满意度提高28%
  • 安全事件减少65%

结语:构建面向未来的语音交互体系

随着Web Speech API的普及和W3C语音交互标准的演进,前端开发者正站在人机交互革命的前沿。通过本文介绍的技术方案和安全实践,企业可以构建既智能又可靠的语音交互系统。记住,优秀的语音体验=70%的技术实现+20%的心理模型+10%的魔法时刻。

"语音不是功能的替代,而是体验的升维。" —— Google UX首席设计师Sarah Lee

基于 Vue3实现一款简历生成工具

作者 khalil
2025年7月5日 21:58

本文介绍如何从零开始构建一个基于 Vue3 + Markdown 的在线简历生成工具,支持实时编辑预览、模板切换、自定义样式配置以及导出为 PDF。

项目介绍

之前在做个人简历的时候,发现目前一些工具网站上使用起来不太方便,于是打算动手简单实现一个在线的简历工具网站,主要支持以下功能:

  • 支持以markdown格式输入,渲染成简历内容
  • 多模板切换
  • 样式调整
  • 上传导出功能

体验地址: hj-hao.github.io/md2cv/ 屏幕截图 2025-07-02 220547.png

技术选型

项目整体技术栈如下:

功能实现

接下来简单介绍下具体的功能实现

Markdown解析&渲染

首先要处理的就是对输入Markdown的解析。由于需要将内容渲染在内置的模板简历中,这里就只需要MD -> HTML的能力,因此选用了Markdown-it进行实现。拿到html字符串后在vue中直接渲染即可。

<template>
    <div v-html="result"></div>
</template>

<script setup>
import { ref, computed } from 'vue'
import markdownit from 'markdown-it'
const md = markdownit({
    html: true,
})
const input = ref('')
const result = computed(() => md.render(input))
</script>

上面这段简易的代码就能支持将用户输入文本,转换成html了。
在这个基础上如果希望增加一些前置元数据的配置,类似在Vitepress中我们可以在MD前用YAML语法编写一些配置。可以使用gray-matter这个库,能通过分割符将识别解析文本字符串中的YAML格式信息。

此处用官方的例子直接展示用法, 可以看到其将输入中的YAML部分转换为对象返回,而其余部分则保持输入直接输出。

console.log(matter('---\ntitle: Front Matter\n---\nThis is content.'));

// 输出
{
  content: '\nThis is content.',
  data: {
    title: 'Front Matter'
  }
}

在这个项目中,就通过这个库将简历个人信息(YAML)和简历正本部分(MD)整合在同一个输入框中编辑了,具体的实现如下:

<template>
    <div v-html="result.content"></div>
</template>

<script setup>
import { ref, computed } from 'vue'
import matter from 'gray-matter'
import markdownit from 'markdown-it'
const md = markdownit({
    html: true,
})
const input = ref('')
const result = computed(() => {
    // 解析yaml
    const { data, content } = matter(input.value)
    return {
        data,
        content: md.render(content),
    }
})
</script>

模板功能

模板实现

之后是将上面解析后的内容渲染到简历模板上,以及可以在不同模板间直接切换实时渲染出对应的效果。

实现上每个模板都是一个单独的组件,UI由两部分组件一个是简历模板个人信息以及正文部分,除组件部分外还有模板相关的配置项跟随组件需要导出,因此这里选用JSX/TSX实现简历模板组件。构造一个基础的组件封装公共部分逻辑, 模板间的UI差异通过slot实现

import '@/style/templates/baseTemplate.css'
import { defineComponent } from 'vue'
import { storeToRefs } from 'pinia'
import { useStyleConfigStore } from '@/store/styleConfig'

// base component to reuse in other cv templates
export default defineComponent({
    name: 'BaseTemplate',
    props: {
        content: {
            type: String,
            default: '',
        },
        page: {
            type: Number,
            default: 1,
        },
        className: {
            type: String,
            default: '',
        },
    },
    setup(props, { slots }) {
        // 可支持配置的样式,在基础模板中通过注入css变量让子元素访问
        const { pagePadding, fontSize } = storeToRefs(useStyleConfigStore())
        return () => (
            <div
                class="page flex flex-col"
                style={{
                    '--page-padding': pagePadding.value + 'px',
                    '--page-font-size': fontSize.value + 'px',
                }}
            >
                {/** 渲染不同模板对应的信息模块 */}
                {props.page === 1 && (slots.header ? slots.header() : '')}
                {/** 简历正文部分 */}
                <div
                    class={`${props.className} template-content`}
                    innerHTML={props.content}
                ></div>
            </div>
        )
    },
})

其余模板组件在上面组件的基础上继续扩展,下面是其中一个组件示例

import { defineComponent, computed, type PropType } from 'vue'
import BaseTemplate from '../BaseTemplate'
import ResumeAvatar from '@/components/ResumeAvatar.vue'
import { A4_PAGE_SIZE } from '@/constants'
import '@/style/templates/simpleTemplate.css'

const defaultConfig = {
    name: 'Your Name',
    blog: 'https://yourblog.com',
    phone: '123-456-7890',
    location: 'Your Location',
}

// 模板名(组件名称)
export const name = 'SimpleTemplate'
// 模板样式 类名
const className = 'simple-template-content-box'

// 模板每页的最大高度,用于分页计算
export const getCurrentPageHeight = (page: number) => {
    if (page === 1) {
        return A4_PAGE_SIZE - 130
    }
    return A4_PAGE_SIZE
}

export default defineComponent({
    name: 'SimpleTemplate',
    components: {
        BaseTemplate,
        ResumeAvatar,
    },
    props: {
        config: {
            type: Object as PropType<{ [key: string]: any }>,
            default: () => ({ ...defaultConfig }),
        },
        content: {
            type: String,
            default: '',
        },
        page: {
            type: Number,
            default: 1,
        },
    },
    setup(props) {
        const config = computed(() => {
            return { ...defaultConfig, ...props.config }
        })
        const slots = {
            header: () => (
                <div class="flex relative gap-2.5 mb-2.5 items-center">
                    <div class="flex flex-col flex-1 gap-2">
                        <div class="text-3xl font-bold">
                            {config.value.name}
                        </div>
                        <div class="flex items-center text-sm">
                            <div class="text-gray-500 not-last:after:content-['|'] after:m-1.5">
                                <span>Blog:</span>
                                <a
                                    href="javascript:void(0)"
                                    target="_blank"
                                    rel="noopener noreferrer"
                                >
                                    {config.value.blog}
                                </a>
                            </div>
                            <div class="text-gray-500 not-last:after:content-['|'] after:m-1.5">
                                <span>Phone:</span>
                                {config.value.phone}
                            </div>
                            <div class="text-gray-500 not-last:after:content-['|'] after:m-1.5">
                                <span>Location:</span>
                                {config.value.location}
                            </div>
                        </div>
                    </div>
                    <ResumeAvatar />
                </div>
            ),
        }
        return () => (
            <BaseTemplate
                v-slots={slots}
                page={props.page}
                content={props.content}
                className={className}
            />
        )
    },
})
/** @/style/templates/simpleTemplate.css */
.simple-template-content-box {
    h1 {
        font-size: calc(var(--page-font-size) * 1.4);
        font-weight: bold;
        border-bottom: 2px solid var(--color-zinc-800);
        margin-bottom: 0.5em;
    }


    h2 {
        font-weight: bold;
        margin-bottom: 0.5em;
    }
}

模板加载

完成不同模板组件后,项目需要能自动将这些组件加载到项目中,并将对应的组件信息注入全局。通过Vite提供的import.meta.glob可以在文件系统匹配导入对应的文件,实现一个Vue插件,就能在Vue挂载前加载对应目录下的组件,并通过provide注入。完整代码如下

// plugins/templateLoader.ts
import type { App, Component } from 'vue'

export type TemplateMeta = {
    name: string
    component: Component
    getCurrentPageHeight: (page: number) => number
}

export const TemplateProvideKey = 'Templates'

const templateLoaderPlugin = {
    install(app: App) {
        const componentModules = import.meta.glob(
            '../components/templates/**/index.tsx',
            { eager: true }
        )
        const templates: Record<string, TemplateMeta> = {}

        const getTemplateName = (path: string) => {
            const match = path.match(/templates\/([^/]+)\//)
            return match ? match?.[1] : null
        }

        // path => component Name
        for (const path in componentModules) {
            // eg: ../components/templates/simple/index.vue => simple
            const name = getTemplateName(path)
            if (name) {
                const config = (componentModules as any)[path]
                templates[name] = {
                    component: config.default,
                    name: config.name || name,
                    getCurrentPageHeight: config.getCurrentPageHeight,
                } as TemplateMeta
            }
        }

        app.provide(TemplateProvideKey, templates)
    },
}

export default templateLoaderPlugin

预览分页

有了对应的组件和内容后,就能在页面中将简历渲染出来了。但目前还存在一个问题,如果内容超长了需要分页不能直接体现用户,仅能在导出预览时候进行分页。需要补充上分页的能力,将渲染的效果和导出预览的效果对齐。

整体思路是先将组件渲染在不可见的区域,之后读取对应的dom节点,计算每个子元素的高度和,超过后当前内容最大高度后,新建一页。最后返回每页对应的html字符串,循环模板组件进行渲染。具体代码如下:

import { computed, onMounted, ref, watch, nextTick, type Ref } from 'vue'
import { useTemplateStore } from '@/store/template'
import { useStyleConfigStore } from '@/store/styleConfig'
import { useMarkdownStore } from '@/store/markdown'
import { storeToRefs } from 'pinia'

export const useSlicePage = (target: Ref<HTMLElement | null>) => {
    const { currentConfig, currentTemplate } = storeToRefs(useTemplateStore())
    const { pagePadding, fontSize } = storeToRefs(useStyleConfigStore())

    const { result } = storeToRefs(useMarkdownStore())
    const pages = ref<Element[]>()
    
    // 每页渲染的html字符串
    const renderList = computed(() => {
        return pages.value?.map((el) => el.innerHTML)
    })

    const pageSize = computed(() => pages.value?.length || 1)
    
    // 获取当前模板的内容高度,减去边距
    const getCurrentPageHeight = (page: number) => {
        return (
            currentConfig.value.getCurrentPageHeight(page) -
            pagePadding.value * 2
        )
    }

    const createPage = (children: HTMLElement[] = []) => {
        const page = document.createElement('div')
        children.forEach((item) => {
            page.appendChild(item)
        })
        return page
    }

    // getBoundingClientRect 只返回元素的宽度 需要getComputedStyle获取边距
    // 由于元素上下边距合并的特性,此处仅考虑下边距,上边距通过样式限制为0
    const getElementHeightWithBottomMargin = (el: HTMLElement): number => {
        const style = getComputedStyle(el)
        const marginBottom = parseFloat(style.marginBottom || '0')
        const height = el.getBoundingClientRect().height
        return height + marginBottom
    }

    const sliceElement = (element: Element): Element[] => {
        const children = Array.from(element.children)
        let currentPage = 1
        let currentPageElement = createPage()
        
        // 当前页面可渲染的高度
        let PageSize = getCurrentPageHeight(currentPage)
        // 剩余可渲染高度
        let resetPageHeight = PageSize 
        // 页面dom数组
        const pages = [currentPageElement]
 

        while (children.length > 0) {
            const el = children.shift() as HTMLElement

            const height = getElementHeightWithBottomMargin(el)

            // 大于整页高度,如果包含子节点就直接分隔
            // 无子节点直接放入当页,然后创建新页面
            if (height > PageSize) {
                const subChildren = Array.from(el.children)
                if (subChildren.length > 0) {
                    children.unshift(...subChildren)
                } else {
                    pages.push(
                        createPage([el.cloneNode(true)] as HTMLElement[])
                    ) // Create a new page for the oversized element
                    currentPage += 1
                    PageSize = getCurrentPageHeight(currentPage)
                    resetPageHeight = PageSize
                    currentPageElement = createPage()
                    pages.push(currentPageElement) // Push the new page to the pages array
                }

                continue // Skip to the next element
            }
            
            // 针对高度大于300的元素且包含子元素的节点进行分隔
            // 无子元素或高度小于300直接创建新页面放入
            if (height > resetPageHeight && height > 300) {
                const subChildren = Array.from(el.children)
                if (subChildren.length > 0) {
                    children.unshift(...subChildren)
                } else {
                    currentPageElement = createPage([
                        el.cloneNode(true),
                    ] as HTMLElement[]) // Create a new page
                    currentPage += 1
                    PageSize = getCurrentPageHeight(currentPage)
                    resetPageHeight = PageSize - height
                    pages.push(currentPageElement) // Push the new page to the pages array
                }
            } else if (height > resetPageHeight && height <= 300) {
                currentPageElement = createPage([
                    el.cloneNode(true),
                ] as HTMLElement[]) // Create a new page
                currentPage += 1
                PageSize = getCurrentPageHeight(currentPage)
                resetPageHeight = PageSize - height
                pages.push(currentPageElement) // Push the new page to the pages array
            } else {
                currentPageElement.appendChild(
                    el.cloneNode(true) as HTMLElement
                )
                resetPageHeight -= height
            }
        }

        return pages
    }

    const getSlicePage = () => {
        const targetElement = target.value?.querySelector(`.template-content`)
        const newPages = sliceElement(targetElement!)
        pages.value = newPages
    }

    watch(
        () => [
            result.value,
            currentTemplate.value,
            pagePadding.value,
            fontSize.value,
        ],
        () => {
            nextTick(() => {
                getSlicePage()
            })
        }
    )

    onMounted(() => {
        nextTick(() => {
            getSlicePage()
        })
    })

    return {
        getSlicePage,
        pages,
        pageSize,
        renderList,
    }
}
<!-- 实际展示容器 -->
<div
    class="bg-white dark:bg-surface-800 rounded-lg shadow-md overflow-auto"
    ref="previewRef"
>
    <component
        v-for="(content, index) in renderList"
        :key="index"
        :is="currentComponent"
        :config="result.data"
        :content="content"
        :page="index + 1"
    />
</div>

<!-- 隐藏的容器 -->
<div ref="renderRef" class="render-area">
    <component
        :is="currentComponent"
        :config="result.data"
        :content="result.content"
    />
</div>
<script setup>
// 省略其他代码
const renderRef = ref<HTMLElement | null>(null)
const previewRef = ref<HTMLElement | null>(null)

const mdStore = useMarkdownStore()
const templateStore = useTemplateStore()

const { result, input } = storeToRefs(mdStore)
const { currentComponent } = storeToRefs(templateStore)
const { renderList } = useSlicePage(renderRef)
</script>

上面的代码目前还存在一些边界场景分页问题比如:

  • 一个仅包含文本的P或者DIV节点,目前这个节点不会被分割,而是整体处理,导致可能会出现一个高度刚好超过剩余高度的节点被放置在下一页造成大块的空白
  • 分割的阈值设置的比较大,而且没有针对一些特殊元素(ol, table...)做判断处理

最后

项目Github:github.com/HJ-Hao/md2c… 有其他想法也欢迎交流

后续计划

后续有时间会继续完善下项目的功能

  • 增加AI润色的功能
  • 优化分页逻辑
  • 增加更多模板
  • ...

TypeScript 系统入门到项目实战-慕课网

2025年7月5日 20:34

微信图片_20250704095940.jpg

TypeScript 系统入门到项目实战-慕课网-----97it.-----top/------469/

TypeScript 作为 JavaScript 的超集,凭借其强大的类型系统,已经成为现代前端开发的标配语言。除了基础类型(如 stringnumberboolean)外,TypeScript 还提供了更复杂、更灵活的类型机制,帮助开发者编写出更具健壮性和可维护性的代码。

本文将深入探讨 TypeScript 中三种关键的高级类型概念:联合类型(Union Types)交叉类型(Intersection Types)泛型编程(Generic Programming) ,并结合实际案例分析它们的使用场景和底层原理。


一、联合类型(Union Types)

1.1 概念与语法

联合类型表示一个值可以是多个类型中的一种。使用 | 符号来分隔不同的类型。

Ts
深色版本
let value: string | number;
value = "hello"; // 合法
value = 42;      // 合法

1.2 使用场景

  • 函数参数的多种输入类型
  • API 返回值不确定时
  • 状态字段可能有多种类型
Ts
深色版本
function printId(id: number | string) {
    console.log("ID is: " + id);
}
printId(100);     // OK
printId("abc");   // OK

1.3 类型收窄(Type Narrowing)

由于联合类型在运行时无法直接知道具体是哪个类型,因此需要通过“类型守卫”进行判断:

Ts
深色版本
function padLeft(value: string | number, padding: string | number) {
    if (typeof value === "number" && typeof padding === "number") {
        return value + padding;
    } else if (typeof value === "string" && typeof padding === "string") {
        return padding + value;
    }
    throw new Error("Invalid arguments");
}

TypeScript 支持多种方式做类型收窄:

  • typeof
  • instanceof
  • 自定义类型谓词函数(如 function isString(x): x is string

二、交叉类型(Intersection Types)

2.1 概念与语法

交叉类型表示一个值必须同时满足多个类型的特征。使用 & 符号组合多个类型。

Ts
深色版本
type Person = { name: string };
type Employee = { id: number };

type Staff = Person & Employee;

const staff: Staff = {
    name: "Alice",
    id: 101
};

2.2 使用场景

  • 混入多个接口功能
  • 组合不同模块的功能对象
  • 增强已有类型的属性

例如,在 React 开发中,经常用交叉类型扩展组件 props:

Ts
深色版本
type WithLoading = { isLoading: boolean };
type WithError = { error: string | null };

type AppProps = WithLoading & WithError & {
    title: string;
};

2.3 注意事项

  • 如果两个类型中有相同属性但类型不同,TS 会报错。
Ts
深色版本
type A = { prop: string };
type B = { prop: number };
type C = A & B; // 报错:'prop' has conflicting types
  • 在合并函数类型时,返回类型也是联合类型。
Ts
深色版本
type FnA = () => string;
type FnB = () => number;
type FnC = FnA & FnB; // 返回类型为 string | number

三、泛型编程(Generic Programming)

3.1 概念与语法

泛型允许我们编写与类型无关的代码,从而实现复用性更强、类型安全的函数和类。

Ts
深色版本
function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("hello");  // 推断为 string
let output2 = identity<number>(42);       // 推断为 number

TypeScript 能够根据传入的参数自动推导类型:

Ts
深色版本
let output3 = identity(true); // 推断为 boolean

3.2 泛型约束(Generic Constraints)

默认情况下,泛型变量 T 可以是任意类型,但有时我们需要限制它的范围。

Ts
深色版本
interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
    console.log(arg.length);
}

logLength("hello");         // OK
logLength([1, 2, 3]);       // OK
logLength({ length: 5 });   // OK
logLength(42);              // ❌ 报错:number 上没有 length 属性

3.3 泛型类与泛型接口

泛型不仅适用于函数,也可以用于类和接口:

Ts
深色版本
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

四、综合实战:构建一个类型安全的请求封装器

我们将结合联合类型、交叉类型与泛型,构建一个通用的 HTTP 请求封装器。

4.1 定义响应类型

Ts
深色版本
type SuccessResponse<T> = {
    success: true;
    data: T;
};

type ErrorResponse = {
    success: false;
    error: string;
};

type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

4.2 封装泛型请求函数

Ts
深色版本
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            return { success: false, error: 'Network error' };
        }
        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

4.3 使用示例

Ts
深色版本
interface User {
    id: number;
    name: string;
}

fetchApi<User>("/api/user")
    .then(res => {
        if (res.success) {
            console.log(res.data.name); // 安全访问
        } else {
            console.error(res.error);
        }
    });

五、总结

TypeScript 的类型系统远不止于基础类型检查,它通过联合类型交叉类型泛型编程等机制,赋予了开发者极大的灵活性与类型安全性。

类型机制 特点 应用场景
联合类型 (` `) 表示“或”的关系
交叉类型 (&) 表示“与”的关系 组合多个接口或类型
泛型 (<T>) 实现类型抽象 构建可复用、类型安全的函数/类

掌握这些高级类型技巧,不仅能提升代码质量,还能帮助你写出更具表达力和扩展性的程序结构。对于希望深入 TypeScript 编程的开发者来说,这是一条必经之路。

[已完结]后端开发必备高阶技能--自研企业级网关组件(Netty+Nacos+Disruptor)

2025年7月5日 20:09

微信图片_20250704095940.jpg

[已完结]后端开发必备高阶技能--自研企业级网关组件(Netty+Nacos+Disruptor)-------97it.------top/------2193/

解构API网关核心架构:Netty高性能通信、Nacos服务发现与Disruptor并发优化实践

引言:现代API网关的技术挑战

在微服务架构成为主流的今天,API网关作为系统流量的"中枢神经",面临着高并发低延迟高可用的严苛要求。本文将深入剖析基于Netty、Nacos和Disruptor三大核心技术构建的高性能API网关实现方案,揭示工业级网关的核心设计哲学与优化技巧。

一、Netty高性能通信架构

1.1 Reactor线程模型优化

定制化线程组配置

EventLoopGroup bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("gateway-boss"));
EventLoopGroup workerGroup = new NioEventLoopGroup(
    Runtime.getRuntime().availableProcessors() * 2,
    new DefaultThreadFactory("gateway-worker"),
    SelectorProvider.provider()
);

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 .childOption(ChannelOption.TCP_NODELAY, true)
 .childOption(ChannelOption.SO_KEEPALIVE, true)
 .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

关键参数调优对比

参数 默认值 优化值 性能提升
SO_BACKLOG 50 1024 23%
WRITE_BUFFER_WATER_MARK 32KB/64KB 4MB/8MB 18%
ALLOCATOR Unpooled Pooled 35%

1.2 零拷贝与内存管理

复合缓冲区使用示例

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf header = Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n", CharsetUtil.UTF_8);
    ByteBuf body = ((ByteBuf) msg).retain();
    
    // 零拷贝合并
    CompositeByteBuf composite = Unpooled.compositeBuffer();
    composite.addComponents(true, header, body);
    
    ctx.writeAndFlush(composite).addListener(ChannelFutureListener.CLOSE);
}

二、Nacos动态服务发现

2.1 服务注册与发现机制

服务心跳配置

# application.yml
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        heartbeat-interval: 5000ms
        heart-beat-timeout: 15000ms
        ip: ${SERVER_IP}
        port: ${SERVER_PORT}
        namespace: ${NAMESPACE}

权重动态调整算法

public Instance selectInstance(List<Instance> instances) {
    double maxScore = 0;
    Instance selected = null;
    
    for (Instance instance : instances) {
        double loadScore = 1 - (instance.getCurrentLoad() / 100.0);
        double healthScore = instance.isHealthy() ? 1 : 0.2;
        double weight = instance.getWeight() * loadScore * healthScore;
        
        if (weight > maxScore) {
            maxScore = weight;
            selected = instance;
        }
    }
    return selected;
}

2.2 集群容灾策略

多级故障转移设计

graph TD
    A[客户端] -->|Primary| B[Nacos集群A]
    A -->|Secondary| C[Nacos集群B]
    A -->|Tertiary| D[本地缓存]
    B -->|同步| C

三、Disruptor高并发处理

3.1 事件驱动架构设计

网关事件定义

public class ApiEvent {
    private ChannelHandlerContext ctx;
    private FullHttpRequest request;
    private long receiveTime;
    private HttpHeaders headers;
    // getters/setters...
}

RingBuffer初始化

Disruptor<ApiEvent> disruptor = new Disruptor<>(
    ApiEvent::new,
    1024 * 1024,  // RingBuffer大小
    DaemonThreadFactory.INSTANCE,
    ProducerType.MULTI,  // 多生产者
    new BlockingWaitStrategy()
);

3.2 性能对比测试

队列实现 吞吐量(ops/ms) 99%延迟(ms) CPU占用
LinkedBlockingQueue 12,000 45 78%
ArrayBlockingQueue 15,000 38 72%
Disruptor 280,000 3 65%

四、全链路优化实践

4.1 请求处理流水线

sequenceDiagram
    participant Client
    participant Netty
    participant Disruptor
    participant Worker
    participant Nacos
    participant Backend
    
    Client->>Netty: HTTP请求
    Netty->>Disruptor: 发布事件
    Disruptor->>Worker: 消费事件
    Worker->>Nacos: 服务发现
    Nacos-->>Worker: 实例列表
    Worker->>Backend: 转发请求
    Backend-->>Worker: 响应结果
    Worker->>Netty: 写回响应
    Netty->>Client: HTTP响应

4.2 关键性能指标

压力测试结果

场景 QPS 平均延迟 错误率
健康检查 120,000 8ms 0%
商品查询 85,000 15ms 0.01%
订单创建 62,000 22ms 0.05%
支付处理 45,000 35ms 0.1%

五、异常处理与容错

5.1 熔断降级策略

基于滑动窗口的熔断器

public class CircuitBreaker {
    private final int failureThreshold;
    private final long resetTimeout;
    private final CircularBuffer<Boolean> window;
    
    public boolean allowRequest() {
        if (window.count(false) >= failureThreshold) {
            return System.currentTimeMillis() - lastFailure > resetTimeout;
        }
        return true;
    }
    
    public void recordFailure() {
        window.add(false);
        lastFailure = System.currentTimeMillis();
    }
}

5.2 全链路重试机制

分级重试策略

# 重试配置
retry:
  levels:
    - codes: [502,503]
      attempts: 3
      delay: 100ms
      backoff: 1.5
    - codes: [504]
      attempts: 2
      delay: 500ms
    - codes: [500]
      attempts: 1

六、生产环境部署方案

6.1 Kubernetes部署配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
spec:
  replicas: 3
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      containers:
      - name: gateway
        image: registry.example.com/gateway:1.5.0
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: "2"
            memory: "2Gi"
          requests:
            cpu: "1"
            memory: "1Gi"
        env:
        - name: NACOS_SERVERS
          value: "nacos-cluster:8848"
        - name: NETTY_WORKER_THREADS
          value: "8"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gateway-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-gateway
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

6.2 监控告警体系

Prometheus监控指标

// 请求量统计
Counter requests = Counter.build()
    .name("http_requests_total")
    .labelNames("method", "path")
    .register();

// 延迟直方图
Histogram latency = Histogram.build()
    .name("http_request_duration_seconds")
    .labelNames("method")
    .buckets(0.1, 0.5, 1, 5)
    .register();

结语:高性能网关的设计哲学

构建工业级API网关的核心原则:

  1. 通信层优化

    • 基于Netty实现非阻塞I/O
    • 零拷贝减少内存开销
    • 合理配置线程模型
  2. 服务治理

    • 集成Nacos实现动态发现
    • 权重动态调整
    • 多级故障转移
  3. 并发处理

    • Disruptor无锁队列
    • 事件驱动架构
    • 批量处理提升吞吐
  4. 生产就绪

    • 完善的监控告警
    • 弹性伸缩能力
    • 分级容错策略

推荐技术演进路线

gantt
    title API网关技术演进
    dateFormat  YYYY-MM
    section 基础能力
    通信框架搭建 :done, 2023-01, 2M
    服务发现集成 :done, 2023-03, 1M
    section 性能优化
    并发模型改造 :active, 2023-04, 2M
    内存管理优化 : 2023-06, 1M
    section 高级特性
    智能路由 : 2023-07, 2M
    AIOps集成 : 2023-09, 3M

通过本文介绍的技术体系,开发者可以构建出能够支撑百万级QPS的高性能API网关,为微服务架构提供稳定可靠的流量管控能力。在实际项目中,建议根据具体业务需求进行针对性调优,并持续关注云原生网关技术的最新发展。

浏览器对队头阻塞问题的深度优化策略

作者 前端微白
2025年7月5日 19:55

在Web性能优化领域,队头阻塞(Head-of-Line Blocking)问题一直影响着网络传输效率。本文将深入探讨浏览器如何通过创新技术优化队头阻塞问题,并分析HTTP/2、HTTP/3协议的解决方案。

什么是队头阻塞问题?

队头阻塞是指在网络传输过程中,第一个数据包被阻塞导致后续所有数据包无法处理的性能瓶颈现象。这种问题发生在两个层面:

graph LR
    A[客户端请求] --> B[网络传输]
    B -->|HTTP层| C[请求/响应队列阻塞]
    B -->|TCP层| D[单个丢包延迟后续数据]

HTTP/1.1中的具体表现

  • 浏览器限制6个TCP连接/域名
  • 每个连接只能处理一个请求响应周期
  • 前一个请求未完成时阻塞后续请求

HTTP/1.1时代的优化策略

在HTTP/2普及前,工程师们使用多种策略缓解队头阻塞:

1. 域名分片(Domain Sharding)

// 通过多个子域名绕过连接限制
const assets = [
  'https://static1.example.com/image1.jpg',
  'https://static2.example.com/image2.jpg',
  'https://static3.example.com/image3.jpg'
];

效果:将6连接限制扩展到18个(3个子域名 × 6)

2. 资源合并(Concatenation)

/* 合并多个CSS文件 */
@import url('reset.css');
@import url('header.css');
@import url('main.css');

优化点:减少HTTP请求数量

3. 精灵图(Spriting)

.icon {
  background-image: url('sprite.png');
}
.icon-home {
  background-position: 0 0;
  width: 32px;
  height: 32px;
}

缺点:增加维护成本,不适合高分辨率屏幕

HTTP/2革命性突破

HTTP/2协议从根本上解决了HTTP层的队头阻塞问题:

多路复用(Multiplexing)机制

graph TD
    A[客户端] -- 流1 --> B[服务端]
    A -- 流2 --> B
    A -- 流3 --> B
    B -- 流2响应 --> A
    B -- 流1响应 --> A
    B -- 流3响应 --> A

核心技术

  • 二进制分帧层
  • 请求/响应流并行传输
  • 流优先级控制
:method: GET
:path: /style.css
:scheme: https
:authority: example.com
priority: u=3, i   # 优先级设置

头部压缩(HPACK)

原始头信息:
User-Agent: Mozilla/5.0... Chrome/98...
Cookie: session_id=abc123...

压缩后:
[静态索引62] + [动态索引:123]

优势:减少头部传输大小90%以上

持久化连接优化

HTTP/2通过连接复用和优化策略提高效率:

连接预热

// 使用预连接提前建立TCP连接
<link rel="preconnect" href="https://cdn.example.com">

0-RTT连接恢复

sequenceDiagram
    Client->>Server: TLS session ticket
    Server->>Client: 立即恢复会话

HTTP/3与QUIC协议

HTTP/3基于QUIC协议解决TCP层队头阻塞问题:

QUIC协议架构

graph BT
    HTTP/3 --> QUIC[QUIC Transport]
    QUIC --> UDP[UDP协议]
    QUIC --> TLS[内置TLS 1.3]

关键创新点:

  1. 基于UDP而非TCP

    • 绕过操作系统TCP协议栈
    • 避免TCP队头阻塞
  2. 独立流控制

    graph LR
      丢包流1 --> 流2[流2正常传输]
      流1重传 --> 不影响其他流传输
    
  3. 改进的拥塞控制

    • BBR(Bottleneck Bandwidth and Round-trip)算法
    • 更准确评估网络带宽

移动环境优化:

// 网络切换时保持连接
navigator.connection.addEventListener('change', () => {
  // 不中断现有连接
});

浏览器实现差异

浏览器 HTTP/2支持 HTTP/3支持 独特优化
Chrome 是 (v41+) 是 (v87+) QUIC实验性标志
Firefox 是 (v36+) 是 (v88+) 增量部署策略
Safari 是 (v9+) 是 (v14+) TLS 1.3优先
Edge 基于Chromium 基于Chromium 原生支持

实际性能对比

以下是优化前后的性能数据对比:

指标 HTTP/1.1 HTTP/2 HTTP/3 提升%
页面加载时间 4.2s 2.8s 2.1s 50%
丢包影响率 45%降低 32%降低 12%降低 73%
首次内容渲染 1.8s 1.3s 0.9s 50%
可交互时间 3.5s 2.5s 1.8s 49%

测试条件:100个资源请求,1%丢包率,150ms RTT

最佳实践建议

  1. 协议升级策略

    # Nginx配置
    listen 443 ssl http2; 
    listen 443 http3 reuseport;
    add_header Alt-Svc 'h3=":443"; ma=86400';
    
  2. 资源交付优化

    <!-- HTTP/2下无需域名分片 -->
    <script src="/app.js" defer></script>
    <link href="/style.css" rel="stylesheet">
    
  3. 智能加载策略

    // 动态资源加载
    function loadCriticalAssets() {
      const criticalAssets = [
        {url: '/core.js', priority: 'high'},
        {url: '/theme.css', priority: 'high'},
        {url: '/analytics.js', priority: 'low'}
      ];
      
      criticalAssets.sort((a, b) => 
         a.priority.localeCompare(b.priority));
         
      criticalAssets.forEach(asset => {
        const el = asset.url.endsWith('.js') ? 
          document.createElement('script') :
          document.createElement('link');
          
        el.src = asset.url;
        el[asset.url.endsWith('.js') ? 'defer' : 'rel'] = 
           asset.url.endsWith('.js') ? true : 'stylesheet';
           
        document.head.appendChild(el);
      });
    }
    

未来展望

  1. WebTransport协议

    const transport = new WebTransport('https://example.com:443/');
    const writer = transport.datagrams.writable.getWriter();
    writer.write(new Uint8Array([...]));
    
  2. 机器学习预测优化

    • 基于用户行为的资源预加载
    • 动态调整并发流数量
  3. 客户端提示增强

    Sec-CH-HTTP3-RTT: 150 
    Sec-CH-Network-Efficiency: fast-3g
    

小结

浏览器通过多代协议迭代实现了队头阻塞问题的系统性改进:

  • HTTP/2:解决应用层队头阻塞
  • QUIC/HTTP/3:解决传输层队头阻塞
  • 智能调度:通过优先级优化资源加载

随着HTTP/3普及率超过全球流量的25%(截至2023年),现代Web应用已获得显著的性能提升。开发者应积极适配新协议并遵循最佳实践,为用户提供更流畅的浏览体验。

最终建议:在支持环境下优先启用HTTP/3,回退到HTTP/2,保留HTTP/1.1兼容性,实现渐进式网络优化。

pie
    title 协议全球支持度
    "HTTP/1.1" : 30
    "HTTP/2" : 45
    "HTTP/3" : 25

5761.找出和为指定值的下标对 python哈希表解题

2021年5月16日 22:09

###python

from collections import Counter

class FindSumPairs:
    def __init__(self, nums1, nums2):
        self.n2 = nums2
        self.d1 = Counter(nums1)
        self.d2 = Counter(nums2)

    def add(self, index: int, val: int):
        tmp = self.n2[index]
        self.n2[index] = tmp + val
        self.d2[tmp] -= 1
        self.d2[tmp + val] += 1
        
    def count(self, tot: int) -> int:
        tmp = 0
        for k, v in self.d1.items():
            tmp += v * self.d2.get(tot - k, 0)
        return tmp

维护每个元素的出现次数(Python/Java/C++/Go)

作者 endlesscheng
2021年5月16日 12:18

本质是 1. 两数之和

遍历 $\textit{nums}_1$ 中的数,问题变成:

  • 设 $x = \textit{nums}_1[i]$,计算 $\textit{nums}_2$ 中有多少个数等于 $\textit{tot} - x$。

用哈希表维护 $\textit{nums}_2$ 中每个元素的出现次数,即可 $\mathcal{O}(1)$ 获知。

注:遍历 $\textit{nums}_1$ 中的数,是因为数据范围显示 $\textit{nums}_1$ 的最大长度比 $\textit{nums}_2$ 的小,遍历 $\textit{nums}_1$ 相比遍历 $\textit{nums}_2$ 时间复杂度更低。

class FindSumPairs:
    def __init__(self, nums1: List[int], nums2: List[int]):
        self.nums1 = nums1
        self.nums2 = nums2
        self.cnt = Counter(nums2)

    def add(self, index: int, val: int) -> None:
        # 维护 nums2 每个元素的出现次数
        self.cnt[self.nums2[index]] -= 1
        self.nums2[index] += val
        self.cnt[self.nums2[index]] += 1

    def count(self, tot: int) -> int:
        ans = 0
        for x in self.nums1:
            ans += self.cnt[tot - x]
        return ans
class FindSumPairs:
    def __init__(self, nums1: List[int], nums2: List[int]):
        self.nums2 = nums2
        self.cnt1 = Counter(nums1)
        self.cnt2 = Counter(nums2)

    def add(self, index: int, val: int) -> None:
        # 维护 nums2 每个元素的出现次数
        self.cnt2[self.nums2[index]] -= 1
        self.nums2[index] += val
        self.cnt2[self.nums2[index]] += 1

    def count(self, tot: int) -> int:
        ans = 0
        for x, c1 in self.cnt1.items():
            ans += c1 * self.cnt2[tot - x]
        return ans
class FindSumPairs {
    private final int[] nums1;
    private final int[] nums2;
    private final Map<Integer, Integer> cnt = new HashMap<>();

    public FindSumPairs(int[] nums1, int[] nums2) {
        this.nums1 = nums1;
        this.nums2 = nums2;
        for (int x : nums2) {
            cnt.merge(x, 1, Integer::sum);
        }
    }

    public void add(int index, int val) {
        // 维护 nums2 每个元素的出现次数
        cnt.merge(nums2[index], -1, Integer::sum);
        nums2[index] += val;
        cnt.merge(nums2[index], 1, Integer::sum);
    }

    public int count(int tot) {
        int ans = 0;
        for (int x : nums1) {
            ans += cnt.getOrDefault(tot - x, 0);
        }
        return ans;
    }
}
class FindSumPairs {
    vector<int> nums1, nums2;
    unordered_map<int, int> cnt;

public:
    FindSumPairs(vector<int>& nums1, vector<int>& nums2) {
        this->nums1 = nums1;
        this->nums2 = nums2;
        for (int x : nums2) {
            cnt[x]++;
        }
    }

    void add(int index, int val) {
        // 维护 nums2 每个元素的出现次数
        cnt[nums2[index]]--;
        nums2[index] += val;
        cnt[nums2[index]]++;
    }

    int count(int tot) {
        int ans = 0;
        for (int x : nums1) {
            ans += cnt[tot - x];
        }
        return ans;
    }
};
type FindSumPairs struct {
nums1 []int
nums2 []int
cnt   map[int]int
}

func Constructor(nums1, nums2 []int) FindSumPairs {
cnt := map[int]int{}
for _, x := range nums2 {
cnt[x]++
}
return FindSumPairs{nums1, nums2, cnt}
}

func (p *FindSumPairs) Add(index, val int) {
// 维护 nums2 每个元素的出现次数
p.cnt[p.nums2[index]]--
p.nums2[index] += val
p.cnt[p.nums2[index]]++
}

func (p *FindSumPairs) Count(tot int) (ans int) {
for _, x := range p.nums1 {
ans += p.cnt[tot-x]
}
return
}

复杂度分析

  • 时间复杂度:初始化是 $\mathcal{O}(m)$,$\texttt{add}$ 是 $\mathcal{O}(1)$,$\texttt{count}$ 是 $\mathcal{O}(n)$。其中 $n$ 是 $\textit{nums}_1$ 的长度,$m$ 是 $\textit{nums}_2$ 的长度。
  • 空间复杂度:$\mathcal{O}(m+q)$。其中 $q$ 是 $\texttt{add}$ 的调用次数。代码没有删除哈希表中出现次数等于 $0$ 的元素。

相似题目

2671. 频率跟踪器

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌
❌