普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月30日技术

JavaScript新手必看系列之预编译

作者 甜味弥漫
2025年11月30日 18:14
前言

预编译是JavaScript的核心概念,也是新手向中级进阶的必经之路。理解它,意味着你能:

  • 彻底搞懂变量提升
  • 理解函数声明的“特权”
  • 避免常见的作用域陷阱

本文用最简单的语言和示例,带你快速掌握全局预编译与函数预编译的完整过程。

What's that?

简单说,JS在执行代码前会进行“准备工作” —— 预编译。在这个阶段JS的V8引擎会进行变量声明,函数声明的提升

预编译的两种场景

1. 全局作用域下的预编译

分三步:

1.创建 GO(Global Object)

  • 在浏览器中就是 window 对象

  • 在 Node.js 中就是 global 对象

2.找到变量声明,作为作为GO属性名,值为undefined

  1. 找到函数声明,作为GO属性名,值为函数体

举个简单的例子:

console.log(a) // undefined
var a = 1
console.log(a) // 1

发什么了什么?

  1. 创建 GO ={}
  2. 找到变量声明 var a ,GO = { a : undefined }
  3. 找函数声明,没有函数声明就不找

执行过程如下:

//预编译后就相当于:
var a = undefined
console.log(a) //输出 undefined
  a = 1
console.log(a) //输出 1

2.函数作用域下的预编译

函数较为复杂,四步:

  1. 创建AO(Activation Object)

  2. 找形参和变量声明,作为AO属性名,值为 undefined

  3. 将实参赋给形参

  4. 找函数声明,作为AO 属性名,值为函数体

举例:

function fn(a) {
  console.log(a);
  var a = 123
  console.log(a);
  function a() {}
  var b = function() {}
  console.log(b);
  function c() {}
  var c = a
  console.log(c);
}
fn(1)

干了什么?

  1. 创建AO = {

2.找形参和变量声明,作为AO属性名,值为 undefined

AO = { a : undefined
b : undefined
c : undefined }

3.将实参赋给形参

AO = { a : 1 b : undefined
c : undefined }

4.找函数声明,作为AO 属性名,值为函数体

AO = { a : function a() {}

b : undefined
c : function c() {} }

函数体内执行过程:

// 预编译后的状态:
var c
var b
var a
console.log(a); // function a() {}
 a = 123
console.log(a); // 123
b = function() {}
console.log(b);  //function b() {}
c = a
console.log(c); //123

掌握了预编译,你就真正理解了 JavaScript 的执行机制,这对后续学习闭包、作用域链等概念至关重要!

希望这篇文章能帮助你彻底理解 JavaScript 预编译,如果有任何疑问,欢迎在评论区讨论!

前端跨页面通讯终极指南②:BroadcastChannel 用法全解析

2025年11月30日 17:50

前言

上一篇介绍了PostMessage跨页面通讯的方式。有没有一种更简洁的方式,兄弟页面也能像父子页面一样通讯。今天就介绍一个更高效、更简洁的方案——BroadcastChannel API,它能轻松搞定父子、子父、兄弟页面间的通讯。

1. BroadcastChannel是什么?

BroadcastChannel 接口表示给定的任何浏览上下文都可以订阅的命名频道。它允许同源的不同浏览器窗口、标签页、frame 或者 iframe 下的不同文档之间相互通信。消息通过 message 事件进行广播,该事件在侦听该频道的所有 BroadcastChannel 对象上触发,发送消息的对象除外。

简单来说,它就像一个“无线电台”,多个页面只要订阅了同一个“频道”(指定相同的频道名称),就能接收该频道发送的所有消息,实现数据的双向流转。

需要注意,BroadcastChannel仅支持同源页面,兼容性略低,需要搭配其他方案作为降级处理。

2. 如何使用

BroadcastChannel的使用流程如下:

  1. 创建频道
  2. 订阅消息
  3. 发送消息
  4. 关闭订阅

2.1 创建频道(主题)

通过 new BroadcastChannel('channel-name')创建一个“频道”(相当于发布-订阅中的“主题”)。所有加入同一频道的上下文,共享同一个通信通道。

// 1. 创建/订阅指定名称的频道(关键:多页面频道名必须一致)
const channel = new BroadcastChannel('my-channel');

2.2 订阅消息(监听)

通过监听 message事件订阅该频道的消息(相当于订阅者注册回调):

channel.onmessage = (e) => {
  console.log('收到消息:', e.data); // e.data 就是发送的消息内容
  // 根据消息类型执行对应逻辑
  if (e.data.type === 'refresh') {
    // 执行刷新操作
  }
};

2.3 发布消息(发送)

通过 postMessage()向频道发送消息(相当于发布者触发发布):

channel.postMessage({
  type: 'refresh',
  data: { id: 123 }
});

2.4 退订(关闭)

通过 close()方法离开频道(可选,浏览器通常会在上下文销毁时自动清理):

window.addEventListener('beforeunload', () => {
  channel.close();
});
// 1. 创建/订阅指定名称的频道(关键:多页面频道名必须一致)
const channel = new BroadcastChannel('my-channel');

// 2. 接收消息
channel.onmessage = (e) => {
  console.log('收到消息:', e.data); // e.data 就是发送的消息内容
  // 根据消息类型执行对应逻辑
  if (e.data.type === 'refresh') {
    // 执行刷新操作
  }
};

// 3. 发送消息(支持字符串、对象等多种数据类型)
channel.postMessage({
  type: 'refresh',
  data: { id: 123 }
});

// 4. 关闭频道(页面卸载时调用,避免内存泄漏)
window.addEventListener('beforeunload', () => {
  channel.close();
});

3、实践场景

下面我们针对前端最常见的父子页面(父窗口打开子窗口)、子父页面(子窗口向父窗口反馈)、兄弟页面(同一父页面打开的多个子窗口)三种场景,分别给出具体的实现代码。

3.1 父子通讯

父页面点击“刷新子页面”按钮,子页面接收到指令后刷新数据。

父页面代码(打开子窗口并发送消息):

// 1. 创建频道
const parentChannel = new BroadcastChannel('parent-child-channel');

// 2. 点击按钮向子窗口发送消息
document.getElementById('refreshChildBtn').addEventListener('click', () => {
  parentChannel.postMessage({
    type: 'refresh-data',
    message: '父页面指令:请刷新数据'
  });
});

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  parentChannel.close();
});

子页面代码(接收父页面消息并执行操作):

// 1. 订阅同一个频道(频道名必须和父页面一致)
const childChannel = new BroadcastChannel('parent-child-channel');

// 2. 接收父页面消息
childChannel.onmessage = (e) => {
  const { type, message } = e.data;
  if (type === 'refresh-data') {
    // 显示接收的消息
    document.getElementById('message').textContent = message;
    // 执行刷新数据的逻辑
    refreshData();
  }
};

// 刷新数据的核心函数
function refreshData() {
  // 模拟请求接口刷新数据
  console.log('子页面正在刷新数据...');
  // 这里写具体的刷新逻辑
}

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  childChannel.close();
});

3.2 子父通讯(子窗口向父窗口反馈结果)

需求:子页面完成表单提交后,向父页面发送“提交成功”的消息,父页面接收到后关闭子窗口并刷新自身数据。

子页面代码(提交表单后发送消息):

// 1. 订阅频道(和父页面保持一致)
const childChannel = new BroadcastChannel('child-parent-channel');

// 2. 表单提交逻辑
document.getElementById('submitForm').addEventListener('submit', async (e) => {
  e.preventDefault();
  try {
    // 模拟表单提交接口请求
    await submitFormData();
    // 提交成功后向父页面发送消息
    childChannel.postMessage({
      type: 'submit-success',
      data: { formId: 456, status: 'success' }
    });
    // 延迟关闭子窗口,确保消息发送完成
    setTimeout(() => {
      window.close();
    }, 300);
  } catch (error) {
    console.error('提交失败:', error);
  }
});

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  childChannel.close();
});

父页面代码(接收子页面消息并执行操作):

// 1. 创建频道
const parentChannel = new BroadcastChannel('child-parent-channel');

// 2. 接收子页面消息
parentChannel.onmessage = (e) => {
  const { type, data } = e.data;
  if (type === 'submit-success') {
    console.log('子页面表单提交成功:', data);
    // 执行父页面刷新逻辑
    parentRefreshData();
    // (可选)关闭子窗口(如果子窗口未自行关闭)
    // childWindow.close();
  }
};

// 父页面刷新数据函数
function parentRefreshData() {
  console.log('父页面正在刷新数据...');
  // 具体刷新逻辑
}

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  parentChannel.close();
});

3.3 兄弟通讯(多个子窗口间同步状态)

需求:父页面打开两个子窗口A和B,当子窗口A修改数据后,子窗口B实时同步更新数据。

子窗口A代码(修改数据后发送消息):

// 1. 订阅兄弟通讯频道
const brotherChannel = new BroadcastChannel('brother-channel');

// 2. 模拟修改数据操作
document.getElementById('updateDataBtn').addEventListener('click', () => {
  const newData = {
    id: 789,
    content: '子窗口A修改后的新内容',
    updateTime: new Date().toLocaleString()
  };
  // 保存修改后的数据
  saveData(newData);
  // 向频道发送消息,通知其他兄弟窗口
  brotherChannel.postMessage({
    type: 'data-updated',
    newData: newData
  });
});

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  brotherChannel.close();
});

子窗口B代码(接收消息并同步数据):

// 1. 订阅同一个兄弟通讯频道
const brotherChannel = new BroadcastChannel('brother-channel');

// 2. 接收子窗口A发送的消息
brotherChannel.onmessage = (e) => {
  const { type, newData } = e.data;
  if (type === 'data-updated') {
    console.log('收到子窗口A的更新消息:', newData);
    // 同步更新页面数据展示
    document.getElementById('dataContent').textContent = newData.content;
    document.getElementById('updateTime').textContent = newData.updateTime;
  }
};

// 3. 页面卸载时关闭频道
window.addEventListener('beforeunload', () => {
  brotherChannel.close();
});

接收输入如下:

image.png

4. 总结

最后总结一下:对比传统的postMessage跨页面通讯方案,BroadcastChannel兄弟页面无需转发,几行代码就能轻松实现通讯。

el-button源码解读3——:class="buttonKls"与颜色系统的关系

作者 Joie
2025年11月30日 17:44

说明 :class="buttonKls" 与颜色系统的关系:

1. buttonKls 的作用

buttonKls 是一个计算属性,生成按钮需要的所有 CSS 类名:

const buttonKls = computed(() => [
  // el-button
  ns.b(),
  ns.m(_type.value),
  ns.m(_size.value),
  ns.is('disabled', _disabled.value),
  ns.is('loading', props.loading),
  ns.is('plain', _plain.value),
  ns.is('round', _round.value),
  ns.is('circle', props.circle),
  ns.is('text', _text.value),
  ns.is('link', props.link),
  ns.is('has-bg', props.bg),
])

假设 type="primary"buttonKls 可能包含:

['el-button', 'el-button--primary', 'el-button--small', 'is-loading', ...]

2. 颜色系统的触发机制

颜色系统通过 CSS 类选择器触发。当 buttonKls 包含 'el-button--primary' 时,会匹配到对应的 CSS 规则。

3. 完整的关系链

用户使用: <el-button type="primary">
    ↓
props.type = 'primary'_type.value = 'primary'buttonKls = ['el-button', 'el-button--primary', ...]
    ↓
:class="buttonKls"  →  class="el-button el-button--primary"
    ↓
CSS 匹配: .el-button--primary { ... }
    ↓
button-variant('primary') 生成 CSS 变量
    ↓
应用颜色样式

4. 在 CSS 中的对应关系

button.scss 中的定义:

  @each $type in (primary, success, warning, danger, info) {
    @include m($type) {
      @include button-variant($type);
    }
  }

$type = 'primary' 时:

  • @include m('primary') → 生成 .el-button--primary 选择器
  • @include button-variant('primary') → 生成颜色相关的 CSS 变量

5. 实际渲染过程

步骤 1:Vue 组件生成类名

<el-button type="primary">按钮</el-button>
// buttonKls 计算后
['el-button', 'el-button--primary']
<!-- 最终渲染 -->
<button class="el-button el-button--primary">
  按钮
</button>

步骤 2:CSS 匹配类名

浏览器看到 class="el-button--primary",匹配到:

// button.scss 中生成的
.el-button--primary {
  // button-variant('primary') 生成的 CSS 变量
  --el-button-bg-color: var(--el-color-primary);
  --el-button-border-color: var(--el-color-primary);
  --el-button-text-color: var(--el-color-white);
  --el-button-hover-bg-color: var(--el-color-primary-light-3);
  --el-button-active-bg-color: var(--el-color-primary-dark-2);
}

步骤 3:应用颜色

基础样式使用这些 CSS 变量:

.el-button {
  background-color: var(--el-button-bg-color);
  border-color: var(--el-button-border-color);
  color: var(--el-button-text-color);
}

因为 .el-button--primary 定义了这些变量,所以按钮会显示 primary 的颜色。

6. 关键理解

:class="buttonKls" 是连接 Vue 组件和 CSS 颜色系统的桥梁:

Vue 组件层面          CSS 样式层面
─────────────────────────────────
buttonKls            .el-button--primary
  ↓                       ↓
'el-button--primary'  →  匹配 CSS 选择器
  ↓                       ↓
应用类名              应用颜色样式

7. 不同类型的关系

