阅读视图

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

前端高频面试题之CSS篇(二)

1、如何实现两栏布局?

两栏布局指的是左边宽度固定,右边宽度自适应

DOM 结构如下:

<body>
  <div class="box">
    <div class="left"></div>
    <div class="right"></div>
  </div>
</body>

1.1 利用 flex 布局实现

实现思路:将父元素设为 flex 布局,左边元素宽度固定,右边元素设为 flex: 1,即自适应。

.box {
  display: flex;
  width: 500px;
  height: 100px;
}
.left {
  width: 200px;
  background: yellow;
}
.right {
  flex: 1;
  background: green;
}

优点:布局灵活、响应式布局。

缺点:IE9 及以下不支持。

1.2 利用 float 布局实现

实现思路:将左边元素设置为浮动元素 float: left,右边元素用 margin-left,这样能让右边元素占据父元素剩余宽度。

.box {
  width: 500px;
  height: 100px;
}

.left {
  float: left;
  width: 200px;
  height: 100%;
  background: yellow;
}

.right {
  margin-left: 200px;
  height: 100%;
  background: green;
}

优点:简单、支持 IE。

缺点:浮动易导致问题(如高度塌陷),不适合复杂布局。

1.3 利用 Grid 布局实现

实现思路:将父元素设为 grid 布局,并设置 grid-template-columns: 200px 1fr 即可。

.box {
  display: grid;
  grid-template-columns: 200px 1fr;
  width: 500px;
  height: 100px;
}

.left {
  background: yellow;
}

.right {
  background: green;
}

优点:二维布局强大,实现多栏布局十分方便。

缺点:IE9 及以下不支持,不适合一维布局。

1.4 利用 position 绝对定位实现

实现思路:父级为相对定位,右边子元素为绝对定位,并同时设置 left、right、top、bottom 值,以实现宽高的自动拉伸。

.box {
  position: relative;
  width: 500px;
  height: 100px;
}

.left {
  width: 200px;
  height: 100px;
  background: yellow;
}

.right {
  position: absolute;
  left: 200px;
  right: 0;
  top: 0;
  bottom: 0;
  background: green;
}

优点:可以精确控制位置。

缺点:脱离文档流,响应式差。

2、如何实现三栏布局?

三栏布局指的是页面分为三栏,左侧和右侧固定宽度,中间自适应。

DOM 结构如下:

<body>
  <div class="box">
    <div class="left"></div>
    <div class="center"></div>
    <div class="right"></div>
  </div>
</body>

2.1 利用 flex 布局实现

.box {
  display: flex;
  width: 500px;
  height: 100px;
}

.left {
  width: 100px;
  background: yellow;
}

.center {
  flex: 1;
  background: pink;
}

.right {
  width: 100px;
  background: green;
}

2.2 利用 float 布局实现

.box {
  width: 500px;
  height: 100px;
}

.left {
  float: left;
  width: 100px;
  height: 100px;
  background: yellow;
}

.center {
  height: 100px;
  margin: 0 100px;
  background: pink;
}

.right {
  float: right;
  width: 100px;
  height: 100px;
  background: green;
}

这里需要注意,中间栏的 DOM 需要放在最后,以避免浮动元素影响,所以其 DOM 结构如下:

<div class="box">
  <div class="left"></div>
  <div class="right"></div>
  <div class="center"></div>
</div>

2.3 利用 grid 布局实现

.box {
  display: grid;
  grid-template-columns: 100px 1fr 100px;
  width: 500px;
  height: 100px;
}

.left {
  background: yellow;
}

.center {
  background: pink;
}

.right {
  background: green;
}

2.4 利用 position 绝对定位实现

.box {
  position: relative;
  width: 500px;
  height: 100px;
}

.left {
  position: absolute;
  left: 0;
  width: 100px;
  height: 100px;
  background: yellow;
}

.center {
  margin: 0 100px;
  height: 100px;
  background: pink;
}

.right {
  position: absolute;
  right: 0;
  top: 0;
  width: 100px;
  height: 100px;
  background: green;
}

2.5 经典三栏布局之圣杯布局

圣杯布局实现三列布局左右列固定宽度、中间列自适应,其原理是通过相对定位和负边距来实现侧边栏的定位。

<style>
  .box {
    padding: 0 150px 0 200px;
  }

  .wrapper::after {
    display: table;
    content: '';
    clear: both;
  }

  .column {
    float: left;
    height: 200px;
  }

  .left {
    width: 200px;
    position: relative;
    margin-left: -100%;
    right: 200px;
    background-color: aqua;
  }

  .center {
    width: 100%;
    background-color: red;
  }

  .right {
    width: 150px;
    margin-right: -150px;
    background-color: green;
  }
