阅读视图

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

CodePush停服后,如何用腾讯Shiply快速实现ReactNative热更新?

1.  背景

CodePush是微软VisualStudioAppCenter服务的一个子功能,支持动态下发RN热更新产物。

但是VisualStudioAppCenter 2025/3/31 已经停止服务,如果想继续使用CodePush需要自己独立部署。

专门独立部署一套后台服务,对于个人开发者尤其是客户端开发来说时间成本较高,后期维护也需要投入不少精力,最好还是使用已有的下发平台。

调研了一下发现目前Pushy和Shiply平台都支持RN热更新。

Pushy 是ReactNative中文网推出的RN下发平台,支持差量更新,CLI和网页双端管理。

Shiply 是腾讯端服务团队推出的面向客户端的全场景发布平台,支持动态下发配置、资源、软件包(App)、Android热更新、flutter热更新,最近刚上线了RN热更新功能。

 

体验下来感觉Shiply的前端交互更友好,有完整的审批发布流程,支持多维度的下发规则,能看到客户端下发加载数据,下面详细介绍下Shiply RN热更新的接入和使用过程。

 

2.  Shiply RN热更新接入实战

2.1.  创建Shiply项目及产品

参考第三点中的发布平台使用指引,创建一个测试项目,并创建Android和iOS产品。

2.2.  创建RN模块

参考第三点中的发布平台使用指引,创建一个名叫「testRN」的模块,绑定2.1中创建的两个产品。

2.3.  接入Shiply RN动态化SDK

2.3.1.  创建RN demo

先创建一个RN demo工程:

npx @react-native-community/cli init TestShiplyRN --version 0.78

 

2.3.2.  添加RN层依赖

在demo项目根目录执行

npm install rn-shiply-upgrade

 

2.3.3.  添加Android层依赖

在项目Android目录下的build.gradle文件中添加maven地址

allprojects {
  repositories {
    maven { url "https://tencent-tds-maven.pkg.coding.net/repository/shiply/repo" }
  }
}

 

2.3.4.  添加热更新检查与加载代码

2.3.4.1.  修改RN层代码
 
import React from 'react';
import {Platform, Text, View} from 'react-native';
 
import { HotUpdateHelper, HotUpdateButton } from 'rn-shiply-upgrade'; // 新增导入辅助类
 
const App = () => {
    // 初始化热更新配置(只需一次)
    HotUpdateHelper.getInstance({
        // 需要修改为业务方自己的android/ios appId和appKey
        appId: Platform.OS === 'ios' ? 'iOSAppId' : 'androidAppId',
        appKey: Platform.OS === 'ios'
            ? 'iOSAppKey'
            : 'androidAppKey',
        deviceId: '33333', // 需要修改为实际的设备id
        appVersion: "1.0.0", // 应用版本号
        shiplyResName: 'testRN', // 资源key名称,对应shiply平台上中创建的RN模块名字
    });
 
    return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
            <Text> 热更新测试: 当前是本地内容</Text>
            <View style={{ height: 10 }} />
            {/* 默认样式按钮 */}
            <HotUpdateButton />
        </View>
            );
};
 
export default App;
 

androidAppId/androidAppKey/iOSAppId/iOSAppKey要改成2.1中创建的Android/iOS产品的真实appId/appKey。

 

 

2.3.4.2.  修改Android层代码
class MainApplication : Application(), ReactApplication {
 
  override val reactNativeHost: ReactNativeHost =
      object : DefaultReactNativeHost(this) {
        override fun getPackages(): List<ReactPackage> =
            PackageList(this).packages.apply {
              // Packages that cannot be autolinked yet can be added manually here, for example:
              // add(MyReactNativePackage())
            }
 
        override fun getJSMainModuleName(): String = "index"
 
        override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
 
        override fun getJSBundleFile(): String? {
              var result: String? = null
              // 从shiply获取RN bundle路径,业务方需要将资源key修改为shiply平台中创建的资源key名称
              result = ShiplyReactNativeUpgradeModule.getJSBundleFilePath(applicationContext as Application, "testRN")
              Log.d("MainApplication", "getJSBundleFile: $result")
              return result
        }
 