用户传入 buttonKls 包含 CSS 匹配 颜色应用
type="primary" 'el-button--primary' .el-button--primary 蓝色 (#409eff)
type="success" 'el-button--success' .el-button--success 绿色 (#67c23a)
type="warning" 'el-button--warning' .el-button--warning 橙色 (#e6a23c)
type="danger" 'el-button--danger' .el-button--danger 红色 (#f56c6c)

8. 完整示例

<el-button type="primary" size="small" :loading="true">
  提交
</el-button>

生成的类名:

buttonKls = [
  'el-button',           // 基础类
  'el-button--primary',  // 类型类(触发颜色系统)
  'el-button--small',    // 尺寸类
  'is-loading'           // 状态类
]

最终 HTML:

<button class="el-button el-button--primary el-button--small is-loading">
  提交
</button>

CSS 匹配:

.el-button--primary {  // ← 这个类触发了颜色系统
  --el-button-bg-color: var(--el-color-primary);
  // ...
}

9. 总结

  • :class="buttonKls" 生成类名(如 el-button--primary
  • CSS 通过类选择器匹配这些类名
  • 匹配到的规则通过 button-variant 生成颜色变量
  • 基础样式使用这些变量,最终显示对应颜色

核心关系buttonKls 中的类型类名(如 el-button--primary)是触发颜色系统的开关,CSS 通过这个类名应用对应的颜色样式。

搞懂作用域链与闭包:JS底层逻辑变简单

作者 闲云ing
2025年11月30日 17:39

JS底层小揭秘:作用域链与闭包,代码+图解一看就懂

在 JavaScript 的学习过程中,理解其底层运行机制是进阶的关键,而作用域链和闭包更是其中的核心概念。,很多人只停留在“会用”,没搞懂底层逻辑。本文结合代码+调用栈图解,从V8引擎的运行机制出发,拆解这两个概念的本质,帮你从底层视角搞懂 JS 的执行规则。

一、先搭好JS底层的基础框架

JS代码能运行,依赖V8引擎的三个核心模块:

  1. 调用栈:分编译阶段(处理变量/函数提升)和执行阶段(创建执行上下文并压入栈,执行完弹出);
  2. 执行上下文:全局执行上下文(始终在栈底)+ 函数执行上下文(函数调用时创建);
  3. 作用域:定义变量的查找范围和生命周期,包含let/const块级作用域(依托栈结构的词法环境),以及var变量提升特性。

二、作用域链:静态的变量查找路径

作用域链(词法作用域链)的核心是:它由函数声明的位置决定,编译阶段就固定了,和调用顺序无关

案例1:为什么bar里的myName取全局值?

对应代码与图示:

function bar(){
  console.log(myName); // 输出“极客时间”
}
function foo() {
  var myName = '极客邦'
  bar() // 在foo内部调用bar
}
var myName = '极客时间'
foo();

1.jpg

2.jpg

运行逻辑拆解:
  1. 编译阶段:barfoo被声明在全局作用域,因此它们的作用域链默认“自身→全局”;

  2. 执行阶段:

    1. foo调用时,创建foo执行上下文(变量环境包含myName="极客邦")并压入栈;
    2. foo内部调用bar,创建bar执行上下文并压入栈;
    3. bar中查找myName:自身变量环境无→通过outer指向的全局执行上下文查找→取全局的"极客时间"

案例2:块级作用域下的变量查找

对应代码与图示:

function bar () {
  var myName = '极客世界';
  let test1 = 100;
  if (1) {
    let myName = "Chrome 浏览器";
    console.log(test); 
  }
}
function foo() {
  var myName = '极客邦';
  let test = 2;
  {
    let test = 3;
    bar();
  }
}
var myName = '极客时间';
let myAge = 10;
let test = 1;
foo();

3.jpg

查找过程:
  1. bar内部if块的console.log(test),先查自身块级词法环境(只有myName="Chrome 浏览器")→ 没找到;
  2. bar函数的词法环境(有test1=100)→ 没找到;
  3. bar的变量环境(有myName="极客世界")→ 没找到;
  4. 查全局执行上下文的词法环境(有test=1)→ 但由于bar的作用域链是“自身块级→bar函数→全局”,运行中会输出1

三、闭包:函数“背着”变量的专属背包

闭包是词法作用域的延伸——外部函数执行后,其内部变量被嵌套函数引用,因此不会被垃圾回收,形成一个“变量背包”供嵌套函数使用

案例:闭包的形成与运行

对应代码与图示:

function foo() {
  var myName = '极客时间'
  let test1 = 1
  const test2 = 2
  var innerBar = {
    getName: function() {
      console.log(test1) // 输出1
      return myName
    },
    setName: function(newName) {
      myName = newName // 修改foo内部的myName
    }
  }
  return innerBar
}

var bar = foo() // foo执行上下文出栈
bar.setName("极客邦")
console.log(bar.getName()); // 输出1 + “极客邦”

5.jpg

6.jpg

闭包运行流程:
  1. foo执行时:创建执行上下文,变量环境存储myName="极客时间",词法环境存储test1=1test2=2,并压入栈;

  2. foo返回innerBar后:foo执行上下文出栈,但getName/setName引用了myNametest1,这两个变量被保留在内存中(形成闭包,即“专属背包”);

  3. 调用bar.setName/getName时

    1. setName执行时,通过闭包找到myName并修改为"极客邦"
    2. getName执行时,通过闭包找到test1并输出1,同时返回修改后的myName

四、核心总结

  1. 作用域链是静态的:由函数声明位置决定,编译阶段固定,和调用顺序无关;
  2. 闭包是词法作用域的延伸:嵌套函数引用外部函数变量,导致外部函数变量不被回收,形成“变量背包”;
  3. 底层逻辑的关键:理解调用栈、执行上下文、作用域的关系,是搞懂JS变量查找和内存管理的基础。

JavaScript 的底层运行机制中,词法作用域是基础,它决定了作用域链的静态查找规则,而闭包则是词法作用域的延伸,通过保留自由变量实现了函数对外部作用域的持久访问。理解这些概念,不仅能帮助我们写出更符合 JS 运行逻辑的代码,还能解决实际开发中变量作用域、内存泄漏等常见问题。掌握作用域链与闭包,是深入理解 JavaScript 语言特性的重要一步。

element-plus源码解读3——【scss】颜色系统完整流程

作者 Joie
2025年11月30日 17:30

一、基础颜色定义(源头)

位置:packages/theme-chalk/src/common/var.scss

scss知识点:$ 表示变量

  • 定义所有颜色的基础值
// types
$types: primary, success, warning, danger, error, info;

// Color
$colors: () !default;
$colors: map.deep-merge(
  (
    'white': #ffffff,
    'black': #000000,
    'primary': (
      'base': #409eff,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  ),
  $colors
);

二、自动生成颜色变体(light/dark)

位置:packages/theme-chalk/src/common/var.scss

定义一个mixin: set-color-mix-level:

将基础颜色与白色或黑色混合

生成指定级别的浅色或深色变体

将生成的颜色添加到$colors map中

// mix colors with white/black to generate light/dark level
@mixin set-color-mix-level(
  $type,
  $number,
  $mode: 'light',
  $mix-color: $color-white
) {
  $colors: map.deep-merge(
    (
      $type: (
        /**
        * roundColor:将颜色的 RGB 通道值四舍五入为整数,并返回 rgba() 格式的颜色。
        * color.mix:混合颜色 第一个参数和第二个参数按照第三个参数的百分比混合颜色
        * map.get($colors, $type, 'base'):获取颜色
        * math.percentage(math.div($number, 10)):将数字转换为百分比
        */
          '#{$mode}-#{$number}': roundColor(
            color.mix(
              $mix-color,
              map.get($colors, $type, 'base'),
              math.percentage(math.div($number, 10))
            )
          ),
      ),
    ),
    $colors
  ) !global;
}

使用

// $colors.primary.light-i
// --el-color-primary-light-i
// 10% 53a8ff
// 20% 66b1ff
// 30% 79bbff
// 40% 8cc5ff
// 50% a0cfff
// 60% b3d8ff
// 70% c6e2ff
// 80% d9ecff
// 90% ecf5ff

// 外层循环:遍历颜色类型
@each $type in $types {
  // 内层循环:生成 1-9 的变体
  @for $i from 1 through 9 {
    // 调用 mixin 生成不同亮度的颜色
    @include set-color-mix-level($type, $i, 'light', $color-white);
  }
}


// 第 1 轮:$type = primary
@include set-color-mix-level(primary, 1, 'light', $color-white);  // 生成 primary-light-1
@include set-color-mix-level(primary, 2, 'light', $color-white);  // 生成 primary-light-2
@include set-color-mix-level(primary, 3, 'light', $color-white);  // 生成 primary-light-3
// ... 继续到 9

// 第 2 轮:$type = success
@include set-color-mix-level(success, 1, 'light', $color-white);   // 生成 success-light-1
@include set-color-mix-level(success, 2, 'light', $color-white);   // 生成 success-light-2
// ... 继续到 9

// 依此类推,直到遍历完所有 6 种类型

三、生成 CSS 变量(全局)

位置:packages/theme-chalk/src/var.scss

// join var name
// joinVarName(('button', 'text-color')) => '--el-button-text-color'
// 将$list遍历中间用-拼接每一个$item
@function joinVarName($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}

===============================================================

// set css var value, because we need translate value to string
// for example:
// @include set-css-var-value(('color', 'primary'), red);
// --el-color-primary: red;
// 返回 变量名:颜色值 的形式
@mixin set-css-var-value($name, $value) {
  #{joinVarName($name)}: #{$value};
}

================================================================

@mixin set-css-color-type($colors, $type) {
  // 生成基础颜色变量
  @include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
  // 结果:--el-color-primary: #409eff;

  // 生成浅色变量(3, 5, 7, 8, 9)
  @each $i in (3, 5, 7, 8, 9) {
    @include set-css-var-value(
      ('color', $type, 'light', $i),
      map.get($colors, $type, 'light-#{$i}')
    );
  }
  // 结果:
  // --el-color-primary-light-3: #79bbff;
  // --el-color-primary-light-5: #a0cfff;
  // --el-color-primary-light-7: #c6e2ff;
  // --el-color-primary-light-8: #d9ecff;
  // --el-color-primary-light-9: #ecf5ff;

  // 生成深色变量
  @include set-css-var-value(
    ('color', $type, 'dark-2'),
    map.get($colors, $type, 'dark-2')
  );
  // 结果:--el-color-primary-dark-2: ...;
}

=================================================================

:root {
  color-scheme: light;

  // --el-color-#{$type}
  // --el-color-#{$type}-light-{$i}
  @each $type in (primary, success, warning, danger, error, info) {
    @include set-css-color-type($colors, $type);
  }
  
生成的css变量,以primary为例子
:root {
  --el-color-primary: #409eff;
  --el-color-primary-light-3: #79bbff;
  --el-color-primary-light-5: #a0cfff;
  --el-color-primary-light-7: #c6e2ff;
  --el-color-primary-light-8: #d9ecff;
  --el-color-primary-light-9: #ecf5ff;
  --el-color-primary-dark-2: #337ecc;
}

四、组件级使用——以el-button为例子

位置:packages/theme-chalk/src/button.scss

// generate css var from existing css var
// for example:
// @include css-var-from-global(('button', 'text-color'), ('color', $type))
// --el-button-text-color: var(--el-color-#{$type});
@mixin css-var-from-global($var, $gVar) {
  $varName: joinVarName($var);
  $gVarName: joinVarName($gVar);
  #{$varName}: var(#{$gVarName});
}

==========================================================

$button-color-types 是一个 SCSS 变量(map),定义在 button-variant mixin 内部。它是一个嵌套的 map,用于定义按钮在不同状态下的颜色映射关系。

$button-color-types: (
    '': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
      ),
      'border-color': (
        'color',
        $type,
      ),
      'outline-color': (
        'color',
        $type,
        'light-5',
      ),
      'active-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'hover': (
      'text-color': (
        'color',
        'white',
      ),
      'link-text-color': (
        'color',
        $type,
        'light-5',
      ),
      'bg-color': (
        'color',
        $type,
        'light-3',
      ),
      'border-color': (
        'color',
        $type,
        'light-3',
      ),
    ),
    'active': (
      'bg-color': (
        'color',
        $type,
        'dark-2',
      ),
      'border-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'disabled': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
        'light-5',
      ),
      'border-color': (
        'color',
        $type,
        'light-5',
      ),
    ),
  );
  
  // 结构层次
  $button-color-types (第一层:状态)
  ├─ '' (默认状态)
  │   ├─ 'text-color' → ('color', 'white')
  │   ├─ 'bg-color' → ('color', $type)
  │   ├─ 'border-color' → ('color', $type)
  │   ├─ 'outline-color' → ('color', $type, 'light-5')
  │   └─ 'active-color' → ('color', $type, 'dark-2')
  │
  ├─ 'hover' (悬停状态)
  │   ├─ 'text-color' → ('color', 'white')
  │   ├─ 'link-text-color' → ('color', $type, 'light-5')
  │   ├─ 'bg-color' → ('color', $type, 'light-3')
  │   └─ 'border-color' → ('color', $type, 'light-3')
  │
  ├─ 'active' (激活状态)
  │   ├─ 'bg-color' → ('color', $type, 'dark-2')
  │   └─ 'border-color' → ('color', $type, 'dark-2')
  │
  └─ 'disabled' (禁用状态)
      ├─ 'text-color' → ('color', 'white')
      ├─ 'bg-color' → ('color', $type, 'light-5')
      └─ 'border-color' → ('color', $type, 'light-5')

================================================================

  @each $type, $typeMap in $button-color-types {
    // 内层循环,遍历第二层(属性):text-color, bg-color, border-color, outline-color, active-color
    // $typeColor:属性名,例如:'text-color'
    // $list:属性值,例如:('color', 'white')
    @each $typeColor, $list in $typeMap {
      // 调用 css-var-from-global 生成 CSS 变量
      // 例如:@include css-var-from-global(('button', 'hover', 'text-color'), ('color', 'white'));
      @include css-var-from-global(('button', $type, $typeColor), $list);
    }
  }
  
 // 当 $type = 'primary' 时,会生成:
 /* 默认状态 */
--el-button-text-color: var(--el-color-white);
--el-button-bg-color: var(--el-color-primary);
--el-button-border-color: var(--el-color-primary);
--el-button-outline-color: var(--el-color-primary-light-5);
--el-button-active-color: var(--el-color-primary-dark-2);

/* 悬停状态 */
--el-button-hover-text-color: var(--el-color-white);
--el-button-hover-link-text-color: var(--el-color-primary-light-5);
--el-button-hover-bg-color: var(--el-color-primary-light-3);
--el-button-hover-border-color: var(--el-color-primary-light-3);

/* 激活状态 */
--el-button-active-bg-color: var(--el-color-primary-dark-2);
--el-button-active-border-color: var(--el-color-primary-dark-2);

/* 禁用状态 */
--el-button-disabled-text-color: var(--el-color-white);
--el-button-disabled-bg-color: var(--el-color-primary-light-5);
--el-button-disabled-border-color: var(--el-color-primary-light-5);

=====================================================================

完整源码:
@mixin button-variant($type) {
  $button-color-types: (
    '': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
      ),
      'border-color': (
        'color',
        $type,
      ),
      'outline-color': (
        'color',
        $type,
        'light-5',
      ),
      'active-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'hover': (
      'text-color': (
        'color',
        'white',
      ),
      'link-text-color': (
        'color',
        $type,
        'light-5',
      ),
      'bg-color': (
        'color',
        $type,
        'light-3',
      ),
      'border-color': (
        'color',
        $type,
        'light-3',
      ),
    ),
    'active': (
      'bg-color': (
        'color',
        $type,
        'dark-2',
      ),
      'border-color': (
        'color',
        $type,
        'dark-2',
      ),
    ),
    'disabled': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
        'light-5',
      ),
      'border-color': (
        'color',
        $type,
        'light-5',
      ),
    ),
  );

  // 外层循环,遍历第一层(状态):'', 'hover', 'active', 'disabled'
  // $type状态名例如'hover';
  // $typeMap:状态对应的属性值,例如:('text-color': (
  //   'color',
  //   'white',
  // ),)
  @each $type, $typeMap in $button-color-types {
    // 内层循环,遍历第二层(属性):text-color, bg-color, border-color, outline-color, active-color
    // $typeColor:属性名,例如:'text-color'
    // $list:属性值,例如:('color', 'white')
    @each $typeColor, $list in $typeMap {
      // 调用 css-var-from-global 生成 CSS 变量
      // 例如:@include css-var-from-global(('button', 'hover', 'text-color'), ('color', 'white'));
      @include css-var-from-global(('button', $type, $typeColor), $list);
    }
  }

  &.is-plain,
  &.is-text,
  &.is-link {
    @include button-plain($type);
  }
}

五、应用样式(最终渲染)

位置:packages/theme-chalk/src/button.scss

@include b(button) {
  display: inline-flex;
  justify-content: center;
  align-items: center;

  line-height: 1;
  // min-height will expand when in flex
  height: map.get($input-height, 'default');
  white-space: nowrap;
  cursor: pointer;
  color: getCssVar('button', 'text-color');
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: getCssVar('button', 'font-weight');
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: getCssVar('button', 'bg-color');
  border: getCssVar('border');
  border-color: getCssVar('button', 'border-color');

  &:hover {
    color: getCssVar('button', 'hover', 'text-color');
    border-color: getCssVar('button', 'hover', 'border-color');
    background-color: getCssVar('button', 'hover', 'bg-color');
    outline: none;
  }
 
 // 这些生成el-button的基础变量
  ======================================================
 
   @each $type in (primary, success, warning, danger, info) {
    @include m($type) {
      @include button-variant($type);
    }
  }

  
  // $type = 'primary' 时,最终编译后的 CSS(.el-button--primary):
  .el-button--primary {
  /* 基础样式 */
  display: inline-flex;
  justify-content: center;
  align-items: center;
  
  /* 颜色(使用 CSS 变量) */
  color: var(--el-button-text-color);
  /* ↓ 展开为 */
  color: var(--el-color-white);  /* #ffffff */
  
  background-color: var(--el-button-bg-color);
  /* ↓ 展开为 */
  background-color: var(--el-color-primary);  /* #409eff */
  
  border-color: var(--el-button-border-color);
  /* ↓ 展开为 */
  border-color: var(--el-color-primary);  /* #409eff */
}

.el-button--primary:hover {
  background-color: var(--el-button-hover-bg-color);
  /* ↓ 展开为 */
  background-color: var(--el-color-primary-light-3);  /* #79bbff */
  
  border-color: var(--el-button-hover-border-color);
  /* ↓ 展开为 */
  border-color: var(--el-color-primary-light-3);  /* #79bbff */
}

.el-button--primary:active {
  background-color: var(--el-button-active-bg-color);
  /* ↓ 展开为 */
  background-color: var(--el-color-primary-dark-2);  /* #337ecc */
  
  border-color: var(--el-button-active-border-color);
  /* ↓ 展开为 */
  border-color: var(--el-color-primary-dark-2);  /* #337ecc */
}

六、完整流程图

┌─────────────────────────────────────────────────────────┐
│ 阶段 1: 基础颜色定义                                     │
│ var.scss: primary.base = #409eff                       │
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 2: 自动生成颜色变体                                 │
│ set-color-mix-level()                                   │
│ - primary.light-3 = #79bbff (30% 白色混合)             │
│ - primary.dark-2 = #337ecc (20% 黑色混合)              │
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 3: 生成全局 CSS 变量                                │
│ var.scss: :root {                                       │
│   --el-color-primary: #409eff                          │
│   --el-color-primary-light-3: #79bbff                  │
│   --el-color-primary-dark-2: #337ecc                    │
│ }                                                       │
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 4: 按钮组件使用颜色                                  │
│ button-variant('primary')                                │
│ 生成组件级 CSS 变量:                                     │
│ --el-button-bg-color: var(--el-color-primary)           │
│ --el-button-hover-bg-color: var(--el-color-primary-light-3)│
│ --el-button-active-bg-color: var(--el-color-primary-dark-2)│
└─────────────────┬───────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────────────┐
│ 阶段 5: 最终渲染                                         │
│ .el-button--primary {                                   │
│   background-color: var(--el-button-bg-color);          │
│   /* 浏览器解析为 #409eff */                            │
│ }                                                       │
└─────────────────────────────────────────────────────────┘

七、优势

  • 统一管理:所有颜色在一个地方定义。
  • 自动计算:light/dark 变体自动生成。
  • 易于定制:修改基础色即可影响所有变体和组件。
  • 性能:使用 CSS 变量,支持运行时动态切换主题。

CSS 视口单位进化论:从 100vh 的「骗局」到 dvh 的救赎

作者 兔子零1024
2025年11月30日 17:22

关键词:CSS / 视口单位 / 移动端适配 / 响应式设计 / dvh / svh / lvh / 100vh bug / 前端工程化 / 用户体验


引言:一个持续了十年的移动端「谎言」

如果你做过移动端 H5 开发,或者写过全屏落地的营销页,你一定遇到过那个著名的「100vh 骗局」。

设计师给了你一张完美的设计稿:首屏全屏,底部按钮固定,不允许有滚动条
你自信满满地写下:

.hero-section {
  height: 100vh;
}

然后你在 iPhone 的 Safari 里打开,灾难发生了:底部的按钮被浏览器底部的工具栏遮住了一半,或者页面莫名其妙出现了一截滚动条

1.jpg

(图解:左侧是理想设计,按钮在最底部;右侧是 Safari 现状,浏览器工具栏无情地盖住了你的按钮)

为了解决这个问题,前端社区发明了无数「歪门邪道」:

  • window.innerHeight 动态计算高度赋给 CSS 变量 --vh
  • 监听 resize 事件疯狂重绘。
  • 使用 position: fixed; top: 0; bottom: 0; 这种古老的 hack。

为什么一个简单的「全屏」需求,在移动端会如此艰难?
这背后其实是 Web 标准与移动端原生 UI(动态地址栏、底部工具栏)长达十年的博弈。

好消息是,CSS 终于迎来了真正解决问题的新一代视口单位:svhlvhdvh 以及它们在宽度和逻辑方向上的兄弟们。

这不是简单的语法升级,这是 Web 终于承认:移动端的屏幕,从来就不是一个静态的矩形。


一、旧时代的妥协:为什么 100vh 不是真的 100% 高度?

1.1 完美的初衷与残酷的现实

在 PC 时代,vh (Viewport Height) 的定义非常完美:视口高度的 1%。浏览器窗口多高,100vh 就多高。

但在移动端,情况变了。iOS Safari 和 Chrome Android 为了给用户更多阅读空间,设计了动态工具栏

  • 初始状态:地址栏、底部工具栏展开,可视区域变小。
  • 上滑状态:地址栏缩小/隐藏,工具栏消失,可视区域变大。

这时候,100vh 该等于「展开时的高度」还是「隐藏时的高度」?

1.2 浏览器的「摆烂」选择

早期的 Safari 做了一个影响深远的决定:为了避免滚动时页面元素跳动(Reflow),100vh 永远等于「地址栏隐藏时的最大高度」

2.jpg

(图解:无论你的工具栏是否展开,Safari 始终认为 100vh 是那个「最大值」。这就导致当工具栏展开(实际只有 80% 高度)时,你的 100vh 内容溢出了 20%。)

这就解释了为什么你的按钮被遮住了:

  • 浏览器告诉你 100vh 是 800px(假设全屏高度)。
  • 但实际上底部的工具栏占了 80px,可视区域只有 720px。
  • 你的按钮画在了第 750px 的位置,正好被工具栏盖得严严实实。

这个「特性」被无数开发者吐槽为 Bug,但浏览器厂商坚持这是「Feature」。

「他们为了滚动的丝滑,牺牲了布局的精确。
在很长一段时间里,我们只能用 JS 来修补 CSS 的谎言。」


二、新秩序的建立:svh、lvh、dvh 的三态哲学

W3C 终于意识到,移动端的视口不是一个固定的值,而是一个在「大」与「小」之间薛定谔的波动状态

于是,CSS Viewport Units Level 3 引入了一套全新的单位体系,把视口高度拆分为三种状态。

3.jpg

2.1 svh (Small Viewport Height):永远不再被遮挡

定义:视口高度的最小值(即地址栏、工具栏全部展开时的可视高度)。

  • 应用场景首屏全屏页、底部固定按钮、Modal 弹窗
  • 价值:如果你写 height: 100svh,你的页面底部绝对不会被工具栏遮挡。它是最安全的「一屏」。
/* 以前的 JS Hack */
.modal {
  height: calc(var(--vh, 1vh) * 100);
}

/* 现在的原生写法 */
.modal {
  height: 100svh;
}

2.2 lvh (Large Viewport Height):沉浸式的极致

定义:视口高度的最大值(即地址栏、工具栏全部收起时的可视高度)。

  • 应用场景背景图、视差滚动容器
  • 价值:当你希望背景铺满整个「潜在」屏幕,不在乎一部分被遮挡时,用 lvh。它等同于旧时代的 100vh

2.3 dvh (Dynamic Viewport Height):动态的完美与代价

定义动态视口高度。它会随着地址栏的伸缩,实时在 svhlvh 之间变化。

  • 应用场景追求极致体验的流式布局
  • 代价:当用户滚动页面时,100dvh 的值会不断变化。这意味着浏览器可能需要不断重绘(Repaint)甚至重排(Reflow)。
  • 现状:现代浏览器对 dvh 做了很多优化,性能损耗在大多数场景下已可忽略不计。

三态哲学总结:

  • svh 是保底的安全(Safe)。
  • lvh 是理想的宏大(Large)。
  • dvh 是真实的动态(Dynamic)。

三、维度的扩张:宽度、逻辑方向与极值

这套逻辑不仅修复了高度问题,还顺带重构了整个视口单位体系。

3.1 宽度的进化:svw / lvw / dvw

虽然移动端宽度的变化(主要是滚动条出现/消失)不如高度那么剧烈,但逻辑是一致的。

  • vw:依然是默认视口宽度(通常包含滚动条)。
  • svw / lvw / dvw:处理滚动条存在与否时的细微差异(在桌面端更明显)。

3.2 逻辑方向:vi / vb —— 国际化的必修课

随着 CSS 逻辑属性(Logical Properties)的普及,我们不再总是谈论 widthheight,而是谈论 inline(文本流方向)和 block(块堆叠方向)。

  • vi (Viewport Inline):视口在行内方向的大小。

    • 横屏/竖屏书写模式下:等于 vw
    • 竖排书写模式(如古诗词页面):等于 vh
    • svi / lvi / dvi:对应的三态变体。
  • vb (Viewport Block):视口在块级方向的大小。

    • 横屏/竖屏书写模式下:等于 vh
    • svb / lvb / dvb:对应的三态变体。

实战案例:一个适配横竖屏书写的古诗卡片

.poem-card {
  /* 无论横排还是竖排,永远占满视口在「块」方向上的 80% */
  block-size: 80vb; 
  
  /* 无论横排还是竖排,永远占满视口在「行」方向上的 90% */
  inline-size: 90vi;
  
  /* 自动适配书写模式 */
  writing-mode: vertical-rl; /* 切换这个属性,布局依然完美 */
}

3.3 极值单位:vmin / vmax 的三态

老朋友 vmin(vw/vh 中较小者)和 vmax(较大者)也全员升级:

  • svmin / svmax
  • lvmin / lvmax
  • dvmin / dvmax

场景:做横竖屏适配的 H5 游戏或画报时,svmin 是保证内容在任何旋转状态下都完整可见的神器。


四、工程实战:如何在 2025 年写好一个全屏页面?

4.1 放弃 100vh,拥抱 dvh(但要做降级)

在 2025 年,如果你的目标浏览器支持率允许(iOS 15.4+, Chrome 108+),dvh 是全屏容器的最佳选择

.full-screen-hero {
  /* 降级方案:给旧浏览器一个固定的值 */
  height: 100vh;
  
  /* 现代方案:使用动态高度,完美贴合 */
  height: 100dvh;
}

4.2 关键交互区域用 svh

对于底部的操作栏(Action Bar),不要用 dvh,因为你不想让按钮在用户手指滑动时「跳来跳去」。svh 锁定位置

.bottom-bar {
  position: fixed;
  bottom: 0;
  width: 100%;
  /* 确保它永远在可视区底部,哪怕地址栏展开 */
  bottom: calc(100vh - 100svh); /* 高级技巧:计算工具栏高度偏移 */
}

/* 或者更简单的布局思维: */
.app-container {
  min-height: 100svh;
  display: grid;
  grid-template-rows: 1fr auto;
}

4.3 慎用 lvh,除非你在做特效

lvh 在实际 UI 布局中用得很少。它更多用于视觉背景,或者那种「滑一下就全屏」的沉浸式阅读体验。如果你用 lvh 做布局容器,用户大概率会因为点不到底部的按钮而骂娘。

「成熟的工程师懂得:
UI 的稳定性(svh)优先于 UI 的充满感(lvh),
而 dvh 是两者之间的优雅平衡。」


五、总结:从「对抗」到「接纳」

回顾 CSS 视口单位的进化史,其实是一部 Web 与移动端原生特性的磨合史。

  • vh 时代:Web 试图假装自己还在 PC 上,无视了移动端复杂的 UI 变化,结果撞得头破血流(100vh bug)。
  • JS Hack 时代:开发者用脚本强行修正高度,对抗浏览器的默认行为,性能差且代码丑陋。
  • svh/dvh 时代:标准终于接纳了移动端的复杂性。我们不再强求一个「唯一的 100%」,而是承认「屏幕是会变的」

这给了我们两个重要的启示:

  1. 不存在完美的静态适配。移动端设备极其碎片化,折叠屏、刘海屏、动态栏……拥抱动态(Dynamic)才是终极解法
  2. 工具的粒度决定了体验的细腻度。从粗糙的 vh 到精细的 svh/lvh/dvh,前端工程化的本质,就是不断提升对像素级体验的掌控力。

下一次,当设计师问你「能不能把这个页面铺满全屏,不要滚动条,也不要被遮挡」时,
你可以自信地合上电脑(或者打开 VS Code),告诉他:
「当然可以,因为现在的 CSS,终于懂手机了。」

简单了解 with

作者 没落英雄
2025年11月30日 17:10

with 语句详解:使用方法和 this 指向问题

目录

  1. with 语句基础
  2. 作用域链查找机制
  3. this 指向问题(核心)
  4. window 对象的指向
  5. 在微前端沙箱中的应用
  6. 实际代码示例
  7. 常见问题和注意事项
  8. 最佳实践

with 语句基础

什么是 with 语句?

with 语句是 JavaScript 的一个特性,它可以将一个对象添加到作用域链的最前端,使得在 with 块内可以直接访问该对象的属性,而不需要使用对象名作为前缀。

基本语法


with (object) {
  // 在这个块内,可以直接访问 object 的属性
  // 不需要写 object.property,直接写 property 即可
}

基本示例

const obj = {
  name: "Alice",
  age: 25,
  city: "Beijing",
};

// 使用 with 语句
with (obj) {
  // 可以直接访问 obj 的属性,不需要 obj. 前缀
  console.log(name); // 输出: Alice
  console.log(age); // 输出: 25
  console.log(city); // 输出: Beijing

  // 也可以修改属性
  age = 26;
  console.log(age); // 输出: 26
}

// with 块外,需要正常访问
console.log(obj.age); // 输出: 26

作用域链查找机制

查找顺序

with 块内,当访问一个变量时,JavaScript 引擎会按照以下顺序查找:

  1. 局部变量let/const/var 声明的)
  2. with 指定的对象with(obj) 中的 obj
  3. 外层作用域
  4. 全局作用域

示例:作用域链查找

const globalVar = "全局变量";

function testScope() {
  const localVar = "局部变量";

  const obj = {
    objVar: "对象变量",
    localVar: "对象中的 localVar",
  };

  with (obj) {
    // 1. 查找局部变量 localVar(在函数作用域中)
    // 2. 如果没找到,查找 obj 中的属性
    // 3. 如果还没找到,查找外层作用域
    // 4. 最后查找全局作用域

    console.log(localVar); // 输出: "局部变量"(优先使用函数作用域的)
    console.log(objVar); // 输出: "对象变量"(在 obj 中找到)
    console.log(globalVar); // 输出: "全局变量"(在外层作用域找到)
  }
}

testScope();

this 指向问题(核心)

关键点:this 不受 with 语句影响

这是最重要的知识点! this 的指向完全不受 with 语句的影响。

this 的指向规则

this 的指向遵循 JavaScript 的标准规则:

  • this 指向函数调用时的上下文(caller)
  • 在全局作用域中,this 指向全局对象(浏览器中是 window
  • 在对象方法中,this 指向调用该方法的对象
  • with 语句不会改变 this 的指向

示例 1:this 在 with 块内的行为

const obj = {
  name: "Context Object",
  test: function () {
    console.log("this.name:", this.name);
    return this;
  },
};

const sandboxContext = {
  window: { name: "Sandbox Window" },
  obj: obj,
};

// 使用 Function 构造器创建函数(避免严格模式限制)
const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    // window 指向 sandboxContext.window
    console.log("window.name:", window.name); // 输出: Sandbox Window
    
    // 但是 this 仍然指向全局对象,不受 with 影响
    // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
    console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
    console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
    
    // 调用 obj.test(),this 指向 obj,不是 sandboxContext
    const result = obj.test(); // 输出: this.name: Context Object
    console.log("obj.test() 返回的 this === obj:", result === obj); // 输出: true
  }
  `
);

func(sandboxContext);

示例 2:对比 window 和 this

const proxyWindow = new Proxy(window, {
  get(target, prop) {
    console.log(`[Proxy] 读取属性: ${String(prop)}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`[Proxy] 设置属性: ${String(prop)} = ${value}`);
    target[prop] = value;
    return true;
  },
});