</style>
<body>
  <div class="box">
    <!-- 中间列 center 放第一个是为了在文档流中优先渲染,因为 DOM 是从上往下依次渲染的-->
    <div class="center column">center</div>
    <div class="left column">left</div>
    <div class="right column">right</div>
  </div>
</body>

2.6 经典三栏布局之双飞翼布局

双飞翼布局实现三列布局左右列固定宽度、中间列自适应,其原理是通过使用嵌套的 div 元素来实现侧边栏的定位,以及使用负外边距将主内容区域撑开。

<style>
  .box {
    background-color: red;
    width: 100%;
  }

  .column {
    float: left;
    height: 200px;
  }

  .center {
    margin: 0 150px 0 200px;
  }

  .left {
    width: 200px;
    background-color: aqua;
    margin-left: -100%;
  }

  .right {
    width: 150px;
    background-color: green;
    margin-left: -150px;
  }
</style>
<body>
  <div class="box column">
    <div class="center">center</div>
  </div>
  <div class="left column">left</div>
  <div class="right column">right</div>
</body>

3、如何实现水平垂直居中?

3.1 文本类可以使用 line-heighttext-align

.box {
  width: 100px;
  height: 100px;
  line-height: 100px;
  text-align: center;
}

3.2 使用 flex 布局

.box {
  display: flex;
  justify-content: center;
  align-items: center;
}

3.3 使用绝对定位 postion + transform/负 margin

.parent {
  position: relative;
}
.child {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

当元素固定宽度时,也可以用 负 margin 替代 transform

.parent {
  position: relative;
  width: 200px;
  height: 200px;
  background-color: red;
}

.child {
  width: 50px;
  height: 50px;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-top: -25px; /* 自身容器高度的一半 */
  margin-left: -25px; /* 自身容器宽度的一半 */
  background-color: green;
}

3.4 使用 absolute + margin: auto

.box {
  position: absolute; 
  inset: 0; 
  margin: auto
}

3.5 使用 Grid 布局

.box {
 display: grid; 
 place-items: center;
}

3.6 使用 table 布局

.box {
 display: table-cell;
 vertical-align: middle;
}

4、实现一个三角形

4.1 利用 border

在 CSS 中,实现三角形最常见的方法是利用元素的**边框(border)**属性。通过设置元素的宽度和高度为 0,然后调整边框的宽度和颜色,可以形成各种方向的三角形。

.triangle {
  width: 0;
  height: 0;
  border-left: 10px solid transparent;  /* 左透明 */
  border-right: 10px solid transparent; /* 右透明 */
  border-bottom: 30px solid red;       /* 底有颜色,形成向上三角 */
}

4.2 使用 clip-path

.triangle {
  width: 100px;
  height: 100px;
  background: purple;
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);  /* 向上三角 */
}

5、实现一个扇形

在上面画三角形的基础上,再增加样式 border-radius: 100%;即可实现一个扇形。

.sector {
  width: 0;
  height: 0;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 30px solid red;
  border-radius: 100%; /* 增加 border-radius */
}

6、实现一个梯形

.box {
  width: 200px;
  height: 60px;
  position: relative;
  margin: 32px auto;
  font-size: 60px;
  text-align: center;
}

.box::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: red;
  transform: perspective(.5em) rotateX(5deg);
}

7、画一条 0.5px 的线

对于大部分普通屏幕来说,像素是最小的显示单位,因此定义 0.5px 是没有意义的,浏览器遇到小于 1px 时会向上取整渲染 1px,这个是浏览器渲染的局限性。

但是在一些高分辨率屏幕(如 Retina 屏幕)上,1 物理像素点可以被分成多个虚拟像素(比如 2x 屏幕将每个物理像素分为 4 个虚拟像素)。这样的话,1px 的物流像素就等于 2 个虚拟像素,所以 0.5px 也能被渲染出来。

所以对于普通屏幕来说,我们需要做一些兼容方案来渲染出 0.5px 的效果。

我们可以使用 CSS transform 缩放法 来实现 0.5px 的线,代码如下:

.line {
  height: 1px;
  background-color: red;
  transform: scaleY(0.5); /* 对高度进行垂直方向的缩放 */
}

小结

上面是整理的前端面试关于 CSS 高频考察的布局和图形,如有错误或者可以优化的地方欢迎评论区指正。

