阅读视图

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

一文搞懂:localhost和局域网 IP 的核心区别与使用场景

前端项目运行时给出的 localhost:3000 和 192.168.1.41:3000 本质上指向同一项目服务,但适用场景和访问范围不同,具体区别及选择建议如下:

一、核心区别

维度 localhost:3000 192.168.1.41:3000
指向对象 仅指向「当前运行项目的本机」(通过本地回环地址 127.0.0.1 实现) 指向本机在局域网中的 IP 地址(192.168.1.41 是本机在路由器分配的私有 IP)
访问范围 只能在「本机」上访问(其他设备无法通过 localhost 访问) 同一局域网内的所有设备(如手机、其他电脑、平板)均可访问
依赖条件 无需网络(断网也能访问),仅依赖本机服务是否启动 需保证本机和其他设备在同一局域网(如同一 WiFi / 网线),且本机防火墙允许端口访问

二、选择建议:根据场景决定

  1. 开发调试时优先用 localhost:3000

    • 优势:访问速度更快(本地回环不经过网络路由),且不受局域网波动影响(断网也能工作),更稳定。
    • 适用场景:自己在电脑上写代码、调试功能、修改样式等。
  2. 需要跨设备测试时用 192.168.1.41:3000

    • 优势:可以在手机、平板或同事的电脑上访问你的项目,验证响应式布局、多设备兼容性等。
    • 适用场景:测试移动端显示效果、让团队成员临时查看项目进度、跨设备联调(如手机扫码测试支付流程)。

三、注意事项

  • 若用局域网地址访问失败,可能是本机防火墙阻止了 3000 端口,或项目配置限制了仅本地访问(部分框架需手动开启局域网访问权限)。
  • 局域网 IP 可能会变化(路由器重启可能重新分配),若后续访问失败,可重新运行项目获取新的局域网地址。

总之,日常开发用 localhost 更高效,跨设备测试时再用局域网 IP 即可。

js深入之从原型到原型链

构造函数创建对象

function A {
}
let a=new A();
a.name="abc";
console.log(a.name);

在这个例子中A就是一个构造函数,我们使用new创建了一个实例对象a

prototype

每个函数都有一个prototype属性

function A {
}
A.prototype.name="张三"
let a1=new A();
let a2=new A();
console.log(a1.name,a2.name)

prototype指向的是调用该函数创建的实例的原型,也就是例子中a1,a2的原型。 每一个对象(null除外)创建的时候,都会关联另外一个对象,这个对象就是原型,每个对象都会从原型“继承”属性

3bdfe951-d6de-4e73-ae8b-8db05e5d54bf.jpeg

_proto_

每个对象(除了null)都有一个__proto__属性,这个属性指向该对象的原型

function A {
}
let a=new A();
console.log(a.__proto__===A.prototype)

a8d6b4f1-e328-44f6-8c7b-421bcfe8ac22.jpeg

constructor

每个原型都有一个constructor属性指向关联的构造函数

function A {
}
console.log(A.prototype.constructor===A)

4e507f00-af31-4942-bf0a-ffc0c89cd49c.jpeg

function A {
}
let a=new A();
console.log(A.prototype.constructor===A)
console.log(a.__proto__===A.prototype)
console.log(a.contructor===A.prototype.constructor)
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(a) === A.prototype)

当读取实例属性时,如果找不到就会从与对象关联的原型上查找,如果还查不到就会查找原型的原型,一直找到最顶层为止。

function A {
}
A.prototype.name="test";
let a=new A();
a.name="aaa";
console.log(a.name);//aaa
delete a.name;
console.log(a.name);//test

原型的原型,其实是有Object构造函数生成

a15399d7-b5ff-4fbc-afa3-9026ca9cddb7.jpeg

原型链

红色部分就代表了原型链的形成

dd4ff7f1-95d8-4007-9c1e-551d3ea83545.jpeg

面试过别人后,我对面试祛魅了

由于公司老员工走了一些,我一不小心变成最老的前端了

所以有幸能够担任公司前端一面的工作

和每位应聘者交流的过程,也是我的学习过程,从中发现一些不一样的视角的东西,写下来记录一下

老了后回来再看看掘金,也是自己坚持写文章的原因之一,到时候抱着孩子说,看爷爷当年写的文章,这么多人点赞嘞!

沟通表达能力非常重要

坦白讲,你的技术实力有时候不如表达能力有竞争力,即使你的技术再NB,问个问题,半天没有表达清楚,怎么和复杂的实际工作中沟通呢

但是这个沟通表达能力有点邪乎,没有明确的标准

遇到过什么难点,怎么解决

这个问题不是很好回答,但是一定不要回答没有遇到什么难点,其实问这个目的 一是正好看你的技术深度,二是看一下表达能力,即使真的没有很难的点,也讲一下自己认为比较费劲实现的 功能,清晰流畅准确的表达出来也是很加分的

八股还是会问的

AI冲击下问八股文好像没啥意义了,不会的都问一下AI就行了,但是多多少少还是有意义的

一个节流防抖使用场景都说不上来的人,即使在AI的加持下,其实力我也持怀疑态度

刚毕业的校招生说不上来也就算了,工作三年不知道节流防抖好像多多少少有点说不过去了

学历还是好使

坦白讲,遇到高学历的,对其技术要求确实放松了条件,我也想说程序员最重要的是技术实力,学历不重要

但是真正轮到自己的时候,技术实力有时候不如学历那么明显,可量化

以及领导一听XX大学的,他也认可,不然还得证明这人不错

所以你看,只要你能证明你的实力优势大于学历的劣势,那么学历完全不是问题

只可惜对大部分人来讲实力劣势与学历劣势共存(别骂了别骂了)

在线简历、Blog、Github

简历中有在线简历、Blog、Github等,还是比较加分的,可能对于大公司来讲,你的github没有很高star,别人不觉得你优秀

但是对于小公司来时,你有Github,至少证明你 用心一些,以及自己想探索一些项目

面试真就是碰运气

面试官也是在繁忙的工作中与你沟通,可能面试不过就是不和面试官的“胃口”,所以大家千万别灰心丧气

END

以上是我自己的一些看法,祝大家一些顺利

别再滥用 Base64 了——Blob 才是前端减负的正确姿势

一、什么是 Blob?

Blob(Binary Large Object,二进制大对象)是浏览器提供的一种不可变、类文件的原始数据容器。它可以存储任意类型的二进制或文本数据,例如图片、音频、PDF、甚至一段纯文本。与 File 对象相比,Blob 更底层,File 实际上继承自 Blob,并额外携带了 namelastModified 等元信息 。

Blob 最大的特点是纯客户端、零网络:数据一旦进入 Blob,就活在内存里,无需上传服务器即可预览、下载或进一步加工。


二、构造一个 Blob:一行代码搞定

const blob = new Blob(parts, options);
参数 说明
parts 数组,元素可以是 StringArrayBufferTypedArrayBlob 等。
options 可选对象,常用字段:
type MIME 类型,默认 application/octet-stream
endings 是否转换换行符,几乎不用。

示例:动态生成一个 Markdown 文件并让用户下载

const content = '# Hello Blob\n> 由浏览器动态生成';
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = 'hello.md';
a.click();

// 内存用完即弃
URL.revokeObjectURL(url);

三、Blob URL:给内存中的数据一个“临时地址”

1. 生成方式

const url = URL.createObjectURL(blob);
// 返回值样例
// blob:https://localhost:3000/550e8400-e29b-41d4-a716-446655440000

2. 生命周期

  • 作用域:仅在当前文档、当前会话有效;页面刷新、close()、手动调用 revokeObjectURL() 都会使其失效 。
  • 性能陷阱:不主动释放会造成内存泄漏,尤其在单页应用或大量图片预览场景 。

最佳实践封装:

function createTempURL(blob) {
  const url = URL.createObjectURL(blob);
  // 自动 revoke,避免忘记
  requestIdleCallback(() => URL.revokeObjectURL(url));
  return url;
}

四、Blob vs. Base64 vs. ArrayBuffer:如何选型?

场景 推荐格式 理由
图片回显、<img>/<video> Blob URL 浏览器可直接解析,无需解码;内存占用低。
小图标内嵌在 CSS/JSON Base64 减少一次 HTTP 请求,但体积增大约 33%。
纯计算、WebAssembly 传递 ArrayBuffer 可写、可索引,适合高效运算。
上传大文件、断点续传 Blob.slice 流式分片,配合 File.prototype.slice 做断点续传 。

五、高频实战场景

1. 本地图片/视频预览(零上传)

<input type="file" accept="image/*" id="uploader">
<img id="preview" style="max-width: 100%">

<script>
uploader.onchange = e => {
  const file = e.target.files[0];
  if (!file) return;
  const url = URL.createObjectURL(file);
  preview.src = url;
  preview.onload = () => URL.revokeObjectURL(url); // 加载完即释放
};
</script>

2. 将 Canvas 绘图导出为 PNG 并下载

canvas.toBlob(blob => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'snapshot.png';
  a.click();
  URL.revokeObjectURL(url);
}, 'image/png');

3. 抓取远程图片→Blob→本地预览(跨域需 CORS)

fetch('https://i.imgur.com/xxx.png', { mode: 'cors' })
  .then(r => r.blob())
  .then(blob => {
    const url = URL.createObjectURL(blob);
    document.querySelector('img').src = url;
  });

若出现图片不显示,99% 是因为服务端未返回 Access-Control-Allow-Origin 头 。


六、踩坑指南与性能锦囊

坑点 解决方案
内存暴涨 每次 createObjectURL 后,务必在合适的时机 revokeObjectURL
跨域失败 确认服务端开启 CORS;fetch 时加 {credentials: 'include'} 如需 Cookie。
移动端大视频卡顿 避免一次性读完整文件,使用 blob.slice(start, end) 分段读取。
旧浏览器兼容 IE10+ 才原生支持 Blob;如需更低版本,请引入 Blob.js 兼容库。

七、延伸:Blob 与 Stream 的梦幻联动

当文件超大(GB 级)时,全部读进内存并不现实。可以借助 ReadableStream 把 Blob 转为流,实现渐进式上传:

const stream = blob.stream(); // 返回 ReadableStream
await fetch('/upload', {
  method: 'POST',
  body: stream,
  headers: { 'Content-Type': blob.type }
});

Chrome 85+、Edge 85+、Firefox 已经支持 blob.stream(),能以流式形式边读边传,内存占用极低。


八、总结:记住“三句话”

  1. Blob = 浏览器端的二进制数据仓库,File 只是它的超集。
  2. Blob URL = 指向内存的临时指针,用完后必须手动或自动释放。
  3. 凡是“本地预览、零上传、动态生成下载”的需求,优先考虑 Blob + Blob URL 组合。

用好 Blob,既能提升用户体验(秒开预览),又能降低服务端压力(无需中转),是每一位前端工程师的必备技能。

JavaScript原型与原型链:深入理解面向对象编程的基石

引言

在JavaScript的世界中,原型(Prototype)是一个核心概念,它构成了JavaScript面向对象编程的基石。对于许多初学者来说,原型和原型链可能是最令人困惑的概念之一,但一旦深入理解,就会发现它实际上是JavaScript最强大、最灵活的特性之一。本文将通过详细的理论解释和丰富的代码示例,全面剖析JavaScript中的原型对象、原型继承以及原型链机制。

一、原型对象:共享属性和方法的智慧

1.1 什么是原型对象

在JavaScript中,每个函数都有一个特殊的属性prototype,这就是我们所说的原型对象。这个属性指向一个对象,其主要目的是包含可以由特定类型的所有实例共享的属性和方法。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}
// 通过构造函数的prototype属性访问原型对象
console.log(Star.prototype); // 输出原型对象

1.2 为什么需要原型对象

考虑以下场景:我们创建了一个构造函数,并实例化了多个对象。如果每个对象都有自己独立的方法副本,会造成内存的极大浪费。

javascript

复制下载

// 不推荐的方式:每个实例都有独立的方法副本
function Star(uname){
  this.uname = uname;
  this.sing = function(){
    console.log(this.uname + '会唱歌');
  }
}

const ldh = new Star('刘德华');
const zxy = new Star('张学友');

console.log(ldh.sing === zxy.sing); // false,方法是不同的函数实例

使用原型对象可以优雅地解决这个问题:

javascript

复制下载

// 推荐的方式:方法定义在原型上,所有实例共享
function Star(uname){
  this.uname = uname;
}

Star.prototype.sing = function(){
  console.log(this.uname + '会唱歌');
}

const ldh = new Star('刘德华');
const zxy = new Star('张学友');

ldh.sing(); // 刘德华会唱歌
zxy.sing(); // 张学友会唱歌

console.log(ldh.sing === zxy.sing); // true,所有实例共享同一个方法

1.3 原型对象的工作原理

当我们访问一个对象的属性或方法时,JavaScript引擎会首先在对象自身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或到达原型链的末端。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

Star.prototype.sing = function(){
  console.log(this.uname + '会唱歌');
}

const ldh = new Star('刘德华');

// ldh对象本身没有sing方法,但通过原型链可以访问到
console.log(ldh.hasOwnProperty('sing')); // false
console.log('sing' in ldh); // true
ldh.sing(); // 刘德华会唱歌