const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  console: proxyWindow.console,
};

const code = `
  // 在 with 块内,window 指向 sandboxContext.window(即 proxyWindow)
  window.myVar = "hello from sandbox";
  console.log("window.myVar:", window.myVar);
  
  // this 仍然指向全局 window(不受 with 影响)
  // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
  console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
  console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
  
  // 但是代码中的 window 指向 proxyWindow
  console.log("代码中的 window === proxyWindow:", window === sandboxContext.window); // 输出: true
`;

const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    ${code}
  }
  `
);

func(sandboxContext);

为什么 this 不受影响?

this 是 JavaScript 的一个特殊关键字,它的值在函数调用时确定,与作用域链无关。with 语句只影响作用域链的查找,不会影响 this 的绑定。

重要区别:window 和 this 在 with 块内

with(sandboxContext) 块内,需要理解以下关键区别:

  1. window 的指向

    • window 会指向 sandboxContext.window(代理对象)
    • 这是通过作用域链查找实现的
  2. this 的指向

    • this 仍然指向全局对象(不受 with 影响)
    • 这是 JavaScript 的 this 绑定机制决定的
  3. 因此

    with (sandboxContext) {
      // window 是 sandboxContext.window(代理对象)
      // this 是全局 window
      console.log(this === window); // false(它们不相等!)
    }
    

关键理解

  • window 标识符通过作用域链查找,在 sandboxContext 中找到,所以指向代理对象
  • this 关键字不受作用域链影响,仍然指向全局对象
  • 这就是为什么在微前端沙箱中,代码里的 window 可以指向代理对象,但 this 仍然指向全局对象

window 对象的指向

关键机制

在微前端沙箱实现中,with 语句的核心作用是替换 window 对象的引用

为什么不能直接用 with(proxyWindow)

// ❌ 错误的方式
const proxyWindow = new Proxy(window, {
  /* ... */
});
with (proxyWindow) {
  window.myVar = "hello"; // window 会去外层作用域查找,找到全局 window
}

问题:如果直接使用 with(proxyWindow),代码中的 window 标识符会去外层作用域查找,找到全局的 window 对象,而不是 proxyWindow

正确的做法

// ✅ 正确的方式
const proxyWindow = new Proxy(window, {
  /* ... */
});

// 创建一个包含 window 属性的上下文对象
const sandboxContext = {
  window: proxyWindow, // 关键:将 proxyWindow 作为 window 属性
  document: proxyWindow.document,
  console: proxyWindow.console,
};

with (sandboxContext) {
  // 现在 window 会在 sandboxContext 中查找
  // 找到 sandboxContext.window(即 proxyWindow)
  window.myVar = "hello"; // 实际访问的是 proxyWindow.myVar
}

执行流程

子应用代码: window.myVar = 'hello'
       ↓
sandboxContext = { window: proxyWindow }
with(sandboxContext) { window.myVar = 'hello' }
       ↓
代码中的 window 在 sandboxContext 中查找
找到 sandboxContext.window(即代理对象 proxyWindow)
       ↓
访问 proxyWindow.myVar,触发 Proxy 的 set 拦截器
       ↓
值被写入 fakeWindow.myVar,而不是真实的 window

在微前端沙箱中的应用

完整的沙箱实现

class WindowProxySandbox {
  private proxy: Window;
  private fakeWindow: Record<string, any> = {};
  private updatedValueSet = new Set<string>();

  constructor() {
    this.fakeWindow = Object.create(null);

    this.proxy = new Proxy(window, {
      get: (_target: Window, prop: string) => {
        // 如果属性在 fakeWindow 中存在,优先返回 fakeWindow 的值
        if (this.updatedValueSet.has(prop)) {
          return this.fakeWindow[prop];
        }
        // 否则返回原始 window 的值
        return (window as any)[prop];
      },

      set: (_target: Window, prop: string, value: any) => {
        // 所有修改都记录到 fakeWindow 中
        this.fakeWindow[prop] = value;
        this.updatedValueSet.add(prop);
        return true;
      },

      has: (_target: Window, prop: string) => {
        return prop in this.fakeWindow || prop in window;
      },

      deleteProperty: (_target: Window, prop: string) => {
        if (this.updatedValueSet.has(prop)) {
          delete this.fakeWindow[prop];
          this.updatedValueSet.delete(prop);
        }
        return true;
      },

      ownKeys: (_target: Window) => {
        const originalKeys = Reflect.ownKeys(window);
        const fakeKeys = Reflect.ownKeys(this.fakeWindow);
        return Array.from(new Set([...originalKeys, ...fakeKeys]));
      },
    });
  }

  /**
   * 执行子应用代码(推荐方式)
   */
  execScriptWith(script: string): any {
    // 创建沙箱上下文,将代理 window 作为属性
    const sandboxContext = {
      window: this.proxy,
      document: (this.proxy as any).document,
      location: (this.proxy as any).location,
      console: (this.proxy as any).console,
    };

    // 使用 Function 构造器创建函数
    const func = new Function(
      "sandboxContext",
      `
      with(sandboxContext) {
        ${script}
      }
    `
    );

    // 执行函数,传入 sandboxContext
    return func(sandboxContext);
  }

  getProxy(): Window {
    return this.proxy;
  }
}

使用示例

const sandbox = new WindowProxySandbox();

// 子应用的代码(模拟从远程加载的代码)
const appCode = `
  // 子应用代码中直接使用 window,不需要任何修改
  window.myApp = 'sub-app-proxy';
  window.myConfig = { version: '1.0.0', env: 'production' };
  
  // 访问 window 的其他属性也会被代理
  console.log('window.location:', window.location);
  
  // 设置全局变量
  window.globalVar = 'hello from sub app';
`;

// 执行子应用代码
sandbox.execScriptWith(appCode);

// 验证隔离性
console.log("代理 window.myApp:", sandbox.getProxy().myApp); // 输出: sub-app-proxy
console.log("真实 window.myApp:", window.myApp); // 输出: undefined(未被污染)

实际代码示例

示例 1:基本 with 使用

function example1_BasicWith() {
  const obj = {
    name: "Alice",
    age: 25,
    city: "Beijing",
  };

  // 使用 Function 构造器(避免严格模式限制)
  const func = new Function(
    "obj",
    `
    with(obj) {
      // 在 with 块内,可以直接访问 obj 的属性
      console.log("name:", name);        // 输出: Alice
      console.log("age:", age);          // 输出: 25
      console.log("city:", city);        // 输出: Beijing
      
      // 也可以修改属性
      age = 26;
      console.log("修改后的 age:", age); // 输出: 26
    }
    
    // with 块外,需要正常访问
    console.log("with 块外访问:", obj.age); // 输出: 26
  `
  );

  func(obj);
}

示例 2:this 指向演示

function example2_ThisBinding() {
  const obj = {
    name: "Context Object",
    test: function () {
      console.log("this.name:", this.name);
      return this;
    },
  };

  const sandboxContext = {
    window: { name: "Sandbox Window" },
    obj: obj,
  };

  const func = new Function(
    "sandboxContext",
    `
    with(sandboxContext) {
      // window 指向 sandboxContext.window
      console.log("window.name:", window.name); // 输出: Sandbox Window
      
      // 但是 this 仍然指向全局对象,不受 with 影响
      // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
      console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
      console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
      
      // 调用 obj.test(),this 指向 obj,不是 sandboxContext
      const result = obj.test(); // 输出: this.name: Context Object
      console.log("obj.test() 返回的 this === obj:", result === obj); // 输出: true
    }
  `
  );

  func(sandboxContext);
}

示例 3:微前端沙箱应用

function example3_MicroFrontendSandbox() {
  // 创建代理 window
  const proxyWindow = new Proxy(window, {
    get(target, prop) {
      console.log(`[Proxy] 读取属性: ${String(prop)}`);
      return target[prop];
    },
    set(target, prop, value) {
      console.log(`[Proxy] 设置属性: ${String(prop)} = ${value}`);
      target[prop] = value;
      return true;
    },
  });

  const sandboxContext = {
    window: proxyWindow,
    document: proxyWindow.document,
    console: proxyWindow.console,
  };

  const code = `
    // 在 with 块内,window 指向 sandboxContext.window(即 proxyWindow)
    window.myVar = "hello from sandbox";
    console.log("window.myVar:", window.myVar);
    
    // this 仍然指向全局 window(不受 with 影响)
    // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
    console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
    console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
    
    // 但是代码中的 window 指向 proxyWindow
    console.log("代码中的 window === proxyWindow:", window === sandboxContext.window); // 输出: true
  `;

  const func = new Function(
    "sandboxContext",
    `
    with(sandboxContext) {
      ${code}
    }
  `
  );

  func(sandboxContext);

  console.log("\n验证结果:");
  console.log("proxyWindow.myVar:", proxyWindow.myVar);
  console.log("真实 window.myVar:", window.myVar);
}

示例 4:with 内外对比

function example4_CompareInsideAndOutside() {
  const sandboxContext = {
    window: { name: "Proxy Window", myVar: "sandbox value" },
    globalVar: "I'm in sandbox",
  };

  const func = new Function(
    "sandboxContext",
    `
    // with 块外
    console.log("=== with 块外 ===");
    console.log("window:", typeof window); // 输出: object(全局 window)
    console.log("globalVar:", typeof globalVar); // 输出: undefined(未定义)
    
    // with 块内
    with(sandboxContext) {
      console.log("=== with 块内 ===");
      // window 现在指向 sandboxContext.window
      console.log("window.name:", window.name); // 输出: Proxy Window
      console.log("window.myVar:", window.myVar); // 输出: sandbox value
      
      // globalVar 现在指向 sandboxContext.globalVar
      console.log("globalVar:", globalVar); // 输出: I'm in sandbox
      
      // this 仍然指向全局对象(不受 with 影响)
      // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
      console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
      console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
    }
    
    // with 块外,恢复原状
    console.log("=== with 块外(恢复)===");
    console.log("window:", typeof window); // 输出: object(全局 window)
    console.log("globalVar:", typeof globalVar); // 输出: undefined
  `
  );

  func(sandboxContext);
}

常见问题和注意事项

1. 严格模式限制

问题with 语句在严格模式下不能直接使用。

"use strict";
with (obj) {
  // ❌ SyntaxError: Strict mode code may not include a with statement
  // ...
}

解决方案:使用 Function 构造器动态创建函数(不在严格模式下执行)。

// ✅ 正确的方式
const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    // 代码在这里执行
  }
  `
);

2. 性能考虑

with 语句会影响 JavaScript 引擎的优化,因为引擎无法在编译时确定变量的作用域。但在微前端沙箱场景中,这是必要的权衡。

3. 调试困难

使用 with 语句会让代码调试变得困难,因为变量的实际来源不明确。建议:

  • 添加详细的日志
  • 使用清晰的变量命名
  • 在开发环境中禁用 with,使用其他方式

4. 作用域污染

with 语句会将对象的所有属性添加到作用域链中,可能导致意外的变量覆盖。

const obj = {
  console: "这不是 console 对象",
  log: "这也不是 log 方法",
};

with (obj) {
  console.log("这可能会出错!"); // ❌ 因为 console 被覆盖了
}

解决方案:只将必要的属性添加到 sandboxContext 中。


最佳实践

1. 始终使用 sandboxContext 对象

// ❌ 错误:不要直接用 with(proxyWindow)
with (proxyWindow) {
  window.myVar = "hello"; // window 会去外层作用域查找
}

// ✅ 正确:使用 sandboxContext 对象
const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  console: proxyWindow.console,
};

with (sandboxContext) {
  window.myVar = "hello"; // window 指向 sandboxContext.window
}

2. 处理全局对象

除了 window,还要处理其他全局对象:

const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  location: proxyWindow.location,
  console: proxyWindow.console,
  // 根据需要添加其他全局对象
};

3. 错误处理

子应用代码执行可能出错,需要添加错误处理:

try {
  sandbox.execScriptWith(appCode);
} catch (error) {
  console.error("执行子应用代码时出错:", error);
  // 记录错误日志,便于调试
}

4. 性能优化

  • 避免频繁创建和销毁沙箱
  • 缓存常用的全局对象引用
  • 只在必要时使用 with 语句

5. 兼容性考虑

  • 检查浏览器是否支持 Proxy(现代浏览器都支持)
  • 不支持时降级到快照沙箱(SnapshotSandbox)

6. 安全性

  • 只加载可信的子应用代码
  • 限制子应用访问某些敏感 API
  • 使用 CSP(Content Security Policy)限制代码执行

总结

核心要点

  1. with 语句的作用

    • 将对象添加到作用域链的最前端
    • with 块内可以直接访问对象的属性
  2. this 的指向(最重要)

    • this 不受 with 语句影响
    • this 始终指向函数调用时的上下文
    • with 块内,this 仍然指向原来的对象(通常是全局对象)
  3. window 的指向

    • 不能直接用 with(proxyWindow)
    • 应该用 sandboxContext = { window: proxyWindow } 然后 with(sandboxContext)
    • 这样代码中的 window 会指向 sandboxContext.window(即代理对象)
  4. 作用域链查找顺序

    • 局部变量 → with 指定的对象 → 外层作用域 → 全局作用域
  5. 实际应用

    • 微前端沙箱实现的核心机制
    • 通过 with 语句替换 window 引用
    • 结合 Proxy 实现完全隔离

关键代码模式

// 1. 创建代理 window
const proxyWindow = new Proxy(window, {
  /* ... */
});

// 2. 创建沙箱上下文
const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  // ... 其他全局对象
};