老司机 iOS 周报 #359 | 2025-12-01

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

文章

🌟 🐕 从「写代码」到「验代码」:AI 搭档写走 3 年,我踩出来的协作路线图

@JonyFang: 这篇文章给我的最大启发是:AI Coding 的价值,不在于让你更快写代码,而在于让你反思、重构「整个工程流程/协作方式」。

把 AI 当成一台「超级写代码机」很容易:但如果你这样想,就容易陷入“生成–调试–纠错–折腾–没效率”的恶性循环。相反,把 AI 当成「半自动化助理 + 智能实习生」,并在团队层面投入标准化 + 自动化 + 质量流程建设 -- 才是值得的、可持续的生产力提升路径。

🐕 Open source case study: Listening to our users

@Barney:文章强调了开源软件相较于 Apple 官方框架的核心优势:与用户的紧密互动。通过 SQLiteData 库的三个案例,用户驱动的功能定制、社区贡献的工具改进、快速修复的共享 bug,充分说明了活跃维护者的价值所在。开源库能在数小时到数周内响应用户需求,无需等待 WWDC 或新版本发布,这种敏捷迭代的开发模式,正是第三方库相比官方框架最值得信赖的地方。

🐎 A deep dive into notifications and messages on iOS 26

@极速男孩:这篇文章分析了 iOS 26 的新消息(Message)API。它作为传统 NotificationCenter 的替代品,提供了编译时的类型安全和并发安全保证,解决了旧 API(如闭包式)易出错的问题。

文章详细对比了新旧 API,并重点介绍了新 API(如 MainActorMessage)的优势。一个关键特性是,新旧 API 可以“桥接”互通,允许开发者逐步迁移。尽管新 API 在观察时获取具体发送者上有限制,但仍是未来的首选方案。

🐎 When To Kill A Project

@含笑饮砒霜:这篇文章是《War Stories》系列访谈的一部分,聚焦 iOS 领域知名开发者戴夫・弗沃(Dave Verwer)30 年职业生涯中的项目经验,核心围绕 “何时该终止一个项目” 展开,通过多个真实案例分享了成功与失败带来的关键启示。戴夫的经验本质上围绕 “理性决策” 展开:无论是失败的项目(及时止损、诚信负责)、误判市场的项目(重视真实需求调研),还是完成使命的成功项目(主动退出),核心都是 “聚焦有价值的部分,砍掉无效投入”。同时,信任、社区协作和对行业趋势的预判,也是项目决策中不可或缺的因素。

🐢 How to Build Scalable White-Label iOS Apps: From Multi-Target to Modular Architecture

@AidenRao:白标 iOS 应用是一个可重用的应用模板,可以针对多个客户 / 品牌进行定制和重新包装。本文探讨了 iOS 白标 App 的架构演进之路,从多 Target 方案的维护困境出发,深入讲解了如何借助模块化思想,通过有效的依赖与配置管理,构建一个更具可维护性与可扩展性的统一代码库。

🐎 Task Identity

极速男孩:文章指出 SwiftUI .task 的常见陷阱:它默认只在视图出现时运行,不会自动响应属性变化。若 View 的入参更新,Task 不会重跑,导致数据不同步。解决方案是使用 .task(id: 依赖项)。通过显式绑定依赖(如 url),当值变化时,SwiftUI 会自动取消旧任务并重启新任务,确保副作用逻辑与最新状态保持一致。

🐎 Pitfalls of Parameterized Tests

@david-clang:本文分享了 Swift Testing 中参数化测试的五大常见陷阱,并提出了关键的最佳实践。其核心思想是:应在测试数据中预先建立清晰的“输入-输出”映射关系,并使用预定义的静态值作为期望结果,从根本上避免测试逻辑与实现逻辑的耦合。

🐎 ScrollView snapping in SwiftUI

@DylanYang:作者向我们介绍了在 SwiftUI 中如何通过设置 scrollTargetBehavior 来调整 ScrollView 滑动的目标位置,除了我们在 UIKit 中熟知的按页滑动 .paging 选项外,还有 .viewAligned 选项允许我们按照 view 的尺寸来决定滑动的终点。文中有较多动图展示,感兴趣的读者可以阅读本文了解更详细的信息。

🐎 Building Peer-to-Peer Sessions: Advertising and Browsing Devices