        override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
        override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
      }
 
  override val reactHost: ReactHost
    get() = getDefaultReactHost(applicationContext, reactNativeHost)
 
  override fun onCreate() {
    super.onCreate()
    SoLoader.init(this, OpenSourceMergedSoMapping)
    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      // If you opted-in for the New Architecture, we load the native entry point for this app.
      load()
    }
  }
}

复写getJSBundleFile方法,从shiply获取bundle路径。

2.3.4.3.  修改iOS层代码
class AppDelegate: RCTAppDelegate {
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    self.moduleName = "TestShiplyRN"
    self.dependencyProvider = RCTAppDependencyProvider()
 
    // You can add your custom initial props in the dictionary below.
    // They will be passed down to the ViewController used by React Native.
    self.initialProps = [:]
 
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
 
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    self.bundleURL()
  }
 
  override func bundleURL() -> URL? {
 
    // 业务方需要将资源key修改为shiply平台中创建的资源key名称
    ShiplyReactNativeUpgradeUtil.getJSBundleFilePath("testRN");
    if let path = ShiplyReactNativeUpgradeUtil.getJSBundleFilePath("testRN") {
        NSLog("bundleURL called,path = %@ ", path);
        return URL(fileURLWithPath: path)
    }
    return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
  }
}

复写bundleURL方法,从shiply获取bundle路径。

 

做完以上步骤后就完成了接入。

 

这时点击检查更新没有什么效果,因为还没有在shiply配置新版本RN热更新产物。

2.4.  打包RN动态化产物

 

2.4.1.  修改Demo测试代码

 
import React from 'react';
import {Platform, Text, View} from 'react-native';
 
import { HotUpdateHelper, HotUpdateButton } from 'rn-shiply-upgrade'; // 新增导入辅助类
 
const App = () => {
    // 初始化热更新配置(只需一次)
    HotUpdateHelper.getInstance({
        // 需要修改为业务方自己的android/ios appId和appKey
        appId: Platform.OS === 'ios' ? 'iOSAppId' : 'androidAppId',
        appKey: Platform.OS === 'ios'
            ? 'iOSAppKey'
            : 'androidAppKey',
        deviceId: '33333', // 需要修改为实际的设备id
        appVersion: "1.0.0", // 应用版本号
        shiplyResName: 'testRN', // 资源key名称,对应shiply平台上中创建的RN模块名字
    });
 
    return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
            {/*<Text> 热更新测试: 当前是本地内容</Text>*/}
            <Text> 热更新测试: 当前是热更新内容</Text>
            <View style={{ height: 10 }} />
            {/* 默认样式按钮 */}
            <HotUpdateButton />
        </View>
            );
};
 
export default App;
 

主要修改下UI文案,区分本地版本和热更新版本。

2.4.2.  打包Android产物

在工程的根目录输入如下命令进行打包:

react-native bundle --entry-file ./index.js --bundle-output ./bundle/androidBundle/index.android.bundle --platform android --assets-dest ./bundle/androidBundle --dev false 

执行后,工程根目录/bundle/androidBundle下会生成Android产物,全选androidBundle下所有文件后进行压缩,得到的zip文件将用于上传到shiply。

 

2.4.3.  打包iOS产物

在工程的根目录输入如下命令进行打包:

react-native bundle --entry-file ./index.js --bundle-output ./bundle/iOSBundle/index.ios.bundle --platform ios --assets-dest ./bundle/iOSBundle --dev false 

执行后,工程根目录/bundle/iOSBundle下会生成iOS产物,全选iOSBundle下所有文件后进行压缩,得到的zip文件将用于上传到shiply。

 

 

2.5.  发布RN动态化产物

参考3中的发布平台使用指引,上传2.4中的zip文件生成发布任务:

 

 

还原测试代码,在Demo Android目录执行./gradlew app:assembleRelease编译release apk,安装后运行九能拉到远端的RN产物了。

这里按钮和弹框都是shiply sdk提供的默认UI,也可以参考SDK中的HotUpdateButton 和HotUpdateHelper来自定义UI和请求时机。

 

安装加载成功后,shiply前端页面可以看到相关的数据上报。

 

