普通视图

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

CSS 迎来重大升级:Chrome 137 支持 if () 条件函数,样式逻辑从此更灵活

作者 大知闲闲i
2025年7月3日 16:26

一、CSS if () 函数:前端样式编程的新范式

从 Chrome 137 版本开始,CSS 正式引入if()条件函数,这是继 CSS 变量、网格布局后又一革命性特性。该函数允许在样式声明中直接编写条件逻辑,彻底改变了传统 CSS 依赖媒体查询、属性选择器实现条件渲染的模式。

核心语法与逻辑结构

property: if(
  condition-1: value-1;
  condition-2: value-2;
  /* 更多条件... */
  else: default-value);
  • 条件类型:支持media()(媒体查询)、supports()(功能检测)、style()(样式值判断)三种条件表达式
  • 执行逻辑:按条件顺序从上至下判断,满足首个条件即返回对应值,全部不满足则返回else默认值

二、三大条件表达式深度解析

1. media ():替代传统 @media 查询

场景:根据屏幕宽度动态调整导航栏布局

nav {
  display: if(
    media(min-width: 768px): flex; /* 平板以上横排 */
    else: block); /* 移动端竖排 */
  
  background-color: if(
    media(hover: hover): rgba(0,0,0,0.9); /* 支持悬停时半透明 */
    else: #000); /* 触摸设备纯黑 */
}

优势

  • 条件逻辑与样式声明同屏展示,避免代码碎片化
  • 支持嵌套在任意属性中,比@media更精准控制单一属性

2. supports ():智能兼容处理

场景:根据浏览器能力自动切换渲染方案

.hero-section {
  backdrop-filter: if(
    supports(backdrop-filter: blur(20px)): blur(20px); /* 新浏览器毛玻璃 */
    else: none); /* 旧浏览器无效果 */
  
  background: if(
    supports(backdrop-filter: blur(20px)): rgba(255,255,255,0.3); /* 配合毛玻璃半透明 */
    else: #fff); /* 纯背景色 */
}

最佳实践

  • 优先检测supports()再处理样式,避免冗余代码
  • 可结合style()实现「能力检测 + 状态判断」复合逻辑

3. style ():基于变量的状态响应

场景:根据元素数据属性动态切换主题色

<button class="btn" data-state="primary">提交</button>
<button class="btn" data-state="secondary">取消</button>

.btn {
  --state: attr(data-state);
  color: if(
    style(--state: primary): white;
    style(--state: secondary): #333;
    else: #666);
  
  background: if(
    style(--state: primary): #409EFF;
    style(--state: secondary): #F5F7FA;
    border: if(
      style(--state: secondary): 1px solid #DCDFE6;
      else: none);
}

技术要点

  • 通过attr()获取元素属性值并赋值给 CSS 变量
  • style()可直接比较变量值,实现类似 JavaScript 的if (state === 'primary')逻辑

三、三大应用场景实战

1. 响应式布局优化:卡片网格自适应

.grid-container {
  display: grid;
  grid-template-columns: if(
    media(min-width: 1200px): repeat(4, 1fr); /* 大屏4列 */
    media(min-width: 768px): repeat(3, 1fr); /* 平板3列 */
    else: repeat(2, 1fr)); /* 手机2列 */
  
  gap: if(
    media(hover: hover): 24px; /* 鼠标设备宽间距 */
    else: 16px); /* 触摸设备窄间距 */
}

对比传统方案

  • 传统方案需写 3 个@media块,现合并为 1 个属性声明
  • 直接控制grid-template-columns属性,避免全局布局切换

2. 交互状态动态样式:按钮加载态

<button class="action-btn" data-loading="false">提交</button>




.action-btn {
  --loading: attr(data-loading);
  cursor: if(
    style(--loading: true): wait; /* 加载中显示沙漏 */
    else: pointer); /* 常态显示小手 */
  
  opacity: if(
    style(--loading: true): 0.6; /* 加载中半透明 */
    else: 1);
  
  &::before {
    content: if(
      style(--loading: true): '加载中...'; /* 加载中显示文字 */
      else: '提交');
  }
}

动态效果

  • 通过 JS 修改data-loading属性,样式自动响应
  • 无需额外类名切换,保持 HTML 结构简洁

3. 暗黑模式智能适配

<html data-theme="light">
  <body class="page">...</body>
</html>




.page {
  --theme: attr(data-theme);
  color: if(
    style(--theme: dark): #f5f5f5; /* 暗黑模式浅色文字 */
    else: #333); /* 亮色模式深色文字 */
  
  background: if(
    style(--theme: dark): #121212; /* 暗黑模式深色背景 */
    else: #f9f9f9); /* 亮色模式浅色背景 */
  
  /* 智能适配系统主题 */
  background: if(
    media(prefers-color-scheme: dark) and not(style(--theme: light)): #121212;
    else: #f9f9f9);
}

逻辑拆解

  1. 优先使用元素自身data-theme属性
  2. 若未指定主题,自动检测系统暗黑模式
  3. 所有逻辑集中在一个选择器中,维护成本降低 50%

四、兼容性与最佳实践

1. 浏览器支持策略

  • 当前支持:Chrome 137+、Edge 137+(需开启实验性特性)

  • 降级方案

    .fallback {
      /* 基础样式(旧浏览器生效) */
      color: #333;
      
      /* 新特性增强(新浏览器覆盖) */
      color: if(style(--theme: dark): #f5f5f5; else: #333);
    }
    

2. 性能优化要点

  • 避免多层嵌套

    /* 推荐 */
    font-size: if(media(min-width: 768px): 18px; else: 16px);
    
    /* 不推荐(嵌套过深影响解析性能) */
    font-size: if(media(min-width: 1200px): if(media(hover: hover): 20px; else: 18px); else: 16px);
    
  • 条件顺序优化:将高频条件放在前面

3. 工程化集成建议

  • 结合 CSS 变量管理

    :root {
      --mobile-breakpoint: 768px;
    }
    
    .component {
      width: if(media(min-width: var(--mobile-breakpoint)): 50%; else: 100%);
    }
    
  • 配合 PostCSS 插件:在低版本浏览器中自动转换if()为传统语法

五、未来展望:CSS 逻辑编程的新纪元

if () 函数的引入标志着 CSS 从「声明式样式」向「逻辑式样式」的重要跨越。未来可能延伸的特性包括:

  1. 循环函数:如for()实现动态生成样式

  2. 数学表达式:支持calc()与条件的组合运算

  3. 函数嵌套:在条件中调用自定义 CSS 函数

对于前端开发者,掌握 if () 将成为必备技能 —— 它不仅简化样式逻辑,更推动 CSS 向完整「样式编程语言」演进,让界面动态性与维护性达到新高度。

IEEE 754 双精度浮点数标准,最大整数和最大的数字

2025年7月3日 16:02

基础:

  1. 所有的数字,都是用 64位双精度浮点数 表示,其内存结构分为3部分

[1位符号位][11位指数位][52位尾数位] 来存储的

符号位:决定数字的正负(0是正数,1是负数)

指数位:表示2的幂次(采用偏移码表示,实际指数 = 存储值 - 1023)

范围:-1022 到 1023(特殊值 0 和 2047用于表示0和无穷大)

尾数位/有效数字(52 bits + 隐含位)

  • 关键点:实际精度是 53 bits(52位显式存储 + 1位隐含的"1") 采用"隐含前导1"的表示法(normalized numbers)

综上: 数值的计算公式为:

image.png

最大整数Number.MAX_SAFE_INTEGER

2^53 -1

最大值:Number.MAX_VALUE

image.png

老板突然发问 公司OA系统突然无法登录 我慌了

作者 poloma
2025年7月3日 15:40

前言

有一天,老板突然登录后台管理系统,说系统卡住了,无法登录,让我马上和运维排查问题

从点击登录到卡住,返回请求错误码足足等待了超过30S,老板挂面,我可能有被裁风险

image.png

排查问题

点击登录以后,前端请求后端的接口,这里的流程是前端通过跨域请求实现的。 方式是通过 nginx 服务器代理请求后端的接口的。

我们定位到 nginx 处理跨域请求的时候的配置上。

Nginx 在使用 proxy_pass 进行域名代理时,可能会出现缓存问题,导致后端服务更新后客户端无法及时获取最新内容。这通常由多种因素引起,包括 Nginx 自身缓存、上游服务器缓存头设置或浏览器缓存策略等诸多因素,我们一个个进行排查

一,确认缓存的来源

在解决问题前,需先确定缓存位置:

  1. 浏览器缓存:通过开发者工具(如 Chrome 的 Network 面板)检查响应头中的 Cache-Control 和 Expires
  2. Nginx 代理缓存:检查 Nginx 配置是否启用了 proxy_cache 或相关指令。
  3. 上游服务器缓存:检查后端应用是否设置了缓存头。

很容易我们就排除了浏览器的缓存和上游服务器后端的缓存,前端浏览器这边 Cache-Control Expires 已经设置好响应头,同时后端应用也设置好了缓存头

接下来我们将问题定位到运维的位置

二、禁用 Nginx 代理缓存

在处理跨域请求(CORS)时,Nginx 需要同时解决两个关键问题:跨域头配置禁用缓存。以下是针对这两个需求的配置方案:

一、基础配置:跨域头 + 禁用缓存

server {
    listen 80;
    server_name your-domain.com;
    
    # 处理跨域请求
    location /api/ {
        # 代理配置
        proxy_pass http://backend-api-server/;  # 后端 API 服务器地址
        
        # 跨域头配置
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
        
        # 允许携带凭证(如 cookies)
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        
        # OPTIONS 请求直接返回 204
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # 禁用缓存配置
        proxy_no_cache $cookie_nocache $arg_nocache $arg_comment;
        proxy_cache_bypass $cookie_nocache $arg_nocache $arg_comment;
        
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
        
        # 其他代理常用配置
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

二、高级配置:针对不同请求类型优化

1. 静态资源与动态 API 差异化处理
server {
    # 静态资源(允许缓存)
    location /static/ {
        proxy_pass http://backend-static/;
        # 默认缓存策略(根据需要调整)
        expires 7d;
    }
    
    # API 请求(禁用缓存)
    location /api/ {
        proxy_pass http://backend-api/;
        
        # 跨域头
        add_header 'Access-Control-Allow-Origin' '*' always;
        # ... 其他 CORS 头
        
        # 禁用缓存
        proxy_no_cache 1;
        proxy_cache_bypass 1;
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }
}
2. 仅对特定 API 禁用缓存
location /api/no-cache/ {
    proxy_pass http://backend/;
    
    # 禁用缓存
    proxy_no_cache $arg_nocache;
    proxy_cache_bypass $arg_nocache;
    
    # 动态添加 nocache 参数来控制是否缓存
    # 例如:/api/no-cache/data?nocache=1
}
3. 针对常用写死的域名,让每次请求都进行 DNS 查询

企业微信截图_17514472935097.png

http {
    resolver 8.8.8.8 valid=10s;

    server {
        listen 80;

        location / {
            set $upstream_host a.b.com;
            proxy_pass https://$upstream_host;
            proxy_set_header Host $upstream_host;
        }
    }
}

三、验证配置是否生效

1. 检查响应头
curl -I http://your-domain.com/api/data

# 预期输出:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,...
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
2. 浏览器开发者工具验证

在 Chrome/Firefox 中:

  1. 打开开发者工具 → Network 面板
  2. 发送请求并检查响应头
  3. 确认没有 (from cache) 标记

通过以上配置,可确保 Nginx 在处理跨域请求时正确禁用缓存,同时满足 CORS 安全要求。根据具体业务需求调整配置细节,平衡性能与安全性。

三、 常见问题与解决方案

缓存仍未完全禁用

  • 问题原因

    • 上游服务器设置了缓存头,覆盖了 Nginx 的配置
    • CDN 缓存未清除
    • 浏览器强缓存
  • 解决方案

    • 使用 proxy_ignore_headers Cache-Control Expires 忽略上游缓存头
    • 若使用 CDN,在 CDN 控制台刷新缓存
    • 要求用户使用 Ctrl+F5(Windows)或 Command+Shift+R(Mac)强制刷新

Nginx 配置生效部分

修改配置后需重启或重载 Nginx:

sudo nginx -s reload

总结

前端针对这种问题需要提前做预防措施,为了避免

预防措施

  1. 开发阶段
  • 使用代理服务器处理跨域(如 Vite 的 server.proxy
  • 在浏览器开发者工具中始终启用「Disable cache」
  1. 生产环境
  • 对静态资源使用版本化(如 app.js?v=1.0.1
  • 为 API 请求设置合理的缓存策略(如频繁更新的数据使用 no-cache
  • 定期审查服务器和 CDN 的缓存配置
解决方案总结
问题类型 解决方案
跨域错误 1. 修改服务器 Nginx 配置,添加正确的 CORS 头
2. 使用代理服务器(如 Vite/Webpack 代理)
3. 确保请求配置与服务器允许的范围一致
浏览器缓存 1. 在开发者工具中禁用缓存
2. 添加响应头 Cache-Control: no-cache
3. 请求 URL 添加随机参数
服务器缓存 1. 修改后端代码,禁用缓存头
2. 检查 Nginx 配置,确保正确设置 proxy_no_cache
CDN 缓存 1. 在 CDN 控制台刷新缓存
2. 调整 CDN 缓存规则(如设置 Cache-Control: no-cache

至此完结

常见简单的知识点

2025年7月3日 15:37

在编程中,?? 是一个 空值合并运算符(Nullish Coalescing Operator) ,主要用于提供默认值。它的作用如下:

语法:

leftExpression ?? rightExpression

行为:

  • 如果 leftExpression 的值为 null 或 undefined,则返回 rightExpression(即默认值)。
  • 否则,直接返回 leftExpression 的值。

示例:

const value1 = null ?? 'default';      // 输出: 'default'(因为左侧是 null)
const value2 = undefined ?? 'fallback'; // 输出: 'fallback'(因为左侧是 undefined)
const value3 = 0 ?? 42;                // 输出: 0(因为左侧不是 null/undefined)
const value4 = '' ?? 'hello';          // 输出: ''(因为左侧不是 null/undefined)

与 || 的区别:

  • || 运算符会对左侧的 假值(falsy) (如 0''falsenullundefined)触发默认值。
  • ?? 仅对 null 或 undefined 触发默认值,更精确。
const a = 0 || 42;   // 输出: 42(因为 0 是假值)
const b = 0 ?? 42;   // 输出: 0(因为 0 不是 null/undefined)

在 Vue 3 中,unref 是一个 响应式工具函数,用于获取一个响应式引用(Ref)的 内部值。如果传入的参数本身不是 Ref,则直接返回该参数。

作用

unref 的作用可以理解为:

  • 如果传入的是 Ref 对象(如 ref() 创建的),则返回它的 .value
  • 如果传入的不是 Ref,则直接返回原值。

源码实现

function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? ref.value : ref;
}

使用场景

  1. 简化 Ref 和普通值的访问

    • 在不确定某个变量是 Ref 还是普通值时,可以用 unref 安全地获取值。
    • 避免手动判断 isRef 再取值。
  2. 在组合式函数(Composables)中处理参数

    • 允许函数同时接受 Ref 或普通值,提高灵活性。

示例

基本用法
import { ref, unref } from 'vue';

const count = ref(1);
const num = 2;

console.log(unref(count)); // 输出: 1(相当于 count.value)
console.log(unref(num));   // 输出: 2(直接返回 num)
在组合式函数中使用
import { ref, unref, computed } from 'vue';

// 该函数可以接受 Ref<number> 或 number
function double(value) {
  const unwrapped = unref(value); // 安全取值
  return unwrapped * 2;
}

const a = ref(3);
const b = 4;

console.log(double(a)); // 输出: 6(a 是 Ref)
console.log(double(b)); // 输出: 8(b 是普通值)
与 toRef 对比
  • toRef:将响应式对象的属性转换为 Ref
  • unref:从 Ref 中提取值(反向操作)。
import { reactive, toRef, unref } from 'vue';

const state = reactive({ foo: 1 });
const fooRef = toRef(state, 'foo');

console.log(unref(fooRef)); // 输出: 1

注意事项

  • unref 不会解除深层响应式(如 reactive 对象),它仅处理 Ref
  • 如果需要深度解包(如嵌套 Ref),可以使用 toRaw 或第三方工具(如 vue-utils 的 deepUnref)。

总结

unref 是 Vue 3 响应式系统中一个轻量级的工具函数,主要用于:

  1. 统一处理 Ref 和普通值。

  2. 在组合式函数中增加参数灵活性。
    它的存在让代码更简洁,避免重复的 isRef 判断。

在 Vue 3 的响应式系统中,toRaw 和 toRef 是两个用途完全不同的工具函数,它们的核心区别如下:

1. toRaw:获取原始非响应式对象

作用
  • 返回一个响应式对象(reactive 或 readonly 创建的)的 原始普通对象(剥离所有响应式特性)。
  • 对 ref 对象,返回其 .value 的原始值(如果 .value 是响应式对象)。
使用场景
  • 需要直接操作原始数据,避免响应式开销(如性能敏感场景)。
  • 临时修改数据但不想触发响应式更新。
示例
import { reactive, toRaw } from 'vue';

const obj = reactive({ foo: 1 });
const rawObj = toRaw(obj); // 原始对象 { foo: 1 }

console.log(rawObj === obj); // false(rawObj 是非响应式的普通对象)

// 修改原始对象不会触发响应式更新
rawObj.foo = 2; 
console.log(obj.foo); // 2(值变化,但不会触发视图更新)
注意事项
  • 对 ref 对象,toRaw(ref) 等价于 toRaw(ref.value)

2. toRef:将响应式对象的属性转换为 Ref

作用
  • 为响应式对象(reactive)的某个属性创建一个 关联的 Ref 引用
  • 修改 Ref 会同步到原始对象,反之亦然。
使用场景
  • 需要将响应式对象的某个属性单独作为 Ref 传递,保持响应式关联。
  • 在组合式函数中解构属性时保持响应性。
示例
import { reactive, toRef } from 'vue';

const state = reactive({ foo: 1 });
const fooRef = toRef(state, 'foo'); // 创建 foo 的 Ref

// 修改 Ref 会同步到原对象
fooRef.value = 2;
console.log(state.foo); // 2

// 修改原对象也会更新 Ref
state.foo = 3;
console.log(fooRef.value); // 3
与 ref 的区别
  • ref(1) 创建一个独立的 Ref,与原对象无关。
  • toRef(state, 'foo') 创建的 Ref 和原对象的 foo 属性保持双向绑定。

核心区别对比

函数 作用对象 返回值类型 是否保持响应式关联 典型用途
toRaw reactive/readonly 原始普通对象 ❌ 完全剥离响应式 获取原始数据,避免响应式开销
toRef reactive 对象的属性 Ref ✅ 双向同步 解构属性并保持响应性

结合使用的场景

import { reactive, toRef, toRaw } from 'vue';

const state = reactive({ foo: { bar: 1 } });

// 将响应式对象的属性转为 Ref
const fooRef = toRef(state, 'foo'); 

// 获取 Ref 的原始值(非响应式)
const rawFoo = toRaw(fooRef.value); 
console.log(rawFoo); // { bar: 1 }(普通对象)

总结

  • toRaw:用于“降级”响应式对象,获取原始数据(非响应式)。
  • toRef:用于“升级”响应式对象的属性为 Ref,保持响应式关联。
  • 两者互补,分别处理响应式系统的不同层级需求。

在 JavaScript 中,不同类型的循环有不同的 停止(中断)方式,它们的用途和特性也有显著区别。以下是详细对比和完整示例:

1. 循环类型对比

循环方法 适用对象 能否被停止? 停止方式 返回值 特点
forEach 数组 ❌ 不能直接停止 抛出异常(不推荐) undefined 简洁,但无法中断
map 数组 ❌ 不能停止 新数组 返回新数组,不改变原数组
for...in 对象(枚举属性) ✅ 可以用 break break / return - 遍历键名(包括原型链属性)
for...of 可迭代对象(数组、字符串等) ✅ 可以用 break break / return - 遍历值(忽略原型链属性)
for 通用 ✅ 可以用 break break / return - 灵活控制循环条件

2. 完整示例代码

(1) forEach:无法直接停止

const arr = [1, 2, 3];
arr.forEach(item => {
  console.log(item);
  if (item === 2) {
    // 无法直接停止!只能通过抛出异常(不推荐)
    throw new Error('强行停止');
  }
});
// 输出: 1, 2, Error

(2) map:无法停止,始终返回新数组

const arr = [1, 2, 3];
const newArr = arr.map(item => {
  console.log(item);
  return item * 2;
});
console.log(newArr); // [2, 4, 6]

(3) for...in:遍历对象键名,可用 break 停止

const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key, obj[key]); // 输出键名和值
  if (key === 'b') break; // 停止循环
}
// 输出: a 1, b 2

(4) for...of:遍历可迭代对象的值,可用 break 停止

const arr = [1, 2, 3];
for (const item of arr) {
  console.log(item);
  if (item === 2) break; // 停止循环
}
// 输出: 1, 2

(5) for 循环:经典循环,完全可控

const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
  if (arr[i] === 2) break; // 停止循环
}
// 输出: 1, 2

3. 如何选择循环方式?

场景 推荐循环方式 原因
需要中途停止循环 for / for...of / for...in 支持 break 和 return
遍历数组并返回新数组 map 简洁,自动返回新数组
遍历数组但不需要返回值 forEach 语法简单,但无法中断
遍历对象属性(包括继承属性) for...in 遍历键名,但需用 hasOwnProperty 过滤原型链属性
遍历可迭代对象(数组、字符串等) for...of 直接遍历值,比 for...in 更适合数组

4. 特殊情况处理

forEach 模拟中断(不推荐)

const arr = [1, 2, 3];
try {
  arr.forEach(item => {
    console.log(item);
    if (item === 2) throw new Error('Stop');
  });
} catch (e) {
  if (e.message !== 'Stop') throw e;
}
// 输出: 1, 2

for...of + return(在函数中使用)

function findTarget(arr, target) {
  for (const item of arr) {
    if (item === target) return item; // 直接返回并停止循环
  }
  return null;
}
console.log(findTarget([1, 2, 3], 2)); // 2

总结

  • 需要中断循环:优先使用 forfor...of 或 for...in + break
  • 遍历数组并返回新数组:用 map
  • 遍历对象属性:用 for...in(注意过滤原型链属性)。
  • forEach 和 map 无法中断,但 map 会返回新数组。

Reflect 在 Vue 3 中的作用及常用 API 方法

Reflect 是 ES6 引入的一个内置对象,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy 处理器方法一一对应。在 Vue 3 的响应式系统中,Reflect 被广泛使用来实现代理行为。

Reflect 的核心作用

  1. 提供操作对象的标准方法:替代一些传统的 Object 方法
  2. 与 Proxy 配合使用:Proxy 的 trap 通常需要调用对应的 Reflect 方法来完成默认行为
  3. 更规范的返回值:相比传统方法,Reflect 方法有更一致的返回值(如成功返回 true,失败返回 false)

Vue 3 中常用的 Reflect API

1. Reflect.get(target, propertyKey[, receiver])

获取对象属性的值

const obj = { foo: 42 };
console.log(Reflect.get(obj, 'foo')); // 42

2. Reflect.set(target, propertyKey, value[, receiver])

设置对象属性的值

const obj = {};
Reflect.set(obj, 'foo', 123);
console.log(obj.foo); // 123

3. Reflect.has(target, propertyKey)

检查对象是否具有某属性

const obj = { foo: 1 };
console.log(Reflect.has(obj, 'foo')); // true
console.log(Reflect.has(obj, 'bar')); // false

4. Reflect.deleteProperty(target, propertyKey)

删除对象属性

const obj = { foo: 1, bar: 2 };
Reflect.deleteProperty(obj, 'foo');
console.log(obj); // { bar: 2 }

5. Reflect.ownKeys(target)

获取对象所有自身属性键(包括不可枚举和Symbol属性)

const obj = {
  [Symbol('id')]: 123,
  name: 'John'
};
console.log(Reflect.ownKeys(obj)); // ['name', Symbol(id)]
const obj = {
  [Symbol('id')]: 123,
  name: 'John'
};
console.log(Reflect.ownKeys(obj)); // ['name', Symbol(id)]

Vue 3 中使用 Reflect 的案例

案例1:响应式系统中的使用

Vue 3 的响应式系统大量使用 Reflect 与 Proxy 配合:

const reactiveHandler = {
  get(target, key, receiver) {
    track(target, key); // 依赖追踪
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    if (result && oldValue !== value) {
      trigger(target, key); // 触发更新
    }
    return result;
  }
  // 其他trap...
};

function reactive(obj) {
  return new Proxy(obj, reactiveHandler);
}

const state = reactive({ count: 0 });

案例2:组合式API中的使用

import { reactive, watchEffect } from 'vue';

const user = reactive({
  name: 'Alice',
  age: 25
});

// 使用Reflect进行属性操作
function updateUser(key, value) {
  if (Reflect.has(user, key)) {
    Reflect.set(user, key, value);
  } else {
    console.warn(`Property ${key} does not exist`);
  }
}

watchEffect(() => {
  console.log('User updated:', Reflect.ownKeys(user).map(k => `${k}: ${user[k]}`));
});

updateUser('age', 26); // 触发更新
updateUser('email', 'alice@example.com'); // 警告

案例3:自定义Ref实现

import { customRef } from 'vue';

function useDebouncedRef(value, delay = 200) {
  let timeout;
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          value = newValue;
          trigger();
        }, delay);
      }
    };
  });
}

// 使用
const text = useDebouncedRef('hello');

案例4:响应式工具函数

import { reactive, toRefs } from 'vue';

function useFeature() {
  const state = reactive({
    x: 0,
    y: 0,
    // 计算属性
    get distance() {
      return Math.sqrt(this.x ** 2 + this.y ** 2);
    }
  });

  function updatePosition(newX, newY) {
    // 使用Reflect批量更新
    Reflect.set(state, 'x', newX);
    Reflect.set(state, 'y', newY);
  }

  return {
    ...toRefs(state),
    updatePosition
  };
}

// 使用
const { x, y, distance, updatePosition } = useFeature();

Reflect 在 Vue 3 中的优势

  1. 与 Proxy 完美配合:每个 Proxy trap 都有对应的 Reflect 方法
  2. 更安全的操作:相比直接操作对象,Reflect 方法提供了更规范的错误处理
  3. 元编程能力:为 Vue 的响应式系统提供了底层支持
  4. 一致性:所有 Reflect 方法都返回布尔值表示操作是否成功

总结

Vue 3 的响应式系统深度依赖 Reflect API 来实现其核心功能。通过 Reflect 与 Proxy 的组合,Vue 能够:

  • 拦截对象操作
  • 跟踪依赖关系
  • 触发更新通知
  • 提供一致的响应式行为

理解 Reflect 的这些用法有助于更好地理解 Vue 3 的响应式原理,并在需要时实现更高级的自定义响应式逻辑。

🧠“一次奇怪的 JS/TS 报错,背后竟是分号惹的祸”

2025年7月3日 15:29

引子:看似无害的一行代码,却让整个程序崩溃

在学习 TypeScript 的时候,我曾因为少写了一个分号,触发了一个让人摸不着头脑的编译错误:

类型“Card[]”不能分配给类型“number”

更诡异的是,这行代码乍看之下完全没有问题:

  shuffle() {
    for(let i = 0; i < this.cards.length; i++) {
      const targetIndex:number = this.getRandom(0, this.cards.length)
      [this.cards[i], this.cards[targetIndex]] = [this.cards[targetIndex], this.cards[i]]
    }
  }

但是 TypeScript 却报错了,怎么回事?明明语法也没问题,我们不是常说 JS 的分号是“可选”的嘛?

这次,它可真的不是。


一、JS 的分号真的是“可选”的吗?

很多人(包括以前的我)都习惯不写分号。因为在 JavaScript 中,确实有一种机制叫做 ASI(Automatic Semicolon Insertion,自动分号插入) ,它会在大多数情况下帮我们补上遗漏的分号。

像下面这样的代码,在不加分号的情况下也能正常运行:

let a = 1
let b = 2
console.log(a + b)

JavaScript 会在每一行后面“想当然地”补上分号。但这并不意味着它总能理解你的意图。 事实上,在某些情况下,ASI 会失效,甚至让代码逻辑彻底跑偏。


二、数组解构 + 缺失分号 = 地狱级 Bug

我们再回到那段引起错误的代码:

const targetIndex: number = this.getRandom(0, this.cards.length)
[this.cards[i], this.cards[targetIndex]] = [this.cards[targetIndex], this.cards[i]]

表面上是没问题的,但 JS 引擎的解释却可能出乎意料:

const targetIndex: number = this.getRandom(...) [this.cards[i], ...]

没错,JavaScript 把第二行开头的 [ 误以为是上一行函数调用的下标访问。它以为你在写:

this.getRandom(...)[...]

而不是一个新的解构赋值语句。

这就是 ASI 的一个坑点:当下一行以 [ 开头时,它不会自动插入分号


三、除了 [,还有哪些坑?

除了数组开头,JS 的 ASI 机制还有一些常见的“踩雷点”:

  • 下一行以 [ 开头(数组、解构)
  • 下一行以 ( 开头(函数调用、立即执行函数)
  • 上一行以 ++-- 结尾(自增/自减)

比如

const x = 123
[x, y] = [y, x]

JS 实际上会尝试当成一行执行:

const x = 123[x, y] = [y, x]

这当然是非法语法,也会抛出莫名其妙的错误。


四、怎么避免这种问题?

其实很简单:不要赌 JS 会自动帮你加分号,关键地方自己加!

写代码的时候,我们往往觉得“少个分号没什么大不了”,但有时候,这一丢,就像埋下了一颗“定时炸弹”,在你最不希望出错的时候,炸了出来。

规范很重要。少些分号看起来“优雅”,但出了问题你可是要多花两小时去 debug 的。

写好每一行代码,从一个分号开始 ✨

面试官:React Diff 算法原理?我:三个假设 + O(n) 复杂度,手撕给你看!

作者 Kincy
2025年7月3日 15:25

🧠 系列前言:

面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 offer!

💬 面试官发问:

"React 的 Virtual DOM diff 算法是怎么实现的?为什么要用 key?三个假设是什么?"

哎呀,这题经典得像老北京炸酱面,每次面试都会遇到。Virtual DOM diff,听起来高大上,其实就是 React 的"比较专家",专门负责找出新旧虚拟 DOM 的差异。

🎯 快答区(初级背诵版)

React 的 diff 算法基于三个假设:同层级比较、不同类型元素会产生不同的树、通过 key 来标识列表元素。算法复杂度从 O(n³) 优化到 O(n),主要通过单层遍历、类型判断和 key 匹配来实现高效更新。

面试官:嗯,还行,但我想听点你背不出来的内容 😏

🧠 深入理解:Diff 算法的江湖传说

📌 1. 为什么需要 Diff 算法?

想象一下,你有一个超大的购物清单,每次修改都要重新写一遍

// 旧清单
const oldList = ['苹果', '香蕉', '橙子', '葡萄'];
// 新清单(只是把香蕉换成了芒果)
const newList = ['苹果', '芒果', '橙子', '葡萄'];

笨方法:撕掉重写 → 浪费纸张 → 环保局找上门
聪明方法:找出差异,只改"香蕉"→"芒果" → 省时省力

React 的 diff 算法就是这个聪明方法,它要在新旧两棵虚拟 DOM 树中找出最小的变化,然后只更新需要变化的部分。

🎯 2. 传统 Diff 的噩梦:O(n³) 复杂度

如果要完美比较两棵树的差异,传统算法需要:

  1. 遍历树 A 的每个节点:O(n)
  2. 遍历树 B 的每个节点:O(n)
  3. 计算编辑距离:O(n)

总复杂度:O(n³)

对于 1000 个节点的应用,就是 10 亿次操作! React 团队一看:这样下去,用户要等到花儿都谢了!

🧠 3. React 的三个"天才假设"

React 团队经过观察发现,实际开发中:

假设 1:同层级比较

不同层级的节点很少会移动

// 这种情况很少发生
<div>
  <span>Hello</span>  // 从这里
</div>
<p>
  <span>Hello</span>  // 移动到这里
</p>

所以 React 只比较同一层级的节点,跨层级直接删除重建

假设 2:不同类型 = 不同树

不同类型的元素会产生不同的树

// 从 div 变成 span
<div>Hello</div>  →  <span>Hello</span>

React 直接删除旧树,创建新树,不做进一步比较!

假设 3:Key 是列表的身份证

开发者可以通过 key 来暗示哪些子元素是稳定的

// 有了 key,React 就知道谁是谁
{items.map(item => 
  <Item key={item.id} data={item} />
)}

基于这三个假设,React 把 O(n³) 优化成了 O(n)

🎢 4. Diff 算法的三大战场

战场1:Tree Diff(树级别)

// 情况1:同类型节点
<div className="old">         <div className="new">
  <span>Hello</span><span>Hello</span>
</div>                        </div>
// 结果:只更新 className

// 情况2:不同类型节点  
<div>Hello</div>  →  <span>Hello</span>
// 结果:删除 div,创建 span

战场2:Component Diff(组件级别)

// 情况1:同类型组件
<MyComponent name="old" />  →  <MyComponent name="new" />
// 结果:更新 props,可能触发重新渲染

// 情况2:不同类型组件
<ComponentA />  →  <ComponentB />
// 结果:卸载 A,挂载 B

战场3:Element Diff(元素级别)

这是最复杂的战场,主要处理列表更新

// 老列表
['A', 'B', 'C', 'D']
// 新列表  
['A', 'C', 'D', 'B']

没有 key 的情况

  • React 按顺序比较
  • B→C,C→D,D→B,最后插入 B
  • 需要 3 次更新 + 1 次插入

有 key 的情况

  • React 知道 B 只是移动了位置
  • 直接移动 B 到末尾
  • 只需要 1 次移动操作

🔍 5. Key 的选择艺术

❌ 错误示范

// 用 index 当 key(面试官最爱问的反面教材)
{items.map((item, index) => 
  <Item key={index} data={item} />
)}

问题:当列表顺序改变时,key 和内容的对应关系就乱了!

✅ 正确示范

// 用稳定的唯一标识当 key
{items.map(item => 
  <Item key={item.id} data={item} />
)}

🎨 6. Diff 算法的核心实现逻辑

function diff(oldVNode, newVNode) {
  // 1. 节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return { type: 'REPLACE', newVNode };
  }
  
  // 2. 文本节点,比较内容
  if (typeof newVNode === 'string') {
    if (oldVNode !== newVNode) {
      return { type: 'TEXT', newVNode };
    }
    return null;
  }
  
  // 3. 同类型元素,比较属性和子节点
  const propsPatches = diffProps(oldVNode.props, newVNode.props);
  const childrenPatches = diffChildren(oldVNode.children, newVNode.children);
  
  if (propsPatches.length || childrenPatches.length) {
    return { type: 'UPDATE', propsPatches, childrenPatches };
  }
  
  return null;
}

🎯 7. 列表 Diff 的核心算法

React 使用了一个聪明的双指针算法

function diffChildren(oldChildren, newChildren) {
  let oldStartIdx = 0, newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 头头比较
    if (sameVnode(oldChildren[oldStartIdx], newChildren[newStartIdx])) {
      // 相同节点,继续比较
      diff(oldChildren[oldStartIdx], newChildren[newStartIdx]);
      oldStartIdx++;
      newStartIdx++;
    }
    // 尾尾比较
    else if (sameVnode(oldChildren[oldEndIdx], newChildren[newEndIdx])) {
      diff(oldChildren[oldEndIdx], newChildren[newEndIdx]);
      oldEndIdx--;
      newEndIdx--;
    }
    // 头尾比较
    else if (sameVnode(oldChildren[oldStartIdx], newChildren[newEndIdx])) {
      // 需要移动节点
      moveNode(oldChildren[oldStartIdx], 'after', oldChildren[oldEndIdx]);
      oldStartIdx++;
      newEndIdx--;
    }
    // 尾头比较
    else if (sameVnode(oldChildren[oldEndIdx], newChildren[newStartIdx])) {
      moveNode(oldChildren[oldEndIdx], 'before', oldChildren[oldStartIdx]);
      oldEndIdx--;
      newStartIdx++;
    }
    // 都不匹配,查找表
    else {
      findAndMove();
    }
  }
  
  // 处理剩余节点
  handleRemainingNodes();
}

💬 面试中可以抛出的装 X 语录

  • "React 的 diff 算法是一种启发式算法,通过合理的假设将复杂度从 O(n³) 降到 O(n)。"
  • "Key 不仅是性能优化的手段,更是帮助 React 理解组件身份的重要线索。"
  • "Same level comparison 是 React diff 的核心思想,体现了工程化中的权衡艺术。"
  • "双指针算法在列表 diff 中的应用,让我看到了算法在实际场景中的优雅实现。"

(说完记得停顿两秒,喝一口水,看面试官点头)

✅ 总结一句话

React Diff 算法 = 基于三个假设的启发式算法,通过同层比较、类型判断和 key 匹配,实现了 O(n) 复杂度的高效 DOM 更新,它是 React 性能优化的核心基石。

🔚 系列结尾:明日继续爆料!

明天继续来一道面试题,咱们聊聊 React Hooks 的原理和闭包陷阱(别看 Hooks 用得溜,原理可能比你想象的更有趣🪝)。

📌 点赞 + 收藏 + 关注系列,让你成为面霸不是梦!

React vs Vue:谁才是轻量级框架的真命天子?

作者 極光未晚
2025年7月3日 15:23

React vs Vue:谁才是轻量级框架的真命天子?

前端江湖的框架之争从未停歇,当团队面临技术选型,Vue 和 React 谁更轻量成为高频争议话题。其实这就像问跑车和越野车谁更快 —— 脱离场景谈结论都是耍流氓,咱们从更多维度掰开揉碎了看!

一、框架核心:代码体积的原始较量

Vue 3 的代码就像精简版瑞士军刀,未压缩的生产环境版本仅约 22.6KB,gzip 压缩后直接 “瘦身” 到 6.4KB 左右 。在 HTML 中引入 Vue 3 简直像请了个轻装上阵的帮手:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Vue示例</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
</head>
<body>
  <div id="app">{{ message }}</div>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          message: 'Hello, Vue!'
        }
      }
    });
    app.mount('#app');
  </script>
</body>
</html>

反观 React,其核心库 React 和 React DOM 合体后,未压缩体积约 100KB,gzip 压缩后仍有 32KB 左右。使用 React 时,除了引入库,还得借助 Babel 处理 JSX 语法:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>React示例</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    ReactDOM.render(
      <div>Hello, React!</div>,
      document.getElementById('root')
    );
  </script>
</body>
</html>

仅从核心库体积看,Vue 确实更轻盈,但这只是冰山一角。

二、运行时刻:性能占用的隐形战场

Vue 的 “聪明更新”

Vue 通过模板静态分析实现 “精准打击”。以下面的 Vue 组件为例:

<template>
  <div>
    <h1>固定标题</h1>
    <p>{{ dynamicText }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      dynamicText: '初始文本'
    }
  },
  methods: {
    updateText() {
      this.dynamicText = '更新后的文本';
    }
  }
}
</script>

当调用updateText方法时,Vue 能识别<h1>是静态内容,只重新渲染<p>标签,大幅减少计算量。

React 的 “严格管控”

React 依靠虚拟 DOM 的 diff 算法,但在复杂组件树中容易触发 “连带反应”。看这个 React 组件示例:

import React, { useState } from'react';
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <ChildComponent />
      <button onClick={() => setCount(count + 1)}>点击计数</button>
    </div>
  );
};
const ChildComponent = () => {
  return <p>我是子组件</p>;
};
export default ParentComponent;