@Damien:文章详解用 Multipeer Connectivity 框架实现 iOS 近场通信:配置权限、初始化 PeerSessionManager,用 MCNearbyServiceAdvertiser 广播自身并自动接受连接,同时用 MCNearbyServiceBrowser 发现与维护设备列表,附完整 Swift 源码展示如何整合广告、浏览、会话管理等功能,实现设备间的加密点对点通信。

工具

Mole:像鼹鼠一样深入挖掘来清理你的 Mac

@EyreFree:Mole 是一款面向 macOS 系统的工具,可以大概理解为是 CleanMyMac + AppCleaner + DaisyDisk + Sensei + iStat 的聚合,主要作用如下:

  • 系统状态监控:通过 mo status 命令提供交互式仪表盘,实时展示 CPU、内存、磁盘、网络等系统关键指标,支持 Vim 风格导航操作;
  • 系统清理功能:可安全清理系统缓存,提供 --dry-run 预览模式和白名单管理功能,降低误删风险,还支持通过 mo touchid 启用 Touch ID 授权 sudo 操作;
  • 磁盘分析能力:借助 mo analyze 命令分析磁盘占用情况,帮助识别大文件和缓存条目,便于用户释放存储空间。

对于需要管理 macOS 系统资源、进行系统清理或监控的团队成员来说,Mole 是一个实用的工具选择。

🐎 30 分钟解决 Claude 封号问题:程序员的终极自救指南

@阿权:文章详细介绍如何通过自建 VPS 解决 Claude 封号问题,包含完整的服务器搭建、客户端配置和开发工具设置步骤,让你稳定使用 Claude Code 等 AI 开发工具。

关键解决的痛点是:IP 不纯净。使用公共代理(机场 / VPN)易触发 Claude 风控封号。解决思路也很简单:自建 VPS。

代码

🐎 MachOSwiftSection: 🔬 A Swift library for parsing mach-o files to obtain Swift information.

@Kyle-Ye: MachOSwiftSection 是一个用于解析 Mach-O 文件并提取 Swift 元数据信息的 Swift 库。它基于 MachOKit 扩展实现 , 可以从编译后的二进制文件中提取协议描述符(Protocol Descriptors)、协议遵循关系(Protocol Conformance)和类型上下文描述符(Type Context Descriptors)等核心信息。该库同时提供了 Swift 包和命令行工具 swift-section,支持对二进制文件进行多架构分析和信息导出。对于需要进行逆向工程、安全分析或从编译产物生成 Swift 接口文件的开发者来说,这是一个实用的底层工具。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

  • [北京/上海] 京东 - iOS/Android/鸿蒙/前端

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

央行:10月债券市场共发行各类债券63574.6亿元

央行发布2025年10月份金融市场运行情况。 10月份,债券市场共发行各类债券63574.6亿元。国债发行11695.5亿元,地方政府债券发行5604.7亿元,金融债券发行8010.8亿元,公司信用类债券1发行11836.2亿元,信贷资产支持证券发行343.4亿元,同业存单发行25649.0亿元。(证券时报)

央行:10月银行间债券市场现券成交26.6万亿元

央行发布2025年10月份金融市场运行情况。10月份,银行间债券市场现券成交26.6万亿元,日均成交1.5万亿元,同比增加10.2%,环比增加3.9%。单笔成交量在500-5000万元的交易占总成交金额的48.06%,单笔成交量在9000万元以上的交易占总成交金额的45.68%,单笔平均成交量4177.69万元。交易所债券市场现券成交3.3万亿元,日均成交1937.9亿元。商业银行柜台债券成交6.6万笔,成交金额587.3亿元。(e公司)

小米汽车:11月新增17家门店

小米汽车11月30日公布开店新进展:11月新增17家门店,全国131城已有441家门店;12月计划新增36家门店,预计覆盖衡阳、绵阳等7座城市;截止11月30日,全国已有249家服务网点,覆盖全国144城。(界面)

安康记1.1.x版本发布

安康记新版本已经上线App Store。目前已经更新了独立用药功能,方便一些简单的小毛病可能会自己用药,可以跳过就诊直接进行记录。

图片

原本这个功能已经上线了有几天了,但因为一直测试发现有一些新的问题,这两周也提交了多次修复版本,目前算是相对稳定了,希望各位能更新体验♪(・ω・)ノ

JavaScript新手必看系列之预编译

前言

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

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

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

What's that?

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

预编译的两种场景

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

分三步:

1.创建 GO(Global Object)

  • 在浏览器中就是 window 对象

  • 在 Node.js 中就是 global 对象

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

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

举个简单的例子:

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