2.6.  小结

总体体验下来接入比较顺畅,花个半天时间就能正常接入shiply rn热更新。因为CodePush下架急需替代方案的开发者可以考虑接入试用下。

掘金图片上传一直失败,也可以看下这篇文章:blog.csdn.net/sckgenius/a…

3.  参考文档

 

Shiply ReactNative 动态化 SDK 接入指引 | Shiply 专业版

ReactNative 动态化发布平台使用指引 | Shiply 专业版

css实现酷炫的边框流光(流动)旋转平移效果

最近为了迎合ai科技感的热度,产品突发奇想想把后台系统整得酷炫一点,比如什么接口请求的时候输入框带点酷炫的旋转效果,请求结果框移入内边框可旋转巴拉巴拉一堆“酷炫的效果”,然后就开始拿一堆酷炫的视屏效果企图让我实现她天马行空的想法,为了满足产品这个刁钻的需求,我把ai都问烂了,最终整理出了下面四种产品比较满意的流光效果(此处效果是从项目抽离出来的案例,具体的应用大家可可根据各自的场景自行使用,公司项目有保密要求我也不好展示,大家就凑活凑活,不好看也别嫌弃)

iShot_2025-04-24_01.29.38.mp4.gif

一、案例中使用到的参数

@property 声明 :

  • 这是一个较新的CSS特性,允许开发者明确定义自定义属性的类型和行为
  • 它比普通的CSS变量( --variable )提供了更多的控制能力
  • 这里关键的一句是:syntax: "";,如果你不声明他为角度类型的变量,那么 turn 这个单位将不能被 conic-gradient 在变化中读取。
@property --border-gradient-angle {
  syntax: '<angle>'; //指定此属性的值必须是角度类型
  inherits: true; // 表示此属性的值会从父元素继承
  initial-value: 0turn; //设置属性的初始值为0圈(即0度), 1turn单位表示一个完整的圆(360度)
} 

linear-gradient(线性渐变):

  • 由两种或多种颜色沿一条直线进行线性过渡的图像