每次点击按钮更新count时,即使ChildComponent与count无关,也可能因父组件重新渲染而触发自身重新渲染,在大型项目中,这种情况可能导致性能损耗。

三、生态依赖:隐形的体积膨胀剂

Vue 的生态像个默契的小团队,官方库 Vue Router、Vuex 与框架无缝衔接。以 Element UI 为例,按需引入按钮组件仅需:

import { Button } from 'element-ui';
export default {
  components: {
    ElButton: Button
  }
}

React 的生态则像个大型集市,引入 Redux 全家桶(Redux、Redux - Thunk、Redux - Persist 等)时,代码量和体积会显著增加。配置 Redux 基本架构如下:

// store.js
import { createStore, applyMiddleware } from'redux';
import thunk from'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);
export default store;

如果依赖管理不当,React 项目很容易 “发福”。

四、跨平台开发:多端适配的能力比拼

Vue 的跨端方案

Vue 通过 uni-app、Taro 等框架实现跨平台开发。以 uni-app 为例,编写一次代码,就能同时发布到微信小程序、H5、APP 等多个平台。比如开发一个简单的计数器应用:

<template>
  <view>
    <text>{{ count }}</text>
    <button @click="increment">+1</button>
  </view>
</template>
<script>
export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};
</script>

这种 “一套代码,多端运行” 的模式,极大减少了开发成本,对于中小团队快速拓展多端业务十分友好。