// 3. 使用 with 语句执行代码
const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    ${appCode}
  }
  `
);

func(sandboxContext);

参考资料

Promise × 定时器全场景手写

作者 二二四一
2025年11月30日 17:01

🥇 01. 并发限制调度器(异步霸榜 No.1)

场景:你要发 100 个请求,但后端限流,每次只能发 N 个。

交互:可以在中途暂停执行,获取已执行的结果。

🤔考点:工程思维能力


🌈实现:模拟一个迷你版“浏览器资源调度器”,这个调度器的核心本质,是通过「running 计数」「idx 游标」「runNext 自驱动」三者配合,实现一个动态的任务池。它保证任务源源不断执行,但同时不会超过给定的并发上限。

  1. 调用方法
export function MyWork() {
  // 生成调度器
  const scheduler = limitRequests(tasks, 3);

  function handleStart() {
    scheduler.start().then((res) => {
      console.log("所有任务完成!");
      console.log("结果:", res);
    });
  }

  function handleEnd() {
    console.log("暂停完成执行~");
    scheduler.stop();
  }

  return (
    <div>
      <button onClick={handleStart}>开始</button>
      <button onClick={handleEnd}>暂停</button>
    </div>
  );
}

2. 自定义调度器

export function limitRequests(tasks, limit) {
  const res = [] // 存所有任务返回的 Promise,用来最终 Promise.all
  let idx = 0 // 当前处理到第几个任务
  let running = 0 // 当前正在执行的任务数量(关键的并发控制变量)
  let stopped = false // 用于标识是否已停止

  // 暴露的停止执行的方法
  function stop() {
    stopped = true
  }

  function start() {
    return new Promise((resolve, reject) => {
      function runNext() {
        // 执行队列处理完毕或者已暂停,返回结果
        if (running === 0 && stopped) {
          return resolve(Promise.all(res))
        }

        // 正在执行的任务数量不超过单次限制,存在未执行的任务
        while (running < limit && idx < tasks.length) {
          // 如果停止标志为 true,阻止新的任务加入
          if (stopped) {
            return
          }
          // 获取当前任务并执行
          const cur = tasks[idx++]()
          res.push(cur)
          running++
          cur.then(() => {
            running--
            runNext()
          }).catch(reject)
        }
      }

      runNext()
    })
  }

  return { stop, start }
}

3. 模拟异步方法、准备数据

// 创建100个任务
export const tasks = Array.from({ length: 100 }, (_, i) => () => fetchData(i))

// 模拟异步请求方法
export function fetchData(id: number) {
  return new Promise(resolve => {
    const time = Math.random() * 2000
    console.log(`开始任务: ${id}`)

    setTimeout(() => {
      console.log(`完成任务: ${id}`)
      resolve(id)
    }, time)
  })
}

4. 自定义hook

const useLimitRequests = (tasks: any[], limit: number)=> {
  const resultRef = useRef<number[]>([]); // 用 useRef 存储任务结果,避免重新渲染
  const isStop = useRef<boolean>(false);
  const idx = useRef<number>(0);
  const reunning = useRef<number>(0);

  const onStop = useCallback(() => {
    isStop.current = true;
  },[]);

  const onStart = useCallback(() => {
    return new Promise((resolve, reject) => {
      function nextRun(){
        if(reunning.current === 0 && isStop.current) {
          return resolve(Promise.all(resultRef.current));
        }

        while(idx.current < tasks.length && reunning.current < limit){
          if(isStop.current){
            return;
          }

          const curTaskRes = tasks[idx.current]();
          idx.current += 1;
          resultRef.current.push(curTaskRes)
          reunning.current += 1;

          curTaskRes.then(() => {
            reunning.current -= 1;
            nextRun();
          }).catch((error: any) => reject(error))
        }
      }

      nextRun();
    })
  },[isStop, limit, tasks]);

  return { onStop, onStart};
}

image.png

🥈 02. 支持指数退避的重试(Backoff Retry)

场景:接口偶尔报错,你希望自动重试 3 次,每次等待时间翻倍。

💡 可靠性思维能力


  1. 自定义重试方法
function retry(fn, times = 3, delay = 500) {
  return new Promise((resolve, reject) => {
    const attempt = (n, d) => {
      fn().then(resolve).catch(err => {
        if (n === 0) return reject(err)
        setTimeout(() => attempt(n - 1, d * 2), d)
      })
    }
    attempt(times, delay)
  })
}

2. 调用

  function handleRetry() {
    retry(mockRequest, 3, 500)
      .then((result) => console.log(result)) // 如果请求成功,输出结果
      .catch((error) => console.log(error)); // 如果重试失败,输出错误
  }

image.png

🥉 03. 带超时控制的 Promise(Timeout Promise)

场景:请求超 3 秒自动失败,不等了。

🕒 超时包装器


  1. 自定义函数实现
export function withTimeout(fn, ms){
  // 存放定时器
  let timer = null;

  // 超时函数
  const timeOut = () => new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('超时了')), ms);
  });

  // Promise.race 会返回一个结果, fn 目标函数
  return Promise.race([fn(), timeOut()]).finally(() => {
    clearTimeout(timer);
  })
}

2. 模拟延迟异步方法

export function slowTask() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Task completed'), 2000); // 模拟一个 3 秒的任务
  });
}

3. 调用

  function handleTimeOut() {
    withTimeout(slowTask, 1000) // 设置 1 秒超时
      .then((result) => console.log(result)) // 如果任务完成,输出结果
      .catch((error) => console.log(error)); // 如果超时,输出超时错误
  }

image.png

🚢 04. 串行任务:一步一步稳扎稳打

每个任务会按顺序一个接一个地执行,直到上一个任务完成后,才会开始下一个任务

📌 场景:分片上传、表单分步骤提交


  1. 自定义方法
export async function runInSequence(tasks){
  const result = [];

  for (const task of tasks) {
    const res = await task();
    result.push(res);
  }

  return result;
}

2. 模拟异步请求

export const fetchData = (task: any) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`Task ${task} completed`);
      resolve(`Result of ${task}`);
    }, 1000); // 每个任务延迟 1 秒
  });
};

// 定义任务列表
export const tasks = [
  () => fetchData('task1'),
  () => fetchData('task2'),
  () => fetchData('task3')
];

3. 调用

async function handleEquence(){
    const result = await runInSequence(tasks);
    console.log('All tasks completed', result);
  }

image.png

⌚️ 05. Promise 版“多方等待 ready”机制

这个机制用于让多个任务或组件等待某个条件(如 ready() 方法被调用)满足后再继续执行

应用场景

  • 多个任务依赖同一个条件:比如,多个组件在等待某个数据加载完成后再开始执行某个操作

  • 等待多个异步任务的准备:多个异步任务可能依赖某个资源,只有当该资源准备好时,才能继续执行后续操作。

  • 协调并发任务的开始:不同的任务或组件可以等待一个共同的“开始信号”,一旦信号发送,所有等待的任务就可以同时开始。


  1. 自定义class
export class Waiter{
  queue: any[];
  readyFlag: boolean;

  constructor(){
    this.queue = []; // 所有等待的任务
    this.readyFlag = false; // 是否已经准备好
  }

  wait(){
    // 条件已经准备好了,直接返回一个已解决的 Promise
    if(this.readyFlag) {
      return Promise.resolve();
    } else {
      // 将该任务的 Promise 放入 queue 队列中,等待
      return new Promise((r) => this.queue.push(r));
    }
  }


  ready(){
    this.readyFlag = true; // 设置条件已准备好
    this.queue.forEach(r => r()); // 遍历队列并触发所有等待的任务
    this.queue = []; // 清空队列
  }
}

2. 调用

function handleReady() {
    const waiter = new Waiter();

    // 任务 1:等待条件准备好后执行
    waiter.wait().then(() => console.log("Task 1 completed"));

    // 任务 2:等待条件准备好后执行
    waiter.wait().then(() => console.log("Task 2 completed"));

    // 任务 3:等待条件准备好后执行
    waiter.wait().then(() => console.log("Task 3 completed"));

    // 在 2 秒后,调用 `ready()`,表示条件准备好,所有任务可以执行
    setTimeout(() => {
      waiter.ready(); // 调用 ready,触发所有等待的任务
    }, 2000);
  }

image.png

06. 可暂停 / 恢复的 setInterval(轮询神器)

可以启动、暂停和恢复一个定时任务,而无需重启整个定时器

🌡️ 场景:页面隐藏暂停轮询,返回恢复


  1. 自定义class
export class PausableInterval{
  delay: number;
  fn: any;
  timer: any;
  running: boolean;

  constructor(fn: any, delay: number){
    this.fn = fn            // 定时任务函数
    this.delay = delay      // 定时器的间隔时间(单位:毫秒)
    this.timer = null       // 存储定时器的标识符
    this.running = false    // 标记定时器是否正在运行
  }

  start(){
    // 如果定时器已经在运行,直接返回,不做重复启动
    if(this.running) return;

    this.running = true;

    const tick = () => {
      if (!this.running) return // 如果定时器已暂停,则不再继续执行
      this.fn(); // 执行定时任务
      this.timer = setTimeout(tick, this.delay) // 使用 setTimeout 模拟 setInterval
    }

    tick();
  }

  pause() {
    clearTimeout(this.timer);
    this.running = false;
  }

  resume(){
    this.start()
  }
}

2. 模拟请求

function printMessage() {
  console.log("Task is running...");
}

export const pausableInterval = new PausableInterval(printMessage, 1000);

3. 调用

  function handleStartInterval() {
    pausableInterval.start();

    // 停止定时器
    setTimeout(() => {
      console.log("Pausing the task...");
      pausableInterval.pause();
    }, 3000); // 3秒后暂停

    // 恢复定时器
    setTimeout(() => {
      console.log("Resuming the task...");
      pausableInterval.resume();
    }, 5000); // 5秒后恢复
  }

image.png

07. 带最大等待 maxWait 的防抖(搜索框的神)

用于优化那些频繁触发的事件,特别是在搜索框、输入框或滚动等高频率操作中,常常用来减少不必要的计算或请求

💡 场景:搜索请求太多?一招治愈


  1. 自定义方法
function debounce(fn, delay, { maxWait = 0 } = {}) {
  let timer = null; // 存放定时器
  let start = null; // 第一次调用时间

  return function (...args) {
    const now = Date.now();  // 获取当前时间戳
    if (!start) start = now; // 记录第一次调用的时间

    clearTimeout(timer);  // 清除之前的定时器,避免多次触发

    const run = () => { 
      start = null;  // 重置 `start`,表示已经执行过操作
      fn.apply(this, args);  // 执行函数,并传入当前的 `this` 和参数
    };

    // 如果到达 `maxWait` 时间,强制执行 `fn`;否则继续延迟执行
    if (maxWait && now - start >= maxWait) run(); 
    else timer = setTimeout(run, delay);  // 在 `delay` 时间后执行
  };
}

2. 模拟短期内多次触发

function searchQuery(query) {
  console.log("Searching for:", query);
}

const debouncedSearch = debounce(searchQuery, 500, { maxWait: 2000 });

// 模拟用户输入
debouncedSearch("apple");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple pie");

08. 可取消的异步任务(不要让旧任务留着捣乱)

场景:页面切换后取消 pending 的 loading。

function cancellable(fn, delay) {
  let timer
  const p = new Promise(resolve => {
    timer = setTimeout(() => resolve(fn()), delay)
  })
  return { promise: p, cancel: () => clearTimeout(timer) }
}

9. 时间窗口限流(搜索框请求合并)

🌈 场景:500ms 内所有输入合并一次请求,节省带宽又快。

function createWindowRequester(fn, ms) {
  let timer = null
  let queue = []

  return function (...args) {
    return new Promise(resolve => {
      queue.push({ args, resolve })

      if (!timer) {
        timer = setTimeout(async () => {
          const batch = [...queue]
          queue = []
          timer = null

          const res = await fn(batch.map(i => i.args))
          batch.forEach((item, i) => item.resolve(res[i]))
        }, ms)
      }
    })
  }
}

10. 带优先级任务调度(Mini Scheduler)

场景:动画、后台任务、预加载策略。

class Scheduler {
  constructor() {
    this.queue = []
    this.running = false
  }
  add(fn, priority = 0) {
    this.queue.push({ fn, priority })
    this.queue.sort((a, b) => b.priority - a.priority)
    this.run()
  }
  async run() {
    if (this.running) return
    this.running = true
    while (this.queue.length) {
      const job = this.queue.shift()
      await job.fn()
    }
    this.running = false
  }
}

12. 并行预加载 + 串行渲染(列表加载体验优化)

🎨 场景:图片墙“先加载、再有序渲染”。

async function preloadAndRender(urls, render) {
  const preloads = urls.map(url => fetch(url).then(r => r.blob()))
  for (let i = 0; i < preloads.length; i++) {
    const data = await preloads[i]
    render(data, i)
  }
}

ts类型工具

作者 Robet
2025年11月30日 16:54

TypeScript 提供了一套强大的内置工具类型(Utility Types),用于从已有类型中派生出新类型,从而提升代码的健壮性、可维护性和开发效率。这些工具类型就像“类型世界的高阶函数”,能对类型进行组合、裁剪、转换等操作。


🧰 常用 TypeScript 工具类型速查表

分类 工具类型 作用
基础修饰 Partial<T> T 的所有属性变为可选
Required<T> T 的所有属性变为必填(移除 ?
Readonly<T> T 的所有属性变为只读
结构挑选 Pick<T, K> T选取指定键 K 的属性
Omit<T, K> T剔除指定键 K 的属性
Record<K, T> 构造一个键为 K、值为 T 的对象类型
类型过滤 Exclude<T, U> 从联合类型 T排除可赋值给 U 的类型
Extract<T, U> 从联合类型 T提取可赋值给 U 的类型
NonNullable<T> T 中移除 nullundefined
函数相关 ReturnType<T> 获取函数 T返回值类型
Parameters<T> 获取函数 T参数类型元组
ConstructorParameters<T> 获取构造函数的参数类型
InstanceType<T> 获取构造函数的实例类型
ThisParameterType<T> / OmitThisParameter<T> 处理函数中的 this 参数

🔍 典型示例与应用场景

1. Partial<T>:局部更新

interface User {
  id: number;
  name: string;
  email: string;
}

// 所有字段变为可选
type UpdateUser = Partial<User>;

function updateUser(id: number, changes: UpdateUser) {
  // 只需传入要修改的字段
}
updateUser(1, { name: "Alice" }); // ✅

2. Pick<T, K> / Omit<T, K>:按需选择或排除字段

type UserPreview = Pick<User, 'id' | 'name'>; // { id: number; name: string }
type UserWithoutId = Omit<User, 'id'>;        // { name: string; email: string }

3. Record<K, T>:构建配置对象

type Theme = 'light' | 'dark';
type ColorMap = Record<Theme, string>; // { light: string; dark: string }

4. ReturnType<T>:推导函数返回类型

function fetchUser() {
  return { id: 1, name: "Bob" };
}

type User = ReturnType<typeof fetchUser>; // { id: number; name: string }

5. Exclude<T, U> / Extract<T, U>

type Status = 'loading' | 'success' | 'error';
type ValidStatus = Exclude<Status, 'loading'>; // 'success' | 'error'
type LoadingOnly = Extract<Status, 'loading'>; // 'loading'

💡 小贴士

  • 这些工具类型基于 映射类型(Mapped Types)条件类型(Conditional Types)infer 等高级特性实现。
  • 它们是不可变的:不会修改原始类型,而是生成一个新类型。
  • 组合使用,例如:
    type SafeUser = Readonly<Partial<User>>;
    

如需深入某个工具类型的源码实现或更多实战案例,可以告诉我具体类型(如 OmitReturnType),我可以进一步详解!

react-native-promise-portal:React Native 弹窗管理的新思路

作者 soul96816
2025年11月30日 16:44
owjymdk6/f1627a1owjymdk6/f1627a1owjymdk6/f1627a1owjymdk6/f1627a1

在 React Native 开发中,我们经常需要弹出对话框、提示框、日期选择器或者其他 overlay 组件。传统方式通常依赖 state + conditional rendering + 回调函数,但在复杂场景下,代码会变得分散、难维护,尤其是当多个弹窗同时存在时。

react-native-promise-portal 提供了一种 Promise + Portal 的解决方案,让你可以像调用普通异步函数一样调用弹窗,同时保持逻辑线性、易于管理。

核心特点

  1. Promise-first 弹窗调用
    弹窗调用返回 Promise,你可以用 await 直接获取用户操作结果。业务逻辑与 UI 逻辑解耦,写出来的代码更直观。

  2. 支持多重弹窗 & name 去重

    • 可以在同一页面同时显示多个弹窗,每个弹窗独立关闭。
    • 通过 index 控制渲染层级,index 越大,显示在最上层。
    • 通过 name 去重,避免重复弹窗触发业务冲突。
  3. 局部 Portal

    • 支持在特定页面或模块内管理弹窗,不影响全局 Portal 状态。
    • 跨组件调用弹窗,保证弹窗只在目标页面显示。
    • 避免不同页面弹窗相互覆盖或冲突,提升模块化管理能力。
  4. 脱离 Hook 调用局部 UI

    • 使用 PortalManager 可以在 非组件上下文 触发局部弹窗。
    • 适用于后台逻辑、网络请求回调、定时任务等场景。
    • Promise 接口仍然保持逻辑线性。
  5. 基于 Portal 渲染

    • 利用 React Native 的 Portal 技术,将弹窗渲染到顶层,解决 z-index 和 clipping 问题。

基本使用

1. 设置 PortalProvider

import { PortalProvider } from 'react-native-promise-portal';

export default function RootLayout() {
  return <PortalProvider>{/* Your app content */}</PortalProvider>;
}

2. 使用 Hook 调用弹窗

import { usePortal } from 'react-native-promise-portal';

function MyComponent() {
  const { showWithOverlay } = usePortal();

  const handleShowDialog = async () => {
    try {
      const result = await showWithOverlay<boolean>({
        component: ({ close }) => (
          <Confirm title="Confirm" subTitle="Are you sure?" close={close} />
        ),
      });
      console.log('Result:', result);
    } catch (error) {
      console.error(error);
    }
  };

  return <Button onPress={handleShowDialog} title="Show Dialog" />;
}

多重弹窗与 name 去重示例

const { showWithOverlay } = usePortal();

// 弹出第一个弹窗
await showWithOverlay({
  name: 'confirm-delete',
  component: ({ close }) => <Confirm close={close} />,
});

// 再次调用同名弹窗会抛出 PortalAlreadyExistsError
await showWithOverlay({
  name: 'confirm-delete',
  component: ({ close }) => <Confirm close={close} />,
});

通过 index 可以控制层级顺序:

const promise1 = showWithOverlay({ title: 'Dialog 1', index: 1 });
const promise2 = showWithOverlay({ title: 'Dialog 2', index: 10 }); // 更高层级
const promise3 = showWithOverlay({ title: 'Dialog 3', index: 5 });

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  console.log('All dialogs closed:', results);
});

局部 Portal 解决的业务痛点

在大型应用中,某些弹窗只在特定页面显示,使用全局 Portal 管理容易导致以下问题:

  • 弹窗状态全局混乱
  • 不同页面弹窗可能相互覆盖
  • 逻辑与 UI 耦合,调用分散

局部 Portal 通过 PortalManager 提供页面级管理:

import { PortalRender, PortalManager } from 'react-native-promise-portal';
import { useLocalPortals } from './helper/LocalPortal';

function HomePage() {
  const homePortalContent = useLocalPortals((state) => state.homePortalContent);

  const handleShowLocalPortal = async () => {
    await HomePagePortalManager.showWithOverlay<boolean>({
      component: ({ close }) => (
        <Confirm
          title="Local portal"
          subTitle="Only on home page"
          close={close}
        />
      ),
    });
  };

  return (
    <>
      <Button onPress={handleShowLocalPortal} title="Show Local Portal" />
      <PortalRender portals={homePortalContent} />
    </>
  );
}

局部 Portal 优势:

  1. 弹窗只影响特定页面或模块
  2. 跨组件 / 跨 Hook 调用更灵活
  3. 避免全局冲突,提高模块化和可维护性

脱离 Hook 调用局部 UI

有些业务场景中,你可能希望在 非 React 组件上下文Hook 不方便使用的地方 调用弹窗,例如网络请求回调或定时任务。

import { HomePagePortalManager } from './helper/LocalPortal';

async function handleAsyncEvent() {
  try {
    const result = await HomePagePortalManager.showWithOverlay<boolean>({
      component: ({ close }) => (
        <Confirm
          title="Async Event"
          subTitle="This dialog is triggered outside React component"
          close={close}
        />
      ),
      overlay: { orientation: 'centerMiddle' },
    });
    console.log('User result:', result);
  } catch (err) {
    console.error('Portal closed or error:', err);
  }
}

特点:

  • 脱离组件 / Hook,任意业务逻辑可触发
  • 弹窗仅显示在目标页面或模块
  • Promise 接口保持逻辑线性

更多示例

Loading 指示器

const { showWithOverlay } = usePortal();

const showLoading = () => {
  const { close } = showWithOverlay<void>({
    component: () => <ActivityIndicator />,
    overlay: { closeable: false },
  });

  setTimeout(() => close(), 3000);
};

日期选择器(底部弹出)

const date = await showWithOverlay<string>({
  component: ({ close }) => (
    <DatePicker
      onSelect={(date) => close(date)}
      onCancel={() => close(new Error('Cancelled'))}
    />
  ),
  overlay: { orientation: 'centerBottom' },
});

错误处理

import { PortalError } from 'react-native-promise-portal';

try {
  const result = await showWithOverlay<boolean>({
    component: ({ close }) => <Confirm close={close} />,
  });
} catch (error) {
  if (error instanceof PortalError) {
    if (error.isCloseByOverlayPressError()) console.log('Closed by overlay');
    else if (error.isCloseByHardwareBackPressError()) console.log('Closed by back button');
    else if (error.isPortalAlreadyExistsError()) console.log('Portal already exists');
  }
}

总结

react-native-promise-portal 将弹窗调用封装为异步函数,支持:

  • 多重弹窗
  • name 去重机制
  • 局部 Portal
  • 脱离 Hook 调用局部 UI
  • Promise 异步接口

它极大简化了 React Native 弹窗管理,保证 UI 与业务逻辑解耦,调用方式直观、可维护,是复杂页面交互场景下的理想选择。


安装

npm install react-native-promise-portal

如果你喜欢这个库,欢迎点个 ⭐️ 支持一下!

GitHub stars


为什么 SVG 能在现代前端中胜出?

作者 吹水一流
2025年11月30日 16:14

如果你关注前端图标的发展,会发现一个现象:

过去前端图标主要有三种方案:

  • PNG 小图(配合雪碧图)

  • Iconfont

  • SVG

到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。
无论 React/Vue 项目、新框架(Next/Remix/Nuxt),还是大厂的设计规范(Ant Design、Material、Carbon),基本都默认 SVG。

为什么是 SVG 胜出?
为什么不是 Iconfont、不是独立 PNG、不是雪碧图?
答案不是一句“清晰不失真”这么简单。

下面从前端实际开发的角度,把 SVG 胜出的原因讲透。


一、SVG 为什么比位图(PNG/JPG)更强?

矢量图永不失真(核心优势)

PNG/JPG 是位图,只能按像素存图。
移动端倍率屏越来越高(2x、3x、4x……),一张 24px 的 PNG 在 iPhone 高分屏里可能看起来糊成一团。

SVG 是矢量图,数学计算绘制:

  • 任意缩放不糊

  • 任意清晰度场景都不怕

  • 深色模式也不会变形

这点直接解决了前端图标领域长期存在的一个痛点:适配成本太高


体积小、多级复用不浪费

同样一个图标:

  • PNG 做 1x/2x/3x 需要三份资源

  • SVG 只要一份

而且:

  • SVG 本质是文本

  • gzip 压缩非常有效

在 CDN 下,通常能压到个位数 KB,轻松复用。


图标换色非常容易

PNG 改颜色很麻烦:

  • 设计师改

  • 重新导出

  • 重新上传/构建

Iconfont 的颜色只能统一,只能覆盖轮廓颜色,多色很麻烦。

SVG 则非常灵活:

.icon {
  fill: currentColor;
}

可以跟随字体颜色变化,支持 hover、active、主题色。

深浅模式切换不需要任何额外资源。


支持 CSS 动画、交互效果

SVG 不只是图标文件,它是 DOM,可以直接加动画:

  • stroke 动画

  • 路径绘制动画

  • 颜色渐变

  • hover 发光

  • 多段路径动态控制

PNG 和 Iconfont 都做不到这种级别的交互。

很多现代 UI 的微动效(Loading、赞、收藏),都是基于 SVG 完成。


二、SVG 为什么比 iconfont 更强?

Iconfont 在 2015~2019 年非常火,但明显已经退潮了。
原因有以下几个:


① 字体图标本质是“字符”而不是图形

这带来大量问题:

● 不能多色

只能 monochrome,彩色图标很难实现。

● 渲染脆弱

在 Windows 某些字体渲染环境下会出现:

  • 发虚

  • 锯齿

  • baseline 不一致

● 字符冲突

不同项目的字体图标可能互相覆盖。

相比之下,SVG 是独立图形文件,没有这些问题。


② iconfont 需要加载字体文件,失败会出现“乱码方块”

如果字体文件没加载成功,你会看到:

☐ ☐ ☐ ☐

这在弱网、支付类页面、海外环境都非常常见。

SVG 就没有这个风险。


③ iconfont 不利于按需加载

字体文件通常包含几十甚至几百个图标:
一次加载很重,不够精细。

SVG 可以做到按需加载:

  • 一个组件一个 SVG

  • 一个页面只引入用到的部分

  • 可组合、可动态切换

对于现代构建体系非常友好。


三、SVG 为什么比“新版雪碧图”更强?

即便抛开 iconfont,PNG 雪碧图也完全被淘汰。

原因很简单:

  • 雪碧图文件大

  • 缓存粒度差

  • 不可按需加载

  • 维护复杂

  • retina 适配麻烦

  • 颜色不可动态变更

而 SVG 天生具备现代开发所需的一切特性:

  • 轻量化

  • 组件化

  • 可变色

  • 可动画

  • 可 inline

  • 可自动 tree-shaking

雪碧图本质上是为了“减少请求数”而生的产物,
但在 HTTP/2/3 中已经没有价值。

而 SVG 不是 hack,而是自然适配现代 Web 的技术方案


四、SVG 为什么能在工程体系里更好地落地?

现代构建工具(Vite / Webpack / Rollup)原生支持 SVG:

  • 转组件

  • 优化路径

  • 压缩

  • 自动雪碧(symbol sprite)

  • Tree-shaking

  • 资源分包

这让 SVG 完全融入工程体系,而不是外挂方案。

例如:

import Logo from './logo.svg'

你可以:

  • 当组件使用

  • 当资源下载

  • 当背景图

  • 动态注入

工程化友好度是它胜出的关键原因之一。


五、SVG 胜出的根本原因总结

不是 SVG “长得好看”,也不是趋势,是整个现代前端生态把它推到了最合适的位置。

1)协议升级:HTTP/2/3 让雪碧图和 Iconfont 的优势全部消失
2)设备升级:高分屏让位图模糊问题暴露得更明显
3)工程升级:组件化开发需要精细化图标
4)体验升级:动画、主题、交互都离不开 SVG

一句话总结:

SVG 不只是“更清晰”,而是从工程到体验全面适配现代前端的图标方案,因此胜出。

前端转战后端:JavaScript 与 Java 对照学习指南 (第一篇 - 深度进阶版)

作者 汤姆Tom
2025年11月30日 15:24

对于习惯了 JavaScript (JS) 灵活性的前端开发者来说,Java 看起来可能充满了繁琐的定义和样板代码。但实际上,现代 Java (Java 8/11/17+) 已经吸收了很多函数式编程的特性,写起来越来越顺手。

本篇指南将通过 JS vs Java 代码对比的方式,深度解析 类型系统流式处理 (Stream API)集合操作 以及 常见的内存陷阱

1. 核心思维转变:从“自由”到“约束”

在开始写代码前,需要建立三个核心认知的转变:

  1. 入口函数:JS 代码通常从上到下执行;Java 程序必须从一个 main 方法开始。
  2. 类型约束:JS 是 let a = 1 (a 随后可以变成字符串);Java 是 int a = 1 (a 永远只能是整数)。
  3. 引用与值:JS 对对象默认是引用传递,Java 也是引用传递(操作内存地址),但 Java 的字符串是不可变的,且比较机制完全不同。

2. 变量声明:var 的真相与基本类型

Java 10 引入了 var,这让前端感到非常亲切,但它和 JS 的 let/var 有本质区别。

场景:类型推断与作用域

JavaScript

// JS: 动态类型
let id = 10;
id = "User-10"; // ✅ 合法,类型变了

// 作用域
if (true) {
    var oldVar = "I leak out"; // var 会提升 (Hoisting)
    let newLet = "I am safe";  // 块级作用域
}
console.log(oldVar); // 能打印

Java

public class VariableDeepDive {
    public static void main(String[] args) {
        // --- 1. Java 10+ 的 var (局部变量类型推断) ---
        // 看起来像 JS,但实际上编译器在编译时就确定了类型
        var id = 10; // 编译器推断 id 是 int 类型
        // id = "User-10"; // ❌ 报错!一旦推断为 int,就永远是 int
        
        // --- 2. 基本数据类型 vs 包装类型 (深度解析) ---
        // int: 存数值,占用内存少,默认值 0
        int count = 0;
        
        // Integer: 存对象的地址,默认值 null
        // 自动装箱(Autoboxing): Java 自动把 int 5 转为 Integer 对象
        Integer score = 5; 
        
        // ⚠️ 坑:空指针异常 (NPE)
        Integer unknownScore = null;
        // int finalScore = unknownScore; // ❌ 运行时崩溃!拆箱 null 会报错
        
        // 最佳实践:
        // 数据库实体类、泛型列表用 Integer
        // 局部变量循环计数用 int
    }
}

3. 字符串:不可变性与内存陷阱

JS 的字符串很简单,Java 的字符串为了性能做了很多底层优化(字符串常量池),导致比较逻辑不同。

场景:拼接与比较

JavaScript

let a = "hello";
let b = "hello";
console.log(a === b); // true

// 模板字符串
let msg = `Value is ${a}`; 

Java

public class StringDeepDive {
    public static void main(String[] args) {
        // --- 1. 比较陷阱 ---
        String s1 = "hello"; // 存放在常量池
        String s2 = new String("hello"); // 强制在堆内存创建新对象
        
        // ❌ == 比较的是内存地址
        System.out.println(s1 == s2); // false
        
        // ✅ equals 比较的是字符内容
        System.out.println(s1.equals(s2)); // true
        
        // --- 2. 拼接的性能问题 ---
        // 简单的拼接编译器会自动优化
        String msg = "Value is " + s1;
        
        // ⚠️ 循环中拼接严禁使用 "+"
        String res = "";
        // ❌ 性能极差,每次循环都会创建新 String 对象
        // for(int i=0; i<100; i++) res += i; 
        
        // ✅ 正确做法:StringBuilder (类似 JS 数组 join)
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<100; i++) {
            sb.append(i);
        }
        System.out.println(sb.toString());
    }
}

4. 数组与列表:Stream API (前端最爱)

Java 8 引入的 Stream API 简直就是前端 Array.prototype 方法(filter, map, reduce)的亲兄弟。

场景:筛选大于 10 的数字并翻倍

JavaScript

const numbers = [5, 12, 8, 20];

// 链式调用:先过滤,再映射
const result = numbers
    .filter(n => n > 10)
    .map(n => n * 2);

console.log(result); // [24, 40]

Java (使用 Stream)

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        // 快速初始化 List (Java 9+)
        List<Integer> numbers = List.of(5, 12, 8, 20); 
        // 注意:List.of 创建的是"不可变列表",不能 add/remove
        
        // --- Stream API ---
        List<Integer> result = numbers.stream() // 1. 开启流
            .filter(n -> n > 10)                // 2. 过滤 (Predicate)
            .map(n -> n * 2)                    // 3. 映射 (Function)
            .collect(Collectors.toList());      // 4. 收集结果回 List
            
        System.out.println(result); // [24, 40]
        
        // --- 传统遍历 (Enhanced For-Loop) ---
        // 类似 JS 的 for (const n of numbers)
        for (Integer n : numbers) {
            System.out.println(n);
        }
    }
}

🔍 差异点:

  • JS 的数组方法直接作用于数组。Java 必须先调用 .stream() 转换成流,处理完后再 .collect() 回集合。
  • Java 的 map 必须返回新值,不能像 JS 某些骚操作里那样不返回值只做副作用(虽然 JS 规范也不建议那样做)。

5. 字典与映射:Map 的花式操作

Map 在后端开发中无处不在,尤其是在处理 JSON 数据时。

场景:初始化与遍历

JavaScript

const map = {
    "key1": "value1",
    "key2": "value2"
};

// 遍历
Object.entries(map).forEach(([k, v]) => {
    console.log(k, v);
});

Java

import java.util.HashMap;
import java.util.Map;

public class MapDeepDive {
    public static void main(String[] args) {
        // --- 1. 快速初始化 (Java 9+) ---
        // 创建不可变 Map,最多支持 10 对
        Map<String, String> quickMap = Map.of(
            "key1", "value1",
            "key2", "value2"
        );
        
        // 常规可变 Map
        Map<String, String> map = new HashMap<>();
        map.put("key1", "value1");
        
        // --- 2. 遍历 ---
        // 方式 A: forEach + Lambda (最像 JS)
        map.forEach((k, v) -> {
            System.out.println("Key: " + k + ", Val: " + v);
        });
        
        // 方式 B: entrySet (性能好,传统方式)
        // Map.Entry 相当于 JS 的 [key, value] 元组
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String k = entry.getKey();
            String v = entry.getValue();
        }
    }
}

6. 常见痛点对照表 (Cheatsheet)

场景 JavaScript Java (最佳实践)
定义不可变常量 const API_URL = "..." static final String API_URL = "...";
模板字符串 `Hello ${name}` String.format("Hello %s", name)"Hello " + name
数组包含 arr.includes(x) list.contains(x)
数组判空 arr.length === 0 list.isEmpty()
对象取值防崩 obj?.prop Optional.ofNullable(obj).map(...) (较复杂) 或简单判空 if (obj != null)
JSON 解析 JSON.parse(str) 使用库:Jackson (objectMapper.readValue(...))
JSON 序列化 JSON.stringify(obj) 使用库:Jackson (objectMapper.writeValueAsString(...))
比较对象 a === b (通常不行) a.equals(b) (必须重写 equals 方法)

核心建议

  1. 善用 IDE:IntelliJ IDEA 是 Java 开发的神器。当你不知道方法名时,输入 . 然后停顿,它会列出所有可用方法,这比查文档快得多。
  2. 拥抱类型:不要为了省事全部用 ObjectMap<String, Object> 模拟 JS 对象。定义一个明确的 User 类(Class)虽然前期麻烦,但在后期维护和重构时,它的优势会碾压动态类型。

一个有趣的CSS题目

作者 小熊哥722
2025年11月30日 15:01

前几天无意间看到一个有趣的题,题目很简单,但是最终的结果却出人意料,题目是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="theme-color" content="blue"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<style>
  .parent{
    display: flex;
    flex-direction: column;
    height: 600px;
    width: 300px;
    background-color: aqua;
  }
  .header{
    height: 200px;
    background-color: red;
  }
  .fotter{
    height: 200px;
    background-color: blue;
  }
  .content{
    height: 100%;
  }
</style>
<body>
  <div class="parent">
    <div class="header"></div>
    <div class="content"></div>
    <div class="fotter"></div>
  </div>
</body>
</html>

这段代码中的content的高度是多少,大家可以自己想一下,稍微思考一下。接下来让我们看一下豆包的回答: 再看一下通义千问的回答:

再看一下deepseek的回答:

最后再看看Gemini3的回答:

大家可能以为content的高度不就是200px吗,但是结果真的是这样的吗?

请大家看一下最终的结果:

       大家会看到content的高度为360px,header和footer的高度为120px,是不是感觉不可思议,为啥会这样嘞,这个问题,想问ai就会发现,这咋问啊,ai全是错的,那我来告诉你们答案(当然我说的也不一定对,但是大差不差,结果肯定是对的):

  • percent height 会被解析为父容器的绝对值:content 的 height:100% → 600px(父高度)。
  • 每个子项的 flex base size(假设基准)分别是 header=200, content=600, footer=200,总和 = 1000px。
  • 可用主轴空间 = 600px,差额(需要收缩) = 1000 - 600 = 400px。
  • 默认 flex-shrink = 1,按基准尺寸比例收缩:content 收缩量 = 600/1000 * 400 = 240 → 最终 content 高度 = 600 - 240 = 360px。

参考(建议重点看 W3C 规范里的算法):

大家可以详细看看理解一下,当然不看也没关系,知道这个计算规则也 可以了,至少比ai强了。

Rect深入学习

2025年11月30日 14:37

React核心机制

虚拟 DOM 和 Diff 算法

什么是虚拟DOM

虚拟DOM可以理解为模拟了DOM树的JS对象树

比如

var element = {
    element: 'ul',
    props: {
        id:"ulist"
    },
    children: [
    { element: 'li', props: { id:"first" }, children: ['这是第一个List元素'] },
    { element: 'li', props: { id:"second" }, children: ['这是第二个List元素'] }
    ]
}

为什么需要虚拟DOM

传统DOM更新方式的问题:

  • 在原生JS中,更新DOM的方式往往是粗颗粒度的更新,直接替换整颗子树,容易造成性能的浪费
  • 如果要做到细颗粒度更新,则需要自己决定修改哪一部分,但这种手动diff很麻烦

虚拟DOM更新的优势:

  • 框架自动对新旧虚拟DOM树进行diff算法
  • 然后精准更新DOM树中变化的部分,大幅度提升性能

举例:

比如有一个列表,我对其进行了修改

//旧UI
<ul id="list">
  <li>苹果</li>
  <li>香蕉</li>
</ul>

//新UI
<ul id="list">
  <li>苹果</li>
  <li>橘子</li> <!-- 改动 -->
</ul>
  • 传统DOM更新:

    • 粗暴做法list.innerHTML = render(items) → 把整个 <ul> 清空并重建 <li>,即使“苹果”没变也会被销毁重建。

    • 精细做法:你必须写逻辑找到第 2 个 <li> 并替换它的文本

      • list.children[1].textContent = '橘子';
      • 但这种手动 diff 很麻烦,开发者必须自己维护 UI 和数据的一致性。
  • 虚拟DOM更新:

    • 框架自动比较新旧虚拟 DOM:

      • 第 1 个 <li> 一样 → 复用。
      • 第 2 个 <li> 文本不同 → 只更新文本。
    • 最终只执行一条 DOM 操作:list.children[1].textContent = '橘子';

diff算法

传统diff算法的时间复杂度是O(n³)

  • 我们要把旧树变成新树,找到最小修改路径

为什么时间复杂度是O(n³)?

  1. 遍历旧树的每个节点(n次)

  2. 遍历新树的每个节点(n次)

    • 对比每个旧树中的节点,找到新树中可能对应的新节点
  3. 比较两个节点的子结构是否完全相同

    • 因为判断“是否同一个节点”不仅要看标签名,还要看它的整个子结构是否相同。 这就需要再深入进去比较它们的子树。

    • 每对匹配节点都可能有一整棵子树;

      每棵子树的节点数也可能接近 n;

      所以在最坏情况下,每一对匹配都要再递归比较一遍整棵子树。

于是复杂度变成:

O(n)(旧树) × O(n)(新树) × O(n)(子树递归) = O(n³)

第 3 层递归比较子树的复杂度,是因为每一对匹配节点都还要递归地比较它们的子树结构

前两层只是找出“候选节点对”,第三层才是深入检查“它们真的一样吗”。

所以整体复杂度是: 旧树节点数 × 新树节点数 × 子树递归 = O(n³)


React的diff算法的时间复杂度是O(n)

React 把问题简化成了三条“经验规则”,正是这三条规则让复杂度从 O(n³) → O(n)。

  1. 同层比较,不跨层

    • 复杂度就从 O(n³)O(n²)
  2. 不同类型节点,直接替换整棵子树

    • 也就是说,不同类型的节点永远不去比较子树。 这避免了对子树的递归匹配,进一步从 O(n²)O(n)
  3. 通过 key 标识子节点的稳定性

    • 对于同一层的子节点列表,React 通过 key 来判断哪些节点是“同一个节点”
//旧
<ul>
  <li key="A">A</li>
  <li key="B">B</li>
  <li key="C">C</li>
</ul>
//新
<ul>
  <li key="B">B</li>
  <li key="A">A</li>
  <li key="C">C</li>
</ul>

React 会通过 key 识别出:

  • A、B、C 都还在;
  • 只是顺序变了;
  • 所以只需调整位置,不需要删除重建

这就让 同层的节点比较只需一次线性扫描

👉 因此,同层 diff 的复杂度变为 O(n)

key 的作用是什么?

  • 是React对于列表元素的唯一标识

    • 如果key相同,那么认为是同一节点,可以复用DOM元素
    • 如果key不同,则会销毁旧的,创建新的节点

为什么不能用 index 作为 key?

因为会导致错误的复用和性能问题

  • 因为列表内容如果从中间新增或者删除一项,那么index对应的元素将会错误的被复用

React 中 reconciliation 的过程是怎样的?

  • 当组件的 stateprops 变化时,React 会比较新旧虚拟 DOM(Fiber 树) ,找出需要更新的部分并同步到真实 DOM。这个比较与更新过程叫 Reconciliation

React 更新是同步还是异步的?

同步更新

同步模式下,React一旦开始渲染,就会一口气渲染完所有组件,期间不会中断

  • 页面上的表现

    • 当你触发一个大型渲染(比如 setState 导致 1000 个组件更新)时,页面会卡顿一下
    • 浏览器在 React 渲染完成前,无法响应用户操作(比如滚动、点击)
function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    for (let i = 0; i < 10000; i++) {
      setCount(i) // 模拟大规模更新
    }
  }

  return <button onClick={handleClick}>count: {count}</button>
}

在同步更新下,点击按钮后:

  • UI 会“卡死”几百毫秒;
  • 最后一次性更新成最终结果。

异步(Concurrent)更新(React 18 createRoot)

在并发模式下,React会把渲染拆分为小任务,在空闲时间片执行,可以随时暂停、恢复或丢弃

  • 页面上的表现

    • 大型渲染不再卡顿;
    • 页面仍能响应滚动、输入、动画;
    • React 会优先处理用户交互(高优先级),低优先级任务(如列表渲染)可延后执行。
import { useState, startTransition } from 'react'

function App() {
  const [value, setValue] = useState('')
  const [list, setList] = useState([])

  const handleChange = (e) => {
    const val = e.target.value
    setValue(val)
    startTransition(() => {
      // 模拟高开销任务
      const items = Array.from({ length: 5000 }, (_, i) => `${val}-${i}`)
      setList(items)
    })
  }

  return (
    <>
      <input value={value} onChange={handleChange} placeholder="输入点东西" />
      <ul>{list.map((item) => <li key={item}>{item}</li>)}</ul>
    </>
  )
}

在异步(Concurrent)模式下:

  • 输入框 不会卡顿
  • React 会优先更新输入框的值
  • 再利用空闲时间慢慢渲染列表;
  • 如果你输入更快,React 会丢弃旧的渲染任务,直接开始最新的。

同步场景

React17及以前的全部更新,默认同步
// React 17 写法(使用 ReactDOM.render)
import ReactDOM from 'react-dom'

function App() {
  const [count, setCount] = React.useState(0)

  console.log('render:', count)

  return (
    <button onClick={() => setCount(count + 1)}>
      Click: {count}
    </button>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

💬 说明:

  • 在 React 17(及更早版本)中,React 没有并发模式(Concurrent Mode)
  • 所有更新(无论大或小)都是同步执行的。
  • 点击按钮时,会立刻执行所有渲染逻辑。

📍页面效果:

即使组件很复杂、渲染耗时,React 也会“卡着”把它一次性渲染完。

React 18 中的旧 Root(非 Concurrent Root)
// React 18,但仍使用 ReactDOM.render(旧 Root)
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))
  • 即使你使用 React 18,只要还用旧的 ReactDOM.render, React 就不会启用并发模式(仍是同步更新)。
  • 所以这种 root 下的渲染依然会一次性执行完,期间不能被打断。

📍页面效果:

和 React 17 完全一样,仍然是同步阻塞渲染。

在 React 事件回调中调用的更新
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    console.log('Before:', count)
    setCount(count + 1)
    console.log('After:', count)
  }

  return <button onClick={handleClick}>Click: {count}</button>
}

即使你在 React 18 并发模式下(使用 createRoot), 在 React 事件回调中触发的更新仍是同步批量更新

React 会立即计算新的 Fiber 树,保证交互即时。

异步场景

使用 createRoot()

使用 createRoot()(并发 root)—— 开启异步渲染能力

// React 18 推荐写法(Concurrent Root)
import ReactDOM from 'react-dom/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
  • createRoot() 会启用 Concurrent Mode(并发模式)
  • 在这种 root 下,React 的更新具备可中断、可延迟的能力;
  • 不代表所有更新都异步,但具备异步调度的基础条件

📍页面效果:

如果组件渲染量大,React 可以暂停、分段渲染,不会卡死主线程。 用户输入或动画依然流畅。

使用 startTransition()(标记低优先级更新)
import { useState, startTransition } from 'react'

function App() {
  const [value, setValue] = useState('')
  const [list, setList] = useState([])

  const handleChange = (e) => {
    const val = e.target.value
    setValue(val)
    // 👇 告诉 React:这是低优先级任务,可延迟执行
    startTransition(() => {
      const items = Array.from({ length: 5000 }, (_, i) => `${val}-${i}`)
      setList(items)
    })
  }

  return (
    <>
      <input value={value} onChange={handleChange} placeholder="输入点东西" />
      <ul>{list.map((item) => <li key={item}>{item}</li>)}</ul>
    </>
  )
}

说明:

  • startTransition() 将内部更新标记为可中断任务
  • 高优先级任务(输入框更新)会先执行
  • 低优先级任务(列表渲染)会在空闲时执行;
  • 若用户继续输入,React 会丢弃旧任务、渲染最新的。

📍页面效果:

输入非常流畅,列表延迟更新但不卡顿。

使用 useDeferredValue()(延迟渲染依赖值)
import { useState, useDeferredValue } from 'react'

function App() {
  const [text, setText] = useState('')
  const deferredText = useDeferredValue(text) // 延迟使用 text 的值

  const list = Array.from({ length: 5000 }, (_, i) => (
    <li key={i}>{deferredText}</li>
  ))

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

说明:

  • useDeferredValue() 会在高优先级更新(输入)后,延迟执行耗时渲染
  • 输入流畅;
  • 列表内容更新稍后完成。

📍页面效果:

输入框立即响应,列表延迟一点点更新。 类似于“防抖 + 并发更新”的效果

一句话总结:

在 React 18 中,只要使用 createRoot() 启动应用, 你就进入了并发世界。 再搭配 startTransition() / useDeferredValue(), 就能让 React 的更新更智能、更流畅。

Fiber 是为了解决什么问题?

  • Fiber 是 React 为了解决「同步更新导致的卡顿问题」而引入的「可中断、可恢复的虚拟 DOM 架构」
  • Fiber 是 React 的底层重构,用于让虚拟 DOM 更新“可中断、可恢复、可调度”,从而提升大规模渲染的流畅度。

背景问题

React15的缺陷,在React15之前,更新流程是这样的:

  1. 状态更新后,React 会从根节点开始,递归遍历整棵虚拟 DOM 树
  2. 每次更新都同步执行到底(不能中断)
  3. 如果组件层级很深、计算复杂,就会长时间占用主线程
  4. 主线程被占用时,浏览器无法响应用户操作(如输入、滚动) → 卡顿、掉帧

fiber的核心目标

React团队为了解决“更新太重,无法中断”的问题,引入了fiber架构

  1. 可中断更新:react更新可以被中断,让浏览器先去响应用户操作
  2. 可分片执行:大任务被拆分为小任务,(每一帧执行一点)
  3. 可恢复与重用:被中断后可以从上次中断的地方继续执行

Fiber的设计思路

React 把每个虚拟 DOM 节点(VNode)包装成一个 Fiber 对象, 这个对象包含:

  • 节点类型、props、state
  • 指向父节点、子节点、兄弟节点的指针(形成链表结构)
  • 更新优先级信息(lane)
  • 副作用标记(如需插入/删除/更新)

➡️ 这样 React 就可以:

  • 用「遍历链表」代替「递归函数调用」(可随时暂停)
  • 在空闲时间片中继续工作(用调度器协调)
  • 动态决定哪部分更新优先(配合 Concurrent Mode)

Fiber 是如何实现可中断渲染的?

  • Fiber 通过把递归改为循环、把组件树改为链表结构、并利用时间片调度机制,使得 React 的渲染可以“暂停—恢复—继续”,从而实现了可中断渲染。

在React15中,更新虚拟DOM时使用的是递归遍历整棵树的方式

function updateComponent(component) {
  component.render()
  component.children.forEach(updateComponent)
}

问题是:

  • JS 是单线程的;
  • 一旦进入这段递归逻辑,就无法中途暂停;
  • 如果组件树很大,浏览器主线程会被长期占用;
  • 用户交互、动画、输入都会卡顿。

React 16 重写架构为 Fiber Reconciler。 目标:让“渲染过程”像执行协程一样 —— 可中断、可恢复、可调度。

Fiber 的关键设计思想是:

“把每个虚拟 DOM 节点(VNode)变成一个 Fiber 对象,并把组件树改成链表结构。”

这样 React 就可以:

  1. while 循环遍历链表(而非递归);
  2. 每处理一个 Fiber 节点,都检查当前帧是否超时;
  3. 如果时间用完,就暂停渲染,把控制权交还浏览器;
  4. 下一帧(或空闲时)再从上次中断的 Fiber 继续工作。

React Hooks

useState

基础概念

useState 是 React 提供的用于在函数组件中声明状态(state)的 Hook。

const [state, setState] = useState(initialValue)

  • state:当前状态值。
  • setState:更新状态的函数,会触发组件重新渲染。
  • initialValue:初始状态值,只在组件首次渲染时使用。

运行机制

当组件执行时(函数重新运行),useState 并不会重新创建新的状态,而是通过 React 内部的 Hook 链表(Fiber)机制 取回上一次保存的状态值。

也就是说:

  • 虽然函数重新执行了,
  • useState 通过闭包 + 内部索引保存并取回之前的状态。

👉 因此即使多次调用 useState(0)state 也不会回到 0。

React 约束: Hook 调用顺序必须一致,否则状态会错位。

let x = []
let index = 0
const myUseState = initial => {
let currentIndex = index 
x[currenIndex] = x[currentIndex] === undefined ? initial : x[currentIndex]

const setInitial = value => {
x[currentIndex] = value
render()
}
}

//模拟render函数
const render = () => {
index = 0
ReactDOM.render(<App/>, document.querySelector("#root"))
}

const App = () => {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
return (
<div>
         <p>n:{n}</p>
         <button onClick={()=>setN(n+1)}>+1</button>
         <p>m:{m}</p>
         <button onClick={()=>setM(m+1)}>+1</button>
     </div>
)
}

异步批处理(Batch Update)

React 会将多个状态更新合并执行(在事件回调中)。

setCount(count + 1)

setCount(count + 1)

// 实际只增加一次React 18 中,异步任务(如 setTimeoutPromise)中的 setState 不再强制合并。

惰性初始化

初始值可以是一个函数:

const [data, setData] = useState(() => heavyCalculation())

heavyCalculation() 只在首次渲染时执行,避免每次渲染重复计算。

更新函数形式

当新状态依赖旧状态时,用函数式更新:

setCount(prev => prev + 1)

useEffect

一、作用

useEffect 用于处理 副作用(side effects) ,比如:

  • 网络请求
  • 订阅 / 事件监听
  • 操作 DOM
  • 定时器

这些逻辑不能直接放在渲染阶段,否则会阻塞或污染渲染。

  • 函数组件需要是纯函数:

    • 相同输入 → 永远相同输出
    • 不修改外部变量
    • 不产生额外行为
  • React 设定函数组件必须满足:

    • 相同的 props & state → 必须产生完全相同的 UI
    • 不依赖外部可变环境
    • 没有无法预测的行为
    • 渲染阶段必须同步、快速、纯净

换句话说:

组件函数必须像数学函数一样:输入 → 输出 UI

二、执行时机

  • 初次渲染后 执行(不会阻塞渲染)。
  • 依赖项变化 时重新执行。
  • 组件卸载前 执行清理函数。

三、依赖数组 [deps]

写法 执行时机
useEffect(fn) 每次渲染都执行
useEffect(fn, []) 仅挂载和卸载时执行一次
useEffect(fn, [a, b]) 当依赖项 a 或 b 改变时执行

⚠️ React 比较依赖项是浅比较,如果依赖对象或数组的引用变了,即使内容没变也会触发。

四、清理函数

返回一个函数,用于卸载或重新执行前清理副作用:

useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000)
  return () => clearInterval(id)
}, [])

执行时机:

  1. 组件卸载时;
  2. 副作用重新执行前。

五、面试常问点

  1. useEffect 为什么在渲染后执行? 为了让渲染过程纯净,不被副作用打断。

  2. 为什么要写依赖数组? 告诉 React 什么时候重新运行副作用,否则可能死循环。

  3. 依赖项写错或少写会怎样? 可能导致状态不同步或逻辑失效(React 会在严格模式下警告)。

  4. useLayoutEffect 和 useEffect 的区别?

    • useEffect:渲染完成后异步执行,不阻塞绘制。
    • useLayoutEffect:DOM 更新后、浏览器绘制前同步执行,可用于测量 DOM。

useRef

核心定义

  • useRef 是一个能在组件整个生命周期内 保持引用不变 的 Hook。
  • 它返回一个可变对象 { current: ... },这个对象在组件的重新渲染中不会被重置
const ref = useRef(initialValue)
console.log(ref.current) // ref.current 保存的数据在组件多次渲染之间是持久的

应用场景

获取DOM节点
  • 在React中使用useRef获取DOM比原生方式获取DOM更可靠
  • inputRef.current 会指向对应的 DOM 元素。
  • 通常用于:聚焦、滚动、测量宽高、绑定第三方库。
function App() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current.focus()
  }, [])

  return <input ref={inputRef} />
}
保存 任意可变值(不触发重新渲染)

count.current 的值在组件重渲染时仍然保持;

修改 ref.current 不会引起重新渲染

所以它非常适合存储:

  • 前一次的值(用于比较)
  • 定时器 id
  • 防抖节流计数器
  • 某个状态的缓存值
function Timer() {
  const count = useRef(0)

  const handleClick = () => {
    count.current += 1
    console.log(count.current)
  }

  return <button onClick={handleClick}>Click</button>
}

useMemo

  • 开发中,我们只要修改了父组件的数据,所有的子组件都会重新渲染,这是十分消耗性能的
  • 如果我们希望子组件不要进行这种没有必要的重新渲染,我们可以将子组件继承PureComponent或者使用memo函数包裹
import React, { memo, useState, useEffect } from 'react'
const A = (props) => {
  console.log('A1')
  useEffect(() => {
    console.log('A2')
  })
  return <div>A</div>
}

const B = memo((props) => {
  console.log('B1')
  useEffect(() => {
    console.log('B2')
  })
  return <div>B</div>
})

const Home = (props) => {
  const [a, setA] = useState(0)
  useEffect(() => {
    console.log('start')
    setA(1)
  }, [])
  return <div><A n={a} /><B /></div>
}
  • 将子组件B使用memo包裹之后,Home组件中的状态a的变化就不会导致B组件的重新渲染
  • 但是在子组件B使用了父组件的某个引用类型的变量或者函数时,那么当父组件状态更新之后,这些变量和函数就会重新赋值,导致子组件B还是会重新渲染
  • 想解决这个问题,就需要使用useMemo和useCallback了

useCallback

  • 当函数组件重新渲染时,其中的函数也会被重复定义多次
  • 如果使用useCallBack对函数进行包裹,那么在依赖(第二个参数)不变的情况下,会返回同一个函数 这样子组件就不会因为函数的重新定义而导致重新渲染了
  • useMemo和useCallBack相似,缓存的是函数的返回值,一般用来优化变量,但是如果将useMemo的返回值定义为返回一个函数就可以实现useCallBack一样的功能
//用useMemo实现同useCallback一样的效果
   const increment = useCallback(fn,[])
   const increment2 = useMemo(()=>fn,[])
import React, { memo, useState, useEffect, useMemo } from 'react'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  useEffect(() => {
    setA(1)
  }, [])

  const add = useCallback(() => {
    console.log('b', b)
  }, [b])

  const name = useMemo(() => {
    return b + 'xuxi'
  }, [b])
  return <div><A n={a} /><B add={add} name={name} /></div>
}

useContext

useContext 是什么?

useContext 是 React 的一个 Hook,用于:

  • 函数组件中直接读取由上层组件 Context.Provider 提供的值。

简单理解:

  • 不用一层层 props 传递,也能让深层组件拿到共享数据。

语法

const value = useContext(MyContext)
  • MyContext 是通过 React.createContext() 创建的上下文对象。
  • useContext() 返回最近的 <MyContext.Provider> 提供的 value
  • 当 Provider 的 value 变化时,所有使用该 context 的组件都会重新渲染。

使用步骤

// context.js
import { createContext } from "react"
export const ThemeContext = createContext("light")

// App.jsx
import { ThemeContext } from "./context"
import Child from "./Child"

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  )
}

// Child.jsx
import { useContext } from "react"
import { ThemeContext } from "./context"

function Child() {
  const theme = useContext(ThemeContext)
  return <div>当前主题:{theme}</div>
}

特性

特性 说明
最近优先 如果组件外层有多个相同类型的 Provider,会取最近一层的 value
响应式更新 Provider 的 value 改变,会触发所有消费该 Context 的组件重新渲染
只能在函数组件或自定义 Hook 中使用 不能在类组件或普通函数中调用
不能脱离 Provider 使用 如果没有 Provider 包裹,会使用 createContext() 时设置的默认值

应用场景

场景 示例
✅ 主题切换 dark / light 模式
✅ 登录状态 用户信息、Token
✅ 多语言切换 中英文语言包
✅ 全局配置 比如分页大小、API地址等

常见问题

useContext 和 props 传递的区别?

对比项 props useContext
数据传递 一层层手动传递 任何层级都可直接拿到
灵活性 高(精确控制) 全局性(可能过度渲染)
适用场景 局部数据传递 全局共享状态

useContext 的缺点是什么?

  • Provider 的 value 改变时,所有消费它的组件都会重新渲染
  • 这可能导致性能问题(无论组件是否使用了 value 的具体字段);
  • 因此大型项目中往往结合 useReducerRedux / Zustand 等状态管理库 一起使用

forwardRef

在React开发中,有些时候我们需要获取DOM或者组件来进行某些操作

  • 如何使用ref来获取DOM

    • 使用createref创建ref对象,并且绑定到DOM元素上
  • forwardRef解决的问题是ref不会通props传递下去,因为ref和key一样被React做了特殊处理

import React, { PureComponent ,createRef} from 'react'
export class App extends PureComponent {
    //创建ref
    this.titleRef = createRef()
  }
  getNativeDOM(){
    console.log(this.titleRef.current)
  }
  render() {
    return (
      <div>
        <h2 ref={this.titleRef}>hello world</h2>
        <button onClick={e=>this.getNativeDOM()}>获取DOM</button>
      </div>
    )
  }
}
export default App

ref 的值根据节点的类型有所不同:

  1. ref属性作用于HTML属性时,接收底层DOM元素作为其current属性
  2. ref属性作用于class组件时,接收组件实例作为其current属性
  3. 不能在函数组件上使用ref属性,因为他们没有实例

想将ref挂载到函数组件内部的某个class组件或者HTML元素上时,我们需要使用React.forwardRef将函数组件包裹,从而将ref传递到组件内部

//获取函数组件的某个DOM
//使用forwardRef之后,可以传入两个参数,第二个为ref,我们可以实现ref转发
const Fun = forwardRef(function (props,ref) {
  return (
    <h1 ref={ref}>hello react</h1>
  )
})

使用ref作用于类组件,并调用类组件实例的方法

import React, { PureComponent, createRef, forwardRef } from 'react'

//类子组件
class HelloWorld extends PureComponent {
  test() {
    console.log("test---")
  }
  render() {
    return (
      <h1>hello world</h1>
    )
  }
}
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {}
    this.hwRef = createRef()
  }
  getComponent() {
    //调用类组件实例的方法
    console.log(this.hwRef.current)
    this.hwRef.current.test()
  }
  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef} />
        <button onClick={e => this.getComponent()}>获取组件实例</button>
      </div>
    )
  }
}
export default App

useImperativeHandle

  • forwardRef使用带来的问题

    • 直接暴露给父组件,使得某些情况不可控
    • 父组件拿到子组件之后可以进行任意的操作
  • 通过useImperativeHandle可以暴露固定的操作

import React, {
  useRef,
  forwardRef, useImperativeHandle
} from 'react'

const HYInput = forwardRef((props,ref)=> {
  const inputRef = useRef()
  useImperativeHandle(ref,()=> {
    return {
      focus: () => {
        console.log(345);
        inputRef.current.focus()
      }
    } 
  })

  return <input ref={inputRef} type="text"/>
})
export default function UseImperativeHandleHookDemo() {
  const inputRef = useRef()
  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e=>inputRef.current.focus()}>聚焦</button>
    </div>
  )
}

原理题

React Hooks 为什么不能放在条件判断里?

  • React Hooks 不能放在条件判断、循环或嵌套函数中,必须在组件的顶层调用。

    • 因为——React 是通过调用顺序来识别每一个 Hook 的。

每个组件渲染时,React 会维护一个“Hook 调用链表”或“数组”, 类似这样(简化理解):

// 第一次渲染
useState('A')   // Hook 1
useEffect(...)  // Hook 2
useState('B')   // Hook 3

React 会按顺序记下每一个 Hook 对应的状态(存在 Fiber 节点上)。 下一次渲染时,React 会再次按相同顺序调用 Hook 来匹配之前的状态。


如果放在条件语句中会发生什么?

if (flag) {
  useState(1)
}
useEffect(() => {})
  • 第一次渲染:

    • flag = true → 执行 useState(Hook 1)
    • 执行 useEffect(Hook 2)
  • 第二次渲染:

    • flag = false → 跳过 useState
    • useEffect 变成了 Hook 1!

🚨 React 内部匹配错位! 本该给 useEffect 的 Hook 状态,被错误地分配成了之前的 useState 状态。

结果可能报错:

Rendered fewer hooks than expected
Invalid hook call

Hooks的执行顺序是如何保证的?

  • React 通过在每个 Fiber 节点上维护一个 Hook 链表
  • 并在每次渲染时按顺序遍历执行
  • 从而确保每个 Hook 的状态和顺序一致。

自定义 Hook 怎么避免闭包陷阱?

  1. 使用函数式更新(最常用)
const increment = () => setCount(prev => prev + 1)
  • prev 永远是最新 state
  • 无需依赖闭包捕获的旧值
  • 适用于事件回调、定时器、异步请求等
  1. useRef 保存最新值

如果你需要在闭包里访问最新状态而不触发重新渲染: const countRef = useRef(count) useEffect(() => { countRef.current = count }, [count])

const logCount = () => { console.log(countRef.current) // 永远是最新值 }

  • 异步函数或事件可以使用 countRef.current
  • 不影响 React 渲染流程

组件渲染与性能优化

React 组件何时重新渲染?

  1. state发生变化:只要你调用 setState 产生了新的值(引用变化),组件就会重新渲染。

  2. props变化:只要父组件重新渲染,子组件也会跟着渲染(除非使用 React.memo)。

    即使 props 内容没变 —— 只要父组件 render,子组件也会 render。

  3. context变化:当某个 Context Provider 的 value 改变,所有消费该 context 的子组件都会重渲染。

  4. 父组件重新渲染导致子组件渲染(哪怕 props 不变)

浅比较会对比:

  • 基本类型值(number / string / boolean / null / undefined) → 直接比较值是否相等。
  • 引用类型(object / array / function)只比较引用地址是否相同,不会比较内部的内容。

React 在性能优化时会用浅比较,比如:

  • React.memo
  • PureComponent
  • shouldComponentUpdate
  • useMemo / useCallback 的依赖项比较
  • useEffect / useCallback 的依赖数组

因为浅比较非常快,不需要深度遍历对象。

  • 浅比较带来的典型问题

    • 使用 inline function 导致子组件重新渲染
<Child onClick={() => setCount(count + 1)} />

每次父组件 render 时都会创建新的函数引用 → 浅比较结果:不同 → 子组件重新渲染

  • 解决方法

    • useCallback 固定函数引用
    • useMemo 固定对象 / 数组引用
    • 子组件用 React.memo

如何优化一个大表格或长列表的渲染性能?(虚拟列表)

  • 为什么需要虚拟列表?

    • 当列表有 成百上千甚至上万条数据时:

      • 浏览器会创建大量 DOM(慢)
      • 布局、重排、重绘消耗巨大(卡)
      • 滚动时频繁触发渲染(卡顿)

👉 核心思路: 只渲染可视区域内的那几十个节点,其余内容用占位高度撑开。

  • 什么时候用虚拟列表

    • 满足任一即可使用虚拟列表:

      • 单页表格数据量 > 200 行
      • 单页列表 > 300 行
      • 存在大量复杂 DOM(图片、按钮、操作列)
      • 有频繁更新、滚动操作
  • 核心原理(一句话版本)

    • 只有可视区域 + 缓冲区的元素真实渲染
    • 其他区域只用一个大容器撑开高度
    • 视觉上像完整列表,但实际 DOM 数量永远保持几十个
  • 虚拟列表关键技术

    • 容器高度:列表容器要固定高度或可计算高度,否则无法计算可视范围。

    • 每行高度:

      • 固定高度:最好实现,可用rowheight直接算
      • 不定高度:需要实时记录高度(难度更高)
    • 计算可视区域的起止 index

      startIndex = Math.floor(scrollTop / rowHeight)
      endIndex = startIndex + 可视区域行数 + buffer
      
    • 渲染可视区域数据

      • 只渲染这一小段数据即可。
    • 使用 translateY 把渲染的内容“挪”到正确位置

      style="transform: translateY(startIndex * rowHeight px)"
      
  • 前端常用虚拟列表方案

    • React:react-window

      • 轻量、简单、性能极佳。
      • <FixedSizeList> 固定行高列表
      • <VariableSizeList> 不定高度列表
      • <FixedSizeGrid> 表格(大表格强烈推荐)
import { FixedSizeList as List } from "react-window";

<List
  height={600}
  width={800}
  itemSize={40}
  itemCount={list.length}
>
  {({ index, style }) => (
    <div style={style}>{list[index].name}</div>
  )}
</List>
  • Ant Design v5
<Table
  scroll={{ y: 600 }}
  virtual
  columns={columns}
  dataSource={data}
/>
  • 自己实现
import React, { useRef, useState, useEffect } from "react";

export default function VirtualList({ itemHeight, height, data, renderItem }) {
  const containerRef = useRef(null);
  const [startIndex, setStartIndex] = useState(0);

  const visibleCount = Math.ceil(height / itemHeight); // 可视区域展示多少条

  // 滚动事件
  const onScroll = () => {
    const scrollTop = containerRef.current.scrollTop;
    const newStartIndex = Math.floor(scrollTop / itemHeight);
    setStartIndex(newStartIndex);
  };

  // 当前需要渲染的数据
  const endIndex = startIndex + visibleCount;
  const visibleData = data.slice(startIndex, endIndex);

  // 用两个 padding 占位本来应该存在的高度
  const paddingTop = startIndex * itemHeight;
  const paddingBottom = (data.length - endIndex) * itemHeight;

  return (
    <div
      ref={containerRef}
      style={{
        height,
        overflowY: "auto",
        border: "1px solid #ccc",
      }}
      onScroll={onScroll}
    >
      <div style={{ paddingTop, paddingBottom }}>
        {visibleData.map((item, i) =>
          renderItem(item, startIndex + i)
        )}
      </div>
    </div>
  );
}

React 中没有 v-model,如何优雅地处理表单输入

作者 凯心
2025年11月30日 14:11

React 中没有 v-model,如何优雅地处理表单输入

在 Vue 中,我们可以很方便地使用 v-model 实现数据的双向绑定。但在 React 的世界里,并没有这样的语法糖,我们需要通过不同的方式来处理表单数据。

Vue 的简洁写法

<template>
  <input v-model="value" />
</template>

React 的几种实现方案

方案一:基础受控组件

function App() {
  const [value, setValue] = useState("");
  
  return (
    <input 
      value={value} 
      onChange={e => setValue(e.target.value)} 
    />
  );
}

这是 React 初学者最常用的写法。在简单场景下表现良好,但在复杂表单或大型应用中,每次输入都会触发组件重新渲染,可能导致性能问题。

方案二:非受控组件 + useRef

function App() {
  const inputRef = useRef("");
  
  return (
    <input 
      onChange={e => (inputRef.current = e.target.value)} 
    />
  );
}

这种方案避免了频繁的重新渲染,适合性能敏感的场景。

方案三:防抖优化

function App() {
  const [value, setValue] = useState("");
  
  const handleChange = useCallback(
    debounce((newValue) => {
      setValue(newValue);
    }, 300),
    []
  );

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

通过防抖函数减少状态更新的频率,在需要实时搜索等场景下特别有用。


深入理解:受控组件 vs 非受控组件

概念解析

受控组件和非受控组件是数据驱动框架中的重要概念:

  • 表面区别:值是否只能由用户输入改变,还是也可以由程序逻辑直接改变
  • 本质区别:数据是由 React 状态托管,还是由 DOM 自身管理

受控组件(Controlled Components)

表单元素的值完全由 React 状态控制,通过 onChange 事件同步更新。

优点:

  • ✅ 符合 React 单向数据流理念,状态完全可控
  • ✅ 便于实现实时验证和输入格式化
  • ✅ 可动态控制表单提交状态
  • ✅ 支持多组件间的数据同步

缺点:

  • ❌ 需要为每个字段编写事件处理逻辑
  • ❌ 表单复杂时可能引发性能问题

适用场景:

  • 需要实时验证用户输入
  • 需要根据输入动态更新UI
  • 需要强制特定的输入格式
  • 表单数据被多个组件共享
function LoginForm() {
  const [formData, setFormData] = useState({
    username: "",
    password: ""
  });

  const handleChange = (field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };

  return (
    <form>
      <input 
        value={formData.username} 
        onChange={handleChange("username")} 
      />
      <input 
        type="password"
        value={formData.password} 
        onChange={handleChange("password")} 
      />
    </form>
  );
}

非受控组件(Uncontrolled Components)

表单数据由 DOM 自身管理,通过 ref 在需要时获取值。

优点:

  • ✅ 代码简洁,减少事件处理逻辑
  • ✅ 性能更优,避免频繁重新渲染
  • ✅ 更接近原生 DOM 操作

缺点:

  • ❌ 不符合 React 数据流最佳实践
  • ❌ 无法实现实时验证和UI反馈
  • ❌ 状态管理不够直观

适用场景:

  • 简单表单,无需实时验证
  • 只在提交时需要获取数据
  • 性能敏感的大型表单
  • 集成第三方表单库
function UncontrolledForm() {
  const usernameRef = useRef();
  const passwordRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    const data = {
      username: usernameRef.current.value,
      password: passwordRef.current.value
    };
    console.log("表单数据:", data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={usernameRef} />
      <input type="password" ref={passwordRef} />
      <button type="submit">提交</button>
    </form>
  );
}

实践建议

  1. 当需要做性能优化时,可以考虑使用非受控组件
  2. 非受控组件和受控组件可以混用

element-plus源码解读2——vue3组件的ref访问与defineExpose暴露机制

作者 Joie
2025年11月30日 13:54

vue3组件的ref访问与defineExpose暴露机制

vue官方文档:

refcn.vuejs.org/api/reactiv…

defineExposecn.vuejs.org/api/sfc-scr…

以el-button举例:

1. 正确的访问方式

看 Button 组件暴露的内容:

defineExpose({
  /** @description button html element */
  ref: _ref,
  /** @description button size */
  size: _size,
  /** @description button type */
  type: _type,
  /** @description button disabled */
  disabled: _disabled,
  /** @description whether adding space */
  shouldAddSpace,
})

2. 实际使用示例

<template>
  <el-button ref="buttonRef" type="primary" size="large">
    按钮
  </el-button>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import type { ButtonInstance } from 'element-plus'

const buttonRef = ref<ButtonInstance>()

onMounted(() => {
  // ✅ 正确:访问所有暴露的属性
  console.log('DOM 元素:', buttonRef.value?.ref)        // HTMLButtonElement
  console.log('按钮尺寸:', buttonRef.value?.size)       // ComputedRef<'large'>
  console.log('按钮类型:', buttonRef.value?.type)      // ComputedRef<'primary'>
  console.log('是否禁用:', buttonRef.value?.disabled)   // ComputedRef<boolean>
  console.log('是否加空格:', buttonRef.value?.shouldAddSpace) // ComputedRef<boolean>
  
  // ✅ 打印整个组件实例,可以看到所有暴露的属性
  console.log('组件实例:', buttonRef.value)
})
</script>

3. 打印结果示例

当你 console.log(buttonRef.value) 时,会看到类似:

{
  ref: HTMLButtonElement,           // DOM 元素
  size: ComputedRef<'large'>,        // 尺寸(注意是 ComputedRef)
  type: ComputedRef<'primary'>,      // 类型(注意是 ComputedRef)
  disabled: ComputedRef<false>,      // 禁用状态(注意是 ComputedRef)
  shouldAddSpace: ComputedRef<false> // 是否加空格(注意是 ComputedRef)
}

4. 重要提示:ComputedRef 的访问

注意 sizetypedisabled 等是 ComputedRef,访问值需要用 .value

// ❌ 错误:这样得到的是 ComputedRef 对象
console.log(buttonRef.value?.size)  // ComputedRef { ... }

// ✅ 正确:需要 .value 才能拿到实际值
console.log(buttonRef.value?.size.value)  // 'large'
console.log(buttonRef.value?.type.value)  // 'primary'
console.log(buttonRef.value?.disabled.value)  // false

说明 Vue 3 的生命周期和 ref 访问时机:

1. Vue 3 没有 onCreated 钩子

在 Vue 3 的 Composition API 中:

  • 没有 onCreated() 钩子
  • setup() 函数本身就相当于 Vue 2 的 created + beforeCreate
  • 如果需要访问 DOM 或组件实例,应该用 onMounted()

2. 为什么必须在 onMounted() 中?

setup() 顶层(组件未挂载)
<script setup>
import { ref } from 'vue'
import type { ButtonInstance } from 'element-plus'

const buttonRef = ref<ButtonInstance>()

// ❌ 错误:此时 buttonRef.value 是 undefined
// 因为组件还没有挂载,ref 还没有被赋值
console.log(buttonRef.value)  // undefined
</script>
onMounted() 中(组件已挂载)
<script setup>
import { ref, onMounted } from 'vue'
import type { ButtonInstance } from 'element-plus'

const buttonRef = ref<ButtonInstance>()

onMounted(() => {
  // ✅ 正确:此时组件已经挂载,ref 已经被赋值
  console.log(buttonRef.value)  // ButtonInstance 对象
  console.log(buttonRef.value?.ref)  // HTMLButtonElement
})
</script>

3. Vue 3 生命周期对比

Vue 2 Options API Vue 3 Composition API 说明
beforeCreate setup() 开始执行 组件创建前
created setup() 执行中 组件创建后(但未挂载)
beforeMount onBeforeMount() 挂载前
mounted onMounted() 挂载后(DOM 已存在)
beforeUpdate onBeforeUpdate() 更新前
updated onUpdated() 更新后
beforeUnmount onBeforeUnmount() 卸载前
unmounted onUnmounted() 卸载后

4. 完整示例对比

错误示例(在 setup 顶层)
<template>
  <el-button ref="buttonRef">按钮</el-button>
</template>

<script setup>
import { ref } from 'vue'
import type { ButtonInstance } from 'element-plus'

const buttonRef = ref<ButtonInstance>()

// ❌ 错误:此时 buttonRef.value 是 undefined
console.log('setup 顶层:', buttonRef.value)  // undefined
</script>
正确示例(在 onMounted 中)
<template>
  <el-button ref="buttonRef">按钮</el-button>
</template>

<script setup>
import { ref, onMounted, onBeforeMount } from 'vue'
import type { ButtonInstance } from 'element-plus'

const buttonRef = ref<ButtonInstance>()

// 在 setup 顶层
console.log('setup 顶层:', buttonRef.value)  // undefined

// 在 beforeMount 中
onBeforeMount(() => {
  console.log('beforeMount:', buttonRef.value)  // 可能还是 undefined
})

// 在 mounted 中
onMounted(() => {
  // ✅ 正确:此时组件已挂载,ref 已赋值
  console.log('mounted:', buttonRef.value)  // ButtonInstance 对象
  console.log('DOM 元素:', buttonRef.value?.ref)  // HTMLButtonElement
})
</script>

5. 为什么 ref 在 onMounted 中才有值?

Vue 的 ref 赋值时机:

  1. 模板编译阶段:Vue 识别 ref="buttonRef"
  2. 组件挂载阶段:创建组件实例,将实例赋值给 buttonRef.value
  3. DOM 渲染完成:onMounted() 执行时,ref 已经有值

6. 如果需要在 setup 中访问怎么办?

可以使用 watchEffectwatch

<script setup>
import { ref, watchEffect } from 'vue'
import type { ButtonInstance } from 'element-plus'

const buttonRef = ref<ButtonInstance>()

// 使用 watchEffect,会在 ref 有值后自动执行
watchEffect(() => {
  if (buttonRef.value) {
    console.log('ref 有值了:', buttonRef.value)
  }
})
</script>

7. 总结

  • Vue 3 没有 onCreated()setup() 本身就相当于 created
  • 访问 ref.value 必须在 onMounted() 中,因为此时组件已挂载
  • setup() 顶层访问 ref.value 会是 undefined
  • 如果需要响应式监听 ref 的变化,可以用 watchEffectwatch

性能优化:从“用户想走”到“愿意留下”的1.8秒

作者 小时前端
2025年11月30日 13:37

前言

"今天,我们来聊聊性能优化。在我们团队看来,性能优化不是简单的'减少加载时间'的技术活,而是数字世界的'流体力学'工程。我们试图在有限的带宽的约束下,让用户体验如流水般顺滑自然。"

"因此,当我考察一个候选人对性能优化的理解时,我核心关注的不是他知道多少种优化技巧,而是他是否具备一种 '性能优化' 的视角。他是否明白,优化某个指标,不仅是应用某个最佳实践,更是要理解整个渲染流水线、网络协议栈和运行时环境。"

"这意味着,你需要去理解每个性能指标背后的运行机制——浏览器是如何构建渲染树的?JavaScript引擎是如何执行代码的?网络请求是如何被调度和处理的?如果对细节不太了解,可以阅读这篇文章前端面试经典题:从URL到页面展示,这一次让你彻底搞懂"

"所以,今天的问题不仅仅是关于'如何减少LCP'这些具体技巧,我更想听到的是,你如何系统性地思考、诊断和解决性能问题。让我们就从这里开始聊起吧。"

当我问起这个问题时,我不仅仅是想听几个优化技巧。我想考察的是:

  1. 深度:你对性能优化的理解是否停留在"用个懒加载"的表面?
  2. 广度:你是否了解从网络到渲染的全链路性能影响因素?
  3. 实践:你是否有过真实的性能优化经验,或者至少深入分析过性能瓶颈?
  4. 数据驱动:你是那个凭感觉优化的人,还是能基于数据做出精准决策的人?

一、核心理念:性能 ≠ 快

首先要明确,性能优化的核心是在技术理想用户感知之间寻找最佳平衡点。

  • 技术指标站在天平的最右端:精确的毫秒数,但可能偏离用户感受
  • 用户感知站在最左端:主观的"快慢",但难以量化衡量
  • 现代性能优化分布在中间的广阔光谱上,每个点代表不同的权衡

所以,性能优化的本质是技术指标与用户体验的匹配游戏

二、性能优化的演进:从"减少字节"到"提升感知"

性能优化的三次进化

  • 第一代:资源优化时代

    • 核心思路:"让文件更小,让请求更少"
    • 关键技术:Gzip压缩、图片优化、CSS Sprites、减少HTTP请求
    • 突破性:首次系统性地从资源层面解决性能问题
    • 局限性:过度聚焦于技术指标,忽略用户感知
  • 第二代:渲染优化时代

    • 核心思路:"让关键内容先出来"
    • 关键技术:关键CSS内联、异步加载、懒加载、服务端渲染
    • 突破性:开始关注用户看到内容的时机,而非单纯的技术指标
    • 局限性:缺乏统一的衡量标准,优化方向分散
  • 第三代:用户体验量化时代

    • 核心思路:"用科学指标衡量用户体验"
    • 关键技术:Core Web Vitals、Performance API、真实用户监控
    • 突破性:建立了标准化的用户体验衡量体系
    • 局限性:指标之间存在权衡,需要业务层面的决策

如何回答(展现你的历史观) "性能优化的演进本质上是不断重新定义'性能'含义的过程。从早期的关注服务器响应时间,到中期的关注首屏渲染,再到现在的关注用户核心任务完成度,每一次演进都在解决前一代的核心局限。"

三、现代性能优化深度对比:不只是技术技巧

核心维度对比

  • 加载性能:用户的第一印象

    • LCP:衡量主要内容加载,直接影响用户对速度的感知
    • 优化策略:图片优化、字体优化、服务端渲染、资源预加载
    • 权衡考量:预加载可能浪费带宽,需要基于用户行为数据决策
  • 交互响应:使用的流畅度

    • FID/INP:衡量输入响应,决定用户操作的顺滑程度
    • 优化策略:代码分割、长任务拆分、Web Worker、优化JavaScript执行
    • 权衡考量:过度拆分可能增加复杂度,需要平衡可维护性
  • 视觉稳定性:体验的可靠性

    • CLS:衡量布局偏移,影响用户的阅读和操作精度
    • 优化策略:设置尺寸属性、预留空间、避免动态插入内容
    • 权衡考量:预留空间可能造成空白,需要设计系统配合

如何回答(展现你的技术判断力) "当我们对比现代性能优化指标时,实际上是在对比不同的用户体验维度。LCP关注的是'什么时候能用',FID关注的是'用起来卡不卡',CLS关注的是'用起来准不准'。好的性能优化不是单独优化某个指标,而是找到这些指标在具体业务场景下的最佳平衡点。"

四、实战:一个LCP从4.2s到1.8s的优化案例

"理论总是灰色的,而性能优化的实践之树常青。下面我想分享一个真实的产品详情页优化案例,看看我们如何将一个 4.2秒的LCP 优化到 1.8秒。"

问题诊断:拨开迷雾,定位七重瓶颈

我们发现了完整的"性能问题瀑布链":

  1. LCP元素:首屏的主商品图
  2. 资源加载:一张450KB的JPEG图片
  3. 渲染阻塞:800KB的Web字体文件
  4. JavaScript竞争:分析脚本抢占带宽
  5. 服务端延迟:TTFB达到600ms
  6. 缓存失效:CDN配置不当
  7. 布局偏移:CLS高达0.25

"诊断性能问题就像破案,你不能只看到表面的'凶器'(那张大图),而是要还原整个'犯罪现场'(从用户点击到屏幕渲染的完整链条)。"

优化措施:一套组合拳,拳拳到肉

第一阶段:资源层面的"瘦身"与"调度"

技术说明:这个阶段的核心目标是让关键资源变得更小,并让浏览器优先处理它们

<!-- 图片格式优化:为不同浏览器提供最合适的图片格式 -->
<picture>
  <!-- 现代浏览器优先使用更小的WebP格式 -->
  <source srcset="hero-image.webp" type="image/webp">
  <!-- 老版本浏览器使用优化后的JPEG作为备选 -->
  <source srcset="hero-image.jpg" type="image/jpeg">
  <!-- 最终回退方案 -->
  <img src="hero-image.jpg" alt="产品主图" width="800" height="600">
</picture>

<!-- 关键图片预加载:告诉浏览器这个图片最重要,请立即下载 -->
<link rel="preload" as="image" href="hero-image.webp" imagesrcset="hero-image.webp 800w, hero-image-mobile.webp 400w">

"这里有个关键洞察:我们发现主图虽然通过<picture>元素做了格式优化,但浏览器仍然需要经过图片发现、请求队列、DNS查找、TCP连接等一系列步骤才能开始下载。通过preload,我们告诉浏览器:'这个资源极其重要,请跳过常规队列,立即开始下载'。"

Preload的实战细节:

  • 时机把握:将preload链接放在HTML的<head>中,确保浏览器在解析完基本结构后立即处理
  • 格式适配:预加载WebP格式,因为它是我们为现代浏览器准备的最优解
  • 响应式考虑:通过imagesrcset告知浏览器不同视口宽度下应该加载的图片版本

第二阶段:渲染层面的"清障"与"加速"

技术说明:这个阶段的目标是消除阻止页面渲染的障碍,让内容尽快显示

/* 字体加载优化:避免文字显示空白期 */
@font-face {
  font-family: 'ProductSans';
  src: url('product-sans-subset.woff2') format('woff2');
  font-display: swap; /* 关键:先显示系统字体,Web字体加载后再替换 */
}
<!-- 非关键脚本延迟加载:让分析工具等不阻塞页面渲染 -->
<script async src="analytics.js"></script>
<script defer src="non-critical.js"></script>

"字体优化是另一个战场。原本800KB的字体文件不仅下载慢,还会导致文本内容在字体加载完成前完全不可见(FOIT问题)。通过font-display: swap,我们让浏览器先用系统字体显示文字,等Web字体下载完成后再静默替换——用户能立即看到内容,而不是面对一片空白。"

第三阶段:服务端与基础设施的"深水区"优化

技术说明:这个阶段处理网络层面和服务端响应速度的问题

<!-- 提前建立CDN连接:减少DNS查询和TCP握手时间 -->
<link rel="preconnect" href="https://cdn.our-platform.com">

"你可能想不到,浏览器在真正开始下载图片前,需要先完成'自我介绍'——DNS查询找到CDN服务器的IP地址,然后TCP三次握手建立连接。通过preconnect,我们在浏览器遇到实际图片URL前就提前完成这些步骤,为后续请求节省了宝贵的几百毫秒。"

效果评估:数据是最好的证明

经过上述优化,我们在下一个发布周期后观察到了显著变化:

指标 优化前 优化后 提升幅度 用户感知
LCP 4.2s 1.8s 57% 从"等待"到"顺畅"
TTFB 600ms 80ms 87% 服务器响应更快
首屏图片体积 450KB 120KB 73% 流量节省,加载更快
跳出率 45% 32% 13个百分点 更多用户留下

"最让我们兴奋的不是漂亮的性能图表,而是业务数据的正面反馈:详情页到购物车的转化率提升了 7%。这实实在在地证明了,性能优化不是技术团队的自我感动,而是真金白银的商业回报。"

经验总结:从一次优化到一种能力

这个案例给我们的启示远不止于技术点:

  1. 性能优化是系统工程:它涉及前端、后端、运维多个环节,需要打破团队壁垒协同作战。
  2. 度量是优化的起点和终点:没有精确的测量,优化就是盲人摸象。我们建立了持续的性能监控仪表盘。
  3. 优化需要勇气做减法:敢于对"历来如此"的设计(如全尺寸大图)和"别人都这么用"的技术(如完整Web字体)提出挑战。
  4. 用户体验是最终裁判:优化的目标不是跑分,而是让用户觉得"快"。即使LCP还有提升空间,但1.8秒的加载速度已经让用户感知从"等待"变成了"顺畅"。

"这个案例之后,我们形成了一种肌肉记忆:每当启动一个新项目,LCP 会作为一项核心验收指标,与功能需求并列进入产品清单。这或许是一次优化带来的最大价值——将性能意识植入了团队的基因。"

五、超越技术优化:构建性能工程体系

一个优秀的候选人,还能聊到性能优化的工程化挑战:

  1. 度量和监控体系

    • 真实用户监控:收集真实场景下的性能数据
    • 关键业务路径追踪:从用户进入到最后转化的全链路分析
  2. 性能文化建设

    • 性能预算:为每个关键指标设置明确的数值目标
    • 代码审核集成:在代码合并前检查性能影响
  3. 渐进式优化策略

    • 基准建立:首先建立当前的性能基线
    • 快速胜利:优先实施高影响低成本的优化
    • 长期投资:规划需要架构层面改变的深度优化

面试官总结:我心目中的理想回答

一个让我眼前一亮的回答,应该是这样的:

"说实话,性能优化这事儿我踩过不少坑。比如上次我们有个页面LCP一直上不去,我开始也以为是图片太大的问题,后来用Performance面板一分析,发现是字体文件阻塞了渲染。"

"我的思路其实挺简单的——先测量,再动手。我不会一上来就说要用什么preload或者WebP,而是先搞清楚瓶颈到底在哪。是网络慢?还是渲染被阻塞?或者是JS执行太耗时?"

"具体到LCP优化,我的经验是分三步走:

  1. 找到元凶——用Lighthouse和DevTools确定到底是哪个元素拖慢了LCP
  2. 资源优先级——如果是图片,就用preload提前加载;如果是字体,就用font-display:swap避免阻塞
  3. 持续监控——优化完了不是结束,要盯着真实用户的数据看效果"

"而且我现在会特别关注业务价值。比如上次我们把LCP从4秒优化到1.8秒后,特意去看了转化率数据,发现确实提升了7%。"

"最重要的是,我觉得性能优化不是一次性的活,而是个持续过程。我们现在会把Core Web Vitals写进需求文档里,就像写功能需求一样自然。"

这样的回答为什么让我印象深刻?因为:

  • 真实经历,不是空谈理论
  • 具体方法,不是泛泛而谈
  • 业务思维,不只关注技术指标
  • 落地经验,知道怎么在团队里推动这些事情

说白了,我想找的不是一个只会背面试题的人,而是一个真正解决过问题的工程师。

思考题: 当Core Web Vitals要求LCP在2.5秒内时,你是否思考过这在不同网络环境、不同设备配置下的实际意义?当你通过代码分割减少初始包体积时,是否评估过这对后续页面加载的潜在影响?这些,才是性能优化思考的真正深度所在。

类属性公共还是私有

作者 Robet
2025年11月30日 13:37

决定一个类的属性(成员变量)是公共(public)还是私有(private) ,是面向对象设计中的核心问题之一。这不仅关乎代码封装性,还直接影响系统的可维护性、可扩展性和健壮性

以下是系统化的思考框架和实用原则,帮助你做出合理决策:


🔑 核心原则:最小暴露原则(Principle of Least Exposure)

“只暴露必须暴露的内容,其余一律隐藏。”

换句话说:
默认私有,只有当确实需要外部访问时,才设为公共。


一、判断标准:问自己这几个问题

1. 外部是否需要直接读取这个值?

  • ✅ 是 → 考虑 public 或提供 getter
  • ❌ 否 → private

📌 示例:

class BankAccount {
  private balance: number; // 外部不应直接读余额(需鉴权/日志)
  getBalance(): number { /* ... */ } // 通过方法控制访问
}

2. 外部是否需要直接修改这个值?

  • ✅ 是 → 考虑 public 或提供 setter(但要谨慎!)
  • ❌ 否 → private

⚠️ 直接暴露可变状态容易导致 bug:

// 危险!
user.profile.settings.darkMode = true; // 绕过校验/事件通知

更好的方式:

user.setTheme('dark'); // 内部可触发 re-render / save / log

3. 这个属性是否属于“内部实现细节”?

  • 如果未来可能重构、重命名或删除它 → 必须私有
  • 如果它是稳定契约的一部分(如 API 返回结构)→ 可 public

💡 例子:

  • 缓存字段(private cache: Map<...>)→ 私有
  • 用户 ID(public id: string)→ 公共(业务标识)

4. 是否需要保持对象的“不变性”(Invariants)?

如果属性参与维持对象的内部一致性,则必须私有,并通过方法控制变更。

📌 示例:矩形的宽高不能为负数

class Rectangle {
  private _width: number;
  private _height: number;

  setWidth(w: number) {
    if (w < 0) throw new Error('Width must be positive');
    this._width = w;
  }
}

二、优先使用 方法(Method)而非公共属性

即使需要“读取”或“设置”,也优先提供方法而非直接暴露属性:

场景 推荐做法
读取计算值 getFullName() 而非 fullName(除非是简单数据)
设置需校验 setEmail(email) 而非 email = ...
触发副作用 activate() 而非 isActive = true

✅ 好处:

  • 未来可加日志、权限、缓存、事件通知等逻辑
  • 避免“属性被意外覆盖”导致状态不一致

三、特殊情况处理

✅ 可以公开的属性类型

类型 说明 示例
不可变数据 初始化后永不改变 public readonly id: string
纯数据载体(DTO/POJO) 仅用于传输,无行为 interface UserDTO { name: string; email: string }
配置对象 明确设计为可读写的配置 public config: RenderConfig(但建议用 getter/setter 封装)

❌ 应避免公开的属性

  • 内部状态(如 isLoading, retryCount
  • 依赖其他属性的派生值(如 fullName = firstName + lastName → 应用 getter)
  • 敏感数据(密码、token、余额)
  • 复杂对象引用(如 private domElement: HTMLElement

四、TypeScript / JavaScript 中的具体实践

方案 1:使用 # 私有字段(推荐,ES2022+)

class Timer {
  #startTime: number;
  #isRunning = false;

  start() {
    this.#startTime = Date.now();
    this.#isRunning = true;
  }

  get elapsed() {
    return this.#isRunning ? Date.now() - this.#startTime : 0;
  }
}

✅ 真正私有,运行时安全

方案 2:TypeScript private(仅开发时保护)

class Logger {
  private logs: string[] = [];
  log(msg: string) { this.logs.push(msg); }
}

⚠️ 注意:编译后仍可被外部访问,仅防“手误”

方案 3:readonly + 公共(用于不可变数据)

class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}
}

✅ 安全暴露,且不可修改


五、团队协作建议

  1. 约定优于配置:团队统一规则,如“所有状态属性默认私有”
  2. 代码审查重点:检查是否有不必要的 public 属性
  3. 文档说明:对 public 属性明确其用途和约束

✅ 快速决策流程图

这个属性需要被外部访问吗?
│
├─ 否 → private / #
│
└─ 是 → 
     ├─ 是否需要修改? 
     │   ├─ 是 → 提供 setter 方法(而非直接 public)
     │   └─ 否 → 
     │        ├─ 是否不可变? → public readonly
     │        └─ 是否计算值? → 提供 getter 方法
     │
     └─ 是否属于稳定数据契约? → 可 public(如 DTO)

🎯 总结:黄金法则

“属性代表状态,状态应受控。
暴露行为(方法),而非状态(属性)。”

  • 默认 私有private#
  • 仅在必要且安全时暴露为公共
  • 优先通过 方法 控制访问,而非直接暴露字段
  • 对于纯数据对象(如 API 响应),可适当放宽

这样做,你的类将更健壮、更易测试、更易演进。

关于微前端框架wujie的一次企业级应用实践demo?

作者 寻找光_sxy
2025年11月30日 13:33

前言

本文将介绍我一种wujie的一次具体的应用,包括使用的场景、方式等等,完成一个具体的demo;

为什么要用微前端

事情是这样的,我们之前的业务有一个vue3+ts+vite的后台项目,后来公司决定新开发一个新的业务线,但是由于人力有限,如果重新搭建一个新的后台时间和人力成本较大都,尤其是其中的权限登录功能的设计都比较复杂,所以我们综合考虑,有没有一种可以直接用旧后台的权限和登录功能,然后其它功能完全隔离的,且旧后台和新后台可用两个部门的人来开发,可以独立开发、测试、部署,甚至技术栈也可以不受影响呢?这里我们想到了微前端方案;

微前端方案选择

我们经过调研,目光逐步瞄向了两种微前端的方案:无界乾坤

对比我们的业务,经过调研发现无界相比于乾坤更有优势:

  • 1、对旧后台项目影响较小,侵入程度低:只需要在旧有后台的项目上新起page页,以及新增一个路由即可;
  • 2、可单独开发、部署:子应用可以单独开发、部署,也可以使用一个全新的技术栈,即使生产环境无界挂了,出现问题了,也可以直接访问子应用;

综上两种原因,我们决定使用无界的方案;

怎么用无界(demo演示)

我们的主应用是vue3,这里将子应用通过菜单栏的形式嵌入到父应用中间,点击菜单即可进入到子应用

登录场景,在子应用请求时,若发现登录失效,通过子组件通信window.$wujie.bus.$emit('notLogin')向父应用传递未登录消息,父应用执行后续逻辑

权限逻辑,天然就互通,当子应用的菜单权限在某些角色下不可见时,在父应用下直接隐藏掉菜单就行;如果是子应用下按钮权限等功能权限时,可在子应用单独再次调用权限接口,或通过父子应用通信方式获取权限信息 image.png

具体步骤

父应用改造

  • 下载新依赖
  • wujie相关文件
  • 路由 image.png

下载相关依赖

pnpm install wujie-vue3

创建wujie文件

用于补充wujie的相关逻辑:

  • wujietemplate相关属性
    • name: 子应用唯一标识
    • url: 子应用运行地址
    • props:向子应用传递的参数
  • 父子应用通信
    • 通知子应用路由发生改变
    • 通知子应用其他数据
    • 子应用告知父应用未登录
    • 子应用告知父应用其他信息 image.png
<template>
  <div class="main-app">
    <h1>Vue3 主应用</h1>
    <!-- 嵌入 React 子应用 -->
    <WujieVue width="100%" height="600px" :url="subAppUrl" :name="subAppName" :props="subAppProps" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { watch } from "vue";
import WujieVue from "wujie-vue3";

const { bus } = WujieVue;

// 子应用配置(React 子应用的运行地址,后续启动子应用后会用到)
const subAppName = ref("react-sub-app"); // 子应用唯一标识(必须唯一)
const subAppUrl = ref("http://localhost:1074/#/wujieDemo1"); // 子应用端口(后续配置 React 子应用为 3001)

// 主应用向子应用传递的 props(可选)
const subAppProps = ref({
  mainAppName: "Vue3 主应用",
  token: "main-app-token-123",
});

const router = useRouter();
/** 监听子应用的数据 */
bus.$on("subAppData", (data: { type: string, payload?: any }) => {
  const { type } = data;
  if (type == "noLogin") {
    alert("未登录")
  }
});

/** 监听子应用的数据 */


watch(
  () => router.currentRoute.value.meta.subAppPath,
  (newVal) => {
    if (newVal === undefined) return;
    bus.$emit("routeChange", newVal);
  },
  {
    immediate: true,
  }
);
</script>

创建wujie路由

这里新建了一个路由的文件wujieRouter.ts

通过监听subAppPath去判断跳转到子应用对应路由,且这里的subAppPath其实对应的是子应用的路由path

const routerName = "wujiePage";

const wujieRouters = [
  {
    path: `/${routerName}`,
    name: `${routerName}`,
    component: () => import("@/pages/wujie/index.vue"),
    meta: {
      title: '新项目-react', // 菜单显示文本
      icon: 'CreditCard', // 菜单图标
      hidden: false,
      level: 0,
    },
    children: [
      {
        path: "wujieDemo1", // 子路由直接使用相对路径,不要包含父路由名称
        name: `${routerName}wujieDemo1`, // 名称保持唯一,不要使用斜杠
        component: () => import("@/pages/wujie/wujie.vue"),
        meta: {
          title: 'wujieDemo1', // 菜单显示文本
          icon: 'Present', // 子菜单图标
          hidden: false,
          level: 1,
          subAppPath: "/wujieDemo1",
        },
      },
      {
        path: "wujieDemo2", // 子路由直接使用相对路径,不要包含父路由名称
        name: `${routerName}wujieDemo2`, // 名称保持唯一,不要使用斜杠
        component: () => import("@/pages/wujie/wujie.vue"),
        meta: {
          title: 'wujieDemo2', // 菜单显示文本
          icon: 'Present', // 子菜单图标
          hidden: false,
          level: 1,
          subAppPath: "/wujieDemo2",
        },
      },
    ]
  },

]

export default wujieRouters;

image.png

子应用改造

  • 运行环境判断
  • 路由通信
  • 嵌入子页面
  • 路由
  • 接口响应拦截器

image.png

运行环境判断

这里我们在main.tsx文件通过判断window.$wujie属性是否存在,来判断当前的运行环境是独立运行还是微前端环境

原理wujie会自动给子应用的window上挂载一个$wujie对象

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { HashRouter } from "react-router-dom";
import "./style/index.css";
import App from "./App";

import { Provider } from "react-redux";
import { store } from "./model/store";

// Wujie 子应用生命周期:挂载(主应用嵌入时调用)
const mount = (container: HTMLElement | ShadowRoot, props: any) => {
  // 将主应用 props 存入 React 上下文(方便子应用内部使用)
  createRoot(container).render(
    <StrictMode>
      <Provider store={store}>
        <HashRouter>
          <App {...props} />
        </HashRouter>
      </Provider>
    </StrictMode>
  );
};

// 判断是否在 Wujie 微前端环境中
if (window.$wujie) {
  mount(document.getElementById("root")!, window.$wujie.props);
} else {
  // 独立运行环境(正常启动)
  mount(document.getElementById("root")!, {
    mainAppName: "独立运行",
    token: "local-token",
  });
}

路由通信

app.tsx文件中修改

子应用监听到父应用的路由发生了改变,立即进行路由跳转

import { router } from "./router/createRouteConfig";
import { useNavigate, useRoutes } from "react-router-dom";
import useLocationChange from "./router/useLocationChange";
import routerListener from "./router/routerListener";
import "./style/index.css";
import { useEffect } from "react";

const App = function (props: any) {
  const elements = useRoutes(router);
  const navigate = useNavigate();

  useEffect(() => {
    const wujieBus = window.$wujie?.bus;
    const routeChangeHandler = (path: string) => {
      navigate(path);
    };
    wujieBus?.$on("routeChange", routeChangeHandler);
    return () => {
      wujieBus?.$off("routeChange", routeChangeHandler);
    };                                                                                                                               
  }, [navigate]);

  useLocationChange((to, from) => {
    routerListener(navigate, to, from);
  });
  return elements;
};

export default App;

嵌入的子页面

新建立一个文件用于放嵌入的子页面,且在该子页面中还可以向父应用通信

const wujieDemo1 = () => {

  return (
    <div>
      <h1>我是子应用(react)的wujieDemo1</h1>
      <button onClick={() =>  window.$wujie?.bus.$emit("subAppData", "我是子应用数据")}>向主应用提交数据</button>
    </div>
  );
};

export default wujieDemo1;

路由

新建路由用于对应上面的子页面

其中需要注意的是,路由的path需要对应父应用路由上的subAppPath

......
  {
      name: "wujieDemo1",
      path: "/wujieDemo1",
      component: lazy(() => import("../page/wujiePage/wujieDemo1/index")),
      isMenu: false,
    },
......

接口响应拦截器

在响应拦截器中,主要是针对未登录的场景,在未登录时,告知父应用

这里也做了运行环境的判断,用于判断是进入子应用的登录页面还是父应用的登录页面

// 将方法封装成一个函数
const http = async (config: IAxiosParam): Promise<any> => {
  return request(config)
    .then((res: IResponse) => {
      switch (res.code) {
        case ResCode.notLogin:
          // 未登录
          if (window.$wujie) {
            window.$wujie?.bus.$emit("subAppData", {
              type: "noLogin"
            })
          } else {
            window.location.href = "/login";
          }
          break;
      }

      if (res.code !== 0 && !config.noAlert) {
        // 异常提示
        alert(res.msg || "出现问题啦~");
        return;
      }
      return config.needRes ? res : res.data;
    })
    .catch((res) => {
      return Promise.reject(res);
    });
};

总结

这里我完成了一个基础的demo,在时间的应用还有一些需要注意或优化的点:

  • 子应用的运行地址可配置化
  • 子应用的预加载与保活
  • 多个子应用的配置

后续可根据自己的实际场景来配置

❌
❌