发什么了什么?

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

执行过程如下:

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

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

函数较为复杂,四步:

  1. 创建AO(Activation Object)

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

  3. 将实参赋给形参

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

举例:

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

干了什么?

  1. 创建AO = {

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

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

3.将实参赋给形参

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

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

AO = { a : function a() {}

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

函数体内执行过程:

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

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

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

swift的inout的用法

基础用法底层原理高级特性注意事项四个方面详细讲解。

1. 基础概念:为什么要用 inout?

在 Swift 中,函数的参数默认是常量(Constant/let) 。这意味着你不能在函数内部修改参数的值。

错误示例:

func doubleValue(value: Int) {
    value *= 2 // ❌ 报错:Left side of mutating operator isn't mutable: 'value' is a 'let' constant
}

如果你希望函数能修改外部传进来的变量,就需要使用 inout

正确示例:

func doubleValue(value: inout Int) {
    value *= 2
}

var myNumber = 10
// 调用时必须在变量前加 '&' 符号,显式表明这个值会被修改
doubleValue(value: &myNumber) 

print(myNumber) // 输出:20

2. 核心原理:输入输出模型 (Copy-In Copy-Out)

这是面试或深入理解时最重要的部分。虽然 inout 看起来像“引用传递”,但 Swift 官方将其描述为 Copy-In Copy-Out(输入复制,输出复制) ,也就是“值结果模式(Call by Value Result)”。

完整过程如下:

  1. Copy In(输入复制): 当函数被调用时,参数的值被复制一份传入函数内部。
  2. Modification(修改): 函数内部修改的是这个副本
  3. Copy Out(输出复制): 当函数返回时,修改后的副本值被**赋值(写回)**给原本的变量。

底层优化:

  • 对于物理内存中的变量:编译器通常会进行优化,直接传递内存地址(也就是真正的引用传递),避免不必要的复制开销。
  • 对于计算属性(Computed Properties) :必须严格执行 Copy-In Copy-Out 流程(因为计算属性没有物理内存地址,只有 getter 和 setter)。

代码证明(计算属性也能用 inout):

struct Rect {
    var width = 0
    var height = 0
    
    // 计算属性:面积
    var area: Int {
        get { width * height }
        set { 
            // 简单逻辑:假设保持 width 不变,调整 height
            height = newValue / width 
        }
    }
}

func triple(number: inout Int) {
    number *= 3
}

var square = Rect(width: 10, height: 10) // area = 100

// 这里传入的是计算属性 area
// 流程:
// 1. 调用 area 的 get,得到 100,Copy In 给 triple
// 2. triple 将 100 * 3 = 300
// 3. 函数结束,将 300 Copy Out,调用 area 的 set(300)
triple(number: &square.area)

print(square.height) // 输出:30 (因为 300 / 10 = 30)

3. inout 的常见应用场景

A. 交换值 (Standard Swap)

Swift 标准库的 swap 就是用 inout 实现的。

func mySwap<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 1
var y = 2
mySwap(&x, &y)
print("x: (x), y: (y)") // x: 2, y: 1

B. 修改复杂的结构体 (Mutating Structs)

当结构体嵌套很深时,使用 inout 可以避免冗长的赋值代码。

struct Color {
    var r: Int, g: Int, b: Int
}

struct Settings {
    var themeColor: Color
}

var appSettings = Settings(themeColor: Color(r: 0, g: 0, b: 0))

// 能够直接修改嵌套深处的属性
func updateBlueComponent(color: inout Color) {
    color.b = 255
}

// 传入路径
updateBlueComponent(color: &appSettings.themeColor)

print(appSettings.themeColor.b) // 255

4. 关键规则与内存安全 (Memory Safety)

这是 Swift 相比 C++ 指针更先进的地方。Swift 编译器会强制执行独占访问权限(Law of Exclusivity) ,防止内存冲突。

规则 1:同一个变量不能同时作为两个 inout 参数传递

如果两个 inout 参数指向同一个变量,会发生“别名(Aliasing)”问题,导致行为不可预测。

var step = 1

func increment(_ number: inout Int, by amount: inout Int) {
    number += amount
}

// ❌ 运行时崩溃或编译错误:Simultaneous accesses to 0x...
// increment(&step, by: &step) 

规则 2:不能将 let 常量或字面量作为 inout 参数

因为它们本质上不可写。

Swift

func change(val: inout Int) {}

// change(val: &5) // ❌ 错误:字面量不可变
let num = 10
// change(val: &num) // ❌ 错误:常量不可变

规则 3:inout 参数在闭包中的捕获(Capture)

inout 参数在逃逸闭包(Escaping Closure)中是不能被捕获的,因为逃逸闭包可能在函数返回后才执行,而那时 inout 的生命周期(Copy-In Copy-Out 过程)已经结束了。

func performAsync(action: @escaping () -> Void) {
    // 异步执行...
}

func badFunction(x: inout Int) {
    // ❌ 错误:Escaping closure captures 'inout' parameter 'x'
    /*
    performAsync {
        x += 1 
    }
    */
}

解决办法: 使用非逃逸闭包,或者显式地捕获变量的副本(如果逻辑允许)。


5. inout vs 类 (Reference Types)

这是一个常见的误区: “类本来就是引用类型,还需要 inout 吗?”

  • 不需要 inout 如果你只想修改类实例内部的属性。
  • 需要 inout 如果你想替换掉整个类实例本身(即改变指针的指向)。

代码对比:

class Hero {
    var name: String
    init(name: String) { self.name = name }
}

// 情况 1:修改内部属性(不需要 inout)
func renameHero(hero: Hero) {
    hero.name = "Batman" // 合法,因为 hero 引用本身没变,变的是堆内存里的数据
}

var h1 = Hero(name: "Superman")
renameHero(hero: h1)
print(h1.name) // Batman

// 情况 2:替换整个实例(需要 inout)
func switchHero(hero: inout Hero) {
    hero = Hero(name: "Iron Man") // 将外部变量指向全新的内存地址
}

var h2 = Hero(name: "Spiderman")
switchHero(hero: &h2)
print(h2.name) // Iron Man

总结

  1. 语法: 定义用 inout,调用用 &
  2. 本质: Copy-In Copy-Out(值结果模式),但在物理内存操作上通常优化为引用传递。
  3. 使用场景: 需要在函数内部修改外部值类型(Struct/Enum)状态,或交换数据。
  4. 限制: 遵守独占访问原则(Exclusivity),不可在逃逸闭包中捕获。

江西铜业:拟收购境外上市公司SolGold Plc股份,目前仍处非正式要约阶段

11月30日,江西铜业(600362.SH)公告称,公司已向伦敦证券交易所上市公司SolGold Plc提交两项非约束性现金要约,最新一项非约束性现金要约拟以每股26便士的价格收购其全部股份。目前公司持有目标公司12.19%的股份,但该收购事宜仍处于非正式要约阶段,且已被目标公司董事会拒绝。公司保留提出正式要约的权利,但具体是否提出存在不确定性。根据英国《城市收购及合并守则》,公司需在2025年12月26日前发布正式要约公告或明确放弃收购的公告。(每经网)

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

前言

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

1. BroadcastChannel是什么?

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

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

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

2. 如何使用

BroadcastChannel的使用流程如下:

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

2.1 创建频道(主题)

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

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

2.2 订阅消息(监听)

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

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

2.3 发布消息(发送)

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

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

2.4 退订(关闭)

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

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

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

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

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

3、实践场景

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

3.1 父子通讯

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

接收输入如下:

image.png

4. 总结

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

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

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

1. buttonKls 的作用

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

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

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

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

2. 颜色系统的触发机制

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

3. 完整的关系链

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

4. 在 CSS 中的对应关系

button.scss 中的定义:

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

$type = 'primary' 时:

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

5. 实际渲染过程

步骤 1:Vue 组件生成类名

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

步骤 2:CSS 匹配类名

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

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

步骤 3:应用颜色

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

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

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

6. 关键理解

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

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

7. 不同类型的关系

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

8. 完整示例

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

生成的类名:

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

最终 HTML:

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

CSS 匹配:

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

9. 总结

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

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

沪电股份:向香港联交所递交H股发行上市申请并刊发申请资料

沪电股份(002463.SZ)公告称,已于2025年11月28日向香港联交所递交了发行H股股票并在香港联交所主板挂牌上市的申请,并于同日在香港联交所网站刊登了本次发行上市的申请资料。该申请资料为草拟版本,其所载资料可能会适时作出更新和变动。公司本次发行上市尚需取得相关政府机关、监管机构、证券交易所的批准、核准或备案。(财联社)

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

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

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

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

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

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

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

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

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

对应代码与图示:

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

1.jpg

2.jpg

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

  2. 执行阶段:

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

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

对应代码与图示:

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

3.jpg

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

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

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

案例:闭包的形成与运行

对应代码与图示:

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

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

5.jpg

6.jpg

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

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

  3. 调用bar.setName/getName时

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

四、核心总结

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

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

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

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

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

scss知识点:$ 表示变量

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

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

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

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

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

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

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

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

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

使用

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

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


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

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

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

三、生成 CSS 变量(全局)

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

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

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

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

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

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

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

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

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

:root {
  color-scheme: light;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

六、完整流程图

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

七、优势

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

赣锋锂业:赣锋国际拟向中非基金发行1亿美元可交换票据

赣锋锂业(002460.SZ)公告称,公司全资子公司赣锋国际拟向中非发展基金有限公司发行总额为1亿美元的可交换票据,中非基金可根据票据金额及应付利息转换成赣锋国际全资子公司MaliLithiumB.V.的A类优先股。公司为本次交易提供担保,并授权经营层办理相关事宜。(财联社)

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

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


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

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

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

.hero-section {
  height: 100vh;
}

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

1.jpg

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

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

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

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

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

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


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

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

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

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

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

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

1.2 浏览器的「摆烂」选择

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

2.jpg

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

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

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

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

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


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

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

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

3.jpg

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

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

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

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

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

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

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

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

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

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

三态哲学总结:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

4.2 关键交互区域用 svh

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

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

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

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

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

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


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

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

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

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

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

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

三百多万人围观的 AI 油画视频,是技术的神作,还是没有灵魂的电子垃圾

「比蒙娜丽莎更美的,就是正在燃烧的蒙娜丽莎」,这是多年前一档辩论节目里,大家对于艺术价值的不同理解方式,那时听到可能觉得挺激进,笑一笑便过了。

最近一段把几幅经典油画「复活」的 AI 视频,在 X 上引起了巨大的争议,视频刷到 300 多万播放,被不少人称之为栩栩如生的艺术;比经典油画作品更好看的,是会流动的油画?AI 做的东西是不是没有任何艺术价值?

网友分享的油画视频里,经典的油画元素不再静止,颜料开始流动,天空的云朵、火山的喷发、还有海浪的汹涌都变得生动自然,仿佛那些存在几百年前的画布,突然拥有了生命一样。

乍看之下,这就是一场视觉盛宴的享受;如果不是其中几个视频,忘了去掉右下角 Google Veo 视频生成的水印,甚至会觉得完全是用 CG 特效制作实现,毕竟对油画风格来说,没有很明显的「AI 味」。

但是点开评论区之后,发现网友们撕成了两派,有人说,这就是新时代的艺术,是全新的审美体验;有人就不买单,用 AI 时代最刻薄的词汇——Slop(垃圾/泔水)一言蔽之,说等到 AI 有意识了,再来谈配不配成为艺术。

同样的一条 AI 视频,让人看到了艺术、技术、恐惧、愤怒、敬畏、厌烦,还有时代变化。

如果不说这是 AI,你的第一反应是

X 网友发布的这则视频,其实并不是他本人的原创,在评论区有人指出来,说他没有标注视频来源,也没说明使用了 AI,只是单纯地为了赚取流量。现在这波流量,也确实是被他赚到了。

视频最早是出现 YouTube 上,一位有着 2000 多粉丝的博主@bandyquantguy,他是宾夕法尼亚州立大学艺术与建筑学院的一名助理教学教授。频道内发布的内容,基本上都是不同油画的动态视频作品,长度在一分半到 3 分钟不等。

而那条被转发到 X 上的视频,并收获了三百多万的观看,是他将近一个月之前的作品。

当我看着满屏的动态油画时,说实话,根本没想到这是否通过 AI 生成。一方面是对油画艺术的不了解,是否有相关的技术,或者爱好者在专门做类似的工作。另一方面,大多数时候,我们所说的 AI 味,主要是在制作一些写实的画面,像现实世界、真人版等。而这种风格化本身就足够突出的内容,AI 的处理反而不会太突兀。

有网友评价,这是他见过最原汁原味的 AI 艺术作品之一,因为视频没有生硬地模仿现实,而是创造了一种介于梦境,与现实之间的流体美感。

第一眼都是觉得「震撼」,而这种迎面而来的视觉冲击,在知道它是 AI 生成的之后,也变成了争议的起点。大多数的人会觉得,这样的作品很棒;但对剩下一部分人来说,如果光靠 AI 就能得到原本属于「艺术」的内容,那该有多可怕。

Slop,AI 是原罪

所以,打压和看衰,成了评论区的另一种态度,Slop 就是代表性的关键词。

在 AI 语境下,Slop 指的是通过 AI 大量生成的、看似有内容实则空洞的劣质品。像是之前奥特曼推出 Sora,就有人犀利的丢下断言,Sora 生成的视频,全部都是 AI Slop。

这次,也有网友说,这样的油画视频,不应该放在社交媒体上,Sora 才是他最好的归宿,那里都是同样的 AI 垃圾。为什么画面如此精美的视频,会被称为 Slop?反而一些 AI 味明显的视频,激不起大家这么热烈的反馈。

因为它是机器盲目的困境。

反对者认为,AI 生成的内容,是缺乏意义,就像是一台机器盲目的梦境,它甚至不知道自己正在做梦。

他们的观点是,艺术不仅仅是停留在好看,那只是媚俗。艺术需要人类的意图、深度和复杂性。在这个视频里,原本油画的笔触,变成了毫无逻辑的像素流动,就像是单纯地为了展示「我能动」而动,没有任何节奏、理由或概念支撑这个视频的内容。

对他们来说,AI 最大的问题从来不是不够好看,而是「不够人」

AI 的每一次选择,只不过是概率。而人类创作一个作品,画一幅油画,背后包含的是对人生、对世界的思考和回应;有情感、时间、技巧、犹豫和失败等经历。

支持者觉得,现在的 AI,就像相机刚被发明时一样,不是在替代传统,而是在扩张想象力。甚至有网友说,「我想艺术家们在作画前,脑海中可能就有这样的画面,现在我们也能走进他们的灵感了。

是结果,还是过程重要

如果这个视频是一个人类艺术家,花费 1000 个小时,一帧一帧手绘出来的,评论区又会发生什么。除了震撼,大概还是一样,会有人说,这视频顶多用来作为我的手机屏保,除了好看也就仅此而已了。

艺术是主观觉得还是客观认定呢,其实都没有明确的界定。网友的期待,大概是希望,艺术应该是需要「努力」才能抵达的地方,而 AI 正在稀释「努力」在艺术中的价值。

前段时间,一幅名为《太空歌剧院》的画作,拿到了艺术比赛的头奖,还有 AI 画作甚至在拍卖市场,以十万、百万的价格被拍走。

我们在一个输入提示词就能生成图像的世界里,任何一个人不需要复杂的技巧,也不需要付出多少汗水,都有机会创作自己的作品,作品的意义也不再靠时间来定义。

▲提示词:将油画纹理动画化为粘稠流体模拟,厚重的颜料笔触融化并流动。旋转的天空、粘稠的黄色光芒、翻腾的蓝色云朵。

当 AI 把时间成本压缩到几秒钟,这种神圣感瞬间崩塌,剩下的就只有廉价。「这不难做吧」、「我用 Veo 3 也能生成」,这样的东西,自然就成不了艺术了。

更有趣的是,有网友提到,这是一种很明显的社会心理学现象,巴浦洛夫的狗。现在我们只要看到 AI 的标签,就有了条件反射,瞬间进入狂怒模式,无论作品本身好坏,一律打成 Slop。

具体来说,就是眼睛告诉我,这个视频还不错,但大脑告诉我,这是 AI,AI 做的都是不好的,为了调和这种矛盾,我就必须强行说服自己,它看起来很糟糕。

心理效应是存在,更多的我想其实还是,AI 内容的泛滥,正在把我们的审美阈值无限拔高。

没有 AI 的时候,让一幅油画像这样动起来,可以说是「魔法」一样的存在。现在如果这些作品没有极强的故事性,或情感内核,仅仅是视觉特效,已经很难打动被各种 AI 效果喂饱的我们。

▲ YouTube 上该博主的其他油画视频

无论评论区吵得多么不可开交,一个事实是无法改变:AI 不会消失,艺术也不会因为 AI 的出现而消失。

就像一些网友说的,「电力曾让蜡烛工厂破产,但人类具有适应性」。现在的混乱,也许只是新旧审美体系,交替时发生的阵痛。

如果在 100 年前我们按一下播放键,就能听到录好的歌,大概也会有人觉得,只有黑胶唱片出来的声音才是真音乐;现在我们只是习惯了,现场、黑胶、手机、音响都有好音乐。

艺术,从来看的是最终的愿景,是我想让你看到什么,而不是用了什么工具,我花了多久才做出来。一个活过来的 AI 艺术,就算是简单的几行提示词,一样倾注了真正属于创作者的叙事、情感与意图。

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

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


❌