React 的跨端表现

React 在跨平台领域也有 React Native 和 Expo 等方案。以 React Native 开发移动端应用为例,使用原生组件渲染,能实现接近原生应用的性能:

import React, { useState } from'react';
import { View, Text, Button } from'react-native';
const App = () => {
  const [count, setCount] = useState(0);
  return (
    <View>
      <Text>{count}</Text>
      <Button title="增加" onPress={() => setCount(count + 1)} />
    </View>
  );
};
export default App;

不过,React Native 在环境配置、原生模块集成等方面相对复杂,上手难度较高,但在打造高性能移动端应用时,优势明显。

五、开发者社区:资源与支持的对比

Vue 社区

Vue 社区以 “友好、活跃” 著称,官方文档详细且易于理解,新手指南手把手教学。在技术论坛和问答平台上,关于 Vue 的问题总能快速得到解答。例如在 Vue 的官方论坛,开发者们会分享各种实战经验、插件使用技巧,还有许多优质的开源项目模板可供参考,帮助新手快速成长。

React 社区

React 社区凭借 Facebook 的支持和庞大的开发者群体,资源极其丰富。GitHub 上每天都有大量与 React 相关的项目更新,从复杂的状态管理库到炫酷的动画效果库应有尽有。但由于生态庞大,新手可能会在海量资源中迷失方向,需要花费更多时间筛选适合自己项目的工具和方案。

六、与其他技术栈融合:扩展性的差异

Vue 的融合

Vue 与 TypeScript、Sass 等技术的融合非常自然。例如在 Vue 项目中使用 TypeScript,只需简单配置,就能利用其静态类型检查功能提升代码质量和可维护性:

<template>
  <div>{{ message }}</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  data() {
    return {
      message: 'Hello, Vue with TypeScript!'
    };
  }
});
</script>

React 的融合

React 与 GraphQL、RxJS 等技术结合,能构建强大的数据驱动型应用。以 React 结合 GraphQL 为例,通过 Apollo Client 库,可以轻松实现高效的数据请求和管理:

import React from'react';
import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client';
const client = new ApolloClient({
  uri: 'https://your - graphql - endpoint.com',
  cache: new InMemoryCache()
});
const GET_DATA = gql`
  query {
    // 具体查询语句
  }
`;
const App = () => (
  <ApolloProvider client={client}>
    {/* 应用组件 */}
  </ApolloProvider>
);
export default App;

七、项目场景:轻量性的终极裁判

小型项目用 Vue 就像开电瓶车逛胡同,轻便灵活。假设开发一个简单的待办事项应用,Vue 实现起来轻松惬意:

<template>
  <div>
    <input v-model="newTask" placeholder="添加任务">
    <button @click="addTask">添加</button>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">{{ task }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      newTask: '',
      tasks: []
    }
  },
  methods: {
    addTask() {
      if (this.newTask) {
        this.tasks.push(this.newTask);
        this.newTask = '';
      }
    }
  }
}
</script>

大型企业级项目中,React 则像重型卡车,虽然自身 “吨位” 大,但能通过组件化架构高效运输复杂业务。例如搭建一个电商后台管理系统,通过 React 的组件拆分和状态管理,可以轻松应对海量数据和复杂交互。

Vue 在核心库和部分场景下确实更轻盈,但 React 凭借强大的扩展性和生态,在合理使用时同样能实现轻量高效。技术选型就像相亲,没有绝对完美的框架,只有最适合项目需求和团队基因的选择。下次选型时,不妨带着这些对比思路,亲自试驾一番!

useAsyncState 异步更新数据,在异步回调调用成功后再更新值

2025年7月3日 15:10

场景描述

image.png

当有需要切换状态并同时调用接口的情况下,为了体验更好,希望点击tab的时候并不真正的更新tab的状态,要直到接口调用成功后再更新。

因此就期望有如下代码,让这种异步更新数据的操作更简单明了。

const [activeKey, setActiveKey, activeKeyRef] = useAsyncState('wait', async (data) => {
  await reloadAsync(data)
})

代码

代码不多,就不过多讲解了

function useAsyncState <T>(
  initData: T,
  callback?: (data?: T) => void | Promise<void>
): [
  T,
  (data: T) => Promise<void>,
  React.RefObject<T>
] {
  const [data, setData] = useState<T>(initData)
  const tempRef = useRef<T>(initData)

  const setDataProxy = async (data: T) => {
    tempRef.current = data
    await callback?.(data)
    setData(data)
  }

  return [data, setDataProxy, tempRef]
}

react 基础API

作者 Rubin93
2025年7月3日 15:03

1. useEffect

  1. 浏览器重新绘制之后触发
  2. 参数(setup,dependencies?)
  3. 异步执行
  4. 挂载组件时执行setup,依赖更新时先执行cleanup,再执行setup

2. useLayoutEffect

  1. 浏览器重新绘制之前触发
  2. 参数(setup,dependencies?)
  3. 同步执行,会阻塞DOM渲染
  4. 挂载组件时执行setup,依赖更新时先执行cleanup,再执行setup

3. useReducer

const [state, dispatch] = useReducer(reducerFn, initiaValue, inituaValueFn?)

reducerFn:处理函数,参数:state, action,返回值:新的state
initiaValue:state默认值
inituaValueFn:默认值函数,用于初始化state,优先执行,非必填


const reducerfn = (state, action) => {
if (action==='inc') {
return state++
}
if (action==='dec') {
return state--
}
}
const [state, dispatch] = useReducer(reducerfn, -1, ()=> {
// 高于默认值的优先级
return 0
})

<Button onClick={() => dispatch('inc')}>增加</Button>
<Button onClick={() => dispatch('dec')}>减少</Button>

4. useSyncExternalStore

useSyncExternalStore 用于从外部存储(例如状态管理库、浏览器 API等)获取状态并在组件中同步显示。跟踪外部状态。

useSyncExternalStore 使用场景:

  1. 订阅外部 store 例如(redux,mobx,Zustand,jotai)
  2. 订阅浏览器AP| 例如(online,storage,location, history hash)等

抽离逻辑,编写自定义hooks

const res = useSyncExternalStore(subscribe, getSnapshot, getSeeverSnapShot?)
sybscribe:订阅数据源的变化,接受一个回调函数在数据源更新时调用此函数
getSnapShot:获取当前数据源的快照(当前信息)
getSeeverSnapShot:服务端使用

// hooks/useStorage.tsx
import { useSyncExternalStore } from 'react'
export const useStorage = (key: string, initiaValue: number) => {
const subscribe = (callback: () => void) => {
window.addEventListener('storage', callback)
return () => {
window.removeEventListener('storage', callback)
}
}
const getSnapShot = () => {
return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : initiaValue
}
const data = useSyncExternalStore(subscribe, getSnapShot)
const setData = (value: any) => {
localStorage.setItem(key, JSON.stringify(value))
// 手动触发eventStorage事件,通知订阅
window.dispatchEvent(new Event('storage'))
}
return [data, setData]
}

// views/page.tsx
import { useStorage } from "./hooks/useStorage";
function App() {
  const [token, setToken] = useStorage('token', 0)
  return (
    <>
      <h1>hooks useStorage</h1>
      <div>
        <p> token is {token} </p>
        <button onClick={() => setToken(token + 1)}> token ++ </button>
        <button onClick={() => setToken(token - 1)}> token -- </button>
      </div>
    </>
  )
}
export default App

5. useTransition

useTransition 用于优化用户界面响应性的 Hook,它允许你在不阻塞用户交互的前提下,优雅地处理状态更新带来的加载状态(如“挂起”或“过渡”)