1.4 原型对象中的this指向

一个重要的细节是:无论方法定义在构造函数中还是原型对象中,方法内部的this都指向调用该方法的实例对象。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

Star.prototype.sing = function(){
  // 这里的this指向调用该方法的实例对象
  console.log(this.uname + '会唱歌');
}

const ldh = new Star('刘德华');
ldh.sing(); // 输出"刘德华会唱歌",this指向ldh实例

二、constructor属性:连接实例与构造函数的桥梁

2.1 原型对象中的constructor属性

每个原型对象都有一个constructor属性,默认指向该原型对象所属的构造函数。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

console.log(Star.prototype.constructor === Star); // true

2.2 实例对象中的constructor属性

通过实例对象访问constructor属性时,实际上是通过原型链访问到原型对象的constructor属性。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

const ldh = new Star('刘德华');
console.log(ldh.constructor === Star); // true

2.3 重写原型对象时的constructor问题

当我们完全重写原型对象时,会丢失原有的constructor属性,需要手动重新指向。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

// 完全重写原型对象
Star.prototype = {
  sing: function(){
    console.log(this.uname + '会唱歌');
  },
  dance: function(){
    console.log(this.uname + '会跳舞');
  }
};

console.log(Star.prototype.constructor === Star); // false
console.log(Star.prototype.constructor === Object); // true

// 正确的方式:重写原型对象时手动设置constructor
Star.prototype = {
  constructor: Star, // 手动指向构造函数
  sing: function(){
    console.log(this.uname + '会唱歌');
  },
  dance: function(){
    console.log(this.uname + '会跳舞');
  }
};

console.log(Star.prototype.constructor === Star); // true

三、对象原型:__proto__与原型链的纽带

3.1 什么是对象原型

每个JavaScript对象(除null外)都有一个内置属性[[Prototype]],在大多数浏览器中可以通过__proto__属性访问。这个属性指向创建该对象的构造函数的原型对象。

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

const ldh = new Star('刘德华');

// 实例对象的__proto__指向构造函数的原型对象
console.log(ldh.__proto__ === Star.prototype); // true

3.2 __proto__与prototype的关系

  • prototype是构造函数的属性,指向原型对象
  • __proto__是实例对象的属性,指向构造函数的原型对象

javascript

复制下载

function Star(uname){
  this.uname = uname;
}

const ldh = new Star('刘德华');

// 三者关系
console.log(ldh.__proto__ === Star.prototype); // true
console.log(Star.prototype.constructor === Star); // true
console.log(ldh.constructor === Star); // true

3.3 对象原型的实际意义

对象原型__proto__的主要意义在于为对象成员查找机制提供一个方向,或者说一条路线,这就是我们接下来要讨论的原型链。

四、原型继承:实现代码复用的优雅方式

4.1 什么是原型继承

原型继承是JavaScript中实现继承的主要方式。其核心思想是:让一个构造函数的原型对象等于另一个构造函数的实例,这样前者就可以继承后者的属性和方法。

4.2 原型继承的实现

javascript

复制下载

// 父类
function Person(){
  this.eyes = 2;
  this.head = 1;
}

// 子类
function Woman(sex){
  this.sex = sex;
}

function Man(sex){
  this.sex = sex;
}

// 实现继承:子类的原型对象是父类的实例
Woman.prototype = new Person();
// 修复constructor指向
Woman.prototype.constructor = Woman;

Man.prototype = new Person();
Man.prototype.constructor = Man;

const red = new Woman('女');
console.log(red.eyes); // 2,继承自Person
console.log(red.head); // 1,继承自Person
console.log(red.sex); // 女,自身的属性

const blue = new Man('男');
console.log(blue.eyes); // 2,继承自Person
console.log(blue.head); // 1,继承自Person
console.log(blue.sex); // 男,自身的属性

4.3 原型继承的内存效率

通过原型继承,所有子类实例共享父类原型上的方法,这大大提高了内存使用效率。

javascript

复制下载

function Person(){
  this.eyes = 2;
}

Person.prototype.breathe = function(){
  console.log('呼吸');
};

function Woman(sex){
  this.sex = sex;
}

Woman.prototype = new Person();
Woman.prototype.constructor = Woman;

const red = new Woman('女');
const pink = new Woman('女');

// 两个实例共享同一个breathe方法
console.log(red.breathe === pink.breathe); // true

4.4 方法重写与属性屏蔽

子类可以重写父类的方法,或者在实例上定义与原型链上同名的属性,实现属性屏蔽。

javascript

复制下载

function Person(){
  this.eyes = 2;
}

Person.prototype.see = function(){
  console.log('用眼睛看');
};

function Superman(){
  this.eyes = 3; // 属性屏蔽
}

Superman.prototype = new Person();
Superman.prototype.constructor = Superman;

// 方法重写
Superman.prototype.see = function(){
  console.log('用超级眼睛看');
};

const clark = new Superman();
console.log(clark.eyes); // 3,访问的是自身属性
clark.see(); // "用超级眼睛看",调用的是重写后的方法

五、原型链:JavaScript对象查找机制的核心

5.1 什么是原型链

原型链是JavaScript中实现继承和属性查找的机制。当访问一个对象的属性时,JavaScript引擎会执行以下步骤:

  1. 首先在对象自身查找该属性
  2. 如果找不到,则在该对象的原型(__proto__指向的对象)上查找
  3. 如果还找不到,则继续在原型的原型上查找
  4. 依此类推,直到找到该属性或到达原型链的顶端(null)

5.2 原型链的图示与理解

考虑以下代码:

javascript

复制下载

function Person(name){
  this.name = name;
}

Person.prototype.sayHello = function(){
  console.log('Hello, I am ' + this.name);
};

function Student(name, grade){
  this.name = name;
  this.grade = grade;
}

// 实现继承
Student.prototype = new Person();
Student.prototype.constructor = Student;

Student.prototype.study = function(){
  console.log(this.name + ' is studying');
};

const tom = new Student('Tom', 5);

此时的原型链关系为:

text

复制下载

tom -> Student.prototype -> Person.prototype -> Object.prototype -> null

属性查找过程:

  • tom.grade:在tom对象自身找到
  • tom.study:在Student.prototype中找到
  • tom.sayHello:在Person.prototype中找到
  • tom.toString:在Object.prototype中找到

9f7a6fe1f768b069e3c5125a949cf993.png

5.3 原型链的终点

所有普通的原型链最终都会指向Object.prototype,而Object.prototype的__proto__指向null,这是原型链的终点。

javascript

复制下载

function Person(){}

const person = new Person();

console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

5.4 instanceof操作符

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

javascript

复制下载

function Person(){}
function Student(){}

Student.prototype = new Person();
Student.prototype.constructor = Student;

const tom = new Student();

console.log(tom instanceof Student); // true
console.log(tom instanceof Person); // true
console.log(tom instanceof Object); // true
console.log(tom instanceof Array); // false

5.5 原型链与性能考虑

虽然原型链提供了强大的继承机制,但过长的原型链可能会影响性能,因为属性查找需要遍历整个原型链。在实际开发中,应尽量避免过深的继承层次。

六、实际应用与最佳实践

6.1 组合使用构造函数和原型模式

这是创建自定义类型的最常见方式,通过构造函数定义实例属性,通过原型定义共享的方法和属性。

javascript

复制下载

// 最佳实践:组合使用构造函数和原型模式
function Person(name, age){
  // 实例属性
  this.name = name;
  this.age = age;
}

// 共享方法
Person.prototype.sayHello = function(){
  console.log('Hello, I am ' + this.name);
};

Person.prototype.toString = function(){
  return '[Person: ' + this.name + ', ' + this.age + ']';
};

const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);

alice.sayHello(); // Hello, I am Alice
bob.sayHello(); // Hello, I am Bob

console.log(alice.toString()); // [Person: Alice, 25]

6.2 原型与对象创建性能

在需要创建大量相似对象的场景中,使用原型可以显著提高性能。

javascript

复制下载

// 性能对比:使用原型 vs 不使用原型

// 方式1:不使用原型(性能较差)
function createUserWithoutPrototype(name, email) {
  return {
    name: name,
    email: email,
    getInfo: function() {
      return this.name + ' <' + this.email + '>';
    }
  };
}

// 方式2:使用原型(性能较好)
function User(name, email) {
  this.name = name;
  this.email = email;
}

User.prototype.getInfo = function() {
  return this.name + ' <' + this.email + '>';
};

function createUserWithPrototype(name, email) {
  return new User(name, email);
}

// 测试性能
console.time('Without Prototype');
for (let i = 0; i < 100000; i++) {
  createUserWithoutPrototype('user' + i, 'user' + i + '@example.com');
}
console.timeEnd('Without Prototype');

console.time('With Prototype');
for (let i = 0; i < 100000; i++) {
  createUserWithPrototype('user' + i, 'user' + i + '@example.com');
}
console.timeEnd('With Prototype');

七、常见问题与解决方案

7.1 原型对象共享引用类型值的问题

当原型对象包含引用类型值时,所有实例会共享同一个引用,这可能导致意外的行为。

javascript

复制下载

// 问题:共享引用类型值
function Person(name){
  this.name = name;
}

Person.prototype.friends = []; // 引用类型值

const alice = new Person('Alice');
const bob = new Person('Bob');

alice.friends.push('Charlie');
console.log(bob.friends); // ['Charlie'],bob也受到了影响

// 解决方案:在构造函数中定义引用类型属性
function BetterPerson(name){
  this.name = name;
  this.friends = []; // 每个实例有自己的friends数组
}

BetterPerson.prototype.addFriend = function(friend){
  this.friends.push(friend);
};

const carol = new BetterPerson('Carol');
const dave = new BetterPerson('Dave');

carol.addFriend('Eve');
console.log(carol.friends); // ['Eve']
console.log(dave.friends); // [],dave不受影响

7.2 原型链与枚举属性

使用for...in循环时会遍历对象自身和原型链上的可枚举属性,这可能不是我们想要的行为。

javascript

复制下载

function Person(name){
  this.name = name;
}

Person.prototype.sayHello = function(){
  console.log('Hello');
};

const person = new Person('Alice');

// for...in会遍历原型链上的属性
for(let key in person){
  console.log(key); // 输出: name, sayHello
}

// 解决方案:使用hasOwnProperty过滤
for(let key in person){
  if(person.hasOwnProperty(key)){
    console.log(key); // 只输出: name
  }
}

结论

JavaScript的原型机制是其面向对象编程的核心,理解原型对象、原型继承和原型链对于掌握JavaScript至关重要。通过原型,JavaScript实现了高效的代码复用和灵活的继承机制。虽然ES6引入了class语法,使其更接近传统面向对象语言,但底层仍然是基于原型的继承。

在实际开发中,我们应该:

  1. 理解原型链的工作原理,避免过深的继承层次
  2. 合理使用原型共享方法,提高内存效率
  3. 注意引用类型值的共享问题
  4. 掌握现代class语法,同时理解其背后的原型机制

通过深入理解和合理应用原型相关概念,我们能够编写出更加高效、可维护的JavaScript代码,充分利用JavaScript这门语言的强大特性。

🔥 React 高频 useEffect 导致页面崩溃的真实案例:从根因排查到彻底优化

如果你在 React 中遇到过“页面卡死 / 高频请求 / useEffect 无限触发”,这篇文章会帮你一次搞懂根因,并提供可直接复制的最佳解决方案。

很多同学遇到性能问题时,会立刻想到:
👉 “加防抖呀?”
👉 “加 useMemo / useCallback 缓存呀?”

但实际上,这些方式在某些场景下根本无效。特别是当问题来自 深层子组件 的 useEffect 重复触发时,你必须回到 React 的底层原则: 单向数据流 + 渲染链传播效应。

下面用一个 真实可复现的代码示例,带你从问题现场走到完整解决方案。

问题现场:子组件 useEffect 高频触发,直接把页面搞崩

来看看最典型的错误写法。

子组件中监听 props 变化,然后发起请求

// Child.jsx
import { useEffect } from 'react';

export default function Child({ value }) {
  useEffect(() => {
    // “监听值变化”
    fetch(`/api/search?q=${value}`)
      .then(res => res.json())
      .then(console.log);
  }, [value]);

  return <div>Child Component: {value}</div>;
}

父组件层级复杂、数据源更新频繁:

// Parent.jsx
import { useState } from 'react';
import Child from './Child';

export default function Parent() {
  const [text, setText] = useState('');

  return (
    <>
      <input onChange={(e) => setText(e.target.value)} />
      <Child value={text} />
    </>
  );
}

触发链:value 更新 → 子组件重渲染 → useEffect 再次执行 → 发请求

只要用户输入速度稍快一点:

  • 会触发几十次请求
  • 浏览器线程被占满
  • 页面直接卡死 / 崩溃

为什么难定位?React 的单向数据流是关键

乍一看你会觉得:

“不是 value 改变才触发 useEffect 吗?怎么会到处连锁反应?”

问题在于:

  • 组件树嵌套太深(真实项目都这样)
  • 上层某个 state 变化导致整个父组件重渲染
  • re-render 会逐层传播到所有子组件
  • 子组件 props 引用被重建
  • useEffect 认为依赖变化 → 再次触发