//渐变轴为 90 度,从左到右渐变 
background: linear-gradient(90deg, #6631ff, #a431ff, #ee89ff, #31daff, #316bff, #44ffdd, #6631ff);

conic-gradient(锥形渐变) :

    background: conic-gradient(
      // 指定渐变的起始角度--border-gradient-angle(这个值会在动画中变化,使得渐变可以随时间旋转),并设置设置渐变的中心点在元素的正中心(水平50%,垂直50%)
      from var(--border-gradient-angle) at 50% 50%,
      // 渐变中使用的颜色(注意首尾颜色相同(#6631ff),确保渐变过渡平滑)
      #6631ff,
      #a431ff,
      #ee89ff,
      #31daff,
      #316bff,
      #44ffdd,
      #6631ff
    );

animation(动画效果):

  • 通过改变 --border-gradient-angle 的值(从0turn到1turn),实现了渐变色彩的旋转效果
 animation: border-wave 5s linear infinite 0ms;

 @keyframes border-wave {
    0% {
      --border-gradient-angle: 0turn;
    }

    100% {
      --border-gradient-angle: 1turn;
    }
  }

filter

//- 高斯模糊效果,使元素产生15像素的模糊效果
filter: blur(10px);

二、实现效果代码

2.1 旋转流光

代码原理很简单,先实现一个旋转的流光背景设置一个内边距

image-20250423173016298.png 然后再在这个背景上叠加上元素,就可实现一个伪旋转边框的效果

2.2 平移流光

代码原理很简单,通过控制background-size实现一个平移的流光背景并设置一个内边距,然后再在这个背景上叠加上元素,就可实现一个伪边框的效果

image-20250423174610941.png

2.3 内边框流光

为了实现内边框旋转的效果此处需要通过伪元素::before创建一个旋转边框

未命名.png

再通过 filter: blur(10px);为这个旋转边框设置一个模糊效果

未命名 2.png

然后再给通过父元素设置overflow: hidden;使超出的边框隐藏

未命名 3.png

2.4 外边框流光

为了不让 filter: blur(10px);影响到子元素的效果,所以需要通过伪元素::before给父元素创建一个模糊的旋转元素 未命名 4.png

创建完成后覆盖上有背景色的子元素就可实现效果

ES2020 都有哪些新写法?

1、可选链操作符

// 传统写法
const street = user && user.address && user.address.street;

// ES2020
const street = user?.address?.street; // 任意一环不存在则返回 undefined

支持的场景:

  • 属性访问 obj?.prop
  • 动态属性 obj?.[expr]
  • 函数调用 func?.()

2、空值合并运算符

作用:精准判断 null/undefined(不包含其他假值如 0 或 '')。

// 传统写法
const value = input !== null && input !== undefined ? input : 'default';

// ES2020
const value = input ?? 'default'; // 仅在 input 为 null/undefined 时生效

对比 ||

const count = 0;
console.log(count || 10); // 10(0 是假值)
console.log(count ?? 10); // 0(精准判断)

3、动态导入

作用:按需异步加载模块。

// 传统静态导入
import module from 'module';

// ES2020 动态导入
button.addEventListener('click', async () => {
  const module = await import('./module.js');
  module.doSomething();
});

4、 BigInt 大整数类型

作用:表示超出 Number.MAX_SAFE_INTEGER 的整数。

Number.MAX_SAFE_INTEGER 是多少?

2^53 - 1 = 9007199254740991

技术背景:

JS使用 IEEE 754 标准的64位双精度浮点数表示所有数字(包括整数) 其中52位用于表示整数部分的尾数

5、Promise.allSettled()

获取所有Promise的结果(无论成功还是失败)

6、String.matchAll()

作用:高效遍历正则匹配的所有分组。

const str = 'test1test2';
const regex = /t(e)(st(\d?))/g;

// 传统写法:循环 exec
// ES2020
const matches = [...str.matchAll(regex)];
matches[0]; // ["test1", "e", "st1", "1", index: 0, ...]

七、globalThis

作用:统一全局对象访问(跨浏览器/Node.js 环境)。

// 传统环境判断
const global = typeof window !== 'undefined' ? window : global;

// ES2020
console.log(globalThis); // 浏览器: window, Node.js: global

八、模块新特性

1. import.meta

console.log(import.meta.url); // 文件 URL(如 "file:///path/to/module.js")

2. 导出命名空间

export * as utils from './utils.js'; // 将模块所有导出作为命名空间

九、for-in 机制标准化

明确规范 for-in 循环的遍历顺序(虽实际仍依赖引擎实现)

记一次replaceAll报错的问题

产生原因

公司大屏在国产机运行不正常,但是运行后台端都正常

排查问题

a.replaceAll() is not a function

查了下MDN

image.png

解决问题

可以使用replace去替换

image.png

//方法一 直接替换
const str = "Hello World, Hello Universe";
const newStr = str.replace(/Hello/g, "Hi");
console.log(newStr); // 输出: "Hi World, Hi Universe


//或者使用函数
function replaceAll(str, search, replacement) {
    let result = str;
    while (result.includes(search)) {
        result = result.replace(search, replacement);
    }
    return result;
}

const str = "Hello World, Hello Universe";
const newStr = replaceAll(str, "Hello", "Hi");
console.log(newStr); // 输出: "Hi World, Hi Universe"
//方法二   如果不兼容使用改造方法
if (!String.prototype.replaceAll) {
  String.prototype.replaceAll = function (newStr, oldStr) {
    return this.replace(new RegExp("oldStr", "g"), "newStr");
  };
}

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

一、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 双精度浮点数标准,最大整数和最大的数字

基础:

  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

常见简单的知识点

在编程中,?? 是一个 空值合并运算符(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 报错,背后竟是分号惹的祸”

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

在学习 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) 复杂度,手撕给你看!

🧠 系列前言:

面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 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:谁才是轻量级框架的真命天子?

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 异步更新数据,在异步回调调用成功后再更新值

场景描述

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

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笔记

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!今天我要和大家分享一个前端开发中非常经典的话题——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 也被裁

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 样式计算与视觉格式化模型详解

前端渲染基础: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库细节

概述

常见的组件库,业务工程项目,都会用到各式各样的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

❌