useTransition 作用:

  • 状态更新通常是同步的,调用 setState 会立即重新渲染组件树,可能会导致页面卡顿,特别是在处理复杂计算或大量数据更新时。useTransition 将某些状态更新标记为“非紧急”(过渡态),从而优先处理更紧急的 UI 更新(比如输入框的响应) 。

useTransition 使用场景:

  1. 数据加载时的“pending”状态展示
  2. 页面切换时的平滑过渡
  3. 大型表单提交等场景

6. useDeferredValue

useDeferredValue 用于优化用户界面响应性的 Hook,它允许你在不阻塞渲染的前提下,延迟更新某些非紧急的状态值,从而保持 UI 的流畅性。

useDeferredValue 作用:

  • 延迟使用新的值进行渲染,优先显示旧值,直到浏览器空闲时再更新为新值。

useDeferredValue 使用场景:

  1. 搜索输入框的自动补全
  2. 列表滚动时的平滑更新
  3. 大量数据展示时的渐进式渲染

7. useDeferred 对比 useTransition

特性 useDeferredValue useTransition
控制对象 值(value) 状态更新(state update)
是否需要包装副作用
是否有 isPending 状态
适用场景 延迟更新某个值(如列表、文本) 控制状态更新优先级,显示 loading 状态

8. useImperativeHandle & forwardRef

用于自定义暴露子组件方法或属性给父组件调用的 Hook,它通常与 forwardRef 一起使用。

import { useImperativeHandle, forwardRef, useRef } from "react";

// 子组件,必须配合 forwardRef
const Child_ = forwardRef((props, ref)=>{
  const Child_reff = useRef(null)
  const focusInput = () => { 
    Child_reff.current!.focus()
  }
  const selectInput = () => {
    Child_reff.current!.select()
  }
  const BlurInput = () => {
    Child_reff.current!.blur()
  }
  // 暴露方法给父组件
  useImperativeHandle(ref,() => ({
    focus: focusInput,
    select: selectInput,
    blur: BlurInput,
  }))
  return <input ref={Child_reff} type="text" />
})

// 父组件
function Parent_() {
  const inputRef = useRef(null);
  const handleFocus = () => {
    inputRef.current!.focus(); // 调用子组件方法
  };
  const handleSelect = () => {
    inputRef.current!.select(); // 调用子组件方法
  };
  const handleBlur = () => {
    inputRef.current!.blur(); // 调用子组件方法
  };
  return (
    <>
        <div>
          <h1>hooks useImperativeHandle forwardRef</h1>
          <p> useImperativeHandle  </p>
          <br />
          <Child_ ref={inputRef} />
          <button onClick={handleFocus}>输入框聚焦</button>
          <button onClick={handleSelect}>选中输入框文本</button>
          <button onClick={handleBlur}>输入框失焦</button>
      </div>
    </>
  )
}

export default Parent_

React笔记

作者 JulianNing
2025年7月3日 14:57

React Hooks

1. useState

用于在函数组件中添加状态管理功能。它让函数组件能够拥有自己的内部状态。

(1)基本语法

const [state, setState] = useState(initialValue);
  • state: 当前状态值
  • setState: 更新状态的函数
  • initialValue: 状态的初始值

(2)重要特性

  • 状态更新是异步的
function AsyncExample() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    console.log('点击前:', count); // 0
    setCount(count + 1);
    console.log('点击后:', count); // 仍然是 0,因为状态更新是异步的
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}
  • 函数式更新:当新状态依赖于前一个状态时,需使用函数式更新:
function Counter() {
  const [count, setCount] = useState(0);
  
  const incrementTwice = () => {
    // 错误方式:可能不会按预期工作
    // setCount(count + 1);
    // setCount(count + 1);
    
    // 正确方式:使用函数式更新
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementTwice}>增加2</button>
    </div>
  );
}
  • React 会对多个状态更新进行批处理:
function BatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  const handleClick = () => {
    // 这些更新会被批处理,只触发一次重新渲染
    setCount(c => c + 1);
    setFlag(f => !f);
  };
  
  console.log('渲染'); // 只会打印一次
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>更新</button>
    </div>
  );
}

2. useEffect

用于处理副作用,包括:请求数据、设置订阅、操作 DOM、定时器、清理资源。

(1)基本语法

useEffect(() => {
  console.log('组件挂载或更新');
  return () => {
    console.log('组件卸载或清理');
  };
}, [count]); // 依赖项改变时才执行

第二个参数是依赖数组: 空数组 []时,只在挂载时执行一次;有依赖项时,依赖项变化时执行;无依赖数组时,每次渲染都执行。

(2)在return 中,可以取消请求、清理定时器、移除事件监听器