哪怕 value 内容没变,也会因为引用变化触发 effect。

这就是为什么:

  • useMemo / useCallback 并不是万能的
  • 防抖也不能解决根因(子组件仍在重复渲染)

你必须从根本上切断触发链。

真正有效的解决路线:把数据源提升到最高层父组件

要解决这种高频触发 effect 的问题,最有效的方式是:

将触发 request 的逻辑,从子组件提取到父组件中进行统一控制。

为什么?

  • 父组件能控制数据源
  • 可以集中做防抖、节流、缓存、限流
  • 子组件变“纯展示组件”,不会再触发副作用
  • 渲染链被隔离,高频触发链路彻底消失

父组件统一管理副作用(正确写法)

// Parent.jsx
import { useState, useEffect } from 'react';
import Child from './Child';

export default function Parent() {
  const [text, setText] = useState('');
  const [result, setResult] = useState(null);

  // 副作用上移:只在父组件执行
  useEffect(() => {
    if (!text) return;

    const controller = new AbortController();

    fetch(`/api/search?q=${text}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setResult)
      .catch(() => {});

    return () => controller.abort();
  }, [text]);

  return (
    <>
      <input onChange={(e) => setText(e.target.value)} />
      <Child value={text} result={result} />
    </>
  );
}

子组件变为纯展示组件(无副作用)

// Child.jsx
export default function Child({ value, result }) {
  return (
    <div>
      <div>Input: {value}</div>
      <pre>{JSON.stringify(result, null, 2)}</pre>
    </div>
  );
}

这种方式为什么最可靠?

  1. 完全切断子组件 effect 高频触发:再也不会因为渲染链导致 API 请求频繁发出。
  2. React 的渲染机制变得可控:副作用从不可控(子组件) → 可控(父组件)。
  3. 适配任何复杂场景:深层嵌套、多层传参、多状态联动、高频输入流、多 API 串联
  4. 不再依赖“防抖、缓存”等外力:这些都是辅助,而不是根治方式。

额外可选优化(视情况使用)

1. useMemo / useCallback

减少无意义渲染(但无法解决副作用重复触发的根因)。

2. 防抖(debounce)

如果希望输入不触发太多请求,可以:

const debouncedValue = useDebounce(text, 300);

但请注意:如果不解决渲染链问题,防抖依旧无法从根本解决 useEffect 高频触发。

总结

把副作用提升到父组件,让子组件保持纯净。这是 React 设计理念下最符合逻辑,同时也最稳定的解决方式。

“一招鲜吃遍天”,React的开发,全部遵循这种方式的开发,是不是也能避免很多 BUG!

你认为呢?欢迎在评论区讨论!

前端实时推送 & WebSocket 面试题(2026版)

一、历史背景 + 时间轴 网页一旦需要 “实时” ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新; 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬; 再加长轮询,试图把“有了新数据再说”

《大厂面试:从手写 Ajax 到封装 getJSON,再到理解 Promise 与 sleep》

大厂面试必考:从手写 Ajax 到封装 getJSON,再到理解 Promise 与 sleep

在前端工程师的求职过程中,尤其是冲击一线大厂(如阿里、腾讯、字节等)时,手写代码题几乎是绕不开的一环。这些题目看似基础,实则考察候选人对 JavaScript 核心机制的理解深度——包括异步编程、事件循环、内存模型以及浏览器原生 API 的掌握程度。

本文将焦三个经典手写题:

  1. 手写原生 Ajax
  2. 封装支持 Promise 的 getJSON 函数
  3. 手写 sleep 函数

我们将逐层深入,不仅写出代码,更要讲清楚“为什么这么写”,帮助你在面试中不仅能写出来,还能讲明白。


一、手写 Ajax:回调地狱的起点

虽然现代开发中我们早已习惯使用 fetchaxios,但 Ajax 是所有网络请求的基石。面试官让你手写 Ajax,不是为了让你重复造轮子,而是检验你是否真正理解 HTTP 请求在浏览器中的实现方式。 ajax 基于回调函数实现,代码复杂,这正是其痛点所在。

手写一个基础版 Ajax

function ajax(url, callback) {
  const xhr = new XMLHttpRequest();
  
  xhr.open('GET', url, true); // 异步请求
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) { // 请求完成
      if (xhr.status >= 200 && xhr.status < 300) {
        // 成功:调用回调并传入响应数据
        callback(null, JSON.parse(xhr.responseText));
      } else {
        // 失败:传入错误
        callback(new Error(`HTTP ${xhr.status}`), null);
      }
    }
  };
  xhr.send();
}

问题在哪?

  • 强依赖回调函数:调用方必须传入 callback,无法链式操作;
  • 错误处理分散:成功和失败逻辑耦合在同一个函数里;
  • 无法组合多个异步操作:比如“先请求 A,再根据 A 的结果请求 B”,代码会迅速变得嵌套混乱——即所谓的“回调地狱”。

这正是为什么我们需要 Promise


二、封装 getJSON:用 Promise 改造 Ajax

“如何封装一个 getJSON 函数。使用 ajax,支持 Promise,get 请求方法,返回是 JSON”

这其实是一个典型的“将传统回调式 API 转为 Promise 化”的过程。

封装思路

  • 创建一个返回 Promise 的函数;
  • Promise 构造函数内部执行 Ajax;
  • 成功时调用 resolve(data),失败时调用 reject(error)
  • 自动解析 JSON 响应体。

实现代码

function getJSON(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Accept', 'application/json');

    xhr.onload = function () {
      if (xhr.status >= 200 && xhr.status < 300) {
        try {
          const data = JSON.parse(xhr.responseText);
          resolve(data);
        } catch (e) {
          reject(new Error('Invalid JSON response'));
        }
      } else {
        reject(new Error(`Request failed with status ${xhr.status}`));
      }
    };

    xhr.onerror = function () {
      reject(new Error('Network error'));
    };

    xhr.send();
  });
}

使用方式(对比 fetch)

// 使用我们封装的 getJSON
getJSON('/api/user')
  .then(user => console.log(user))
  .catch(err => console.error('Failed:', err));

// 等价于 fetch 写法(但 fetch 不自动抛出 HTTP 错误)
fetch('/api/user')
  .then(res => {
    if (!res.ok) throw new Error('HTTP error');
    return res.json();
  })
  .then(user => console.log(user))
  .catch(err => console.error(err));

为什么 Promise 更好?

“fetch 简单易用,基于 Promise 实现,(then)无需回调函数”

Promise 的核心优势在于:

  • 状态机模型:初始为 pending,只能变为 fulfilled(通过 resolve)或 rejected(通过 reject),且状态不可逆;
  • 链式调用.then().catch() 形成清晰的流程控制;
  • 统一错误处理:任意环节出错,都会被最近的 .catch 捕获。

这使得异步代码更接近同步逻辑的阅读体验。


三、深入 Promise:不只是语法糖

“promise 类 ,为异步变同步而(流程控制)实例化,事实标准。接收一个函数,函数有两个参数,resolve reject,他们也是函数。”

  • new Promise(executor) 中的 executor 是一个立即执行的函数;
  • 它接收两个参数:resolvereject,都是由 Promise 内部提供的函数;
  • 调用 resolve(value) 会将 Promise 状态转为 fulfilled,并将 value 传递给下一个 .then
  • 调用 reject(reason) 则转为 rejected,触发 .catch

例如:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    Math.random() > 0.5 ? resolve('ok') : reject('fail');
  }, 1000);
});

p.then(console.log).catch(console.error);

这种设计让开发者能主动控制异步结果的“成功”或“失败”路径,是构建可靠异步系统的基础。


四、手写 sleep:Promise

实现

 function sleep(n){
            let p;
                 p = new Promise ((resolve,reject)=>{
                
                setTimeout(()=>{
                // pending 等待
                console.log(p);
                //resolve();
                reject();
                // fulfilled  成功
                console.log(p);
                }
                    ,n);
            })
            return p;
        }
        sleep(3000)
        .then(()=>{
            console.log('3s后执行');

        })
        .catch(()=>{
            console.log('3s后执行失败');
        })
        // promise 状态改变 就会执行
        .finally(()=>{
            console.log('finally');
        })

手写题的本质是理解机制

大厂面试之所以反复考察这些“老掉牙”的手写题,是因为它们像一面镜子,照出你对 JavaScript 运行机制的理解深度:

  • Ajax → 浏览器网络 API + 回调模型;
  • Promise 封装 → 异步流程控制范式升级;
  • sleep → Promise 与定时器的创造性结合;

当你不仅能写出这些代码,还能清晰解释其背后的原理时,你就已经超越了大多数候选人。

记住:面试不是考你会不会用库,而是考你知不知道库为什么存在。

前端高频面试题之Vuex篇

1、Vuex 是什么?什么情况下应该使用 Vuex?

Vuex 是专门为 Vue.js 应用提供状态管理模式的一个库,也是 Vue.js 官方推荐的状态管理方案,它将所有数据集中存储到一个全局 store 对象中,并制定了一定的规则,保证状态以预期的方式发生变化。

它的核心概念有:

  • state:存储状态,并提供响应式能力。
  • getter: 从 state 中派生出一些状态,相当于 Vue.js 中的计算属性 computed。
  • mutation: 通过提交 mutation,是 Vuex 中修改 state 的推荐方式。
  • action:可以包括异步操作,异步操作处理完后,通过提交 mutation 修改状态。
  • module: 模块化,可以将 store 分割成一个个小模块,每个模块拥有自己的 state、getter、mutation、action,甚至是嵌套子模块。

在构建中大型单页应用时,各组件和模块的状态流转逻辑会相当复杂,这时候就可以使用 Vuex 进行全局状态管理,并且里面用严格的 mutation 保证了状态的预期流转,使得项目的数据流变得清晰,提高了项目可维护性。

2、如何解决页面刷新后 Vuex 的数据丢失问题?

数据丢失原因:Vuex 中的状态 state 是存储在内存中的,刷新页面会导致内存清空,所以数据丢失。

解决方案:

2.1 第一步:使用持久化存储保存数据

将 Vuex 的数据在合适时机(比如监听 window 的beforeunload 事件)保存到浏览器的本地存储(localStoragesessionStorage),也可以直接采用 vuex-persistedstate 持久化插件(默认会存储到 localStorage 中,可通过配置修改)进行本地存储。

2.2 第二步:初始化应用,替换状态

应用初始化加载时,获取存储中的状态进行替换。Vuex 给我们提供了一个 replaceState(state: Object) API,可以很方便进行状态替换。

2.3 第三步:检查数据,发起请求

在状态替换后,还需要检查 Vuex 中的数据是否存在,如果不存在则可以在 action 中发送接口请求拿到数据,通过提交 mutation 修改状态把数据存储到 store 中。

2.4 第四步:状态同步

状态变化后将状态同步到浏览器存储中,保证本地存储中状态的实时性。

不过要注意的是,如果把数据持久化到 localStorage 或者 sessionStorage 中,会有一定的安全风险:

  1. 数据直接全部暴露在 storage 可通过控制台的 Application 选项卡进行查看,数据容易泄漏。持久化的数据毕竟没有内存中的数据安全。
  2. 用户可以直接在控制台 Application 中直接修改数据,从而可能绕过某些权限校验,看到一些预期外的界面和交互。

3、mutation 和 action 的区别有哪些?

  • 作用不同:action 是用来处理异步逻辑或者业务逻辑,而 mutation 是用来修改状态的。
  • 使用限制:action 中可以调用 mutation 或者其他 action,而 mutation 中则只能修改 state。
  • 返回值不同dispatch 时会将 action 包装成 promise,而 mutation 则没进行包装。
  • 严格模式下的差异:在 Vuex 开启严格模式 strict: true 后,任何非 mutation 函数修改的状态,将会抛出错误。

扩展:vuex 严格模式是如何监听非 mutation 函数修改状态的?

其核心思路如下:

  1. this._committing 表示程序是否处于 commit 执行过程。
  2. 用同步 watch(同步监听的意思是,一旦数据发生变化会立即调用回调,而不是在 下一次 Tick 中调用) 监听 store 中的 state 状态(深度监听)。
  3. 如果在 commit 执行过程中,state 发生了变化,在开发环境会报错。
class Store {
  commit(_type, _payload, _options) {
    this._withCommit(() => {
      // commit 中的处理
      entry.forEach(function commitIterator(handler) {
        handler(payload);
      });
    });
  }
  _withCommit(fn) {
    const committing = this._committing;
    this._committing = true;
    fn(); // 如果函数内部有异步修改状态逻辑,则下面的 watch 时会报错
    this._committing = committing;
  }
}
function enableStrictMode(store) {
  watch(
    () => store._state.data,
    () => {
      if (__DEV__) { // 开发环境报错
        assert(
          store._committing,
          `do not mutate vuex store state outside mutation handlers.`
        );
      }
    },
    { deep: true, flush: "sync" } // 定义同步的 watcher 进行同步监控
  );
}

4、Vuex 的 module 在什么情况下会使用?

用官方的话来说就是,“使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。”

所以我们在开发复杂应用时,可以按照业务逻辑将应用状态进行 modules 拆分,比如:

  1. 用户模块 user;
  2. 订单模块 order;
  3. 课程模块 course;
  4. ...等其它模块。

这样在开发应用和维护状态时更加精细和清晰,可维护性更强。

5、Vuex 和 Pinia 的区别?

Pinia 是以 Vuex 5 为原型,由 Vue.js 官方团队开发的新一代 Vue 官方推荐的状态管理方案。

它对比 Vuex 有以下区别:

5.1 API 设计和使用方式

  • Vuex:采用单一 store 结构,需要严格区分 mutation(同步修改状态)和 action(异步操作)。状态修改必须通过 commit mutations 进行,虽然让数据流向更清晰,但也会让代码更加冗长。
  • Pinia:更简单的 API 设计,所见即所得,也提供了符合组合式 API 风格的 API(比如用 defineStore 定义 store)。去掉了 mutation,直接在 actions 中修改 state(支持同步/异步)。

5.2 模块化和结构

  • Vuex:支持模块化(modules),但需要在单一 store 中组织,可能导致大型项目 store 膨胀。
  • Pinia:天生模块化,每个 store 独立定义和导入,支持动态注册和热重载。更适合大型应用,便于拆分成小 store。

5.3 TypeScript 支持

  • Vuex:TypeScript 支持一般,需要额外配置;
  • Pinia:本身源码就是用 TypeScript 编写,所以对TypeScript 支持十分友好,具备自动推断类型、类型安全和代码补全。

5.4 性能和集成

  • Vuex:Vuex4 在 Vue3 中可用,但与 Composition API 集成不够顺畅,可能需要额外的适配;
  • Pinia:更轻量(体积小,约1kb),性能更好;完美支持 Vue 3 的 Composition API 和 reactivity 系统。

6、Pinia 和 Vuex 如何选择?

  • 新项目:强烈推荐用 Vue3 + Pinia
  • 老 Vue2 项目:如果不把项目升级到 Vue3 还是建议用 Vuex,如果需要升级到 vue3,就可以逐步把 Vuex 替换为 Pinia,Vuex 和 Pinia 是可以同时安装在同一个项目中,这也为项目升级提供了一定的便利。当然,由 Vuex -> Pinia,是一次,无疑和 Vue2 -> Vue3 一样,是一次大的破坏性升级,工作量还是相当大的。

结语

以上是整理的 Vuex 的高频面试题,如有错误或者可以优化的地方欢迎评论区指正,后续还会更新 Vue-router 相关面试题。

CSS 像素≠物理像素:0.5px 效果的核心密码是什么?

先明确两者的关系:CSS 像素是 “逻辑像素”(页面布局用),物理像素是屏幕实际发光的像素点,两者通过 设备像素比(DPR)  关联,公式为:1 个 CSS 像素 = DPR × DPR 个物理像素(仅高清屏缩放为 1 时)。

理解这个核心关系后,再看 0.5px 效果的实现逻辑就更清晰了,以下重新整理(重点补充像素关系,再对应方法):

一、先搞懂:CSS 像素、物理像素、DPR 的核心关系

  1. 定义

    • CSS 像素:写代码时用的单位(如 width: 100px),是浏览器渲染布局的 “逻辑单位”,和屏幕硬件无关。

    • 物理像素:屏幕面板上实际的发光点(如手机屏分辨率 1080×2340,就是横向 1080 个、纵向 2340 个物理像素),是屏幕的硬件属性。

    • DPR(设备像素比):DPR = 物理像素宽度 / CSS 像素宽度(默认页面缩放为 1 时),由设备硬件决定。

      • 例 1:老款普通屏(DPR=1):1 个 CSS 像素 = 1×1 个物理像素(写 1px 就对应屏幕 1 个发光点)。
      • 例 2:高清屏(DPR=2,如 iPhone 8):1 个 CSS 像素 = 2×2 个物理像素(写 1px 实际占用屏幕 4 个发光点,视觉上更粗)。
      • 例 3:超高清屏(DPR=3,如 iPhone 14 Pro):1 个 CSS 像素 = 3×3 个物理像素(写 1px 占用 9 个发光点,更粗)。
  2. 关键结论

    • 我们想要的 “0.5px 效果”,本质是 让线条只占用 1 个物理像素(视觉上最细)。
    • 但高清屏(DPR≥2)默认下,1 个 CSS 像素会占用多个物理像素,所以不能直接写 1px,需要通过方法 “压缩” CSS 像素对应的物理像素数量,最终落到 1 个物理像素上。

二、按 DPR 要求分类的 0.5px 实现方法(结合像素关系)

(一)仅 DPR≥2 生效:直接让 CSS 像素对应 1 个物理像素

核心逻辑:利用 DPR≥2 的像素映射关系,让 CSS 像素经过计算后,刚好对应 1 个物理像素。

1. 直接声明 0.5px
  • 像素关系:DPR=2 时,0.5px CSS 像素 = 0.5×2 = 1 个物理像素(刚好满足需求);DPR=3 时,0.5px CSS 像素 = 0.5×3 = 1.5 个物理像素(接近细线条,视觉可接受)。
  • 前提:DPR≥2 + 浏览器支持亚像素渲染(iOS 9+、Android 8.0+)。
  • 代码border: 0.5px solid #000;
  • 局限:DPR=1 时,0.5px CSS 像素 = 0.5×1 = 0.5 个物理像素(屏幕无法渲染,会四舍五入为 0px 或 1px)。
2. transform: scale(0.5) 缩放
  • 像素关系:先写 1px CSS 像素(DPR=2 时对应 2 个物理像素),再缩放 50%,最终 2×50% = 1 个物理像素。

  • 前提:DPR≥2(只有 DPR≥2 时,1px CSS 像素才会对应 ≥2 个物理像素,缩放后才能落到 1 个)。

  • 代码

    .line::after {
      content: '';
      width: 200%;
      height: 1px; /* 1px CSS = 2 物理像素(DPR=2) */
      background: #000;
      transform: scale(0.5); /* 2 物理像素 × 0.5 = 1 物理像素 */
    }
    
  • 局限:DPR=1 时,1px CSS 像素 = 1 物理像素,缩放后变成 0.5 物理像素(屏幕无法渲染,线条消失或模糊)。

3. viewport 缩放(全局方案)
  • 像素关系:通过 initial-scale=1/DPR 改变页面缩放比例,让 1px CSS 像素直接对应 1 个物理像素。

    • 例:DPR=2 时,缩放 50%(1/2),此时 1px CSS 像素 = 1 物理像素(原本 2 物理像素,缩放后压缩为 1);DPR=3 时,缩放 33.3%(1/3),1px CSS 像素 = 1 物理像素。
  • 前提:DPR≥2(高清屏),需配合布局单位(如 rem)调整。

  • 代码

    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <script>
      const dpr = window.devicePixelRatio || 1;
      document.querySelector('meta[name="viewport"]').setAttribute('content', 
        `width=device-width, initial-scale=${1/dpr}, user-scalable=no`
      );
    </script>
    
  • 优势:直接写 border: 1px 就是 1 物理像素,适配所有 DPR≥2 的设备。

  • 局限:全局缩放会影响布局,需重新计算 rem 基准值(如 html { font-size: 16px * dpr })。

(二)DPR≥2 最优,DPR=1 可模拟:视觉层面实现 “细于 1px”

核心逻辑:不依赖像素映射的精准计算,而是通过视觉欺骗或矢量渲染,让线条看起来比 1px 细(DPR=1 时无法实现 1 物理像素,只能模拟)。

1. SVG 绘制
  • 像素关系:SVG 是矢量图,不依赖 CSS 像素和物理像素的映射,直接按 “坐标 + 线条宽度” 渲染。

    • DPR≥2 时:stroke-width="1" + y1="0.5" 直接渲染为 1 个物理像素(矢量渲染支持亚像素精准控制)。
    • DPR=1 时:同样的代码会渲染为 “视觉上 0.5px 细的线条”(实际还是 1 物理像素,但矢量缩放让边缘更细腻,比直接写 1px 看起来细)。
  • 前提:无严格 DPR 要求,所有支持 SVG 的浏览器(几乎所有移动端)。

  • 代码

    <svg width="100%" height="1" xmlns="http://www.w3.org/2000/svg">
      <line x1="0" y1="0.5" x2="100%" y2="0.5" stroke="#000" stroke-width="1" />
    </svg>
    
2. 背景渐变(background-image
  • 像素关系:利用 1px 高的 CSS 容器,通过颜色分割模拟 “半像素”。

    • DPR=2 时:1px CSS 容器 = 2 物理像素高,渐变 “透明 50% + 有色 50%” 刚好对应 1 个物理像素的有色线条。
    • DPR=1 时:1px CSS 容器 = 1 物理像素高,渐变后视觉上是 “半透明细线”(比纯 1px 细,但本质是 1 物理像素的颜色叠加)。
  • 前提:支持 CSS3 渐变的浏览器(iOS 7+、Android 4.4+)。

  • 代码

    .line {
      height: 1px;
      background: linear-gradient(to bottom, transparent 50%, #000 50%);
    }
    
3. box-shadow 模拟
  • 像素关系:DPR=2 时,box-shadow: 0 0.5px 0 #000 中,0.5px CSS 偏移量 = 1 物理像素,形成 1 物理像素的细阴影(视觉上是细线条)。
  • 前提:DPR≥2(DPR=1 时,0.5px 偏移 = 0.5 物理像素,屏幕无法渲染,阴影不显示或模糊)。
  • 代码box-shadow: 0 0.5px 0 #000;

三、最终总结(结合像素关系)

实现方式 像素映射逻辑(核心) 依赖 DPR 视觉效果
直接 0.5px DPR≥2 时,0.5px CSS = 1 物理像素 DPR≥2 精准细线条
transform: scale DPR≥2 时,1px CSS(2 物理像素)缩放 50% = 1 物理像素 DPR≥2 兼容性好,精准细线条
viewport 缩放 DPR≥2 时,缩放 1/DPR 让 1px CSS = 1 物理像素 DPR≥2 全局适配,精准细线条
SVG 绘制 矢量渲染,直接控制 1 物理像素(DPR≥2)或模拟细线条(DPR=1) 无(DPR≥2 最优) 跨设备,细腻无模糊
背景渐变 DPR≥2 时 1px CSS(2 物理像素)颜色分割 = 1 物理像素;DPR=1 时视觉欺骗 无(DPR≥2 最优) 模拟细线条,无兼容性问题
box-shadow DPR≥2 时,0.5px CSS 偏移 = 1 物理像素阴影 DPR≥2 非边框线条适用

核心一句话:所有 “真实 0.5px 效果”(1 物理像素)都依赖 DPR≥2 的高清屏(利用 CSS 像素与物理像素的映射关系);DPR=1 时只能模拟,无法实现物理级半像素。

以下是包含 CSS 像素 / 物理像素 / DPR 关系说明 的 0.5px 兼容代码合集,每个方法都标注核心逻辑和适用场景,可直接复制使用:

一、说明(所有方法通用)

  • 核心目标:让线条最终占用 1 个物理像素(视觉最细)。
  • 像素关系:1 CSS 像素 = DPR × DPR 物理像素(默认缩放 1 时),高清屏(DPR≥2)需通过代码 “压缩” 映射关系。
  • 适配原则:优先选兼容性广、无布局影响的方法(如 SVG、transform 缩放)。

二、6 种实用兼容代码

1. 推荐首选:transform: scale (0.5) 缩放(DPR≥2 生效,兼容性最好)

  • 核心逻辑:1px CSS 像素(DPR=2 时对应 2 物理像素)→ 缩放 50% → 最终 1 物理像素。
  • 适用场景:边框、独立线条,不影响布局。
/* 通用细线条类(上下左右可按需调整) */
.thin-line {
  position: relative;
  /* 父容器需触发 BFC,避免线条溢出 */
  overflow: hidden;
}

.thin-line::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px; /* 1px CSS = 2 物理像素(DPR=2) */
  background: #000; /* 线条颜色 */
  transform: scaleY(0.5); /* 垂直缩放 50% → 2 物理像素 → 1 物理像素 */
  transform-origin: 0 0; /* 缩放原点避免偏移 */
}

/* 横向线条(默认)、纵向线条(按需添加) */
.thin-line-vertical::after {
  width: 1px;
  height: 100%;
  transform: scaleX(0.5);
}
  • 使用方式:<div class="thin-line">内容</div>

2. 跨 DPR 优选:SVG 绘制(所有设备适配,精准无模糊)

  • 核心逻辑:SVG 矢量渲染不依赖像素映射,直接指定 1 物理像素线条(DPR≥2 精准,DPR=1 模拟细线条)。
  • 适用场景:UI 严格还原、跨设备兼容(推荐用于分割线、边框)。
<!-- 横向细线条(直接嵌入,可复用) -->
<svg class="svg-thin-line" width="100%" height="1" xmlns="http://www.w3.org/2000/svg">
  <!-- y1="0.5" + stroke-width="1" → 直接对应 1 物理像素(DPR≥2) -->
  <line x1="0" y1="0.5" x2="100%" y2="0.5" stroke="#000" stroke-width="1" />
</svg>

<!-- 纵向细线条(宽度 100%,高度自适应) -->
<svg class="svg-thin-line-vertical" width="1" height="100%" xmlns="http://www.w3.org/2000/svg">
  <line x1="0.5" y1="0" x2="0.5" y2="100%" stroke="#000" stroke-width="1" />
</svg>

<!-- 样式优化(可选) -->
<style>
  .svg-thin-line {
    display: block;
    margin: 8px 0; /* 上下间距 */
  }
</style>
  • 使用方式:直接嵌入 HTML,修改 stroke 颜色、width/height 适配场景。

3. 现代设备:直接 0.5px 声明(简洁高效,DPR≥2 + 现代浏览器)

  • 核心逻辑:DPR=2 时,0.5px CSS 像素 = 1 物理像素,浏览器直接渲染。
  • 适用场景:iOS 9+、Android 8.0+ 设备,无需兼容旧机型。
/* 直接声明,简洁高效 */
.simple-thin-line {
  border-bottom: 0.5px solid #000; /* 横向线条 */
  /* 纵向线条:border-left: 0.5px solid #000; */
}

/* 兼容写法(部分浏览器需前缀) */
.compact-thin-line {
  border-bottom: 0.5px solid #000;
  -webkit-border-bottom: 0.5px solid #000;
}
  • 使用方式:<div class="simple-thin-line">内容</div>

4. 全局适配:viewport 缩放(DPR≥2,全局细线条统一)

  • 核心逻辑:缩放页面为 1/DPR,让 1px CSS 像素 = 1 物理像素(需配合 rem 布局)。
  • 适用场景:整个页面需要大量细线条,愿意调整布局单位。
<!-- 第一步:设置 viewport(初始缩放 1.0) -->
<meta name="viewport" id="viewport" content="width=device-width, user-scalable=no">

<!-- 第二步:动态调整缩放比例 -->
<script>
  (function() {
    const dpr = window.devicePixelRatio || 1;
    const viewport = document.getElementById('viewport');
    // 缩放 1/DPR,让 1px CSS = 1 物理像素(DPR=2 → 缩放 50%)
    viewport.setAttribute('content', `width=device-width, initial-scale=${1/dpr}, user-scalable=no`);
    
    // 可选:调整 rem 基准值(避免布局错乱)
    const html = document.documentElement;
    html.style.fontSize = `${16 * dpr}px`; // 1rem = 16*dpr px(适配缩放后布局)
  })();
</script>

<!-- 第三步:直接写 1px 即可(此时 1px = 1 物理像素) -->
<style>
  .global-thin-line {
    border-bottom: 1px solid #000; /* 实际是 1 物理像素细线条 */
    margin: 0.5rem 0; /* rem 单位适配缩放后布局 */
  }
</style>
  • 使用方式:全局引入脚本,之后所有 1px 边框都会变成细线条。

5. 视觉模拟:背景渐变(无兼容性问题,DPR≥2 最优)

  • 核心逻辑:1px CSS 容器(DPR=2 时 2 物理像素)→ 颜色分割为 50% 透明 + 50% 有色 → 视觉上 1 物理像素。
  • 适用场景:背景线条、无法用边框 / 伪元素的场景。
/* 横向线条 */
.gradient-thin-line {
  height: 1px;
  width: 100%;
  /* 上半透明,下半有色 → 视觉上细线条 */
  background: linear-gradient(to bottom, transparent 50%, #000 50%);
  background-size: 100% 1px;
}

/* 纵向线条 */
.gradient-thin-line-vertical {
  width: 1px;
  height: 100%;
  background: linear-gradient(to right, transparent 50%, #000 50%);
  background-size: 1px 100%;
}
  • 使用方式:<div class="gradient-thin-line"></div>(独立线条容器)。

6. 非边框场景:box-shadow 模拟(DPR≥2,适合阴影类线条)

  • 核心逻辑:DPR=2 时,0.5px CSS 偏移 = 1 物理像素,阴影即细线条。
  • 适用场景:无需占用布局空间的线条(如文字下方细下划线)。
.shadow-thin-line {
  height: 0;
  /* y 轴偏移 0.5px → 1 物理像素,无模糊、无扩散 */
  box-shadow: 0 0.5px 0 #000;
  -webkit-box-shadow: 0 0.5px 0 #000; /* 兼容 Safari */
}

/* 文字下划线示例 */
.text-thin-underline {
  display: inline-block;
  box-shadow: 0 0.5px 0 #000;
  padding-bottom: 2px;
}
  • 使用方式:<span class="text-thin-underline">带细下划线的文字</span>

三、使用建议

  1. 优先选 transform 缩放 或 SVG 绘制:兼容性广、无布局影响,覆盖 99% 场景。
  2. 现代设备(iOS 9+/Android 8.0+)直接用 0.5px 声明:代码最简洁。
  3. 全局大量细线条用 viewport 缩放:需配合 rem 布局,一次性解决所有线条问题。

面试官:JWT、Cookie、Session、Token有什么区别?

JWT、Cookie、Session、Token 是 Web 开发中常用的身份认证和状态管理技术,它们之间既有区别,也有联系

一、JWT(JSON Web Token)

JWT 是一种开放标准(RFC 7519),用于在网络应用之间安全地传输信息(通常是身份认证信息)。它是一个自包含的、可验证的、不可篡改的字符串,格式如下:

Header.Payload.Signature

三部分组成:

  1. Header(头部):声明类型和签名算法(如 HS256)。
  2. Payload(载荷):包含用户信息(如用户 ID、角色等)和元数据(如过期时间)。
  3. Signature(签名):用密钥对 Header 和 Payload 签名,防止篡改。

特点:

  • 无需服务器存储(无状态)。
  • 可跨域使用(常用于分布式系统、微服务)。
  • 一旦签发,在过期前无法撤销(除非引入黑名单机制)。

二、Cookie

Cookie 是浏览器存储的一小段文本信息,由服务器通过 HTTP 响应头 Set-Cookie 设置,浏览器在后续请求中自动携带。

特点:

  • 自动携带(浏览器行为)。
  • 可设置过期时间、作用域、HttpOnly、Secure 等属性。
  • 容量小(约 4KB)。
  • 可用于存储 Session ID 或 JWT。

三、Session(会话)

Session 是服务器端维护的用户会话状态。通常流程如下:

  1. 用户登录后,服务器创建一个 Session,生成一个唯一的 Session ID
  2. Session ID 通过 Cookie 返回给浏览器。
  3. 浏览器后续请求自动携带该 Cookie,服务器通过 Session ID 查找对应的用户状态。

特点:

  • 状态存储在服务器端(通常是内存、Redis、数据库)。
  • 安全性较高(用户无法直接篡改)。
  • 不适合分布式系统(需要共享 Session 存储)。

四、Token(令牌)

Token 是一个广义概念,指用于身份验证的凭证。JWT 就是一种 Token。

常见 Token 类型:

  • Access Token(访问令牌):用于访问资源。
  • Refresh Token(刷新令牌):用于获取新的 Access Token。
  • JWT:一种结构化的 Token。

五、它们之间的关系与区别

名称 存储位置 状态管理 安全性 适用场景
JWT 客户端 无状态 分布式系统、移动端、API 认证
Cookie 客户端 无状态 存储小量数据、自动携带
Session 服务器端 有状态 传统 Web 应用
Token 客户端 无状态 通用身份凭证(JWT 是其一)

六、常见组合方式

方式一:Session + Cookie(传统 Web)

  • 登录后服务器创建 Session,Session ID 存 Cookie。
  • 每次请求带 Cookie,服务器查 Session 验证身份。

方式二:JWT + Header(前后端分离)

  • 登录后服务器返回 JWT,前端存 localStorage 或 Cookie。
  • 每次请求手动在 Header 中加 Authorization: Bearer <JWT>

方式三:JWT + Cookie(安全增强)

  • JWT 存 Cookie,设置 HttpOnly + Secure,防止 XSS。
  • 浏览器自动携带,服务器解析 JWT 验证身份。

七、总结

  • JWT 是一种自包含的 Token不依赖服务器存储
  • Cookie浏览器存储机制,可存 Session ID 或 JWT。
  • Session服务器存储的用户状态,依赖 Cookie 传递 ID。
  • Token身份凭证,JWT 是其中一种实现。

React组件命名为什么用小写开头会无法运行?

在React项目实际开发中,我们经常用到一些约定俗成的语法,今天我们来聊一聊为什么组件命名时以小写字母开头的组件无法运行的这个现象,这个现象是由什么原因导致的。这个背后有重要的设计原理。

这就不得不谈 JSX,JSX是一种语法扩展,它允许我们在JavaScript中编写类似HTML的代码。

React项目中遇到JSX中的元素时,函数组件首字母大小写决定了React编译这个元素 是原生DOM元素还是自定义组件。 具体来说:

  • 当JSX标签以小写字母开头时,React会将其视为原生DOM元素(如divspan等),并尝试在DOM中创建对应的标签。
  • 当JSX标签以大写字母开头时,React会将其视为自定义组件,并去查找当前作用域中对应的函数或类组件。

那么,这个问题产生的根本原因:JSX 的编译机制

// 当 Babel 编译 JSX 时,它会根据标签的首字母大小写来决定如何转换:
<MyComponent />
<div />

// 编译后的 JavaScript
React.createElement(MyComponent, null);  // 大写 - 作为变量/组件
React.createElement("div", null);        // 小写 - 作为字符串(HTML 标签)
// ❌ 错误:小写组件名
function avatar({ src, alt }) {
  return <img src={src} alt={alt} />;
}

function UserProfile() {
  return (
    <div>
      {/* 这会导致错误 */}
      <avatar src="user.jpg" alt="User" />
      {/* 编译为:React.createElement("avatar", { src: "user.jpg", alt: "User" }) */}
      {/* React 会寻找 <avatar> HTML 标签,但不存在 */}
    </div>
  );
}

Babel有一个插件(通常是@babel/plugin-syntax-jsx或@babel/preset-react)来处理JSX语法。这个插件会将JSX转换为React.createElement调用。

实现这一转换的Babel插件内部,会有一个Visitor来处理JSXElement节点。在Visitor中,它会检查JSXOpeningElement的name属性。如果name是一个JSXIdentifier,并且首字母是小写,则将其作为字符串;如果是大写,则保留为标识符。 那么我们来模拟插件内部是怎么解析的呢?看下方代码

<MyComponent prop="value" />
<div className="container" />

// Babel 解析为 AST(抽象语法树)
{
  type: 'JSXElement',
  openingElement: {
    type: 'JSXOpeningElement',
    name: {
      type: 'JSXIdentifier',
      name: 'MyComponent'  // 或 'div'
    }
    // ...
  }
}

转换阶段核心代码

export default function (babel) {
  const { types: t } = babel;
  
  return {
    name: "transform-jsx",
    visitor: {
      JSXElement(path) {
        const openingElement = path.node.openingElement;
        const tagName = openingElement.name.name;
        
        // 关键判断逻辑
        let elementType;
        if (/^[a-z]/.test(tagName)) {
          // 小写开头 -> HTML 标签 -> 字符串
          elementType = t.stringLiteral(tagName);
        } else {
          // 大写开头 -> 组件 -> 标识符
          elementType = t.identifier(tagName);
        }
        
        // 转换为 React.createElement 调用
        const createElementCall = t.callExpression(
          t.identifier('React.createElement'),
          [elementType, ...processAttributes(openingElement.attributes)]
        );
        
        path.replaceWith(createElementCall);
      }
    }
  };
}

实际 Babel 插件源码分析

在 @babel/plugin-transform-react-jsx 中:

function transformJSX() {
  return {
    visitor: {
      JSXElement(path) {
        const { node } = path;
        const tag = node.openingElement.name;
        
        let tagExpr;
        if (tag.type === 'JSXIdentifier') {
          const tagName = tag.name;
          
          // 关键判断:首字母是否小写
          if (
            /^[a-z][a-z0-9]*$/.test(tagName) || 
            // 或者是已知的 SVG 标签等
            knownHTMLTags.has(tagName) ||
            knownSVGTags.has(tagName)
          ) {
            // HTML/SVG 标签 -> 字符串字面量
            tagExpr = types.stringLiteral(tagName);
          } else {
            // 组件 -> 标识符
            tagExpr = types.identifier(tagName);
          }
        } else if (tag.type === 'JSXMemberExpression') {
          // 处理 <MyComponent.SubComponent /> 这种情况
          tagExpr = transformJSXMemberExpression(tag);
        }
        
        const createElementCall = types.callExpression(
          types.identifier('React.createElement'),
          [tagExpr, ...createAttributes(node.openingElement.attributes)]
        );
        
        path.replaceWith(createElementCall);
      }
    }
  };
}

完整的编译示例如下

JSX

  return (
    <div className="app">
      <Header title="Welcome" />
      <main className="content">
        <UserList users={users} />
        <footer className="site-footer">
          <Copyright year={2024} />
        </footer>
      </main>
    </div>
  );
}

Babel 编译后的 JavaScript

  return React.createElement(
    "div", 
    { className: "app" },
    React.createElement(Header, { title: "Welcome" }),
    React.createElement(
      "main", 
      { className: "content" },
      React.createElement(UserList, { users: users }),
      React.createElement(
        "footer", 
        { className: "site-footer" },
        React.createElement(Copyright, { year: 2024 })
      )
    )
  );
}

以上就是组件命名大小写在react插件中的运行示例演示,解释了为什么组件用小写开头无法运行。

看似简单的首字母大小写判断,实际上是整个 React 开发设计和生态的重要一环。

我是大布布将军,一个AICodeing时代下的前端开发思考者。

前端高频面试题之Vue(高级篇)

1、说一下 Vue.js 的响应式原理

1.1 Vue2 响应式原理

核心原理就是通过 Object.defineProperty 对对象属性进行劫持,重新定义对象的 gettersetter,在 getter 取值时收集依赖,在 setter 修改值时触发依赖更新,更新页面。

Vue2 对数组和对象做了两种不同方式的处理。

监听对象变化:

针对对象来说,Vue 会循环遍历对象的每一个属性,用 defineReactive 重新定义 gettersetter


function defineReactive(target,key,value){
    observer(value);
    Object.defineProperty(target,key,{ ¸v
        get(){
            // ... 收集依赖逻辑
            return value;
        },
        set(newValue){
            if (value !== newValue) {
                value = newValue;
                observer(newValue) // 把新设置的值包装成响应式
            }
            // ...触发依赖更新逻辑
        }
    })
}
function observer(data) {
    if(typeof data !== 'object'){
        return data
    }
    for(let key in data){
        defineReactive(data,key,data[key]);
    }
}

监听数组变化:

我们都知道,数组其实也是对象,同样可以用 Object.defineProperty 劫持数组的每一项,但如果数组有100万项,那就要调用 Object.defineProperty 一百万次,这样的话性能太低了。鉴于平时我们操作数组大都是采用数组提供的原生方法,于是 Vue 对数组重写原型链,在调用7个能改变自身的原生方法(pushpopshiftunshiftsplicesortreverse)时,通知页面进行刷新,具体实现过程如下:

// 先拿到数组的原型
const oldArrayProtoMethods = Array.prototype
// 用Object.create创建一个以oldArrayProtoMethods为原型的对象
const arrayMethods = Object.create(oldArrayProtoMethods)
const methods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'sort',
    'reverse',
    'splice'
]
methods.forEach(method => {
    // 给arrayMethods定义7个方法
    arrayMethods[method] = function (...args){
        // 先找到数组对应的原生方法进行调用
        const result = oldArrayProtoMethods[method].apply(this, args)
        // 声明inserted,用来保存数组新增的数据
        let inserted
        // __ob__是Observer类实例的一个属性,data中的每个对象都是一个Observer类的实例
        const ob = this.__ob__
        switch(method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
            default:
                break
        }
        // 比如有新增的数据,新增数据也要被定义为响应式
        if(inserted) ob.observeArray(inserted)
        // 通知页面更新
        ob.dep.notify()
        return result
    }
})

Object.defineProperty的缺点:

  1. 无法监听新增属性和删除属性的变化,需要通过 $set$delete 实现。
  2. 监测数组的索引性能太低,故而直接通过数组索引改值无法触发响应式。
  3. 初始化时需要一次性递归调用,性能较差。

1.2 Vue3 的响应式改进

Vue3 采用 Proxy + Reflect 配合实现响应式。能解决上述 Object.defineProperty 的所有缺陷,唯一缺点就是兼容性没有 Object.defineProperty 好。

let handler = {
  get(target, key) {
    if (typeof target[key] === "object") {
      return new Proxy(target[key], handler);
    }
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    let oldValue = target[key];
    if (oldValue !== value) {
      return Reflect.set(target, key, value);
    }
    return true;
  },
};
let proxy = new Proxy(obj, handler);

2、介绍一下 Vue 中的 diff 算法?

Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。

比较过程:

  1. 先比较是否是相同节点。
  2. 相同节点比较属性,并复用老节点。
  3. 比较儿子节点,考虑老节点和新节点儿子的情况。
  4. 优化比较:头头、尾尾、头尾、尾头。
  5. 比对查找进行复用。

Vue3 在这个比较过程的基础上增加了最长递增子序列实现diff算法。

  • 找出不需要移动的现有节点。
  • 只对需要移动的节点进行操作。
  • 最小化 DOM 操作次数。

3、Vue 的模板编译原理是什么?

Vue 中的模板编译就是把我们写的 template 转换为渲染函数(render function) 的过程,它主要经历3个步骤:

  1. 解析(Parse):将 template 模板转换成 ast 抽象语法树。
  2. 优化(Optimize):对静态节点做静态标记,减少 diff 过程中的比对。
  3. 生成(Generate):重新生成代码,将 ast 抽象语法数转化成可执行的渲染函数代码。

3.1 解析阶段

<div id="app">
  <p>{{ message }}</p>
</div>
  • 用 HTML 解析器将模板解析为 AST。
  • AST中用 js 对象描述模板,里面包含了元素类型、属性、子节点等信息。
  • 解析指令(v-for、v-if)和事件(@click)、插值表达式{{}}等 vue 语法。

3.2 优化阶段

  • 遍历上一步生成的 ast,标记静态节点,比如用 v-once 的节点,以及没有用到响应式数据的节点。
  • 标记静态根节点,避免不必要的渲染。

3.3 代码生成阶段

vue2 解析结果:

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('p', [_v(_s(message))])])
  }
}
  • _c: 是 createElement 的别名,用于创建 VNode。
  • _v: 创建文本 VNode。
  • _s: 是 toString 的别名,用于将值转换为字符串。

vue3 解析结果:

import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}
  • _openBlock: 开启一个"block"区域,用于收集动态子节点。
  • _createElementBlock: 创建一个块级虚拟 DOM 节点。
  • _createElementVNode: 创建一个普通虚拟 DOM 节点。
  • _toDisplayString: 将响应式数据 _ctx.message 转换为显示字符串,或者处理 null/undefined 等值,确保它们能正确渲染为空白字符串。

vue2在线编译:template-explorer.vuejs.org/

vue3在线编译:v2.template-explorer.vuejs.org/

运行时+编译(runtime-compiler) vs 仅运行时(runtime-only):

  1. 完整版(运行时+编译):
    • 包含编译模块,可以写 template 模版。
    • 体积较大(~30kb)。
  2. 仅运行时版本
    • 需要在打包时使用 vue-loader 进行编译。
    • 体积较小(~20kb)。

平时开发项目推荐使用仅运行时(runtime-only)版本。

编译后的特点:

  1. 虚拟DOM:渲染函数生成的是虚拟DOM节点(VNode)。
  2. 响应式绑定:渲染函数中的变量会自动建立依赖关系。
  3. 性能优化:通过静态节点标记减少不必要的更新。

4、v-show 和 v-if 的原理

简单来说,v-if 内部是通过一个三元表达式来实现的,而 v-show 则是通过控制 DOM 元素的 display 属性来实现的。

v-if 源码:

function genIfConditions (
    conditions: ASTIfConditions,
    state: CodegenState,
    altGen?: Function,
    altEmpty?: string
    ): string {
    if (!conditions.length) {
        return altEmpty || '_e()'
    }
    const condition = conditions.shift()
    if (condition.exp) {   // 如果有表达式
        return `(${condition.exp})?${ // 将表达式作为条件拼接成元素
        genTernaryExp(condition.block)
        }:${
        genIfConditions(conditions, state, altGen, altEmpty)
        }`
    } else {
        return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
    }

    // v-if with v-once should generate code like (a)?_m(0):_m(1)
    function genTernaryExp (el) {
        return altGen
        ? altGen(el, state)
        : el.once
            ? genOnce(el, state)
            : genElement(el, state)
    }
}

v-show 源码:

{
    bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    const originalDisplay = el.__vOriginalDisplay =
        el.style.display === 'none' ? '' : el.style.display // 获取原始显示值
        el.style.display = value ? originalDisplay : 'none' // 根据属性控制显示或者隐藏
    }  
} 

5、v-if 和 v-for 哪个优先级更高?为什么?

  • vue2 中 v-for 的优先级比 v-if 高,它们作用于一个节点上会导致先循环后对每一项进行判断,浪费性能。
  • vue3 中 v-if 的优先级比 v-for 高,这就会导致 v-if 中的条件无法访问 v-for 作用域名中定义的变量别名。
<li v-for="item in arr" v-if="item.visible">
  {{ item}}
</li>

以上代码在 vue3 的编译结果如下:

import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createCommentVNode as _createCommentVNode } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_ctx.item.visible)
    ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(_ctx.arr, (item) => {
        return (_openBlock(), _createElementBlock("li", null, _toDisplayString(item), 1 /* TEXT */))
      }), 256 /* UNKEYED_FRAGMENT */))
    : _createCommentVNode("v-if", true)
}

可以看出 vue3 在编译时会先判断 v-if,然后再走 v-for 的循环,所以在 v-if 中自然就无法访问 v-for 作用域名中定义的变量别名。

这样的写法在 vue3 中会抛出一个警告⚠️,[Vue warn]: Property "item" was accessed during render but is not defined on instance,导致渲染失败。

以上代码在 vue2 还不能直接编译,因为 vue2 的组件需要一个根节点,所以我们在外层加一个 div

<div>
  <li v-for="item in arr" v-if="item.visible">
    {{ item}}
  </li>
</div>

其编译结果如下:

function render() {
  with(this) {
    return _c('div', _l((arr), function (item) {
      return (item.visible) ? _c('li', [_v("\n    " + _s(item) + "\n  ")]) :
        _e()
    }), 0)
  }
}

很明显是先循环 arr,然后每一项再用 item.visible 去判断的,也印证了在 vue2 中, v-for 的优先级高于 v-if

所以不管是 vue2 还是 vue3,都不推荐同时使用 v-ifv-for,更好的方案是采用计算属性,或者在外层再包裹一个容器元素,将 v-if 作用在容器元素上。

6、nextTick 的原理是什么?

6.1 Vue2 的 nextTick:

  • 首选微任务:
    • Promise.resolve().then(flushCallbacks):最常见,使用 Promise 创建微任务。
    • MutationObserver:如果 Promise 不可用,创建一个文本节点,修改其内容触发 MutationObserver 的观察器回调。
  • 回退宏任务:
    • setImmediate:如果环境支持 setImmediate,比如 node 环境,则会优先使用 setImmediate 。
    • setTimeout(flushCallbacks, 0):最后使用定时器。

这里体现了优雅降级的思想。

6.2 Vue3 的 nextTick:

  • 由于 Vue3 不再考虑 promise 的兼容性,所以 nextTick 的实现原理就是 promise.then 方法。

7、Vue.set 方法是如何实现的?

Vue2的实现:在 Vue 2 中,Vue.set 的实现主要位于 src/core/observer/index.js 中:

export function set (target: Array | Object, key: any, val: any): any {
    // 1.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    // 2.如果是对象本身的属性,则直接添加即可
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    const ob = (target: any).__ob__
    // 3.如果是响应式的也不需要将其定义成响应式属性
    if (!ob) {
        target[key] = val
        return val
    }
    // 4.将属性定义成响应式的
    defineReactive(ob.value, key, val)
    // 5.通知视图更新
    ob.dep.notify()
    return val
}

Vue3 中 set 方法已经被移除,因为 proxy 天然弥补 vue2 响应式的缺陷。

8、Vue.use 是干什么的?原理是什么?

Vue.use 是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。

Vue.use 源码:

Vue.use = function (plugin: Function | Object) {
    // 插件不能重复的加载
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
        return this
    }
    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)  // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
    if (typeof plugin.install === 'function') { // 调用插件的install方法
        plugin.install.apply(plugin, args)  Vue.install = function(Vue,args){}
    } else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
        plugin.apply(null, args) 
    }
    installedPlugins.push(plugin) // 缓存插件
    return this
}

9、介绍下 Vue 中的 mixin,Vue3 为何不再推荐使用它?

mixin 是 Vue 2 中一种复用组件逻辑的方式,允许将可复用的配置(data、methods、computed、lifecycle hooks 等)抽离成一个对象,然后通过 mixins: [] 合并到组件中。支持全局注入和局部注入。

  • 作用:抽离公共的业务逻辑
  • 原理:类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。

mixin 的优点:

  • 复用逻辑(如表单验证、权限判断)。
  • 全局注入(如日志、埋点)。
  • 减少重复代码。

mixin 中有很多缺陷:

  • 命名冲突问题:mixin 中的变量、函数名可能会与组件中的重名。
  • 依赖问题:
  • 数据来源问题:

vue3 不再推荐使用它的理由如下:

问题 说明
1. 隐式依赖 & 数据来源不明确 组件行为来自多个 mixin,难以追踪 data、methods 是从哪里来的。
2. 命名冲突 多个 mixin 可能定义同名 data、methods,合并规则复杂(同名 methods 后者覆盖前者,data 合并为对象,同名 Key 后者覆盖前者)。
3. 调试和维护困难 父组件无法知道子组件内部有哪些 mixin 注入的属性,排查 bug 和调试困难。
4. 不利于 Tree-shaking 打包时难以移除未使用的 mixin 代码。
5. 与 Composition API 理念冲突 Mixin 是“横切关注点”,而 Composition API 强调显式、可组合的逻辑。

Vue 3 推荐替代方案:Composition API + 可复用函数(Composables)。

特性 Mixin Composables
数据来源明确 隐式 显式(import)
是否有命名冲突问题
逻辑封装 全局污染 按需引入
Tree-shaking 支持
TypeScript 支持

对于全局混入(Global Mixin),Vue3 虽然提供了 app.mixin(),但不推荐,推荐使用:

  1. app.config.globalProperties
  2. app.provide 在顶层提供数据,组件通过 inject 方法消费数据。

10、介绍下 Vue.js 中的函数式组件、异步组件和递归组件

10.1 函数式组件(Functional Components)

函数式组件是一种轻量级、无状态的组件形式。它们很像纯函数:接收 props,返回 虚拟 DOM(vnode)。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this),也不会触发常规的组件生命周期钩子。没有响应式系统、生命周期和实例的开销,函数式组件自然在渲染上更加高效和快速。

总而言之,函数式组件有无状态无this无生命周期性能更高等特点。

使用场景:适合简单、静态的 UI 元素,如列表项或包装组件。

在 Vue 2 中,通过 functional: true 声明;在 Vue 3 中,函数式组件更简单,直接返回渲染函数。

Vue2 示例:

<template functional>
  <div>{{ props.msg }}</div>
</template>

或者 js 形式:

export default {
  functional: true,
  props: ['msg'],
  render(h, { props }) {
    return h('div', props.msg);
  }
};

Vue3 示例:

<script setup>
import { h } from 'vue';

const FunctionalComp = (props) => h('div', props.msg);
</script>

<template>
  <FunctionalComp msg="Hello Functional" />
</template>

10.2 异步组件(Async Components)

异步组件是一种懒加载(Lazy Loading)机制,用于按需加载组件代码,优化初始加载时间和性能。Vue 会动态导入组件,只有在使用时才下载和渲染,常用于路由或大型组件。

特点:

  • 通过 import() 动态加载,返回 Promise。
  • 支持加载中(loading)、错误(error)和超时(timeout)处理。
  • 在 Vue 3 中,使用 defineAsyncComponent 更规范,支持与 <Suspense> 结合(Vue 3 独有,用于统一处理异步)。

Vue2 示例:

<script>
export default {
  components: {
    AsyncComp: () => import('./AsyncComp.vue')
  }
};
</script>

<template>
  <AsyncComp />
</template>

Vue3 示例:

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'));
</script>

<template>
  <Suspense>
    <template #default>
      <AsyncComp />
    </template>
    <template #fallback>加载中...</template>
  </Suspense>
</template>

10.3 递归组件(Recursive Components)

递归组件是指组件内部调用自身,用于处理树形或嵌套数据结构,如菜单、树视图或评论回复。Vue 支持组件自引用,但需注意避免无限循环(通过条件终止递归)。

特点:

  • 组件需有名称(name 选项),才能自引用。
  • 常结合 v-for 和 props 传递数据。

Vue 2 示例:

<template>
  <ul>
    <li v-for="item in tree" :key="item.id">
      {{ item.name }}
      <Tree v-if="item.children" :tree="item.children" />
    </li>
  </ul>
</template>

<script>
export default {
  name: 'Tree',  // 必须有 name
  props: ['tree']
};
</script>

Vue 3 示例:

<script setup>
import { defineAsyncComponent } from 'vue';  // 可选:异步加载避免循环

const Tree = defineAsyncComponent(() => import('./Tree.vue'));  // 自引用
defineProps(['tree']);
</script>

<template>
  <ul>
    <li v-for="item in tree" :key="item.id">
      {{ item.name }}
      <Tree v-if="item.children" :tree="item.children" />
    </li>
  </ul>
</template>

11、Vue.js 中的 vue-loader 是什么?

Vue-loader 是一个专为 Vue.js 设计的 Webpack loader(加载器),其主要作用是将 Vue 的单文件组件(Single-File Components,简称 SFC,即 .vue 文件)转换为可执行的 JavaScript 模块。 它允许开发者以一种结构化的方式编写组件,将模板(template)、脚本(script)和样式(style)封装在同一个文件中,便于管理和维护。

核心功能:

  • 解析 SFC 文件:Vue-loader 会自动处理 .vue 文件中的三个部分:
    • template 部分:编译为 Vue 的渲染函数(render function)。
    • script 部分:提取为组件的 JavaScript 逻辑,支持 ES 模块和 TypeScript。
    • style部分:处理 CSS,支持预处理器(如 Sass、Less)并可选地应用 scoped(作用域样式)或 CSS Modules。
  • 热重载(Hot Module Replacement,HMR):在开发模式下,支持组件的热更新,无需刷新页面即可看到变化,提高开发效率。
  • 自定义块(Custom Blocks):支持扩展,如添加 <docs> 或其他自定义标签,用于文档生成或其他工具集成。
  • 预处理器支持:无缝集成 Babel、PostCSS 等工具链。

12、Vue.extend 方法的作用?

Vue.extend方法可以作为基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

Vue2 示例:

var dialog = Vue.extend({
  template: "<div>{{hello}} {{world}}</div>",
  data: function () {
    return {
      hello: "hello",
      world: "world",
    };
  },
});
// 创建 dialog 实例,并手动挂载到一个元素上。
new dialog().$mount("#app");

注意:在 Vue.extend 中的 data 必须是一个函数,要不然会报错。

Vue3 示例:

Vue3 中不在使用 Vue.extend 方法,而是采用render方法进行手动渲染。

<!-- Modal.vue -->
<template>
  <div class="modal">这是一个弹窗</div>
</template>

<script>
export default {
  name: 'Modal',
}
</script>
<template>
  <div id="box"></div>
</template>

<script setup>
import { h, render, createApp, onMounted } from 'vue'
import Modal from './Modal.vue'

onMounted(() => {
  render(h(Modal), document.getElementById('box'));
})
</script>

13、keep-alive 的原理

<keep-alive> 是 Vue.js 的内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例,避免重复渲染和状态丢失,提高性能。它是一个抽象组件(abstract: true),不会渲染到 DOM,也不会出现在组件树中,而是通过插槽(slots)和自定义 render 函数实现缓存逻辑。

核心实现机制:

  1. 抽象组件与 Render 函数:
  • <keep-alive> 通过 render 函数处理包裹的内容(通常是动态组件,如 <component> 或 v-if 切换的组件)。
  • 在 render 中,它从插槽获取子组件的 VNode(虚拟节点)。如果子组件有 key(推荐使用),则用 key 作为缓存标识;否则用组件的 tag 或 cid(组件 ID)。
  • 如果组件已缓存,直接返回缓存的 VNode(设置 vnode.componentInstance.keepAlive = true 以标记缓存状态);否则,渲染新实例并存入缓存。
  1. 缓存存储:
  • 内部使用一个对象(this.cache)作为缓存 Map,以 key 为键,值为 VNode 对象(包含组件实例)。
  • 当组件首次渲染时,存入缓存;切换回时,从缓存取出,避免重新创建实例和执行 mounted 钩子。
  1. LRU 缓存算法:
  • 支持 max 属性设置最大缓存数量(Vue 2.5+)。
  • 使用 Least Recently Used(最近最少使用)算法:当缓存超出 max 时,删除最久未访问的组件(通过 this.keys 数组跟踪访问顺序)。
  • 访问组件时,将其 key 移到数组末尾(最近使用);超出时,删除数组开头的 key,并销毁对应实例(调用 $destroy)。
  1. 过滤规则(include/exclude):
  • 通过 include(白名单)和 exclude(黑名单)属性决定哪些组件缓存,支持字符串、正则、数组或函数。
  • 在 created 钩子中,监听这些 prop 的变化,并调用 pruneCache 更新缓存(移除不匹配的组件)。
  1. 生命周期钩子:
  • 缓存组件不会触发 destroyed/unmounted,而是使用 activated(激活时)和 deactivated(失活时)钩子。
  • 这允许开发者在切换时管理状态(如暂停定时器),而非完全销毁。

注意事项:

  • 只缓存一个直接子组件(插槽内容),不支持多个。
  • Vue 3 中原理类似,但优化了 VNode 处理和 Composition API 支持。
  • 潜在问题:缓存过多导致内存占用;需手动清理资源(如在 deactivated 中停止监听)。

14、Vue.js 中使用了哪些设计模式?

1. 观察者模式 (Observer Pattern)

  • 描述:Vue 的响应式系统使用观察者模式,通过 Proxy (Vue 3) 或 Object.defineProperty (Vue 2) 拦截对象属性的 get/set 操作。当数据变化时,通知订阅者(Watcher)更新视图。
  • 应用:在 reactive() 函数中,返回 Proxy 对象,get 陷阱用于依赖收集 (track),set 陷阱用于触发更新 (trigger)。
  • 优势:实现了细粒度的变更检测,避免全局重渲染。

2. 发布-订阅模式 (Publish-Subscribe Pattern)

  • 描述:Vue 的事件系统和响应式通知机制采用 Pub-Sub 模式。数据变化时发布事件,订阅者(如组件渲染函数)接收并响应。
  • 应用:在响应式系统中,trigger() 函数检索订阅者效果并调用它们;事件 API 如 emitemit 和 on 也基于此。
  • 优势:解耦了数据生产者和消费者,支持异步更新。

3. 代理模式 (Proxy Pattern)

描述:Vue 3 的响应式系统直接使用 ES6 Proxy 作为代理层,拦截对象操作,实现透明的响应式。 应用:reactive() 返回 Proxy 对象,代理目标对象的访问和修改。 优势:比 Vue 2 的 defineProperty 更强大,支持数组和 Map/Set 等类型。

4. 策略模式 (Strategy Pattern)

  • 描述:Vue 的虚拟 DOM diff 算法使用不同策略(如 key-based diff 或简单 patch)来优化更新。
  • 应用:在渲染过程中,根据节点类型选择 diff 策略。
  • 优势:最小化 DOM 操作,提高渲染效率。

5. 单例模式(Singleton Pattern)

  • 描述:整个程序中有且仅有一个实例。
  • 应用:vuex 的 store 和插件。
  • 优势:全局唯一、节约资源、便于管理。

6. 工厂模式(Factory Pattern)

  • 描述:提供了一种创建对象的方式,使得创建对象的过程与使用对象的过程分离。
  • 应用:Vue2 中的组件创建,传入参数给 createComponentInstance 就可以创建实例。
  • 优势:解藕,易于维护。

15、Vue.js 应用中常见的内存泄漏来源有哪些?

  1. 未清理的事件监听器、定时器、动画
<script setup>
import { onMounted, onUnmounted } from 'vue';

let timer = null;
let controller = null;
let raf = null;

onMounted(() => {
  // 定时器
  timer = setInterval(() => {}, 1000);
  // 动画
  raf = requestAnimationFrame(() => {});
  // 事件监听
  window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
  clearInterval(timer);
  cancelAnimationFrame(this.raf);
  window.removeEventListener('resize', handleResize);
});
</script>
  1. 未移除的第三方库实例
<script setup>
import { onMounted, onUnmounted } from 'vue';

let chart = null;

onMounted(() => {
  chart = echarts.init(this.$refs.chart);
});

onUnmounted(() => {
  chart?.dispose();
});
</script>
  1. 事件总线(Event Bus)未解绑

vue2 用可以用 new Vue 全局创建一个事件总线实例,或者在组件中直接使用 this.$onthis.$emitthis.$off

vue3 则需要借助第三库,比如 mitt 来实现事件总线。

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

// 创建事件总线实例
const emitter = mitt();

onMounted(() => {
  emitter.on('update', this.handler);
});

onUnmounted(() => {
  emitter.off('update', this.handler);
});
</script>

顺便提一下, vue3 为啥去掉 $on、$emit、$off 这些 API,主要有以下原因:

  1. 设计理念的调整

Vue 3 更加注重组件间通信的明确性和可维护性。$on 这类事件 API 本质上是一种 "发布 - 订阅" 模式,容易导致组件间关系模糊(多个组件可能监听同一个事件,难以追踪事件来源和流向)。Vue 3 推荐使用更明确的通信方式,如:

  • 父子组件通过 props 和 emit 通信
  • 跨组件通信使用 provide/inject 或 Pinia/Vuex 等状态管理库
  • 复杂场景可使用专门的事件总线库(如 mitt
  1. 与 Composition API 的适配

Vue 3 主推的 Composition API 强调逻辑的封装和复用,而 $on 基于选项式 API 的实例方法,与 Composition API 的函数式思维不太契合。移除后,开发者可以更自然地使用响应式变量或第三方事件库来实现类似功能。

  1. 减少潜在问题
  • $on 容易导致内存泄漏(忘记解绑事件)
  • 事件名称可能冲突(全局事件总线尤其明显)
  • 不利于 TypeScript 类型推断,难以实现类型安全

4. 未清理的 Watcher

Vue 本身不会泄漏内存,泄漏几乎都来自开发者未清理的副作用。养成“创建即清理”的习惯,使用 beforeDestroy 或者 onUnmounted 集中清理,在使用 keep-alive 的组件中,视情况在 deactivated 钩子中清理资源。

16、Vue.js 中的性能优化手段有哪些?

16.1 数据相关

  • Vue2 中数据层级不易过深,合理设置响应式数据;
  • Vue2 非响应式数据可以通过 Object.freeze()方法冻结属性;
  • 合理使用 computed,利用其缓存能力提高性能。

16.2 组件相关

  • 控制组件粒度(Vue 采用组件级更新);
  • 合适场景可使用函数式组件(函数式组件开销低);
  • 采用异步组件(借助构建工具的分包的能力,减少主包体积);
  • 在组件卸载或者非激活状态及时清除定时器、DOM事件、事件总线、三方库的实例等。
  • v-on 按需监听、使用动态 watch 和及时销毁 watch。

16.3 渲染相关

  • 合理设置 key 属性;
  • v-show 和 v-if 的选取;
  • 使用防抖、节流、分页、虚拟滚动、时间分片等策略;
  • 合理使用 keep-alive 、v-once、v-memo 进行逻辑优化。

结语

以上是整理的 Vue 高级的高频面试题,如有错误或者可以优化的地方欢迎评论区指正,后续还会更新 Vuex 和 Vue-router 相关面试题。

Ajax 数据请求详解与实战

在现代前端开发中,网页与服务器的数据交互已经成为核心功能之一。
而支撑这一功能的技术之一,正是 Ajax(Asynchronous JavaScript and XML)
今天我们就来系统了解一下 Ajax 的工作原理、请求流程以及一个完整的示例。


一、什么是 Ajax?

Ajax 全称是 异步 JavaScript 和 XML
中文意思是“异步的 JavaScript 与 XML”。

虽然名字里有 XML,但如今开发中我们更多使用 JSON 格式来传输数据。
它最大的特点是:在不刷新页面的情况下与服务器通信,动态更新网页内容。


二、Ajax 的基本工作流程

Ajax 的实现依赖浏览器内置的一个对象:XMLHttpRequest(简称 XHR)。
通过这个对象,我们可以主动发起 HTTP 请求并接收响应。

流程如下:

  1. 创建请求对象

    const xhr = new XMLHttpRequest();
    
  2. 配置请求信息

    xhr.open(method, url, async);
    
    • method:请求方式(如 GETPOST
    • url:目标接口地址
    • async:是否异步(true 为异步,false 为同步)
  3. 发送请求

    xhr.send();
    
  4. 监听请求状态变化

    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
            const data = JSON.parse(xhr.responseText);
            console.log(data);
        }
    };
    

三、readyState 状态说明

在 Ajax 请求过程中,readyState 表示请求的不同阶段:

状态码 含义 说明
0 初始化 请求未初始化
1 打开 已调用 open(),还未发送
2 发送 已发送请求,接收到响应头
3 接收 正在接收服务器数据
4 完成 请求完成,已接收到全部响应数据

同时要注意:

  • xhr.status 表示 HTTP 响应状态(例如 200 表示成功)。
  • xhr.responseText 是服务器返回的字符串数据。

四、实战示例:请求 GitHub 数据

下面是一个完整的 Ajax 请求示例,用来获取 GitHub 上某组织的成员数据:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ajax 数据请求</title>
</head>
<body>
    <ul id="members"></ul>

    <script>
        // 1. 创建 XMLHttpRequest 对象
        const xhr = new XMLHttpRequest();

        // 2. 打开请求 (异步)
        xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);

        // 3. 监听状态变化
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4 && xhr.status === 200) {
                // 4. 解析 JSON 数据
                const data = JSON.parse(xhr.responseText);
                console.log(data);

                // 5. 渲染到页面
                document.getElementById('members').innerHTML =
                    data.map(item => `<li>${item.login}</li>`).join('');
            }
        };

        // 6. 发送请求
        xhr.send();
    </script>
</body>
</html>

执行后,浏览器会在控制台打印出返回的数据,同时在网页中显示成员列表。


五、同步与异步的区别

  • 同步请求(async = false)
    浏览器会等待服务器响应后再执行后续代码,页面会“卡住”。
  • 异步请求(async = true)
    浏览器不会等待,能继续执行后续代码,响应回来后再触发回调函数。
    —— 这正是 Ajax 的核心优势所在。

六、现代替代方案:Fetch 与 Axios

如今,在实际开发中,我们更常用以下方式:

  • Fetch API:更简洁现代的异步请求方式。
fetch('https://api.github.com/orgs/lemoncode/members')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));

🧩 七、小结

要点 内容
Ajax 全称 异步 JavaScript 和 XML
核心对象 XMLHttpRequest
关键方法 open()send()onreadystatechange
常用属性 readyStatestatusresponseText
主要用途 实现网页的动态数据加载,不刷新页面即可更新内容

✅ 结语

Ajax 是前端与服务器通信的基础技术之一。
理解其底层原理不仅能帮助你更好地使用 fetch
更能让你彻底理解浏览器异步通信机制的本质。

iOS 社招 - Runtime 相关知识点

核心概念 本质:runtime是 oc 的一个运行时库(libobjc.A,dylib),它为 oc 添加了 面向对象的能力 以及 运行时的动态特性。 面向对象的能力:rutime用 C 语言实现了类

前端代码规范体系建设与团队落地实践

一、为什么需要前端代码规范? 在现代前端开发中,代码规范是团队协作的基石。随着项目规模扩大和团队成员增多,统一的代码规范能够带来显著的收益: 核心价值 提升代码可读性:一致的代码风格让团队成员能够快速

如何解析 zip 文件

41b1cd13e28d537170538a42a63d1018.jpg

前言

最近在做 zip 包解析的相关工作,总结 zip 包相关协议以及 jszip 的解析流程是怎么样的。

什么是 zip 文件

zip 文件是一种压缩文件格式,它可以将多个文件压缩成一个文件,从而节省空间。

如何解析

在解析 zip 文件之前,我们在项目中创建了一个 zip 文件,并添加了 1 个 txt 文件。

15.png

本文的 zip 案例文件

读取 zip 文件内容

<body>
    <div>
        <div>上传一个zip文件,并在控制台查看内存存储方式</div>
        <input id="uploadInput" type="file" multiple="">
        <br>
    </div>
    <script>
        const uploadInput = document.getElementById("uploadInput");
        uploadInput.addEventListener(
            "change",
            () => {
                const file = uploadInput.files[0];
                console.log('File 对象,只读不能写:', file)
                const reader = new FileReader();
                reader.onload = function (e) {
                    console.log('Array Buffer对象,不能读写:', e.target.result)
                    const uint8arr = new Uint8Array(e.target.result);
                    console.log('Uint8Array对象,可读写:', uint8arr);
                };
                reader.onerror = function (e) {
                    console.log("error", e.target.error);
                };
                reader.readAsArrayBuffer(file);
            },
            false
        );

    </script>
</body>

image.png

zip 文件协议格式

zip文件官方规范可以看这里。从 官方文档 可以看出,

组成标准zip文件:本地文件头 + 中央目录头 + 中央目录记录结尾

[local file header 1] // 本地文件头
[file data 1] 

[local file header 2] // 本地文件头
[file data 2] 

[local file header 3] // 本地文件头
[file data 3] 

[central directory header 1] // 中央目录头
[central directory header 2] // 中央目录头
[central directory header 3] // 中央目录头

[end of central directory record] // 中央目录记录结尾

本地文件头

本地文件头是 zip 文件的第一个部分,它包含了文件的名称、大小、压缩方式等信息。其格式如下:

local file header signature     4 bytes  (0x04034b50) // 本地文件头签名
version needed to extract       2 bytes // 版本需要提取
general purpose bit flag        2 bytes // 通用目的位标志
compression method              2 bytes // 压缩方法
last mod file time              2 bytes // 最后修改文件时间
last mod file date              2 bytes // 最后修改文件日期
crc-32                          4 bytes // CRC-32
compressed size                 4 bytes // 压缩大小
uncompressed size               4 bytes // 未压缩大小
file name length                2 bytes // 文件名称长度
extra field length              2 bytes // 额外字段长度
file name (variable size) // 文件名称 (可变大小)
extra field (variable size) // 额外字段 (可变大小)

以当前的 zip 文件为例,我们可以看到本地文件头的内容如下:

local file header signature: 50 4B 03 04 // 本地文件头签名,因为是小端模式,也就是 16 进制的 0x04034b50
Version needed to extract: 0A 00 // 10
general purpose bit flag:  00 00 // 通用目的位标志
compression method: 00 00 // 压缩方法
last mod file time: 1B 7C // 最后修改文件时间
last mod file date: 4F 5B // 最后修改文件日期
crc-32: 52 9E D6 8B // CRC-32
compressed size: 0B 00 00 00 // 压缩大小
uncompressed size: 0B 00 00 00 // 未压缩大小
file name length: 09 00 // 文件名称长度
extra field length: 1C 00 // 额外字段长度
file name: 68 65 6C 6C 6F 2E 74 78 74 // 文件名称
extra field: 00 00 00 00 // 额外字段 这里可以根据 extra field length 的长度获取

// file Data.. 

中央目录头

central file header signature   4 bytes  (0x02014b50)
version made by                 2 bytes
version needed to extract       2 bytes
general purpose bit flag        2 bytes
compression method              2 bytes
last mod file time              2 bytes
last mod file date              2 bytes
crc-32                          4 bytes
compressed size                 4 bytes
uncompressed size               4 bytes
file name length                2 bytes
extra field length              2 bytes
file comment length             2 bytes
disk number start               2 bytes
internal file attributes        2 bytes
external file attributes        4 bytes
relative offset of local header 4 bytes

file name (variable size)
extra field (variable size)
file comment (variable size)

大部分字段本地文件头协议差不多,需要留意的是 relative offset of local header 字段,他标识了本地文件头的所在位置。

以当前的 zip 文件为例,我们可以看到 中央目录头 的内容如下:

central file header signature: 50 4B 01 02 // 中央目录头标识
version made by: 1E 03
version needed to extract: 0A 00
general purpose bit flag:  00 00 
compression method: 00
last mod file time: 1B 7C
last mod file date: 4F 5B
crc-32            : 52 9E D6 8B 
compressed size   : 0B 00 00 00 
uncompressed size : 0B 00 00 00
file name length  : 09 00 
extra field length: 18 00
file comment length : 00 00
disk number start   : 00 00
internal file attributes        01 00
external file attributes        00 00 A4 81
relative offset of local header 00 00 00 00
file name (variable size): 68 65 6C 6C 6F 2E 74 78 74
extra field: 55 54 05 00 03 26 4E EF 68 75 78 0B 00 01 04 F5 01 00 00 04 14 00 00 // 这里是根据 extra field length 的长度来获取的。

中央目录记录结尾

end of central dir signature    4 bytes  (0x06054b50)
number of this disk             2 bytes
number of the disk with the start of the central directory  2 bytes
total number of entries in the central directory on this disk  2 bytes
total number of entries in the central directory           2 bytes
size of the central directory   4 bytes
offset of start of central directory with respect to the starting disk number        4 bytes
.ZIP file comment length        2 bytes
.ZIP file comment       (variable size)

以当前的 zip 文件为例,我们可以看到 中央目录记录结尾 的内容如下:

end of central dir signature: 50 4B 05 06 
number of this disk: 00 00
number of the disk with the start of the central directory: 00 00
total number of entries in the central directory on this disk: 01 00 // 存储的是文件总数
total number of entries in the central directory: 01 00 // 存储的是文件总数
size of the central directory : 4F 00 00 00 // 中央目录区占据的字节大小。
offset of start of central directory with respect to the starting disk number: 4E 00 00 00 // 中央目录区开始的位置。
.ZIP file comment length: 00 00

综上,我们大概了解了 zip 包协议的内容,并根据协议的内容读取了 案例文件 对应数据。

接下来,我们看看 JSzip 是怎么 “读懂” 这段数据的。

JSZip 是如何解析的?

依旧以 当前的案例文件 为准。

JSZip 内部流程大致是这样👇

1. 定位 “50 4B 05 06”,找到 中央目录记录结尾 的位置

JSZip 先从文件尾部向前扫描,寻找 0x06054B50(50 4B 05 06),这是 End of Central Directory Record(中央目录的结束标志)。

17.png

找到之后,JSZip 会根据 offset of start of central directory with respect to the starting disk number 字段,找到中央目录区的开始位置。

const offset = centralDirectoryHeader.offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber;

2. 找到 中央目录区的开始位置 + 找到 中央目录区的结束位置, 锁定中央目录区

找到中央目录区的开始位置后,JSZip 会根据 total number of entries in the central directory 字段,找到中央目录区的结束位置。

const end = centralDirectoryHeader.offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber + centralDirectoryHeader.sizeOfTheCentralDirectory;

找到中央目录区的结束位置后,JSZip 会根据 central directory header 字段,找到中央目录区的内容。

3. 遍历中央目录区,找到每个文件的本地文件头

找到每个文件的本地文件头后,JSZip 会根据 local file header 字段,找到每个文件的本地文件头的内容。

18.png

这就是 JSZip 自底向上的解析流程。

❌