// 取消请求
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    
    const abortController = new AbortController();
    
    const searchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/search?q=${query}`, {
          signal: abortController.signal
        });
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('搜索失败:', error);
        }
      } finally {
        setLoading(false);
      }
    };
    
    searchData();
    
    // 清理函数:取消请求
    return () => {
      abortController.abort();
    };
  }, [query]);
  
  return (
    <div>
      {loading && <div>搜索中...</div>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

3. useContext

用于在组件树中跨层级传递数据,避免了逐层传递 props的问题。能够在不通过 props 的情况下,将数据传递给深层嵌套的组件。

  • Context: 上下文对象,用于存储共享数据
  • Provider: 提供者组件,用于提供数据
  • Consumer: 消费者组件,用于使用数据
import React, { createContext, useContext, useState } from 'react';

// 1. 创建 Context
const ThemeContext = createContext();

// 2. 创建 Provider 组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. 在子组件中使用 Context
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <header style={{ 
      backgroundColor: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#333' : '#fff'
    }}>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>
        切换到 {theme === 'light' ? '深色' : '浅色'} 模式
      </button>
    </header>
  );
}

function Content() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <main style={{ 
      backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
      color: theme === 'light' ? '#333' : '#fff',
      padding: '20px'
    }}>
      <p>当前主题: {theme}</p>
    </main>
  );
}

// 4. 在应用中使用
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
}

4. useRef

用于获取 DOM 节点或保存可变值,useRef 返回一个对象,该对象有一个 current 属性,可以通过修改 current 来存储任何值。与 useState 不同的是,修改 useRef 的值不会触发组件重新渲染。

function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

5. useMemo

可以缓存计算结果,只有在依赖项发生变化时才会重新计算。

(1)基本语法

import { useMemo } from 'react';

const memoizedValue = useMemo(() => {
  return expensiveCalculation(a, b);
}, [a, b]);

useMemo 接收两个参数:

  • 计算函数:返回需要缓存的值
  • 依赖数组:当数组中的值发生变化时,才会重新执行计算函数

6. useCallback

用于缓存函数,返回一个记忆化的回调函数,只有在依赖项发生变化时才会重新创建函数。

import { useCallback } from 'react';

const memoizedCallback = useCallback(() => {
  // 函数逻辑
}, [dependency1, dependency2]);

useCallback 接收两个参数:

  • 回调函数:需要缓存的函数
  • 依赖数组:当数组中的值发生变化时,才会重新创建函数

7. useReducer

useState 的替代方案,特别适用于复杂的状态逻辑管理。

import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState);
function Counter() {
  const initialState = { count: 0 };
  
  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      case 'reset':
        return initialState;
      default:
        throw new Error();
    }
  }
  
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}

从XHR到Fetch:Promise封装Ajax的奇幻之旅

作者 FogLetter
2025年7月3日 14:51

大家好,我是你们的前端小伙伴Fogletter!今天我要和大家分享一个前端开发中非常经典的话题——Ajax请求的演进史,以及如何用Promise封装传统的XHR对象来模拟现代Fetch API的效果。

一、Ajax:前端开发的里程碑

还记得2005年,Google在Gmail和Google Maps中大规模使用Ajax技术时,整个Web开发界为之震撼的场景吗?Ajax(Asynchronous JavaScript and XML)彻底改变了Web应用的交互方式,让我们告别了整页刷新的时代。

1.1 传统XHR的"原始社会"

在ES6之前,我们只能使用XMLHttpRequest对象(简称XHR)来进行异步请求。看看这段"考古代码":

const xhr = new XMLHttpRequest(); // 实例化
xhr.open('GET', 'https://api.github.com/users/fogletter/repos');
xhr.send(); // 发送请求

xhr.onreadystatechange = function() {
    if(xhr.readyState == 4){
        const data = JSON.parse(xhr.responseText);
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    }
}

这段代码有几个痛点:

  1. 回调地狱:当有多个依赖请求时,代码会形成金字塔形状
  2. 状态管理:需要手动检查readyState
  3. 错误处理:需要额外代码处理网络错误

1.2 readyState的五个阶段

XHR对象的状态变化很有意思,它经历了五个阶段:

  • 0 (UNSENT): 代理被创建,但尚未调用 open() 方法
  • 1 (OPENED): open() 方法已经被调用
  • 2 (HEADERS_RECEIVED): send() 方法已经被调用,并且头部和状态已经可获得
  • 3 (LOADING): 下载中;responseText 属性已经包含部分数据
  • 4 (DONE): 下载操作已完成

我们通常只关心阶段4,也就是请求完成的时候。

二、Promise:异步编程的救星

随着前端应用越来越复杂,回调地狱问题日益严重。ES6引入的Promise成为了解决这一问题的利器。

2.1 Promise的三种状态

Promise对象代表一个异步操作的最终完成(或失败)及其结果值。它有三种状态:

  1. pending: 初始状态,既不是成功,也不是失败
  2. fulfilled: 意味着操作成功完成
  3. rejected: 意味着操作失败

2.2 用Promise封装XHR

让我们用Promise来改造传统的XHR:

const getJSON = (url) => {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.send();
        xhr.onreadystatechange = function() {
            if(xhr.readyState == 4){
                if(xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject(new Error(xhr.statusText));
                }
            }
        }
        xhr.onerror = function() {
            reject(new Error('Network Error'));
        }
    })
}

这样封装后,我们就可以像这样使用:

getJSON('https://api.github.com/users/fogletter/repos')
    .then(data => {
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    })
    .catch(error => {
        console.error('请求失败:', error);
    });

是不是清爽多了?这种链式调用的方式让代码更加线性化,易于理解和维护。

三、Fetch API:现代浏览器的原生支持

ES6不仅带来了Promise,还引入了更现代的Fetch API。Fetch基于Promise设计,提供了更强大、更灵活的功能。

3.1 Fetch的基本用法

fetch('https://api.github.com/users/fogletter/repos')
    .then(res => res.json())
    .then(data => {
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    });

Fetch API的优点:

  1. 语法简洁,更符合现代JavaScript风格
  2. 内置Promise支持,无需额外封装
  3. 提供了Request和Response对象,功能更强大
  4. 默认不会接收或发送cookies,安全性更好

3.2 结合async/await使用

ES8引入的async/await语法让异步代码看起来像同步代码一样直观:

document.addEventListener('DOMContentLoaded', async() => {
    try {
        const result = await fetch('https://api.github.com/users/fogletter/repos');
        const data = await result.json();
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    } catch (error) {
        console.error('请求失败:', error);
    }
});

这种写法几乎消除了所有回调,代码可读性大大提高。

四、为什么还要学习XHR?

虽然Fetch API已经很优秀了,但学习XHR和Promise封装仍然很有必要:

  1. 兼容性考虑:一些老旧项目或浏览器可能需要XHR
  2. 理解底层原理:了解XHR有助于深入理解网络请求机制
  3. 特殊需求:如上传进度监控等,Fetch API支持还不够完善
  4. 面试必备:很多面试官喜欢考察对底层原理的理解

八、最佳实践建议

  1. 现代项目优先使用Fetch API:语法简洁,功能强大
  2. 需要兼容性时使用Promise封装XHR:保证代码风格一致
  3. 始终处理错误:不要忽略.catch或try/catch
  4. 合理设置超时:避免请求长时间挂起
  5. 考虑使用拦截器:统一处理请求和响应

九、总结

从前端的异步请求发展史中,我们可以看到JavaScript语言的不断进化:

  1. XHR时代:回调地狱,手动管理状态
  2. Promise封装:链式调用,代码更清晰
  3. Fetch API:原生Promise支持,语法更现代
  4. async/await:同步写法,异步效果

理解这个演进过程不仅能帮助我们写出更好的代码,还能在面试中展现出对前端技术的深刻理解。记住,技术总是在不断进步的,今天的Fetch API也许明天就会被更优秀的方案取代,但核心的异步编程思想是不变的。

希望这篇笔记能帮助大家更好地理解Ajax和Promise封装!如果有任何问题,欢迎在评论区留言讨论。

微软再裁 9000 人,白领「大屠杀」来袭:不用 AI 要被裁,用了 AI 也被裁

作者 张子豪
2025年7月3日 14:17

AI 裁员潮已经有了苗头。

今天微软被曝确认了年内新一轮裁员计划,预计将影响约 9000 个工作岗位,占其全球员工总数的 4%。这是微软今年宣布的第二次大规模裁员,也是其在 18 个月内的第四次大规模人员调整。

与此同时,微软正在要求部分管理者根据员工在内部使用 AI 工具的情况来评估其工作表现,并考虑在下一财年的绩效考核中,正式加入与 AI 使用相关的考核指标。

一边裁员,一边强制留任员工提升 AI 使用效率,正在成为硅谷科技巨头的标准动作。

18 个月内的第 4 次裁员

微软发言人表示,此次裁员涉及不同部门、地区以及各个经验层级的员工,其中微软的 Xbox 部门(微软游戏部门)受到的影响较大 。

这并非微软今年首次大规模裁员。早在今年 5 月,微软就已宣布在全球范围内裁员约 6000 人,约占其员工总数的 3% 。根据微软向华盛顿州就业官员提交的通知,6 月微软还在华盛顿州雷德蒙德总部裁员 300 人,5 月在普吉特海湾地区裁员近 2000 人 。

更详细的数据显示,2024 年 1 月,微软裁减了 1900 名 Activision Blizzard 和 Xbox 员工,随后在 5 月关闭了多个游戏工作室并进行了裁员,6 月又有 1000 名 HoloLens 和 Azure 云团队的员工被裁 。作为收购 Activision Blizzard 后重组的一部分,微软在 9 月再次裁员 650 名 Xbox 员工

彭博社报道,现为微软旗下的 King 部门——《糖果传奇》的开发团队,正在裁员约 10%, 200 人左右 。微软还在其 Forza Motorsport 工作室 Turn 10 裁员 70 余人,并取消了《完美黑暗》和《永野》两款游戏。负责《完美黑暗》的工作室 The Initiative 也将作为此次裁员的一部分关闭 。

Xbox 负责人 Phil Spencer 在给团队的内部备忘录中表示:

为了确保游戏业务的长期成功,并专注于战略性增长领域,我们将结束或缩减部分业务,同时借鉴微软的做法,精简管理层级以提升灵活性和工作效率。

 

我意识到这些变化发生在我们拥有比以往更多的玩家、游戏和游戏时长的时候。我们的平台、硬件和游戏路线图从未如此强大。我们目前看到的成功是基于我们过去做出的艰难决定。

当 AI 成为硅谷大厂的考核指标

在裁员的同时,微软对留任员工的考核标准也在悄然改变。

Business Insider 获悉,微软正在要求部分经理根据员工在内部使用人工智能的情况进行评估,考虑在绩效评审中加入相关的考核指标。

▲微软开发者部门总裁 Julia Liuson

这一变化的核心推动者是微软开发者部门总裁 Julia Liuson,她负责包括 AI 编码服务 GitHub Copilot 在内的开发者工具。Liuson 最近发出邮件,要求各位经理根据员工使用内部 AI 工具的情况来评估他们的工作表现。

在这封邮件中,Liuson 用了一个极为明确的表述:「人工智能已经成为必需品,不能再选择不使用」。她进一步阐述道:「人工智能如今已成为我们工作方式的基础组成部分。正如协作、数据驱动的思维和有效沟通一样,使用人工智能已不再是可选项,而是每个岗位和每个层级的核心要素」。

Liuson 明确告诉各位经理,人工智能「应当纳入你们对员工绩效和影响的全面评估中」。这意味着员工的 AI 使用情况将直接影响他们的绩效评分和职业发展。

据一位知情人士透露,微软的绩效标准因团队不同而有所差异,一些团队正考虑在下一财年的绩效考核中正式纳入使用内部人工智能工具的相关指标。

据另外两位知情人士透露,这些变动旨在解决微软内部对其 Copilot AI 服务采用率偏低的问题 。公司不仅希望大幅提升使用率,也希望负责开发这些产品的员工能更深入地了解这些工具 。

这一策略的紧迫性还来自于竞争压力。在 Liuson 的团队里,GitHub Copilot 正受到包括 Cursor 在内的多款 AI 编码服务的激烈竞争 。

亚马逊 CEO :「更少的人」与「更多的 AI」

微软的战略调整并非个例。亚马逊 CEO Andy Jassy 在近期发给全体员工的一封内部信中,用前所未有的坦诚态度,详细阐述了生成式 AI 将如何重塑公司结构。

▲ 亚马逊 CEO Andy Jassy

Jassy 在信中写道:

目前,我们已有超过 1000 个生成式人工智能服务和应用正在开发或已完成,但以我们的规模来看,这只是未来将打造的冰山一角。接下来的几个月里,我们会加大投入,简化代理的构建流程,并在各业务部门及行政管理领域推出(或合作开发)多个新代理。

 

随着我们推广更多生成式人工智能和智能代理,工作方式将发生改变。一些现有岗位所需的人数会减少,而其他类型的岗位则需要更多人。虽然具体影响难以预测,但未来几年内,随着公司广泛应用人工智能提升效率,我们预计整体员工规模将有所缩减。

 

▲ X 截图,亚马逊  CEO 公开信消息

他这封公开信几乎是另一种形式的「警告」,要员工主动适应这一变化:「那些拥抱变革、熟悉人工智能的人,将有机会产生深远影响,助力我们重塑公司」。

在这场正在发生的「白领大屠杀」,硅谷高管们手中的「屠刀」,就是正在冉冉升起的 AI 公司。

▲Anthropic 公司 CEO Dario Amodei

Anthropic 公司 CEO Dario Amodei 在接受 Axios 采访时预测,AI 可能在未来 5 年内淘汰一半的入门级白领职位,失业率将上升到 10% 至 20% 。

他直言不讳地指出,技术、金融、法律、咨询等白领行业,尤其是初级岗位,将面临大规模裁员。

Amodei 认为,企业对 AI 的使用正在从「增强」人类工作,快速转向「自动化」并直接承担工作本身 。他指出,包括 OpenAI、Google 和 Anthropic 在内的大型 AI 公司,都在竞相开发能够以极低成本完成人类工作的「智能代理」。

更令人担忧的是,Amodei 表示,这种集体性的威胁正被普遍忽视。公众「还没有意识到这件事即将发生」,「这听起来很不可思议,人们根本不相信」。

AI 裁员潮中的白领

实际上类似的事今年已经屡见不鲜。

沃尔玛正在裁减 1500 个企业职位,为即将到来的重大转变简化运营;网络安全公司 CrowdStrike 削减了 500 个职位或 5% 的员工,理由是「市场和技术拐点,AI 正在重塑每个行业」。

▲ 自2020年2月1日起,Indeed 上的职位发布数量变化,Indeed 为全球知名求职网站。

扎克伯格曾公开表示,中级程序员很快将变得不必要,可能在今年内实现 。他指出,到 2025 年,Meta 及其他公司将拥有能有效扮演「公司中级工程师」角色的 AI,从而减少对人类程序员的需求。此后,Meta 便宣布裁员 5%。

AGI 的宏大叙事还是蓝图,但 AI 带来的变化已经落在了具体的个体身上。

也许没有哪个行业比科技行业受到的冲击更大,互联网论坛上充满了员工们分享自己已经被裁员,或者在担心什么时候会被裁员的消息 。

21 年经验的工程师:从 AI 提效到被 AI 替代

软件工程师 Shawn K 有着 21 年的行业经验,年薪 15 万美元 。2024 年 3 月,42 岁的他在 FrameVR.io 担任全栈工程师,公司鼓励员工使用 ChatGPT,团队生产力也因此大幅提升 。

一个月后,他被裁员了 。

他在 Substack 上分享了自己因人工智能接管公司而被裁员的经历,这篇帖子现已广泛传播,标题为:「大规模岗位替代已经开始」。

我们一直在将公司转型为人工智能方向,在整个软件中加入人工智能功能,努力为客户利用人工智能创造价值。就在完成这次重组和战略调整后不久……我被裁员了

失业后的生活异常艰难。他有两笔房贷需要偿还,于是开始在纽约中部家附近通过 Door Dash 做送餐工作,勉强维持生活 8。在投出近 800 份求职申请、坚持了一年多后,他终于在本月初拿到了一份合同工作 。

「我尝试了很多方法,能想到的都试过了。在过去一年里,我降低了申请职位的标准,也降低了考虑工作的门槛」,他说。「到了某个时候,情况变成了你需要立即拿到现金来维持基本的吃饭和付账单」。

尽管如此,K 依然对 AI 保持着复杂的态度:「人工智能比我更擅长编程,但这并不代表我没有价值。我觉得这反而让我能做的事情增加了 100 倍,还能解决以前根本不会尝试的更复杂的问题」。

但他对未来的判断却十分悲观:「我真的相信,凡是整天在电脑上完成工作的职位都将消失,这只是时间早晚的问题」

HR:从晋升轨道到被自动化替代

另一位化名为「简」的人力资源专员,则亲眼见证了自己被替代的全过程。人工智能对工作的威胁常被提及,但当她的人力资源岗位被自动化取代并于一月被裁员时,这一威胁才真正变得令人震惊和切实。

她在公司负责福利管理已有两年,正处于晋升的轨道上。她注意到老板在搭建人工智能基础设施,但并不认为自己年薪约 7 万美元的职位会受到影响。

「我以为自己投入了大量时间,在高层次的工作中表现出色,老板会看重我」,这位 45 岁的湾区居民在接受《独立报》采访时谈到她的前雇主 。「结果,一旦他找到自动化替代的方法,就立刻用了,然后就把我辞退了」。

更糟糕的是,当前的经济形势让找工作变得异常艰难。二月份,她的一次电话面试是由一套人工智能系统进行的 。「这感觉就像是在和自动语音信箱面试」,她说「机器人」问了她一些关于自己的问题,但回答都很泛泛,让她觉得这项技术无法帮助她进入下一轮 。

Dario Amodei 所预言的「白领大屠杀」似乎在按照既定路径展开,一条清晰的逻辑链条正在浮现:以 AI 提升效率,以效率为名精简人员,这正在成为硅谷新一轮技术变革下无法回避的现实。

「简」现在已经找到新工作,她依然表示:「现在的情况是白领职位大幅减少,我觉得很多工作正在消失」。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


CSS 样式计算与视觉格式化模型详解

2025年7月3日 11:19

前端渲染基础:CSS 样式计算与视觉格式化模型详解

一、CSS 样式计算:从规则到实际效果

CSS 样式计算是浏览器将 CSS 规则应用到 DOM 元素,计算出每个元素最终样式的过程。这个过程看似简单,实则涉及复杂的算法和优先级规则。

1. 收集样式规则

浏览器首先需要收集所有相关的 CSS 规则,这些规则来源包括:

  • 外部样式表(通过引入)

  • 内部样式表(位于标签中)

  • 行内样式(直接写在 HTML 元素的 style 属性中)

  • 浏览器默认样式(User Agent Stylesheet)

浏览器会构建一个样式表集合,并将这些规则解析为内部数据结构(通常是哈希表或树)以便快速查找。例如:

css

/* 外部样式表 */
body { font-family: Arial; }
.container { width: 960px; }

/* 内部样式表 */
<style>
  .header { background-color: #f5f5f5; }
</style>

/* 行内样式 */
<div class="container" style="margin: 0 auto;">内容</div>
2. 层叠与优先级

当多个 CSS 规则应用到同一个元素时,浏览器需要通过层叠(Cascade)机制决定最终应用哪些样式。优先级由以下因素决定(从高到低):

  1. !important 声明

  2. 行内样式

  3. ID 选择器

  4. 类选择器、属性选择器、伪类

  5. 元素选择器、伪元素

  6. 通配符选择器

  7. 继承的样式

优先级计算可以用一个简单的公式表示:

plaintext

!important > 行内样式 > (ID数量, 类数量, 元素数量)

例如:

css

/* 优先级: 0,0,1 */
p { color: red; }

/* 优先级: 0,1,0 */
.text-danger { color: blue; }

/* 优先级: 1,0,0 */
#special-paragraph { color: green; }
3. 继承与默认值

有些 CSS 属性会自动从父元素继承值,这称为继承(Inheritance)。例如:

  • color

  • font-family

  • font-size

  • text-align

而有些属性则不会继承,例如:

  • width

  • height

  • margin

  • padding

  • border

当没有显式指定值时,浏览器会使用属性的默认值。例如,display属性的默认值是inline,而position的默认值是static

二、视觉格式化模型:从样式到布局

视觉格式化模型是浏览器根据计算出的样式,将元素转换为实际屏幕上可见的盒子(Box)的过程。

1. 盒模型(Box Model)

盒模型是 CSS 布局的基础,每个元素都被视为一个矩形盒子,由内容区(content)、内边距(padding)、边框(border)和外边距(margin)组成。

plaintext

+---------------------+
|      margin         |
|  +---------------+  |
|  |    border     |  |
|  |  +---------+  |  |
|  |  | padding |  |  |
|  |  | +-----+ |  |  |
|  |  | |content| |  |  |
|  |  | +-----+ |  |  |
|  |  +---------+  |  |
|  +---------------+  |
+---------------------+

盒模型的宽度和高度计算方式:

plaintext

总宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
总高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

可以通过box-sizing属性修改盒模型的计算方式:

css

/* 标准盒模型(默认值) */
.box { box-sizing: content-box; }

/* 怪异盒模型(宽度包含padding和border) */
.box { box-sizing: border-box; }
2. 布局模式

CSS 提供了多种布局模式,用于控制元素在页面中的排列方式:

  • 块级布局(Block Layout) :元素按垂直方向排列,每个块级元素独占一行

  • 行内布局(Inline Layout) :元素按水平方向排列,不会换行

  • 表格布局(Table Layout) :元素按照表格结构排列

  • 弹性布局(Flexbox) :用于一维布局,提供灵活的对齐和分布能力

  • 网格布局(Grid) :用于二维布局,可以同时控制行和列

  • 浮动布局(Float) :元素脱离正常流,向左或向右浮动

例如,使用 Flexbox 布局:

css

.container {
  display: flex;
  justify-content: center;
  align-items: center;
}
3. 格式化上下文(Formatting Context)

格式化上下文是一个独立的渲染区域,规定了内部元素如何布局,并且与外部元素相互隔离。常见的格式化上下文包括:

  • 块级格式化上下文(BFC) :由浮动元素、绝对定位元素、行内块元素等创建

  • 行内格式化上下文(IFC) :由行内元素创建

  • 网格格式化上下文(GFC) :由 display: grid 创建

  • 弹性格式化上下文(FFC) :由 display: flex 创建

BFC 的主要作用是清除浮动,防止 margin 重叠等。创建 BFC 的常见方式:

css

.element {
  float: left; /* 浮动元素 */
  overflow: hidden; /* 触发BFC */
  display: inline-block; /* 行内块元素 */
  position: absolute; /* 绝对定位元素 */
}
4. 层叠上下文(Stacking Context)

层叠上下文决定了元素在 Z 轴上的显示顺序,即哪些元素会覆盖在其他元素之上。层叠上下文由以下条件创建:

  • 根元素(HTML)

  • position 值为 absolute 或 relative 且 z-index 不为 auto

  • position 值为 fixed 或 sticky

  • display 值为 flex 或 grid 的子元素且 z-index 不为 auto

  • opacity 值小于 1 的元素

层叠顺序从低到高:

  1. 背景和边框(层叠上下文的背景和边框)

  2. 负 z-index 值的子元素

  3. 块级子元素

  4. 浮动子元素

  5. 行内子元素

  6. z-index: 0 的子元素

  7. 正 z-index 值的子元素

例如:

css

.container {
  position: relative;
  z-index: 1; /* 创建层叠上下文 */
}

.overlay {
  position: absolute;
  z-index: 2; /* 显示在container之上 */
}

总结

CSS 样式计算和视觉格式化模型是前端开发的核心基础知识,深入理解这些概念对于解决布局难题、优化渲染性能至关重要。通过掌握样式规则的收集、层叠优先级、盒模型、布局模式、格式化上下文和层叠上下文,开发者能够更精确地控制页面的外观和行为,避免常见的布局陷阱。

扩写一些

前端渲染基础:CSS 样式计算与视觉格式化模型详解(扩展版)

一、CSS 样式计算:从规则到实际效果

CSS 样式计算是浏览器将 CSS 规则应用到 DOM 元素,计算出每个元素最终样式的过程。这个过程看似简单,实则涉及复杂的算法和优先级规则。

1. 收集样式规则

浏览器首先需要收集所有相关的 CSS 规则,这些规则来源包括:

  • 外部样式表(通过引入)

  • 内部样式表(位于标签中)

  • 行内样式(直接写在 HTML 元素的 style 属性中)

  • 浏览器默认样式(User Agent Stylesheet)

  • 用户自定义样式(浏览器扩展或用户偏好设置)

浏览器会构建一个样式表集合,并将这些规则解析为内部数据结构(通常是哈希表或树)以便快速查找。值得注意的是,不同来源的样式表加载顺序也会影响最终的样式计算,特别是当存在相同优先级的规则时。

css

/* 外部样式表 */
body { font-family: Arial; }
.container { width: 960px; }

/* 内部样式表 */
<style>
  .header { background-color: #f5f5f5; }
</style>

/* 行内样式 */
<div class="container" style="margin: 0 auto;">内容</div>

现代浏览器在解析样式表时会进行优化,例如并行下载多个外部样式表,以及使用样式表缓存机制提高重复加载时的性能。

2. 层叠与优先级

当多个 CSS 规则应用到同一个元素时,浏览器需要通过层叠(Cascade)机制决定最终应用哪些样式。优先级由以下因素决定(从高到低):

  1. !important 声明

  2. 行内样式

  3. ID 选择器

  4. 类选择器、属性选择器、伪类

  5. 元素选择器、伪元素

  6. 通配符选择器

  7. 继承的样式

优先级计算可以用一个简单的公式表示:

plaintext

!important > 行内样式 > (ID数量, 类数量, 元素数量)

例如:

css

/* 优先级: 0,0,1 */
p { color: red; }

/* 优先级: 0,1,0 */
.text-danger { color: blue; }

/* 优先级: 1,0,0 */
#special-paragraph { color: green; }

特殊情况说明

  • 当两条规则优先级相同时,后定义的规则会覆盖先定义的规则
  • !important 声明会覆盖任何优先级规则,但应谨慎使用,过度使用会导致样式难以维护
  • 继承的样式优先级最低,即使父元素的样式优先级很高
3. 继承与默认值

有些 CSS 属性会自动从父元素继承值,这称为继承(Inheritance)。理解哪些属性会继承,哪些不会继承,对于编写高效的 CSS 代码至关重要。

常见继承属性

  • color

  • font-family

  • font-size

  • font-weight

  • text-align

  • line-height

  • letter-spacing

常见非继承属性

  • width

  • height

  • margin

  • padding

  • border

  • background

  • position

  • display

当没有显式指定值时,浏览器会使用属性的默认值。默认值由 CSS 规范定义,但不同浏览器可能存在细微差异。例如,display属性的默认值是inline,而position的默认值是static

二、视觉格式化模型:从样式到布局

视觉格式化模型是浏览器根据计算出的样式,将元素转换为实际屏幕上可见的盒子(Box)的过程。

1. 盒模型(Box Model)

盒模型是 CSS 布局的基础,每个元素都被视为一个矩形盒子,由内容区(content)、内边距(padding)、边框(border)和外边距(margin)组成。

plaintext

+---------------------+
|      margin         |
|  +---------------+  |
|  |    border     |  |
|  |  +---------+  |  |
|  |  | padding |  |  |
|  |  | +-----+ |  |  |
|  |  | |content| |  |  |
|  |  | +-----+ |  |  |
|  |  +---------+  |  |
|  +---------------+  |
+---------------------+

盒模型的宽度和高度计算方式:

plaintext

总宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
总高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

可以通过box-sizing属性修改盒模型的计算方式:

css

/* 标准盒模型(默认值) */
.box { box-sizing: content-box; }

/* 怪异盒模型(宽度包含padding和border) */
.box { box-sizing: border-box; }

外边距折叠(Margin Collapsing)

  • 相邻的块级元素之间的垂直外边距会发生折叠,取两者中的较大值
  • 父子元素之间如果没有边框、内边距、行内内容或 clear 分隔,垂直外边距也会发生折叠
  • 浮动元素、绝对定位元素、行内块元素等不会发生外边距折叠
2. 布局模式

CSS 提供了多种布局模式,用于控制元素在页面中的排列方式:

  • 块级布局(Block Layout) :元素按垂直方向排列,每个块级元素独占一行

  • 行内布局(Inline Layout) :元素按水平方向排列,不会换行

  • 表格布局(Table Layout) :元素按照表格结构排列

  • 浮动布局(Float) :元素脱离正常流,向左或向右浮动

  • 弹性布局(Flexbox) :用于一维布局,提供灵活的对齐和分布能力

  • 网格布局(Grid) :用于二维布局,可以同时控制行和列

  • 定位布局(Positioning) :通过 position 属性精确定位元素

Flexbox 布局示例

css

.container {
  display: flex;
  flex-direction: row; /* 主轴方向 */
  justify-content: space-between; /* 主轴对齐方式 */
  align-items: center; /* 交叉轴对齐方式 */
  flex-wrap: wrap; /* 换行设置 */
}

.item {
  flex: 1 1 200px; /* 灵活增长、收缩和基准尺寸 */
}

Grid 布局示例

css

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 三列,每列等宽 */
  grid-template-rows: auto; /* 行高自动 */
  gap: 20px; /* 行列间距 */
}

.item {
  grid-column: span 1; /* 跨越1列 */
  grid-row: span 1; /* 跨越1行 */
}
3. 格式化上下文(Formatting Context)

格式化上下文是一个独立的渲染区域,规定了内部元素如何布局,并且与外部元素相互隔离。常见的格式化上下文包括:

  • 块级格式化上下文(BFC) :由浮动元素、绝对定位元素、行内块元素等创建

  • 行内格式化上下文(IFC) :由行内元素创建

  • 网格格式化上下文(GFC) :由 display: grid 创建

  • 弹性格式化上下文(FFC) :由 display: flex 创建

BFC 的主要作用是清除浮动,防止 margin 重叠等。创建 BFC 的常见方式:

css

.element {
  float: left; /* 浮动元素 */
  overflow: hidden; /* 触发BFC */
  display: inline-block; /* 行内块元素 */
  position: absolute; /* 绝对定位元素 */
  display: table-cell; /* 表格单元格 */
  display: flex; /* Flex容器 */
  display: grid; /* Grid容器 */
}

IFC 的特性

  • 行内元素会在一行内水平排列,直到一行排满换行
  • 行内元素的垂直对齐由 vertical-align 属性控制
  • 行内格式化上下文的高度由行高 (line-height) 决定
4. 层叠上下文(Stacking Context)

层叠上下文决定了元素在 Z 轴上的显示顺序,即哪些元素会覆盖在其他元素之上。层叠上下文由以下条件创建:

  • 根元素(HTML)

  • position 值为 absolute 或 relative 且 z-index 不为 auto

  • position 值为 fixed 或 sticky

  • display 值为 flex 或 grid 的子元素且 z-index 不为 auto

  • opacity 值小于 1 的元素

  • transform 值不为 none 的元素

  • mix-blend-mode 值不为 normal 的元素

层叠顺序从低到高:

  1. 背景和边框(层叠上下文的背景和边框)

  2. 负 z-index 值的子元素

  3. 块级子元素

  4. 浮动子元素

  5. 行内子元素

  6. z-index: 0 的子元素

  7. 正 z-index 值的子元素

示例代码

css

.container {
  position: relative;
  z-index: 1; /* 创建层叠上下文 */
  opacity: 0.9; /* 也会创建层叠上下文 */
}

.overlay {
  position: absolute;
  z-index: 2; /* 显示在container之上 */
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
}

三、实际应用中的常见问题与解决方案

1. 浮动元素导致的父容器高度塌陷

问题:当子元素设置为 float:left 或 float:right 时,父容器会失去高度,无法包裹子元素。

解决方案

  • 使用 clearfix 方法:

    css

    .clearfix::after {
      content: "";
      display: block;
      clear: both;
    }
    
  • 让父容器成为 BFC:

    css

    .parent {
      overflow: hidden; /* 触发BFC */
    }
    
2. 垂直居中难题

解决方案

  • Flexbox 方案

    css

    .parent {
      display: flex;
      justify-content: center; /* 水平居中 */
      align-items: center; /* 垂直居中 */
    }
    
  • Grid 方案

    css

    .parent {
      display: grid;
      place-items: center; /* 水平和垂直居中 */
    }
    
  • 绝对定位 + transform 方案

    css

    .child {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
    
3. 响应式布局实现

方法

  • 使用媒体查询(Media Queries):

    css

    @media (max-width: 768px) {
      .container {
        width: 100%;
      }
    }
    
  • 使用弹性布局(Flexbox)和网格布局(Grid):

    css

    .container {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    }
    
  • 使用 viewport 单位:

    css

    .hero-title {
      font-size: 5vw; /* 相对于视口宽度的5% */
    }
    

四、性能优化考虑

  1. 减少重排(Reflow)和重绘(Repaint)

    • 批量修改 DOM 样式
    • 使用requestAnimationFrame处理动画
    • 避免频繁读取和修改布局信息
  2. 合理使用层叠上下文

    • 避免过度使用 z-index
    • 为动画元素创建独立层叠上下文
  3. 优化样式选择器

    • 避免深层嵌套选择器
    • 使用类选择器代替元素选择器组合

vite和webpack打包lib库细节

作者 gnip
2025年7月3日 10:49

概述

常见的组件库,业务工程项目,都会用到各式各样的npm包,打包的格式也很多元,比如umd,cjs,es等,不同打包格式,适合不同环境不同导入方式,以下是关于现在主流webpack和vite这两个环境打包的配置信息,如果自己要写npm包给别人用,打包配置必不可少。

webpack配置

webpack.config.js配置如下, 依赖如下:

  • webpack
  • webpack-cli
  • vue-loader
  • clean-webpack-plugin
  • ts-loader
  • style-loader
  • css-loader
  • postcss-loader
  • sass-loader/less-loader(根据情况定)

如下配置会将所有资源输出到一份文件中,如果需要将资源分块输出,可以参考webpack分解到不同文件配置

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');

//注意:wenbapack单入口输出多文件,需要数组形式,CleanWebpackPlugin关闭,不然会覆盖其他生成的文件
const baseConfig = {
  mode: 'production',
  entry: path.resolve(__dirname, './src/index.js'),
  resolve: {
    extensions: ['.ts', '.js', '.vue', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          reactivityTransform: true // 可选:启用 Vue 3 响应性语法糖
        }
      },
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/],
          transpileOnly: true,
          // 使用项目中的 tsconfig.json
          configFile: path.resolve(__dirname, 'tsconfig.json')
        },
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [
         'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.scss$/,
        use: [
         'style-loader',
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      },
      {
        test: /\.(png|jpe?g|gif|svg|webp)$/,
        type: 'asset/inline',
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/inline',
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    // new CleanWebpackPlugin(),
  ],
  optimization: {
    minimize: true,
    splitChunks: false // 禁用代码分割,因为我们打包的是库
  },
  performance: {
    hints: false,
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  }
};
module.exports = (env, argv) => {
  return [
    {
      ...baseConfig,
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'index.umd.js',
        library: {
          name: ['myNamespace','MyComponent'],
          type: 'umd',
        },
        umdNamedDefine: false,
  
      },
    },
    {
      ...baseConfig,
      experiments: {
        outputModule: true // 启用实验性 ESM 输出支持
          },
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'index.es.js',
  
        library: {
          type: 'module',
        },
        umdNamedDefine: false,
  
      },
    },
    {
      ...baseConfig,
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'index.cjs',
        library: {
          name: ['myNamespace','MyComponent'],
          type: 'commonjs',
        },
        umdNamedDefine: false,
  
      },
    },
  ]
};

vite配置

vite.config.js 依赖如下:

  • vite-plugin-css-injected-by-js(将所有资源内聚到js中)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
// https://vite.dev/config/
export default defineConfig({
 plugins: [vue(),
   cssInjectedByJsPlugin() // 强制 CSS 内联到 JS
],
 build: {
   minify: true, 
   emptyOutDir: true ,
   cssCodeSplit: false, //禁止代码分割  
   assetsInlineLimit: 100 * 1024 * 1024, // 100MB
   lib: {
     entry: './src/index.js', // 组件入口文件
     name: `myNamespace.MyComponent`,     // 全局变量名(与平台约定)
     formats: ['umd',"es","cjs"],        // 必须使用UMD格式
     fileName: 'index' // 输出文件名
   },
   rollupOptions: {
    
     output: {
       manualChunks: undefined, // 强制内联动态导入
       inlineDynamicImports: true,
       entryFileNames:'[name].[format].js'
     }
   }
 }
})

最后执行输入如下

  • vite

image.png

  • webpack

image.png

tauri项目在windows上的c盘没有权限写入文件

作者 1024小神
2025年7月3日 10:46

在使用 Tauri 开发时,如果尝试在 C:\Program Files\ 这样的受保护系统目录写入或读取文件,Windows 会阻止操作(除非以管理员权限运行)。以下是解决方案:


1. 避免写入 Program Files(推荐)

Windows 对 Program Files 有严格的权限控制,普通应用不应在此目录写入数据。改用以下目录:

  • 用户数据目录(推荐):

    <pre>
    

    use tauri::api::path::{app_data_dir, resolve}; let app_data_path = app_data_dir(&tauri::Config::default()).unwrap(); std::fs::create_dir_all(&app_data_path).unwrap(); let icon_path = app_data_path.join("app.ico");

    <ul>
    <li>路径示例:<code>C:\Users\&lt;用户名&gt;\AppData\Roaming\&lt;你的应用名&gt;\app.ico</code></li>
    </ul>
    </li>
    <li>
    <p><strong>临时目录</strong></p>
    
    <pre>
    

    let temp_dir = std::env::temp_dir(); let icon_path = temp_dir.join("app.ico");


2. 如果必须写入 Program Files(不推荐)

方法 1:以管理员权限运行应用

  • 在 tauri.conf.json 中启用管理员权限:
    <pre>
    

    { "tauri": { "windows": [{ "webviewInstallMode": { "type": "offline" }, "runAsAdmin": true // 以管理员运行 }] } } 缺点:用户每次启动都会看到 UAC 弹窗,体验差。

方法 2:安装时修改目录权限

  • 用 NSIS 或 WiX 安装包脚本,在安装时赋予 Program Files\YourApp 可写权限(仍需谨慎)。

3. 检查文件是否存在(错误处理)

在读取文件前,先检查路径是否存在:

use std::path::Path;
if !Path::new(&icon_path).exists() {
  // 提供默认图标或报错
}

4. 开发时调试路径

在 main.rs 或事件处理中打印路径,确认是否正确:

println!("当前路径: {:?}", icon_path);

总结

  • 推荐方案:改用 %APPDATA% 或用户目录存储数据(Tauri 的 app_data_dir 已封装)。
  • 临时方案:以管理员运行(不推荐长期使用)。
  • 调试技巧:检查路径是否存在,打印日志定位问题。

如果仍有问题,可以提供更多代码片段(如 tauri.conf.json 和文件操作部分),我会进一步分析!

前端真的需要懂算法吗?聊聊感受

作者 ErpanOmer
2025年7月3日 10:39

image.png 在公司干了几年,带个小团队,零零总总也面试了上百个前端候选人了。说实话,有时候面完一天,感觉人都是麻的。

最让我头疼的是什么?就是“算法题”这个环节。

我经常遇到两种候选人。一种是一听算法题,就两手一摊,表情痛苦,说“哥,我天天写业务,真没准备这个”。另一种呢,正好相反,题目一出,眼睛一亮,不出三十秒,就把LeetCode上背得滚瓜烂熟的最优解,一字不差地敲了出来,然后一脸期待地看着我。

说实话,这两种,都不是我最想看到的。

这就引出了一个很多候选人都想问,但不敢问的问题:“你们这些面试官,到底怎么想的?你们明知道我们前端平时工作中,99%的时间都用不上这些,为什么非要折磨我们?”

今天,我就想站在桌子对面,跟大伙掏心窝子地聊聊,我们问算法题,到底图个啥。


首先,我得承认一件事:我们知道你工作中不怎么写算法

对,你没看错。

我心里门儿清,我团队里的小伙伴们,每天的工作是跟产品经理“吵架”,是跟UI设计师对像素,是封装React/Vue组件,是处理浏览器兼容性,是调CSS。我招你进来,也不是为了让你用动态规划来给按钮加border-radius的。

我们不会天真地以为,前端开发就是算法竞赛。如果你能把一个复杂的业务表单组件写得清晰、可维护、可扩展,在我眼里,这远比你徒手写一个红黑树要来得有价值。

所以,请你先放轻松。我们不是在考察你是不是一个“算法大神”。


那我们到底在看什么?——思路远比答案重要

既然不是看你会不会背最优解,那我们花这宝贵的20分钟,到底在考察什么?

其实,算法题只是一个“载体”,一个“媒介”。通过这个载体,我想看到的是这几样东西:

1. 你是怎么“解读”问题的(沟通与理解能力)

一个靠谱的工程师,拿到需求不会立刻动手。他会先问问题,搞清楚所有的边界和约束。

我出一道题:“写个函数,找出数组中第二大的数。”

  • 普通候选人:埋头就开始写代码。
  • 我欣赏的候选人:会先问我,“这个数组里会有重复的数字吗?会是无序的吗?会有负数吗?如果数组长度小于2怎么办?”

你看,这就是差距。我能通过这些问题,看出你是否严谨,是否有处理边界情况的意识。这个能力,在你将来面对产品经理那些模糊的需求时,至关重要。

2. 你的“思路”是否清晰(逻辑思维)

我最喜欢看到的,不是你直接写出最优解,而是你告诉我你的思考过程。

比如,你可以说:“我首先想到的,是一个最笨的办法,先排序,然后取倒数第二个。这个时间复杂度是O(n log n)。但感觉可以优化,我再想想……也许我只需要遍历一遍,用两个变量来维护最大值和第二大值,这样时间复杂度就降到O(n)了。”

这个“先暴力,再优化”的思考过程,在我看来,比你直接默写出最优解要加分得多。因为它展示了你的逻辑推理能力优化意识

3. 你的代码“品味”(工程素养)

算法题的代码量不大,但足以管中窥豹,看出一个人的代码“品味”。

你的变量是怎么命名的?a, b, c 还是 max, secondMax, current?

你有没有处理我刚才提到的那些边界情况?

你的代码有没有基本的缩进和格式?

这些细节,都反映了你平时的编码习惯。一个连算法题都写得乱七八糟的人,我很难相信他在业务项目里能写出整洁的代码。

4. 当你卡住时,你会怎么办?(抗压与学习能力)

我有时候会故意出一些有点难度的题。我不是为了让你难堪,而是想看看你卡住的时候,会有什么反应。

是直接放弃,说“不会”?还是会尝试跟我沟通,说“我卡在xxx了,能不能给点提示?”

我非常乐意给提示。我更想招一个能和我一起“协作”解决问题的人,而不是一个遇到困难就“躺平”的人。你面对一道题的态度,很可能就是你未来面对一个技术难题的态度。


给求职者的一些真心话

所以,聊了这么多:

  • 别光背题,没用。 我只要稍微改动一下题目条件,或者问你为什么这么写,背题的同学马上就露馅了。
  • 多练习“说” 。刷题的时候,试着把你的思路说出来,录下来自己听听,或者讲给朋友听。面试时的口头表达,和自己闷头做题是两回事。
  • 重点理解“为什么” 。不要满足于“这道题这么解”,要去理解它为什么要用双指针,为什么要用哈希表。理解了思路,才能举一反三。
  • 面试时,心态放平。 没做出最优解,真没关系。把你思考的过程、你的尝试、你的权衡都清晰地表达出来,你已经赢了很多人了。

我知道,让前端去卷算法,这个“游戏规则”本身就不那么公平。我们想找的是一个会思考、会沟通、有工程素养的“解决问题的人”。

算法题,只是恰好成了当前最方便、成本最低的考察工具而已。

希望这些“面试官的牢骚”,能让你稍微不那么焦虑一点。 你们怎么看?

React 渲染全流程剖析:初次渲染与重渲染的底层逻辑

作者 G扇子
2025年7月3日 10:38

附上Vue渲染机制以作对比:Vue3渲染机制解析:编译时优化与虚拟DOM的性能跃迁

一、初次渲染

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);

如以上代码初次渲染是调用createRoot方法转入目标DOM节点,然后调用render函数完成的渲染,以下是详细步骤:

1. JSX编译成虚拟DOM树

通过babel编译转化jsxjs格式代码,如

return (
<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>
)

转化为

return (
React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)
)

createElement接受以下参数:

  • component:元素类型标签
  • props:标签属性
  • children:子节点 编译后的元素即是虚拟DOM树节点 注意render函数在类组件中即是指render方法,如果是在函数组件中则是函数组件本身,即
function Foo() {
    return <h1> Foo </h1>;
}

2. Fiber树构建

创建根节点

function createFiberRoot(containerInfo) {
  // 创建 FiberRoot(React应用根节点)
  const root = new FiberRootNode(containerInfo);
  
  // 创建未初始化的 HostRootFiber(Fiber树的根节点)
  const uninitializedFiber = createHostRootFiber();
  root.current = uninitializedFiber;  // FiberRoot.current 指向 HostRootFiber
  uninitializedFiber.stateNode = root; // HostRootFiber.stateNode 回指 FiberRoot
  
  // 初始化更新队列
  initializeUpdateQueue(uninitializedFiber);
  return root;
}

创建更新对象

function updateContainer(element, container) {
  const current = container.current; // 这里是HostRootFiber
  const lane = requestUpdateLane(current); // 获取更新优先级
  
  // 创建更新对象
  const update = createUpdate(lane);
  update.payload = { element }; // 存储 ReactElement (<App/>)
  
  // - 将根组件 <App/> 存入 HostRootFiber 的更新队列
  enqueueUpdate(current, update);
  
  // 开始调度更新
  scheduleUpdateOnFiber(current, lane);
}

// 更新后内存结构:
HostRootFiber.updateQueue.shared.pending = {
  payload: { element: <App/> }, // 存储要渲染的组件
  next: update // 环形链表
}

3. 协调阶段:深度优先构建Fiber树

  • workInProgress 指针跟踪当前处理的 Fiber
  • 此处无alternate 指针,因为是初次渲染,无旧树进行比较
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  
  // 创建子Fiber节点
  const next = beginWork(current, unitOfWork, renderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 完成当前节点
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; // 继续处理子节点
  }
}

例子1:

class App extends React.Component {
  componentDidMount() {
    console.log(`App Mount`);
    console.log(`App 组件对应的fiber节点: `, this._reactInternals);
  }

  render() {
    return (
      <div className="app">
        <header>header</header>
        <Content />
      </div>
    );
  }
}

class Content extends React.Component {
  componentDidMount() {
    console.log(`Content Mount`);
    console.log(`Content 组件对应的fiber节点: `, this._reactInternals);
  }

  render() {
    return (
      <React.Fragment>
        <p>1</p>
        <p>2</p>
      </React.Fragment>
    );
  }
}

export default App;

例子1中Fiber 树构建过程: 1. 从 HostRootFiber 开始,创建 <App> Fiber 节点 2. 处理 <App> 的 render 结果,创建 <div> Fiber 节点 3. 处理 <div> 的子节点,依次创建 <header> 和 <Content> Fiber 节点 4. 处理 <Content> 的 render 结果,创建 <Fragment> 和两个 <p> Fiber 节点

4. 完成阶段:创建DOM和收集副作用

  • 所有标记了 Placement 的 Fiber 组成单向链表
  • 顺序:深度优先,子节点在前,父节点在后
function completeWork(current, workInProgress) {
  switch (workInProgress.tag) {
    case HostComponent:
      // 创建DOM实例
      const instance = createInstance(workInProgress.type, workInProgress.pendingProps);
      
      // 将子DOM附加到当前DOM
      appendAllChildren(instance, workInProgress);
      
      // 设置DOM属性
      finalizeInitialChildren(instance, workInProgress.type, workInProgress.pendingProps);
      
      workInProgress.stateNode = instance; // 关联DOM
      
      // 标记插入操作
      if (workInProgress.flags & Placement) {
        markUpdate(workInProgress);
      }
      break;
    case HostText:
      // 文本节点处理...
    // 其他类型...
  }
  
  // 收集副作用到父节点
  if (workInProgress.flags > PerformedWork) {
    appendEffectToList(workInProgress.return, workInProgress);
  }
}

例子1的副作用链表的形成:<header><p>1 → <p>2 → <div><App>HostRootFiber

5. 提交阶段:DOM操作

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  let effect = finishedWork.firstEffect;
  
  // 提交所有插入操作
  while (effect !== null) {
    commitPlacement(effect); // 实际DOM插入操作
    effect = effect.nextEffect;
  }
  
  // 调用生命周期
  commitLifeCycles(finishedWork);
}

image.png

二、重渲染

组件(或者是其祖先之一)的状态发生了改变会导致重新渲染。

1. 状态变更触发更新

创建更新对象

  • 当组件状态变更(通过 setStateuseStateforceUpdate)时,React 会创建一个更新对象(update object)。这个对象包含更新内容、优先级信息等,并添加到对应 Fiber 节点的更新队列中
  • 当使用setState时:
enqueueSetState(inst, payload) {
  const fiber = getInstance(inst); // 获取组件对应fiber
  const lane = requestUpdateLane(fiber); // 确定更新优先级
  const update = createUpdate(eventTime, lane);
  update.payload = payload; // 存储更新数据
  
  // 将更新加入fiber的更新队列
  enqueueUpdate(fiber, update);
  
  // 开始调度
  scheduleUpdateOnFiber(fiber, lane);
}

标记更新路径

  • React 需要确定哪些节点会受到更新的影响,它会从触发更新的 Fiber 节点开始,向上遍历父节点,标记出整个更新路径
  • 更新路径可帮助后续遍历跳过未标记的虚拟DOM子树
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) {
  // 设置当前fiber的lanes(标记当前节点需要更新)
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  
  // 向上遍历父节点,设置childLanes(标记子树需要更新)
  let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    node = parent;
    parent = parent.return;
  }
  
  // 返回FiberRoot
  return node.stateNode;
}

2. 准备阶段:双缓存结构初始化

  • 使用双缓存技术,在更新时创建一棵新的 workInProgress 树,与 current 树(当前显示树)交替使用
function prepareFreshStack(root, lanes) {
  // 创建workInProgress树
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 初始化workInProgress节点
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRoot = root;
  workInProgressRootRenderLanes = lanes;
}

3. 协调阶段:对比更新

beginWork与节点对比

  • 在 beginWork 阶段,React 对比新旧节点,决定是否需要更新或复用子树
function beginWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    // 检查props是否变化
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    // 判断是否需要更新
    if (oldProps === newProps && !hasLegacyContextChanged()) {
      // 检查优先级:是否在本次渲染范围内
      if (!includesSomeLane(renderLanes, workInProgress.lanes)) {
        // 无需更新,进入bailout逻辑
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
    }
    didReceiveUpdate = true;
  }
  
  // 需要更新:调用具体updatexxx更新函数
  switch (workInProgress.tag) {
    case ClassComponent: 
      return updateClassComponent(current, workInProgress, ...);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // 其他类型...
  }
}

bailout 逻辑:跳过未变更子树

  • 当节点不需要更新时,React进入bailout逻辑
  • bailout 条件:props 未变化且优先级不匹配
  • 根据更新路径可以跳过整个子树的重渲染
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  // 检查子节点是否需要更新
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // 整个子树都不需要更新,直接跳过
    return null;
  }
  
  // 克隆子节点(复用现有 Fiber)
  cloneChildFibers(current, workInProgress);
  
  // 返回第一个子节点继续处理
  return workInProgress.child;
}

4. updatexxx函数与reconcileChildren调和函数(此时需要更新)

当节点需要更新时,React 会调用调和函数,其实现的 Diff 算法会对比新旧子节点,决定复用、移动或删除节点。 传统的Diff算法是循环递归每一个节点(真实DOM节点),算法复杂度是O(n^3)。Vue和React都使用虚拟DOM节点的Diff算法,虚拟DOM是将目标所需的UI通过数据结构虚拟表现出来,保存到内存中,再将真实DOM与之保持同步。 Vue2的Diff算法使用双端对比虚拟节点,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作;Vue3在从=此基础借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型。 React的Diff算法遵循以下三个层级的策略:tree Diff(只比较同层级节点,不跨层级比较)、component Diff(相同类型组件复用实例)、element Diff(使用 key 标识稳定元素)。本节不对diff算法展示细说,后面会出一篇新文章diff算法,到时候附上链接。 本节只需要了解reconcileChildren调和函数:

  • 调和函数是updateXXX(如: updateHostRoot, updateClassComponent 等)函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置fiber.flags. 与初次渲染对比
  • 初次创建时fiber节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑
  • 对比更新时需要把ReactElement对象与旧fiber对象进行比较, 来判断是否需要复用旧fiber对象. 调和函数的目的
  1. 给新增,移动,和删除节点设置fiber.flags,flags标记:
    • Placement:新增或移动节点
    • Update:更新属性
    • Deletion:删除节点
  2. 如果是需要删除的fiber, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork函数, 但是该节点(被删除)会脱离fiber树, 不会再进入completeWork阶段, 所以在beginWork阶段提前加入副作用队列)

5. 完成阶段:completeWork与副作用收集

在 completeWork 阶段,React 完成节点处理并收集副作用(即DOM 操作)。 completeWork函数与初次更新时的completeWork函数逻辑一致,只是此时current不为null

function completeWork(current, workInProgress) {
  const newProps = workInProgress.pendingProps;
  
  switch (workInProgress.tag) {
    case HostComponent: // DOM 元素
      if (current !== null && workInProgress.stateNode != null) {
        // 对比更新:比较新旧属性
        updateHostComponent(current, workInProgress, newProps);
      } else {
        // 新增节点:创建 DOM 实例
        const instance = createInstance(
          workInProgress.type,
          newProps,
          workInProgress
        );
        // 关联 DOM 与 Fiber
        workInProgress.stateNode = instance;
        // 设置初始属性
        finalizeInitialChildren(instance, newProps);
      }
      break;
    // 其他类型处理...
  }
  
  // 收集副作用(flags)
  if (workInProgress.flags > PerformedWork) {
    // 添加到父节点的副作用链表
    if (returnFiber.firstEffect === null) {
      returnFiber.firstEffect = workInProgress;
    } else {
      returnFiber.lastEffect.nextEffect = workInProgress;
    }
    returnFiber.lastEffect = workInProgress;
  }
}

触发updateHostComponent函数更新DOM属性

function updateHostComponent(current, workInProgress, newProps) {
  const oldProps = current.memoizedProps;
  
  // 比较新旧属性差异
  if (oldProps !== newProps) {
    // 计算属性差异
    const updatePayload = prepareUpdate(
      workInProgress.stateNode,
      workInProgress.type,
      oldProps,
      newProps
    );
    
    // 存储差异到 updateQueue
    workInProgress.updateQueue = updatePayload;
    
    // 标记 Update 副作用
    if (updatePayload) {
      workInProgress.flags |= Update;
    }
  }
}

6. 提交阶段:DOM 操作与生命周期

提交阶段遍历副作用链表,执行 DOM 操作并调用生命周期方法

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  let effect = finishedWork.firstEffect;
  
  // 阶段1: BeforeMutation(调用 getSnapshotBeforeUpdate 获取 DOM 快照)
  commitBeforeMutationEffects();
  
  // 阶段2: Mutation(DOM操作)
  while (effect !== null) {
    const nextEffect = effect.nextEffect;
    const flags = effect.flags;
    
    // 处理 Placement(插入/移动)
    if (flags & Placement) {
      commitPlacement(effect);
    } 
    // 处理 Update(属性更新)
    else if (flags & Update) {
      commitWork(effect);
    }
    // 处理 Deletion(删除)
    else if (flags & Deletion) {
      commitDeletion(effect);
    }
    
    effect = nextEffect;
  }
  
  // 阶段3: Layout(同步生命周期)
  effect = finishedWork.firstEffect;
  while (effect !== null) {
    const nextEffect = effect.nextEffect;
    if (effect.flags & Update) {
      // 类组件:componentDidMount/Update
      // 函数组件:useLayoutEffect
      commitLayoutEffects(effect);
    }
    effect = nextEffect;
  }
  
  // 阶段4: Passive(异步 useEffect)
  scheduleCallback(NormalPriority, () => {
    flushPassiveEffects();
  });
}

7. 清理与切换:完成更新

提交完成后,React 清理临时状态并切换 current 指针

function finishCommit() {
  // 清理临时状态
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 切换 current 树,使新树成为当前树
  root.current = finishedWork;
  
  // 调度未处理的更新
  if (root.pendingPassiveEffects !== null) {
    scheduleCallback(NormalPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
  
  // 检查是否有待处理的更新
  ensureRootIsScheduled(root);
}

image.png

三、初次缓存和重缓存的区别

  • 对于初次渲染, React 会调用根组件。
  • 对于重渲染, React 会调用内部状态更新触发了渲染的函数组件
    • 这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
特性 初次渲染 重渲染
缓存机制 无缓存 双缓存树(current/workInProgress)
节点创建 全量新建节点 复用节点 + 条件创建新节点
对比机制 无旧节点比较 Fiber节点Diff算法(复用决策)
副作用标记 全部标记为Placement 动态标记Update/Placement/Deletion
子树处理 全量处理 bailout机制跳过未变更子树
DOM操作 全量插入 增量更新(仅变更部分)
性能优化 无优化空间 优先级调度 + 子树跳过
alternate指针 不存在 新旧节点互相引用

Clipboard_Screenshot_1751456908.png

四、React和Vue的渲染机制的区别

1. 虚拟DOM

  • React:通过JSX编译生成虚拟DOM树,所有UI逻辑(插值、循环、条件)均用原生JavaScript实现。状态变化时生成新虚拟DOM树,通过Diff算法 计算最小变更集,再更新真实DOM
  • Vue:模板编译为虚拟DOM树,但依赖响应式系统追踪数据变化。数据变更时直接定位受影响组件,生成局部虚拟DOM并比对,减少不必要的树遍历

2. 响应式系统

  • React无自动依赖追踪。状态更新(setState/useState)默认触发当前组件及所有子组件的重渲染,需开发者手动优化(如 React.memo 或 shouldComponentUpdate
  • Vue依赖收集+发布订阅
    • Vue2:使用 Object.defineProperty 拦截数据读写
    • Vue3:换成使用Proxy实现细粒度依赖追踪,数据变更时仅触发关联组件的更新

参考资料: fiber 树构造(初次创建) fiber 树构造(对比更新) React文档-渲染和提交

JavaScript 数据扁平化方法大全

作者 绅士玖
2025年7月3日 10:31

前言

数据扁平化是指将多维数组转换为一维数组的过程。由于嵌套数据结构增加了访问和操作数据的复杂度,所以·我们可以将嵌套数据变成一维的数据结构,下面就是我搜集到的一些方法,希望可以给你带来帮助!!

1. 使用 Array.prototype.flat()(推荐)

ES2019 引入的专门方法:

const nestedArr = [1, [2, [3, [4]], 5]];

// 默认只扁平化一层
const flattened1 = nestedArr.flat();
console.log(flattened1); // [1, 2, [3, [4]], 5]

// 指定深度为2
const flattened2 = nestedArr.flat(2);
console.log(flattened2); // [1, 2, 3, [4], 5]

// 完全扁平化
const fullyFlattened = nestedArr.flat(Infinity);
console.log(fullyFlattened); // [1, 2, 3, 4, 5]

解析

  • flat(depth) 方法创建一个新数组,所有子数组元素递归地连接到指定深度
  • 参数 depth 指定要提取嵌套数组的结构深度,可选的参数,默认为1
  • 使用 Infinity 可展开任意深度的嵌套数组,Infinity 是一个特殊的数值,表示无穷大

2. 使用 reduce() 和 concat() 递归

function flatten(arr) {
  // 使用 reduce 方法遍历数组元素
  return arr.reduce((acc, val) => {
    // 如果当前元素是数组,则递归调用 flatten 继续展开,并拼接到累积数组 acc
    if (Array.isArray(val)) {
      return acc.concat(flatten(val));
    } 
    // 如果当前元素不是数组,直接拼接到累积数组 acc
    else {
      return acc.concat(val);
    }
  }, []); // 初始累积值是一个空数组 []
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]

解析

  1. 递归处理嵌套数组

    • 遇到子数组时,递归调用 flatten(val) 继续展开,直到所有层级都被展开为单层。
  2. reduce 方法的作用

    • 遍历数组,通过 acc(累积值)逐步拼接结果,初始值设为 [](空数组)。
  3. Array.isArray(val) 检查

    • 判断当前元素是否为数组,决定是否需要递归展开。
  4. concat 拼接结果

    • 将非数组元素或递归展开后的子数组拼接到累积数组 acc 中。

3. 使用 concat() 和扩展运算符递归

function flatten(arr) {
  // 使用扩展运算符 (...) 展开数组的第一层,并合并成一个新数组
  const flattened = [].concat(...arr);

  // 检查当前展开后的数组中是否仍然包含嵌套数组
  // 如果存在嵌套数组,则递归调用 flatten 继续展开
  // 如果所有元素都是非数组类型,则直接返回展开后的数组
  return flattened.some(item => Array.isArray(item)) 
    ? flatten(flattened) 
    : flattened;
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]

解析

  1. [].concat(...arr) 展开一层数组

    • 使用扩展运算符 ... 展开 arr 的最外层,并通过 concat 合并成一个新数组。
    • 例如:[].concat(...[1, [2, [3]]]) → [1, 2, [3]](仅展开一层)。
  2. flattened.some(Array.isArray) 检查嵌套

    • 使用 Array.prototype.some() 检查当前数组是否仍然包含子数组。
    • 如果存在,则递归调用 flatten 继续展开。
  3. 递归终止条件

    • 当 flattened 不再包含任何子数组时,递归结束,返回最终结果。

4. 使用 toString() 方法(仅适用于数字数组)

const nestedArr = [1, [2, [3, [4]], 5]];
const flattened = nestedArr.toString().split(',').map(Number);
console.log(flattened); // [1, 2, 3, 4, 5]

解析

  1. toString() 的隐式转换

    • JavaScript 的 Array.prototype.toString() 会自动展开嵌套数组,并用逗号连接所有元素。
    • 例如:[1, [2, [3]]].toString() → "1,2,3"
  2. split(',') 分割字符串

    • 将字符串按逗号拆分成字符串数组,但所有元素会是字符串类型(如 "2")。
  3. map(Number) 类型转换

    • 通过 Number 构造函数将字符串元素转换为数字类型。
    • 注意:如果原数组包含非数字(如 ['a', [2]]),结果会变成 [NaN, 2]

优缺点

  • 优点:代码极其简洁,适合纯数字的嵌套数组。

  • 缺点

    • 仅适用于数字数组(其他类型会被强制转换,如 true → 1null → 0)。
    • 无法保留原数据类型(如字符串 '3' 会被转成数字 3)。

适用场景

  • 快速展开纯数字的嵌套数组,且不关心中间过程的性能损耗(toString 和 split 会有临时字符串操作)。

5. 使用 JSON.stringify() 和正则表达式

function flatten(arr) {
  // 1. 使用 JSON.stringify 将数组转换为字符串表示
  //    例如:[1, [2, [3]], 'a'] → "[1,[2,[3]],\"a\"]"
  const jsonString = JSON.stringify(arr);

  // 2. 使用正则表达式移除所有的 '[' 和 ']' 字符
  //    例如:"[1,[2,[3]],\"a\"]" → "1,2,3,\"a\""
  const withoutBrackets = jsonString.replace(/[\[\]]/g, '');

  // 3. 按逗号分割字符串,生成字符串数组
  //    例如:"1,2,3,\"a\"" → ["1", "2", "3", "\"a\""]
  const stringItems = withoutBrackets.split(',');

  // 4. 尝试将每个字符串解析回原始数据类型
  //    - 数字会变成 Number 类型(如 "1" → 1)
  //    - 字符串会保留(如 "\"a\"" → "a")
  //    - 其他 JSON 可解析类型也会被正确处理
  return stringItems.map(item => {
    try {
      // 尝试 JSON.parse 解析(处理字符串、数字等)
      return JSON.parse(item);
    } catch (e) {
      // 如果解析失败(如空字符串或非法 JSON),返回原始字符串
      return item;
    }
  });
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5, 'a', { b: 6 }];
console.log(flatten(nestedArr)); 
// 输出: [1, 2, 3, 4, 5, "a", { b: 6 }]

解析

  1. JSON.stringify 的作用

    • 将整个数组(包括嵌套结构)转换为 JSON 字符串,保留所有数据类型信息。
  2. 正则替换 /[[]]/g

    • 移除所有方括号字符 [ 和 ],只保留逗号分隔的值。
  3. split(',') 分割字符串

    • 生成一个字符串数组,但每个元素可能仍是被 JSON 字符串化的(如 ""a"")。
  4. JSON.parse() 尝试恢复数据类型

    • 通过 JSON.parse 将字符串转换回原始类型(数字、字符串、对象等)。
    • 使用 try-catch 处理不合法的 JSON 字符串(如空字符串或格式错误的情况)。

优缺点

  • 优点

    • 支持任意数据类型(数字、字符串、对象等)。
    • 能正确处理嵌套对象(如 { b: 6 })。
  • 缺点

    • 性能较低(涉及 JSON 序列化、正则替换、解析等操作)。
    • 如果原始数组包含特殊字符串(如 "[1]" ,可能会被错误解析。

适用场景

  • 需要处理混合数据类型(非纯数字)的嵌套数组。
  • 对性能要求不高,但需要代码简洁的场景。

6. 使用堆栈的非递归实现

function flatten(arr) {
  // 创建栈并初始化(使用扩展运算符浅拷贝原数组)
  const stack = [...arr];
  const result = [];
  
  // 循环处理栈中的元素
  while (stack.length) {
    // 从栈顶取出一个元素
    const next = stack.pop();
    
    if (Array.isArray(next)) {
      // 如果是数组,展开后压回栈中(保持顺序)
      stack.push(...next);
    } else {
      // 非数组元素,添加到结果数组前端(保持原顺序)
      result.unshift(next);
    }
  }
  
  return result;
}

const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]

解析

  1. 栈结构初始化

    • 使用扩展运算符 [...arr] 创建原数组的浅拷贝作为初始栈
    • 避免直接修改原数组
  2. 栈处理循环

    • 使用 while 循环处理栈直到为空
    • 每次从栈顶 pop() 一个元素进行处理
  3. 元素类型判断

    • 使用 Array.isArray() 检查元素是否为数组
    • 如果是数组则展开后重新压入栈
    • 非数组元素则添加到结果数组
  4. 顺序保持

    • 使用 unshift() 将元素添加到结果数组前端,当然这样比较费性能,可以改用 push() + reverse() 替代 unshift()
    • 确保最终结果的顺序与原数组一致

优缺点

  • 优点

    • 支持任意数据类型(不限于数字)
    • 可以处理深层嵌套结构(无递归深度限制)
    • 相比递归实现,不易导致栈溢出
  • 缺点

    • 使用 unshift() 导致时间复杂度较高(O(n²))
    • 需要额外空间存储栈结构
    • 相比原生 flat() 方法性能稍差
    • 无法控制扁平化深度(总是完全扁平化)

适用场景

  • 需要处理混合数据类型的深层嵌套数组
  • 需要避免递归导致的栈溢出风险

7. 使用 Array.prototype.some() 和扩展运算符

function flatten(arr) {
  // 循环检测数组中是否还包含数组元素
  while (arr.some(item => Array.isArray(item))) {
    // 使用扩展运算符展开当前层级的所有数组
    // 并通过concat合并为一层
    arr = [].concat(...arr);
  }
  return arr;
}

const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]

解析

  1. 循环条件检测

    • 使用 arr.some() 方法检测数组中是否还存在数组元素
    • Array.isArray(item) 判断每个元素是否为数组
  2. 层级展开

    • 使用扩展运算符 ...arr 展开当前层级的数组
    • 通过 [].concat() 将展开的元素合并为新数组
  3. 迭代处理

    • 每次循环处理一层嵌套
    • 重复直到没有数组元素存在

性能比较

对于大多数现代应用:

  1. 优先使用 flat(Infinity)(最简洁且性能良好)
  2. 对于深度嵌套的大数组,考虑非递归的堆栈实现
  3. 递归方法在小数据集上表现良好且代码简洁
  4. 避免 toString() 方法除非确定只有数字数据

总结

JavaScript 提供了多种扁平化数组的方法,从简单的内置 flat() 方法到各种手动实现的递归、迭代方案。选择哪种方法取决于:

  • 运行环境是否支持 ES2019+
  • 数据结构的复杂程度
  • 对性能的要求
  • 代码可读性需求

在大多数现代应用中,flat(Infinity) 是最佳选择,因为它简洁、高效且语义明确。

如何丝滑使用JavaScript的装饰器?

作者 best666
2025年7月3日 10:21

在 JavaScript 里,装饰器(Decorators)是一种能对类、方法、属性的行为进行扩展或者修改的语法。它的核心原理是借助元编程,在不改变原有代码结构的前提下,为目标添加新功能。

基本概念

直接show code,现有如下代码,用来记录log日志:

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

装饰器函数参数解析

在 JavaScript 装饰器中,log 函数的三个参数分别代表:

  1. target:被装饰的类或原型对象。

    • 若装饰的是类方法,target 就是类的原型(prototype)。
    • 若装饰的是类,target 就是类本身。
  2. name:被装饰的方法或属性的名称(字符串类型)。

  3. descriptor:属性描述符对象(与 Object.defineProperty 中的描述符相同),包含以下属性:

    • value:被装饰的方法或属性的值(即原始函数)。
    • writable:是否可修改(布尔值)。
    • enumerable:是否可枚举(布尔值)。
    • configurable:是否可配置(布尔值)。

函数实现原理详解

log 装饰器的核心逻辑是替换原始方法,在执行前后添加日志:


    function log(target, name, descriptor) {
      // 1. 保存原始方法的引用
      const original = descriptor.value;

      // 2. 修改 descriptor.value 为新函数
      descriptor.value = function(...args) {
        // 3. 执行前置逻辑(打印入参)
        console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
        
        // 4. 执行原始方法并保存结果
        const result = original.apply(this, args);
        
        // 5. 执行后置逻辑(打印返回值)
        console.log(`方法 ${name} 返回:${result}`);
        
        // 6. 返回原始结果
        return result;
      };

      // 7. 返回修改后的描述符
      return descriptor;
    }

为什么要这样实现?

这种写法的关键点在于:

  1. 不改变原始方法的核心逻辑:通过包装原始方法,在不修改其代码的前提下添加新功能。

  2. 保留上下文(this

    • 使用 original.apply(this, args) 确保原始方法在调用时的 this 指向不变。
    • 若直接调用 original(args),可能导致 this 指向全局对象(非严格模式)或 undefined(严格模式)。
  3. 支持任意参数

    • 使用剩余参数 ...args 收集所有传入参数。
    • 使用 JSON.stringify(args) 将参数序列化为字符串(需注意无法处理函数或 undefined 类型的参数)。
  4. 遵循装饰器规范

    • 装饰器必须返回一个描述符对象(或新类)。
    • 通过修改 descriptor.value 替换原始方法。

应用示例

使用该装饰器的类方法会自动添加日志功能:


    class Calculator {
      @log
      add(a, b) {
        return a + b;
      }
    }

    const calc = new Calculator();
    calc.add(3, 4);

    // 输出:
    // 调用 add 方法,参数:[3,4]
    // 方法 add 返回:7

注意事项

  1. 参数序列化限制

    • JSON.stringify 无法处理函数或 undefined 参数,可能导致日志不完整。
    • 改进方案:使用 args.map(arg => String(arg)).join(', ') 或自定义序列化函数。
  2. 异步方法处理

    • 若原始方法返回 Promise,需使用 await 等待结果:

      
          descriptor.value = async function(...args) {
            // ...
            const result = await original.apply(this, args);
            // ...
          };
      
  3. 兼容性

    • 装饰器语法需 Babel 或 TypeScript 支持。

    • 确保项目配置中启用了装饰器(如 @babel/plugin-proposal-decorators)。

通过这种方式,装饰器实现了 ** 横切关注点(Cross-cutting Concerns)** 的分离,让日志、权限等功能与核心业务逻辑解耦。

下面介绍装饰器常见的应用场景:

1. 日志记录

装饰器能够在方法执行的前后添加日志,这样可以对函数的调用情况进行监控。

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

2. 权限验证

可以在执行方法前对用户权限进行检查,防止未授权的访问。


    function auth(requiredRole) {
      return function(target, name, descriptor) {
        const original = descriptor.value;
        descriptor.value = function(...args) {
          if (this.userRole !== requiredRole) {
            throw new Error("权限不足");
          }
          return original.apply(this, args);
        };
        return descriptor;
      };
    }

    class AdminPanel {
      userRole = "admin";

      @auth("admin")
      deleteUser() {
        return "用户已删除";
      }
    }

3. 性能分析

装饰器能够对函数的执行时间进行测量,有助于性能优化。


    function benchmark(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = async function(...args) {
        const start = performance.now();
        const result = await original.apply(this, args);
        const end = performance.now();
        console.log(`${name} 方法执行耗时:${end - start}ms`);
        return result;
      };
      return descriptor;
    }

    class DataService {
      @benchmark
      async fetchData() {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: "大量数据" };
      }
    }

4. 自动绑定

在 React 等框架中,装饰器可以解决方法上下文丢失的问题。


    function autobind(target, name, descriptor) {
      const original = descriptor.value;
      return {
        configurable: true,
        get() {
          const bound = original.bind(this);
          Object.defineProperty(this, name, {
            value: bound,
            configurable: true,
            writable: true
          });
          return bound;
        }
      };
    }

    class Component {
      constructor() {
        this.state = { count: 0 };
      }

      @autobind
      increment() {
        this.state.count++;
      }
    }

5. 单例模式实现

装饰器可以确保一个类仅有一个实例。


    function singleton(constructor) {
      let instance;
      return function(...args) {
        if (!instance) {
          instance = new constructor(...args);
        }
        return instance;
      };
    }

    @singleton
    class AppState {
      constructor() {
        this.data = {};
      }
    }

    const state1 = new AppState();
    const state2 = new AppState();
    console.log(state1 === state2); // 输出 true

6. 类型检查

在运行时对函数参数的类型进行验证。


    function validateTypes(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = function(...args) {
        const paramTypes = Reflect.getMetadata("design:paramtypes", target, name);
        args.forEach((arg, i) => {
          if (arg && paramTypes[i] && !(arg instanceof paramTypes[i])) {
            throw new TypeError(`参数 ${i} 类型错误,期望 ${paramTypes[i].name}`);
          }
        });
        return original.apply(this, args);
      };
      return descriptor;
    }

    class MathUtils {
      @validateTypes
      add(a: number, b: number) {
        return a + b;
      }
    }

7. 缓存机制

对函数的计算结果进行缓存,避免重复计算。


    function memoize(target, name, descriptor) {
      const original = descriptor.value;
      const cache = new Map();
      descriptor.value = function(...args) {
        const key = args.toString();
        if (cache.has(key)) {
          return cache.get(key);
        }
        const result = original.apply(this, args);
        cache.set(key, result);
        return result;
      };
      return descriptor;
    }

    class Fibonacci {
      @memoize
      calculate(n) {
        return n <= 1 ? n : this.calculate(n - 1) + this.calculate(n - 2);
      }
    }

装饰器使用注意要点

  • 要启用装饰器语法,需要在 Babel 或者 TypeScript 中进行配置。

  • 装饰器的执行顺序是从下往上,例如:

    
        @a
        @b
        method() {} // 先执行 b,再执行 a
    
    
  • 装饰器可以返回一个新的类或者修改原有的描述符(descriptor)。

装饰器的主要价值在于它遵循了开放 - 封闭原则,即对扩展开放,对修改封闭。它能让代码变得更加简洁,同时增强代码的可复用性。

❌
❌