阅读视图
老铺周大福宣布上调金饰价格,市场数据显示老铺开年调价近30%
俞永福的一句话促成一个 IPO
作者丨黎曼
编辑丨王庆武
来源丨东四十条资本
二级市场AI的狂热直接传导至一级市场,形成了AI企业上市的“窗口期红利”。
更为关键的一点是,资本市场对AI的关注点也开始发生转移,比如从“谁的模型更大”转向“谁能把AI变成可持续现金流”。
眼下,便有一家来自广州的AI应用层公司勇闯港交所。2026年2月底,钛动科技向港交所递表,冲击港股“Multi-Agent(营销多智能体)第一股”。钛动科技此时递表,有希望抢在热度消退前完成IPO定价。
更值得关注的是财务层面,钛动科技已实现持续盈利且毛利率稳定在82%以上,这种少见的高利润AI企业在当前市场环境下也具有稀缺性。
IPO前,钛动科技的投资方包括险峰、IDG资本、金沙江联合资本、人人游戏、钟鼎资本、温氏投资、新犁资本、头头是道基金等。
俞永福的一句话促成一个IPO
钛动科技创始人李述昊是典型的“大厂出走者”,是1989年出生的山东人,2011年从天津大学电子科学与技术专业毕业后,在华为待了一年,便加入UC浏览器海外版,成为第一批做海外市场的“拓荒者”。2013年,他把UC浏览器做到印度市场第一名,干翻了谷歌——这段经历让他日后回忆起来,仍然带着少年意气。
2014年,UC被阿里巴巴全资收购,李述昊随之进入阿里体系,负责支付宝、速卖通的海外投放。那是中国互联网出海的黄金年代,他成了最早一批接触数字化出海的人。但在一次采访中,他说自己心里一直有个念头:“代表中国队出战”,创建一家千亿美元级别的公司。
2016年,他带着UC被收购后变现的股票,决定离开。那一年他27岁,前程未卜。创业初期,他尝试过几款工具类产品,都不顺利。
转折源自前领导俞永福。这位原阿里大文娱、本地生活的掌舵者建议他把目光投向To B营销。“在俞永福的建议下”这个表述,在后来多家媒体的报道中被反复提及。
2017年,钛动科技在广州成立,李述昊的愿景清晰起来:“用AI抹平文化差异,用商业智能抹平经验差异”。
在出海浪潮袭来之际,李述昊观察到,企业出海其实存在两大痛点:安全感与体验感。企业受语言文化、消费习惯、商业环境等限制,国内企业对海外市场认知有一定局限,对海外媒体、投放等营销环节更是相对陌生。随着品牌出海竞争愈发激烈,加上政策、平台限制等影响,仅靠人力或传统营销工具已不能满足快速增长的出海企业营销需求。
因此,钛动科技专注于为全球商业增长提供效果导向营销解决方案的AI营销。公司以自研的“钛极”多模态大模型为技术底座,通过AI原生、效果驱动的产品组合为出海企业提供从市场洞察、内容生成到广告投放与效果优化的端到端服务。
用大白话说,假如你是一家中国App或品牌想开拓海外市场,但不懂当地语言、文化和广告渠道,钛动科技就用他们自己开发的AI系统,帮你自动生成符合当地人口味的广告、精准投放到Facebook等海外平台,并实时优化投放策略。传统广告代理要花一个月人工试错的事,他们用AI几小时就能搞定。
历经八年,这家公司服务着超过10万家广告主,头部客户覆盖率超过80%,业务覆盖全球200多个国家和地区。阿里巴巴、字节跳动、斯凯奇、361°、三七互娱……这些名字出现在钛动科技的客户名单上。
而俞永福的那句话,最终也变成了一份具体的股权——通过Redefine Capital HK,他持有钛动科技11.27%的股份,位列第二大机构股东。
金沙江、IDG、险峰也投了
钛动科技的股东名单,拿出来的是一份属于中国互联网投资圈的“豪华套餐”。
2017年公司成立仅两个月,险峰、IDG资本、人人游戏投出1440万元天使轮。在后续的轮融资中,钟鼎资本、温氏投资、新犁资本、头头是道基金、金沙江联合资本相继入局。
但资本的盛宴里也有提前离席者。据报道,粤财投资等多个股东在公司IPO前已经通过老股转让、企业方回购等方式实现了退出。
招股书披露的股权结构显示:IPO前,李述昊通过直接持股及多层合伙企业合计控制公司46.74%的股份,为控股股东。机构股东中,钟鼎资本合计持股12.90%,为最大外部机构股东;俞永福的Redefine Capital HK持股11.27%,位列第二;新犁资本持股9.79%;险峰实体合计持股8.08%;IDG资本通过珠海和谐持股4.04%。
值得注意的是,2023年9月,钛动科技被认定为广东省专精特新中小企业。而在今年2月25日的广州市高质量发展大会上,李述昊作为企业代表发言时透露,公司将持续加大在广州的研发与投资力度,助力广州打造“垂类模型之都”。两天后,公司就递表港交所——这个时间点上的巧合,也显现出地方政府与企业之间的同频共振。
毛利率持续稳定在82%以上
钛动科技是一家少见的高利润AI企业。
2023年至2025年前九个月,公司营收从7282万美元增至1.3亿美元,期内利润从3435万美元增至5568万美元。最引人注目的是毛利率——报告期内分别为84.6%、82.4%、82.2%,持续稳定在82%以上。
这个数字意味着什么?它接近纯软件公司的盈利水平,印证了钛动科技“AI驱动的SaaS模式”具备极强的边际效应。根据弗若斯特沙利文的资料,按2024年收入计算,钛动科技在中国本土出海AI营销科技服务提供商中位列前茅。
钛动科技的收入主要来自围绕客户营销支出所收取的服务费,而非客户投放的总预算。这一模式使其和传统广告代理区分了开来。
此外,公司研发开支由2024年的1008万美元增至2025年前九个月的1670万美元,同比增长137.8%,反映出对AI的持续投入。
钛动科技的隐忧则来自流量依赖。钛动科技的业务高度依赖Meta、Google、TikTok等全球头部媒体平台,前三大媒体平台占其营销成本的88.7%。这一结构决定了市场对其存在定价分歧:如果能逐步摆脱对单一媒体渠道的依赖,实现数据资产的完全自有化,或能获得软件股的高估值。
此前,钛动科技还做出了一个关键决策。招股书披露,公司曾于2024年1月与中信证券签署A股上市辅导协议,后考虑到“宏观政策及公司自身的融资战略”,于2026年1月终止并转向港交所递表。
这个决策背后,是A股对AI企业盈利要求的考量,也是港股对“Agentic AI第一股”概念的追捧。2025年11月,明略科技以“全球Agentic AI第一股”的名号登陆港交所。如今,钛动科技打出“Multi-Agent第一股”的旗号,显然是希望在资本市场上讲一个更新的故事。
如果成功上市,这也将成为港股市场对“多智能体商业化落地”的一次公开定价。
本文来自微信公众号“东四十条资本”,作者:黎曼,36氪经授权发布。
三亚至伊尔库茨克客运航线开通
Canvas 直线点击事件处理优化
在平常Canvas开发中,经常会遇到直线的点击事件问题,对于这类问题通常的做法就是使用isPointInStroke,但直接使用存在一个问题就是直线的宽度较小时,鼠标点击不太容易选中。下面是针对这类问题总结的一些优化方法。
使用isPointInStroke
平常开发中,经常使用isPointInStroke方法判断鼠标点击位置是否位于直线上,常规代码如下:
<script setup>
import { ref, onMounted } from 'vue';
const canvasRef = ref();
let ctx;
let isLineSelected = false;
// 直线的起点和终点坐标
const lineStart = { x: 100, y: 200 };
const lineEnd = { x: 500, y: 200 };
const clear = () => {
// 清除画布
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
// 绘制直线的函数
const drawLine = () => {
// 设置线条样式
ctx.strokeStyle = isLineSelected ? '#ff0000' : '#000000';
ctx.lineWidth = isLineSelected ? 4 : 2;
// 绘制直线
ctx.beginPath();
ctx.moveTo(lineStart.x, lineStart.y);
ctx.lineTo(lineEnd.x, lineEnd.y);
ctx.stroke();
};
onMounted(() => {
if (canvasRef.value) {
const canvas = canvasRef.value;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext('2d');
drawLine();
// 添加鼠标点击事件监听器
canvasRef.value.addEventListener('click', e => {
const rect = canvasRef.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (ctx.isPointInStroke(x, y)) {
isLineSelected = !isLineSelected;
clear()
drawLine();
}
});
}
});
</script>
这样我们就可以实现鼠标点击的选中效果,但是这种方法并不完美,当线的宽度较小时,这是就很难选中这条线。 下面我们来优化一下,依旧使用isPointInStroke这个方法,代码如下:
// 检测点击是否在直线上的函数
const isPointOnLine = (x, y) => {
if (!ctx) return false;
// 创建直线路径
ctx.beginPath();
ctx.moveTo(lineStart.x, lineStart.y);
ctx.lineTo(lineEnd.x, lineEnd.y);
// 设置鼠标点击时的容错率
ctx.lineWidth = 10;
// 使用 Canvas API 的 isPointInStroke 方法检测点击是否在直线上
return ctx.isPointInStroke(x, y);
};
if (isPointOnLine(x, y)) {
isLineSelected = !isLineSelected;
clear()
drawLine();
}
我们把判断条件写成一个方法,在判断之前模拟一条起始坐标和终点坐标相同的线,为了解决线的宽度较小时不太容易选中的问题, 我们在模拟这条线是设置一个较大的宽度,这样就可以优化鼠标点击时不容易选中的问题了。
使用点到直线的距离公式
除了使用isPointInStroke方法判断鼠标点击位置是否位于直线上,我们还可以使用点到直线的距离公式判断鼠标点击位置是否位于直线上。计算点到直线的距离公式有很多种方法,比如一般式、参数式、向量式等。因为这里我们已知直线的两个坐标和鼠标点击
位置的坐标,使用向量叉积来计算点到直线的距离更为方便。
点到直线的距离公式如下:
其中, 和 是直线的两个坐标, 是鼠标点击位置的坐标。代码实现如下:
/**
* 计算点到直线的距离
* @param x0 点的 x 坐标
* @param y0 点的 y 坐标
* @param x1 直线上一点的 x 坐标
* @param y1 直线上一点的 y 坐标
* @param x2 直线上另一点的 x 坐标
* @param y2 直线上另一点的 y 坐标
* @param threshold 距离阈值,默认为 10
* @returns 点到直线的距离是否小于阈值
*/
function pointToLineDistance(x0, y0, x1, y1, x2, y2, threshold = 10) {
// 计算向量 AB
const vectorABx = x2 - x1;
const vectorABy = y2 - y1;
// 计算向量 AP
const vectorAPx = x0 - x1;
const vectorAPy = y0 - y1;
// 计算叉乘的绝对值(点到直线的距离的分子)
const crossProduct = Math.abs(vectorABx * vectorAPy - vectorABy * vectorAPx);
// 计算线段 AB 的长度
const segmentLength = Math.hypot(vectorABx, vectorABy);
// 处理线段长度为 0 的情况(两点重合)
if (segmentLength < 1e-6) {
// 计算点到点的距离
const pointDistance = Math.hypot(vectorAPx, vectorAPy);
return pointDistance < threshold;
}
// 计算点到直线的距离
const distance = crossProduct / segmentLength;
return distance < threshold;
}
总结
这两种方法都可以解决线的宽度较小时鼠标点击不容易选中的问题。在数据量不是很大的时候,推荐使用isPointInStroke方法, 在Canvas中直线是最小的单位,创建和绘制直线都是非常快的操作,不会对性能造成太大影响。当数据量大的时候,频繁的创建也会导致性能问题,这时候使用数学方法计算点到直线的距离会更加高效,不依赖 Canvas 状态,计算精确,可定制性强。
高盛称油价下周或突破100美元
CSS 里的「if」:@media、@supports 与即将到来的 @when/@else
CSS 里的「if」:@media、@supports 与即将到来的 @when/@else
梳理 CSS 中实现「条件判断」的几种方式:媒体查询、特性查询,以及规范中的 @when/@else,并给出简单用法与兼容性说明。
一、CSS 有 if 吗?
CSS 没有像 JavaScript 那样的 if (x) { } 语句,但可以通过 @ 规则 做「条件式」样式:满足某条件时才应用某段样式。常见的有两类:媒体查询(@media) 和 特性查询(@supports);规范里还有正在推进的 @when / @else,写法更接近「if-else」,但目前浏览器尚未普遍支持。下面按「能用 today」和「即将到来」分开说。
二、@media:按视口/设备「if」
@media 用来根据媒体类型与媒体特征(如视口宽度、横竖屏、分辨率)决定是否应用样式,相当于「如果屏幕满足某条件,就用这段 CSS」。
/* 视口宽度 ≥ 768px 时用栅格布局 */
@media (min-width: 768px) {
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}
/* 横屏时调整内边距 */
@media (orientation: landscape) {
.panel {
padding: 2rem;
}
}
常见条件:min-width / max-width、min-height、orientation、prefers-color-scheme(深色/浅色)、prefers-reduced-motion 等。多条件用 and 连接;需要「或」时写多个 @media 或在一个规则里用逗号。兼容性:现代浏览器均支持,是响应式布局的基础。
三、@supports:按浏览器能力「if」
@supports 是特性查询:如果浏览器支持某 CSS 属性或语法,就应用这段样式;不支持则跳过。适合做渐进增强(先写基础样式,再在支持新特性的浏览器里增强)。
/* 支持 Grid 时用 Grid 布局 */
@supports (display: grid) {
.container {
display: grid;
gap: 1rem;
}
}
/* 不支持时回退 */
@supports not (display: grid) {
.container {
display: flex;
flex-wrap: wrap;
}
}
/* 同时支持多个特性时 */
@supports (display: grid) and (gap: 1rem) {
.container {
display: grid;
gap: 1rem;
}
}
逻辑:@supports (条件)、not、and、or;还可检测选择器,如 @supports selector(:has(a))。兼容性:主流浏览器早已支持,可放心用。
四、@when / @else:规范里的「if-else」(即将到来)
@when 和 @else 是 CSS Conditional Rules Level 5 中的新规则,用来统一写条件:把媒体条件、特性支持等写进同一套「when-else」链里,语义更接近「if-else if-else」,减少多层 @media 嵌套。
示例(语法以最终规范为准):
@when media(min-width: 800px) {
.sidebar { width: 300px; }
}
@else media(min-width: 600px) {
.sidebar { width: 240px; }
}
@else {
.sidebar { width: 100%; }
}
还可组合 media 与 supports:
@when media(min-width: 1024px) and supports(display: grid) {
.layout { display: grid; }
}
@else {
.layout { display: block; }
}
现状:截至 2024–2025 年,主流浏览器尚未支持 @when/@else,目前只能在支持该规范的实验环境或未来版本中使用。写新项目时仍以 @media + @supports 为主;等 @when/@else 普及后,再考虑重构为更简洁的条件链。
五、对比与使用建议
| 方式 | 作用 | 兼容性 | 典型场景 |
|---|---|---|---|
| @media | 视口/设备条件 | 全面支持 | 响应式、深色模式、动效偏好 |
| @supports | 浏览器能力条件 | 全面支持 | 渐进增强、Grid/Flex 回退 |
| @when/@else | 统一条件链 | 尚未支持 | 未来多条件、互斥分支 |
建议:
- 需要「根据屏幕大小/横竖屏/主题」切换样式 → 用 @media。
- 需要「根据是否支持某 CSS 特性」切换样式 → 用 @supports。
- 两者可以组合:先
@media再在块内写@supports,或反过来。 - @when/@else 先了解语法即可,等 Can I Use 显示普遍支持后再在实际项目中使用。
六、小结
- CSS 没有字面意义的 if,但用 @media(媒体条件)和 @supports(特性条件)可以实现「满足条件才应用样式」。
-
@media:按视口宽度、横竖屏、
prefers-*等写响应式与偏好适配。 - @supports:按浏览器是否支持某属性/选择器写渐进增强与回退。
- @when/@else:规范中的统一条件语法,可读性更好,目前浏览器未支持,可关注 CSS Conditional Level 5 与 Can I Use 的更新。
若对你有用,欢迎点赞、收藏;你若有基于 @supports 或 @media 的实战写法,也欢迎在评论区分享。
我国已对50个国家实施单方面免签,同29个国家全面互免签证
春运第35天,预计全社会跨区域人员流动量19409万人次
0.99元黑暗人格测试刷屏 , 有人靠它月入过万
作者|黄诗韵
编辑|朱人奉
题图|pexels
0.99元可以做什么?可以测测你是“灵珠”还是“魔丸”。
《哪吒》系列动画电影或许怎么都想不到,它最出圈的两个词会和“黑暗人格三角测试”产生联系。
这样一份测试题,只需0.99元,你花大概10分钟完成70道题,就可以得到属于自己的黑暗三角人格测试报告。
(图/社交媒体截图)
(图/社交媒体截图)
不只黑暗人格,你的自身天赋、年上年下恋爱倾向、天选之城、情感占有欲,甚至是依恋类型、NPD人格等,都可以花0.99元进行测试。
于是,在一种“买不了吃亏,买不了上当”的心态驱使下,社交平台上悄然掀起了一阵0.99元心理测试的热潮。
这种付费测试为什么能在社交平台上火起来?我们为什么会对这种方式如此着迷?0.99元的测试,真的能帮我们认识自己吗?
0.99元,有人无痛下单,有人起店赚钱
“你生理性喜欢的事情才是你的天赋赛道!”看到这个标题的时候,我忍不住好奇:什么叫“生理性喜欢的事情”?
于是我点开帖子,滑到最后,顺理成章地点开了“自身天赋测试”的商品链接,又自然而然地付了款,成为2.6万单的一份子。
这份自测一共40条题目,大概5分钟就能完成。最后得出的结果显示,我属于媒体艺术型。它还从语言、逻辑、算术、空间等10个方面,分别对我进行天赋评分,再划分为非常擅长、有些擅长和不太擅长,由此解读我的核心优势和成长空间。
“自身天赋测试”说明。(图/测试截图)
这种测试并不新。数年前报大学志愿时,我也通过志愿填报模拟系统测过一次,结果显示,我适合干审计。但在这次的测试中,审计职业却被标注为“建议谨慎的方向”。
在商品评价区,有人感叹“这是我测过最准的一版”,有人指出“问题不太专业,指向性太明显”,但大部分人的心态还是“测着玩玩,不贵,图个乐呵”。
之后,我又好奇地购买了该商家的黑暗三角人格测试。商家提供的链接介绍,该测试有两个理论基础,分别是由Paulhus和Williams提出的黑暗三角理论,以及Moshagen等人提出的D因子理论。黑暗三角包括马基雅维利主义、自恋和精神病态这三个相关但独立的人格特质,D因子则揭示了更广泛的黑暗人格的共同核心。
做完题后,我又得到了一份报告,其中有D因子得分、特质分布可视化图表、黑暗三角三个维度的解读,还给出了人格画像、生活领域影响、优劣势分析以及发展建议。
“黑暗三角人格测试”说明。(图/测试截图)
在这之后,我在社交平台上看到了越来越多的测评。很多人花0.99元,就测出了自己适合在上海还是广州发展,性格更像狮子还是棕熊,恋爱前有多喜欢对方,心理年龄是几岁。
(图/社交媒体截图)
除此之外,伴随着NPD、BPD、ADHD等概念的走红,一些带有医学或心理学诊断意味的测试也很受欢迎。
以某店铺的“测自己自恋型人格特质清单”和“测他人自恋型人格特质清单”为例,截至2026年2月10日下午4时,两份问卷分别售出了5778单、9021单。
在这股热潮下,有博主列出了详细的心理测试创业操作清单,包括前期准备、内容运营、售后与风控、执行节奏、核心忠告五大部分,具体细致到确认心理测试链接成本是多少钱一条、选品方向要优先什么类型、每天在什么时间段发布多少条帖子等。还有博主分享经验称,免费的测试也可以卖,���会有人追究。
(图/社交媒体截图)
这种赚钱方式,其实是在卖“信息差”。有媒体报道,一位心理测试链接卖家透露,只需花299元购买一份套餐,就能得到6个类型的2300条心理测试链接,之后将链接以每条0.99元的价格在线上店铺售卖,就能开始赚钱。
以上文提及的一个已售出超2.6万单的测试为例,其销售额超过2.5万元,如此算来,起店资金回本是非常简单的事情。
那么,作为测试者的我们,为什么乐此不疲地花这个钱?
测试结果越“黑暗”,网上越受欢迎?
实际上,与个人性格相关的心理测试一直都有很高的热度。最简单的是“你第一眼看到的是什么”图片测试,号称能够秒测一个人的心理状态和内在性格。
我们为什么喜欢做这些心理测试?关于这个问题,通常有两大解释。
其一,当代人普遍处于焦虑不安的状态,面临着身份认知的危机。而通过心理测试得出人格类别,可以帮助人们确认“我是谁”,同时快速地获得群体的关注和认同。
其二,心理学上有个概念叫“福勒效应”,常用于指人们在面对笼统的、一般性的人格描述时,会认为其准确地揭示了自己的特点,产生一种“真的很准”的感觉。并且,这些表达越朦胧,我们感受到的肯定和夸赞可能会越多。
除此之外,最近在社交平台兴起的“0.99元心理自测热潮”,又是为什么?
首先,是“0.99元”的价格以及“社交平台”这个场域。一般人可能会认为,免费测试才是最多人使用的方式。实际上,在社交平台拍下商品、即做即分析的测试,可能比免费测试的渗透范围更广。
因为在大多数情况下,免费测试的代价是自主搜索,你需要在信息海量的浏览器里筛选出某款测试题目。但现在,你偶然看完一个引发情绪共鸣的帖子之后,很可能就会刷到付费心理测试,然后以几乎可以忽略不计的费用和几分钟的时间,完成一个测试。
试想一下,在某个郁闷的午后,你被老板大骂了一顿,辞职的冲动瞬间飞升,这时你看到“去端你天生该吃的那碗饭”,大概率就会点进去,花掉那0.99元,测试所谓“天赋领域”。
(图/社交媒体截图)
此后,在大数据的帮助下,平台算法会源源不断地给你推荐更多的测试,形成循环。
这一循环与更大的媒介内容生态是相辅相成的。当NPD概念因为综艺节目大火时,类似“教你快速判断一个人是不是NPD”“来细数一下我遇到过的NPD的特点”“NPD的血包特质以及怎么破”等标题的帖子,也在社交平台上获得极大的传播声量,有关NPD评估测试也会明显增多。
最后,当你将测试结果分享在社交平台时,松散的、情感程度不深的弱关系网络,反而帮你聚集了更大范围的认同。在彼此不认识的空间中,大家没有需要长期维护的人设,也没有来自多方的期待,不需担心承担负面评价的后果。
黑暗三角人格测试结果中“自我为中心”“恶毒倾向”“施虐倾向”等表述,在朋友圈中可能会成为敏感词,给别人留下不好的印象。但在社交平台,晒出得分低的D因子,会有人夸你“善良的好宝宝”;如果你的D因子得分高,大家也只是发出“第一集魔丸降世”的调侃,一笑而过。这时候,比起朋友圈,社交平台上的你能以更小的压力获得更大的共鸣。
(图/社交媒体截图)
(图/社交媒体截图)
问题是,作为测试者的你,真的能对这样的测试结果一笑而过吗?
真的心理需求,伪的心理学测试?
几年前,MBTI人格测试开始大火。有人担心,这个测试简单地将人分成16个类型,会忽视个体的丰富与复杂;也有人认为,如果将其合理使用,也不失为了解自己的一种方式;更多人可能将类似性格测试当作“图一乐”的方式。因此,只要有群体感受到乐趣,那么0.99元测试就会永远有市场。
如今在社交网络上,一些商家为了起店,不断通过情绪化的话题和内容来吸引平台用户的注意,从而获得流量、促进销售。
例如,为了吸引你做“依恋类型测试”,他们会说“焦虑型依恋才是真正的纯爱天花板”;为了让你购买“童年创伤类型测试”,他们会说“爱照顾人,其实是创伤的后遗症”;为了卖出“症状自评量表SCL-90”,他们会说“我说抑郁了,可他们都说我装的”。
(图/社交媒体截图)
看到这些内容,如果你现在或曾经在一段亲密关系中有过被抛弃的恐惧,如果近段时间正在思考来自原生家庭的创伤,如果感到情绪低落、兴趣减退,那么,你就有可能会买下这些测试。
但是,这些引导测试的内容,其严谨性需要打个大大的问号:焦虑型依恋和纯爱,相关性从何谈起?爱照顾人,就一定是童年创伤的结果吗?通过90道题目,抑郁就能确诊吗?
当自我认知变得商品化、碎片化,我们就不能仅仅“图一乐”了。
上个月,有媒体揭露了网络“伪心理学”套路。一些视频通过总结某种人格或心理问题特征,打着为网友诊断心理问题的旗号,推出付费测评、高价心理课程以及所谓心理咨询服务,形成了一条流量变现的灰色链条。
0.99元的低价测试,似乎没有什么破财的风险,但在进行心理疾病诊断时,不恰当的引导和测试,可能会加剧测试者的负面情绪。对此,有一些付费测试也意识到了可能存在的风险。
在一个自恋型人格特质清单中,其最后部分有这样的说明:“真正的NPD诊断需由合格的精神健康专业人士进行长时间且全面的临床评估,包括深入的访谈,有时还需要辅以其他专业的心理测评工具,远比上述清单更复杂。该自我评估工具具有一定的局限性,因为NPD的一个核心特征就是缺乏病识感,即自己很难认识到自己的问题……最后再次重申:本清单是一个简化的自制测试模型,仅供参考,不具备诊断效力,其结果必须谨慎对待……寻求专业的心理健康支持永远是最好、最勇敢的选择。”
跟心理疾病和人格类型有关的自测,很难有一个明确的判断来告诉大家测试是否可信。在医学和心理学领域,相关研究和诊断也在不断发展中,流派极多,众说纷纭。要对心理状况和人格类型进行科学评估,远不是简单在网上做做测试、或者问问AI就可以搞清楚的事情。
但是,大多时候,我们只是想要一个测试的过程和结果而已。也许,这是人类内心无法由理性控制的冲动。起源于公元前6世纪的德尔斐箴言,最有名的一句就是镌刻在阿波罗神殿门前的“认识你自己”。
当这个认识自我的成本可能低至0.99元时,人们很难不心动。
校对:严严;排版:韵韵紫
本文来自微信公众号“新周刊”,作者:黄诗韵,36氪经授权发布。
为什么“从1数到10”这件事,所有视频模型都不会?
头图来源:Nano Banana
作者|汤一涛
编辑|靖宇
Seedance 2.0 有多猛,过去一个月大家已经见识过了。好莱坞已经集体下场发了声明,西半球最强法务部迪士尼也给字节跳动发了律师函。
但如果你让它做一件事:生成一个男人从 1 数到 10 的视频,它就露馅了。
生成出来的「人」五官端正、皮肤质感逼真,厨房背景细节丰富得像是实拍。他说出「one」的时候还一切正常,然后就开始鬼打墙,嘴里不断重复「t、t、t」这个音节(不是从 1 到 10 中任何一个数字的发音);或者伸出三根手指,口中却自信地说出「ten」。从头到尾,他竖起的手指没超过三根。
因为背景和人物都太真实了,所以手指崩坏的瞬间反而制造出了一种强烈的「伪人感」。
这道题不只是 Seedance 2.0 的噩梦。
视频来自一位在 X 网友 fofr(简介显示是在 DeepMind 的开发者)。去年他就发现,「从 1 数到 10 并用手指比出数字」这个对三岁小孩都毫无难度的任务,是当前所有 AI 视频模型的共同死穴。
Seedance 2.0 发布后,他第一时间把这道老题扔了过去,果然也翻车了。
网友在这条推文下面掀起了一场自发的「AI 数数挑战赛」。他们把同一道题喂给了 Sora、Veo、Kling 等几乎所有主流模型,结果全军覆没,没有一个能正确地从 1 数到 10。
当一个行业最强的产品们被一道幼儿园级别的题目集体难倒,这其实指向了一个问题:为什么这些模型已经能骗过你的眼睛,却无法理解常识?
它们到底「理解」了什么,又缺失了什么?
01统计预测 vs 理解世界:AI 视频的能力边界
「数不到 10」不是一个孤立的 bug,它揭示了一整片当前 AI 的能力盲区。
原因也不复杂:所有的视频模型本质上做的是同一件事,从海量视频数据中学习统计规律,然后在生成每一帧画面时预测「接下来什么样的像素排列最可能出现」。这和大语言模型的「预测下一个词」(Next-Token Prediction)是同一套逻辑。
所以它们能把人脸毛孔、厨房光影、衣服褶皱渲染得以假乱真,因为训练数据里有海量样本,统计规律足够丰富。但一旦任务超出了样本的范畴,进入「常识」的领域,问题就来了。
这些问题大致可以分成三类。
首先就是手部精细动作,这是最广为人知的「AI 照妖镜」。从图像生成时代的「六指人」,到视频生成时代的「软糖手指」,手一直是 AI 的噩梦。
Midjourney 和 DALL-E 爆火的 2022 年,「手部多指」是当时文生图最明显的 Bug|图片来源:Medium
公平地说,AI 在「画手」这件事上已经取得了巨大进步。日常场景里,六指人和软糖手已经越来越少见了。
但 fofr 的测试之所以能让所有模型集体翻车,是因为它只是一个视觉渲染问题,同时还暗含了一个逻辑推理问题。它要求在 10 秒内连续变换 10 个不同手势,每个手势的手指数量严格递增,同时嘴里说的数字还要对得上。
人的手有 27 块骨骼、34 块肌肉、超过 100 条韧带,单个手掌就有 18 个自由度。即便采用高分辨率扩散模型,若缺乏明确的三维先验知识,也难以表达这种精确度。
人体运动维度示意图|图片来源:ScienceDirect
况且,在训练数据中,手通常出现在画面边缘、被物体遮挡或处于运动模糊中。模型能学到的高质量手部样本远少于面部。
第二类 AI 的能力盲区是物理规律。流体怎么流、物体怎么碰撞、织物怎么飘……这些人类靠直觉就能判断的东西,AI 视频经常给出违反物理定律的答案。OpenAI 在发布 Sora 时的官方技术报告中就明确承认:Sora 无法准确模拟许多基本物理交互,比如玻璃破碎,也无法正确反映某些物体状态变化。
第三类是时序逻辑的一致性。视频不是一组彼此独立的图片,而是一条有因果关系的时间链:第 3 秒的画面必须建立在第 2 秒的基础上。但当前的扩散模型把时间当作一个潜在的数学维度来处理。它在生成第 N 帧时,没有内部机制去「记住」前面伸了几根手指、下一步该加 1。时间一长,前后就对不上了。
作个类比的话,当前的 AI 视频模型像一个从没见过真手的画家,看了一百万张手的照片之后凭印象画手。大部分时候画得挺像,但他不知道手指只有五根,不知道伸出三根手指代表数字 3,更不知道从 3 到 4 意味着要再伸出一根。
02另一条路:世界模型
既然问题的根源是「不理解物理世界」,那有没有人在试图从根本上解决这个问题?
事实上,这正在成为 AI 领域最受关注的新方向之一。一个正在凝聚共识的思路是:与其让模型从海量视频中学习「世界看起来是什么样的」,不如让它先理解「世界是怎么运作的」。
这条路径有一个共同的名字,叫做世界模型(world model)。世界模型的核心思路是让 AI 建立对三维物理世界的结构性理解,包括空间的几何关系、物体的物理属性、运动的动力学规律等。
这就和当前视频生成模型的路径产生了本质区别。当前模型在二维平面上预测像素排列的统计概率,世界模型则试图让 AI 在「懂」物理规律的基础上做生成。
这个方向最知名的创业者是李飞飞。这位 ImageNet 的缔造者在 2024 年创办了 World Labs,核心目标是让 AI 拥有「空间智能」。她在去年的一篇长文中写道:
「语言是人类认知的产物,但世界遵循更复杂的规则——重力控制运动,原子结构决定光线如何产生颜色,无数物理定律约束着每一次交互。要让 AI 真正理解这一切,需要一种全新的、远超大语言模型的架构」。
今年 2 月,World Labs 完成了 10 亿美元融资,其首个产品 Marble 已经上线,可以从图像或文本生成持久的 3D 环境。
Marble 可以从一张图片或一段文字生成一个你能在里面自由走动、持续编辑的 3D 世界|图片来源:World Labs
李飞飞不是唯一的入局者。杨乐昆从 Meta 离职后创办了 AMI Labs,同样聚焦世界模型方向;Google DeepMind 的 Genie 系列模型也在探索 3D 环境的生成与模拟;Nvidia 则推出了 Cosmos,定位为「世界基础模型」,试图将视频生成、物理感知模拟和机器人工作流统一到一个框架里。
当这个领域最顶级的几位研究者和最有钱的几家公司同时往一个方向走,这本身就说明了一些问题。纯数据驱动路径的天花板,正在成为越来越多人的共识,只是解法还在探索中。
Seedance 2.0 刚出来的时候确实引起了一大波恐慌。《死侍》编剧 Rhett Reese 看完演示后甚至写下了「我不想承认,但我们可能完了」。
这种反应可以理解,Seedance 2.0 确实很强,但「数不到 10」提供了一个有用的校准视角:这些模型学会了电影的视觉语法,但还没学会世界的物理语法。它们的进步,更多是「看起来更真」,而不是「更懂现实」。
从本质上说,一个不知道手指只有五根的系统,距离真正取代人类创作者,中间还隔着一次范式级别的跨越。
人类可以稍稍松口气了,至少在 AI 学会数到 10 之前。
本文来自微信公众号“极客公园”,作者:汤一涛,36氪经授权发布。
崔东树:由于新能源车购税恢复征收,指导价对消费者购车交税影响体现
PM2 使用指南 - 踩坑记录
最近把本地项目改成用 PM2 跑,踩了一点坑,记录一下免得下次又忘。
基础配置
先装 PM2:
npm install -g pm2
或
pnpm install -g pm2
创建一个 ecosystem.config.cjs 文件,这是 PM2 的配置文件:
module.exports = {
apps: [
{
name: 'blog', // 应用名称
script: 'npx', // 用什么命令跑
args: 'next start -H 0.0.0.0', // 命令参数
cwd: '/path/to/project', // 项目路径
instances: 1, // 实例数量
exec_mode: 'fork', // 运行模式
autorestart: true, // 崩溃自动重启
watch: false, // 不监听文件变化
max_memory_restart: '1G', // 内存超限重启
env: {
NODE_ENV: 'production',
PORT: 3000,
},
},
],
};
实例数量这个坑
刚开始配置的时候,我把 instances 设置成了 'max',结果一启动就开了 16 个实例。查了一下才知道,'max' 会根据 CPU 核心数自动启动对应数量的实例。我家里主机是 16 核的,所以直接开了 16 个。
对于小项目来说,2-4 个实例就够用了,没必要开那么多。直接写数字就行:
instances: 2, // 开 2 个实例
重启不生效的问题
改完配置文件后,我直接用了 pm2 restart,结果配置根本没生效。查了文档才知道,pm2 restart 不会重新读取配置文件,只是重启现有的进程。
正确的做法是:
pm2 delete blog # 先删除
pm2 start ecosystem.config.cjs # 再启动
或者用我配置好的命令:
npm run pm2:delete
npm run pm2:start
script 和 args 的选择
一开始我用 script: 'npm' 和 args: 'start',结果各种问题。后来改成直接用 npx 就好多了:
// 不推荐
script: 'npm',
args: 'start',
// 推荐
script: 'npx',
args: 'next start -H 0.0.0.0',
用 npx 直接运行命令更稳定,npm 作为中间层有时候会有奇怪的问题。
开发环境和生产环境的区别
这个坑我解决了好一会。开发环境要用 next dev,生产环境用 next start。
开发环境配置:
{
script: 'npx',
args: 'next dev --turbopack -H 0.0.0.0',
exec_mode: 'fork', // 开发环境不支持 cluster
env: {
NODE_ENV: 'development',
},
}
生产环境配置:
{
script: 'npx',
args: 'next start -H 0.0.0.0',
exec_mode: 'cluster', // 生产环境可以用 cluster
instances: 2,
env: {
NODE_ENV: 'production',
},
}
注意!!!开发环境不能用 cluster 模式,只能用 fork。
监听地址的问题
Next.js 默认只监听 localhost,外部访问不了。需要加 -H 0.0.0.0 参数。
我试过用环境变量 HOSTNAME: '0.0.0.0',但在生产模式下不起作用,还是得用命令行参数。
args: 'next start -H 0.0.0.0', // 这样才生效
Next.js 的 basePath
如果 Next.js 配置了 basePath,访问的时候要加上这个路径。比如:
// next.config.ts
export default {
basePath: '/blog',
}
那访问地址就是 http://127.0.0.1:3000/blog,不是 http://127.0.0.1:3000。
常用命令集合
# 启动
pm2 start ecosystem.config.cjs
# 停止
pm2 stop blog
# 重启(不重读配置)
pm2 restart blog
# 删除
pm2 delete blog
# 查看日志
pm2 logs blog
# 查看状态
pm2 list
# 查看详情
pm2 show blog
# 监控面板
pm2 monit
开机自启
# 保存当前进程列表
pm2 save
# 生成开机启动脚本
pm2 startup
package.json 脚本
把常用命令写到 package.json 里,方便使用:
{
"scripts": {
"pm2:start": "pm2 start ecosystem.config.cjs",
"pm2:stop": "pm2 stop blog",
"pm2:restart": "pm2 restart blog",
"pm2:delete": "pm2 delete blog",
"pm2:logs": "pm2 logs blog",
"pm2:monit": "pm2 monit"
}
}
总结复盘
-
instances: 'max'会开很多实例,小项目直接写数字 - 改配置后要先
delete再start,restart不重读配置 - 用
npx比npm稳定 - 开发环境用
next dev,生产环境用next start - 开发环境只能用
fork模式 - 用
-H 0.0.0.0让服务监听所有地址 - 注意 Next.js 的
basePath配置
差不多就这些点了,希望能帮到后面用 PM2 的同学。
React 中 useState、useEffect、useRef 的区别与使用场景详解,终于有人讲明白了
一、先用一句话概括这三个 Hook
如果你现在还很懵,先别慌,先记住下面这三句话。
useState
让组件记住会影响页面展示的数据
useEffect
让组件在渲染后去执行额外操作
useRef
让组件保存一个不会触发重新渲染的值,或者拿到 DOM 元素
这三句话,已经把它们最本质的区别说出来了。
如果还觉得抽象,没关系,接下来我一个个拆开讲。
二、先说 useState:它是“状态管理”的
React 组件最大的特点之一,就是:
数据一变,页面跟着变。
而 useState,就是专门用来保存这种“会驱动页面变化的数据”的。
先看最经典的例子。
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
当前点击了 {count} 次
</button>
);
}
export default Counter;
这里这句最关键:
const [count, setCount] = useState(0);
它的意思你可以直接翻译成人话:
React,帮我准备一个状态,初始值是 0,当前值叫 count,修改它的方法叫 setCount。
也就是说:
-
count是当前状态值 -
setCount是更新状态的方法 -
0是初始值
当你点击按钮执行:
setCount(count + 1);
React 会做两件事:
- 更新状态值
- 重新渲染组件
所以页面上的 count 就会变。
useState 最典型的应用场景
useState 常用于这些地方:
- 计数器数字
- 输入框内容
- 弹窗是否显示
- 下拉框选中项
- 当前分页页码
- 列表数据
- 加载状态
loading - 错误提示信息
比如控制弹窗:
const [visible, setVisible] = useState(false);
比如保存输入框内容:
const [keyword, setKeyword] = useState("");
比如保存接口返回的数据:
const [list, setList] = useState([]);
这些都属于:
一旦数据变化,页面就要跟着变化。
这时候就应该用 useState。
三、再说 useEffect:它是“副作用处理”的
很多人第一次看到“副作用”这个词,容易被吓到。
其实它没有那么玄乎。
你可以简单把副作用理解成:
除了渲染页面以外,还要额外做的事情。
比如:
- 请求接口
- 设置定时器
- 监听事件
- 修改浏览器标题
- 操作本地存储
- 手动操作 DOM
- 组件销毁时做清理
这些都不是“渲染 JSX”本身,而是页面渲染之后要顺便做的事。
这时候就轮到 useEffect 出场了。
先看一个最简单的例子:
import React, { useEffect } from "react";
function Demo() {
useEffect(() => {
console.log("组件渲染完成了");
}, []);
return <div>Hello React</div>;
}
这段代码的意思就是:
页面渲染完以后,执行
console.log
所以你可以理解成:
useEffect = 渲染后执行任务
useEffect 最常见的使用场景
1. 请求接口
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then((data) => {
console.log(data);
});
}, []);
2. 设置定时器
useEffect(() => {
const timer = setInterval(() => {
console.log("每秒执行一次");
}, 1000);
return () => clearInterval(timer);
}, []);
3. 监听事件
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
4. 修改页面标题
useEffect(() => {
document.title = "用户中心";
}, []);
这些都是副作用。
也就是说:
只要不是单纯为了渲染页面,而是渲染后还要做点别的事,大概率就要想到 useEffect。
四、再说 useRef:它是“持久容器”和“DOM 引用”
useRef 是很多初学者最容易迷糊的 Hook。
因为它不像 useState 那么直观,也不像 useEffect 那么容易理解成“执行动作”。
其实 useRef 可以简单理解成两个作用。
作用一:获取 DOM 元素
比如你想让输入框在页面加载后自动获取焦点:
import React, { useEffect, useRef } from "react";
function InputFocus() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="请输入内容" />;
}
export default InputFocus;
这里可以这样理解:
-
useRef(null)创建一个引用对象 -
inputRef.current会指向真实的 input DOM - 通过
focus()就可以让输入框聚焦
也就是说:
useRef 可以帮你“拿到页面中的真实元素”。
作用二:保存一个值,但不触发页面重新渲染
这是 useRef 更重要、也更容易被忽略的能力。
比如保存定时器 id:
const timerRef = useRef(null);
赋值:
timerRef.current = setInterval(() => {
console.log("running");
}, 1000);
清除:
clearInterval(timerRef.current);
这个值会一直保留在组件生命周期里,但它变化时不会导致页面重渲染。
所以你可以把 useRef 理解成:
组件里的一个“小盒子”,你可以往里面放东西,它会一直记着,但不会因为盒子里的东西变了就刷新页面。
五、它们三个最大的区别,到底是什么?
这是本文最核心的部分。
我先直接给你一个最重要的结论:
| Hook | 核心作用 | 数据变化后会不会触发重新渲染 |
|---|---|---|
useState |
保存状态 | 会 |
useEffect |
执行副作用 | 本身不是存数据的 |
useRef |
保存引用/持久值 | 不会 |
把这张表吃透,你就不容易乱用了。
接下来我一个个解释。
六、useState 和 useRef 的区别,初学者最容易搞混
很多人学到这里时,最大的疑问就是:
既然 useState 能存值,useRef 也能存值,那到底啥时候用谁?
答案非常简单:
需要更新页面的,用 useState
不需要更新页面的,用 useRef
来看例子。
场景 1:页面上要显示这个值
import React, { useState } from "react";
function Demo() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前数字:{count}</p>
<button onClick={() => setCount(count + 1)}>加一</button>
</div>
);
}
这里 count 是显示在页面上的。
点击按钮后,页面中的数字也要变化。
所以必须用 useState。
场景 2:只是内部记一下,不需要显示
import React, { useRef } from "react";
function Demo() {
const clickTimesRef = useRef(0);
const handleClick = () => {
clickTimesRef.current += 1;
console.log("点击次数:", clickTimesRef.current);
};
return <button onClick={handleClick}>点击我</button>;
}
这里点击次数只是打印在控制台,并没有显示在页面上。
那就没必要用 useState,用 useRef 就够了。
再总结一遍
用 useState 的场景
- 页面要展示这个数据
- 数据变化后希望组件重新渲染
- 数据会驱动 UI 更新
用 useRef 的场景
- 只是临时保存一个值
- 不希望因为这个值变化而重新渲染
- 保存 DOM、定时器 id、上一次值等
七、为什么 useRef 改了值,页面不更新?
这个问题特别经典,面试也爱问。
比如下面这段代码:
import React, { useRef } from "react";
function Demo() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(countRef.current);
};
return (
<div>
<p>{countRef.current}</p>
<button onClick={handleClick}>点击</button>
</div>
);
}
很多初学者会以为点击按钮后,页面上的数字会变。
但实际上,页面大概率不会更新。
为什么?
因为:
修改 ref.current 不会触发组件重新渲染。
React 只会在这些情况下重新渲染组件:
-
props变了 -
state变了 - 父组件重新渲染导致子组件重新渲染
而 ref.current 的变化,不在 React 的“响应式更新系统”里。
所以它改了,React 不会主动刷新页面。
这就是 useRef 和 useState 最大的区别之一。
八、useEffect 和 useState 的关系是什么?
开发中经常看到这俩一起出现。
比如页面加载后请求数据:
import React, { useEffect, useState } from "react";
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
});
}, []);
return (
<ul>
{users.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
这里的配合方式非常典型:
-
useState负责存数据 -
useEffect负责获取数据
也就是说:
useState管“保存结果”,useEffect管“执行动作”。
你可以理解成:
-
useState是仓库 -
useEffect是工人 - 工人出去搬货,最后把货放进仓库里
这是它们最经典的协作模式。
九、useEffect 的依赖数组到底怎么理解?
这个问题,是 React 初学者最容易卡壳的地方之一。
我们先看写法:
useEffect(() => {
console.log("执行副作用");
}, []);
第二个参数 [],就叫 依赖数组。
它决定这个副作用什么时候执行。
1. 传空数组 []
useEffect(() => {
console.log("只执行一次");
}, []);
表示:
组件首次渲染完成后执行一次。
常见用途:
- 页面加载请求一次接口
- 初始化某些逻辑
- 绑定事件监听并在销毁时清理
2. 不传依赖数组
useEffect(() => {
console.log("每次渲染都执行");
});
表示:
组件每次渲染后都会执行。
这个一般要慎用,否则可能造成不必要的执行。
3. 传某个依赖项
useEffect(() => {
console.log("count 变化了");
}, [count]);
表示:
首次渲染执行一次,以后只有 count 变化时才执行。
4. 传多个依赖项
useEffect(() => {
console.log("count 或 keyword 变化了");
}, [count, keyword]);
表示:
只要 count 或 keyword 中任意一个变化,副作用就会重新执行。
最通俗的理解方式
你可以把依赖数组理解成一句话:
只要数组里的这些值变了,就重新执行这段副作用代码。
这就很好记了。
十、useEffect 的清理函数是干嘛的?
很多人刚开始看到这种写法会有点懵:
useEffect(() => {
const timer = setInterval(() => {
console.log("执行中");
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
为什么 useEffect 里面还要 return 一个函数?
这个函数叫:
清理函数
它一般会在这些时候执行:
- 组件卸载时
- 副作用重新执行前,先清理上一次的副作用
最常见的用途有:
- 清除定时器
- 移除事件监听
- 取消订阅
- 中断请求
比如监听窗口大小变化:
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
这里如果不做清理,组件销毁后事件还在,就可能造成内存泄漏或者逻辑混乱。
所以你可以这样记:
副作用用了什么外部资源,离开时就记得清掉。
十一、三个 Hook 的生活化比喻,一下就记住
为了让你更容易记住,我给你打个特别通俗的比方。
把 React 组件想象成一个办公室员工。
useState:员工的记事本
员工需要记住今天要做什么、当前完成多少、按钮是开还是关。
这些会影响工作展示给老板看。
所以:
useState = 会展示出来的正式数据
useEffect:员工的任务清单
员工上班后要做事:
- 给客户打电话
- 发邮件
- 开会
- 定时汇报
这些不是“展示内容”,而是要执行的动作。
所以:
useEffect = 渲染后执行的额外任务
useRef:员工的抽屉
员工抽屉里放着一些东西:
- 钥匙
- 工牌
- 上一次会议记录
- 某个客户电话
- 临时编号
这些不需要写到汇报 PPT 上,但又得一直留着备用。
所以:
useRef = 持久保存但不驱动页面变化的数据容器
这个比喻基本能帮很多初学者彻底理顺。
十二、实际开发中该怎么选?
这里我给你一个非常实战的判断口诀。
场景一:数据变了,页面也要变
用 useState
比如:
- 输入框输入内容
- 列表数据变化
- loading 状态
- tab 切换
- 当前选中项
场景二:页面出来后要执行动作
用 useEffect
比如:
- 请求接口
- 绑定事件
- 启动定时器
- 修改标题
- 同步本地存储
场景三:只想记个值,不想刷新页面
用 useRef
比如:
- 保存 timer id
- 保存上一次值
- 防抖节流中的锁
- 获取 input DOM
- 防止重复提交标记
这个口诀非常适合业务开发时快速判断。
十三、一个综合案例,把三个 Hook 串起来理解
下面我们写一个小案例:搜索框自动聚焦,并在输入时同步标题,同时记录输入次数。
import React, { useEffect, useRef, useState } from "react";
function SearchDemo() {
const [keyword, setKeyword] = useState("");
const inputRef = useRef(null);
const changeCountRef = useRef(0);
useEffect(() => {
inputRef.current.focus();
}, []);
useEffect(() => {
document.title = keyword ? `正在搜索:${keyword}` : "搜索页面";
}, [keyword]);
const handleChange = (e) => {
setKeyword(e.target.value);
changeCountRef.current += 1;
console.log("输入次数:", changeCountRef.current);
};
return (
<div>
<h2>搜索示例</h2>
<input
ref={inputRef}
value={keyword}
onChange={handleChange}
placeholder="请输入关键词"
/>
<p>当前关键词:{keyword}</p>
</div>
);
}
export default SearchDemo;
这个案例里:
useState
保存输入框内容 keyword
因为它要显示到页面上,所以必须用状态。
第一个 useEffect
页面加载后让输入框自动聚焦
因为这是渲染后执行的动作,所以用 useEffect。
第二个 useEffect
每当 keyword 变化时更新浏览器标题
这也属于副作用,所以还是 useEffect。
useRef
一个拿 DOM:inputRef
一个记录输入次数:changeCountRef
输入次数只是打印日志,并不展示到页面,所以没必要用 useState,用 useRef 更合适。
这个案例基本把三个 Hook 的职责划分得很清楚了。
十四、面试中怎么回答它们的区别?
如果面试官问你:
useState、useEffect、useRef 的区别是什么?
你可以这么回答:
useState 主要用于管理组件状态,当状态变化时会触发组件重新渲染,通常用来保存那些会影响页面展示的数据。useEffect 主要用于处理副作用,也就是组件渲染之后需要执行的额外逻辑,比如请求接口、事件监听、定时器、修改标题等。useRef 主要用于保存引用或者持久化数据,它既可以获取 DOM 元素,也可以保存一些不需要触发组件重新渲染的值,比如定时器 id、上一次的值等。
它们的核心区别在于:useState 管状态并驱动视图更新,useEffect 管副作用执行,useRef 管持久化引用但不会触发视图更新。
这段话很适合面试时直接说。
十五、初学者最常犯的几个错误
1. 该用 useRef 的地方用了 useState
比如只是存一个定时器 id,却写成:
const [timer, setTimer] = useState(null);
其实这类数据不参与页面展示,用 useRef 更合理。
2. 该用 useState 的地方用了 useRef
比如页面上的数字要变化,却写成:
const countRef = useRef(0);
countRef.current += 1;
结果发现页面不更新。
因为 useRef 的变化不会触发渲染。
3. 把所有逻辑都往 useEffect 里塞
有些逻辑其实只是普通计算,不一定非要写 useEffect。
不要一上来就觉得“只要是逻辑就放 useEffect”。
4. useEffect 依赖数组乱写
比如副作用里明明用到了 count,却不写到依赖数组里,容易造成旧值问题。
5. 忘记清理副作用
比如监听事件、开定时器却不清理,组件销毁后可能引发 bug。
十六、最后给你一个最简单的判断公式
以后开发时,如果你一时分不清到底该用谁,就套这三句判断。
第一问:这个数据要不要显示到页面上?
要,就优先考虑 useState
第二问:这个逻辑是不是要在渲染之后执行?
是,就优先考虑 useEffect
第三问:我是不是只是想记个值,或者拿 DOM,但不想刷新页面?
是,就优先考虑 useRef
这三个问题,基本能帮你解决 80% 的判断场景。
十七、总结
这篇文章讲了很多,其实最后你真正要记住的,就这几句话。
useState 是什么?
保存会影响页面展示的状态,状态变了会重新渲染。
useEffect 是什么?
处理渲染后的副作用,比如请求接口、事件监听、定时器等。
useRef 是什么?
保存不会触发重新渲染的值,或者获取 DOM 元素。
它们的最大区别是什么?
-
useState:存状态,更新会刷新页面 -
useEffect:执行副作用,不是拿来存数据的 -
useRef:存引用或值,但更新不会刷新页面
如果你之前一直觉得这三个 Hook 很绕,那你现在可以直接把它们理解成:
-
useState:页面数据管理员 -
useEffect:页面行为执行器 -
useRef:页面内部小仓库
这样再看 React Hook,很多东西就没那么抽象了。
晚安,明天见:Arrows N 与富士通的退场
从业者集体辟谣:AI短剧《霍去病》根本搜不到,不存在3000块钱80集

本文来自微信公众号:短剧黑马,作者:短剧黑马,题图来自:AI生成
一、离谱新闻广为流传
今天这篇稿子,是被读者催着写的。

“所谓的一部短剧80集,其实48小时只做了几分钟,根本没有成片。所谓5个亿播放量——那剧都没有,何来播放量?播放量有这么算的吗?三观要碎了啊。”

“这里面好几个甲方直接截图马上来问我们其他制作公司了。是不是一部几十集AI短剧只要3000块还有5亿播放量你们怎么做不到呀?这种事会天天有人贴脸来问我们?”
“传播法是每个从业者都需要学习的,有学习过没有?就事论事,你扯到情绪上去,居心可以啊。”

“播放5个亿......如果不是通稿满天飞,得扣多少个0。问题现在媒体也是无聊,假新闻根本不会管的。”
3月6日,青岛广播电视台的点时新闻登上了热搜第二。词条是:#国产AI短剧火到海外、#破5亿播放AI短剧成本仅3000#。
该热搜指出,杨涵涵导演带着3人团队,用5天,以3000元成本做出了80集的AI短剧《霍去病》。该剧播放量已经超过了5个亿。该剧每秒的特效成本,只有3块钱,还火到了海外。
在该微博的下面,纳米漫剧流水线的回复道:家人们!来认领一下。《霍去病》这部AI短片是用我们纳米漫剧流水线做的!欢迎更多小伙伴来玩我们纳米漫剧流水线namistory.com哦。

该新闻今天仍在持续发酵,已经从行业事件,拓展为了社会向新闻事件。
截止发稿,短剧黑马观察到,抖音上#AI短剧霍去病持续刷屏#的热搜,已经冲到了文娱榜第二。江西旅游广播、大象新闻等官方媒体也纷纷下场报道。路人有惊叹AI工具厉害的,有夸纳米工具厉害的,更多的,则是在问剧在哪儿看的。

根据多方信息判断,AI短剧霍去病的新闻,和8个月前媒体之间疯传的一则新闻类似。
该新闻称,一部名为《特朗普爱上在白宫当保洁的我》精准抓住了美国观众的猎奇心理,尤其以50岁以上女性观众反响最为热烈,在社交平台上引发“边骂边看边付费”狂潮,三个月内狂收1.5亿美元。实际上,该剧、该事件子虚乌有。

短剧黑马就此事的真伪,咨询了AI短片《霍去病》的创作者杨涵涵,截止发稿,未有回复。
二、找不到剧,也数不齐5亿播放量
3月6号下午三点开始,一篇名为《千元成本“撬动”5亿播放量,AI短剧《霍去病》凭啥刷屏》的通稿,开始出现在新浪科技、新浪财经、齐鲁晚报等媒体上。

与此同时,多位漫剧从业者,对该新闻的真实性提出了质疑:
“整个事情的重点是,他们自己亲口撒谎。一杆子打翻一船人,还理直气壮。没有这样玩的,虚假宣传不能被他们自己强行定义为宣传吧?不实成本不能被他们强行定义为成本吧?子虚乌有根本不存在的几十集AI短剧,不能被强行定义为有吧?”
“80集3000成本5亿播放,这个成本简直是屁股决定脑袋!!真人短剧玩烂的套路,在AI短剧重新再玩一遍,抓住几个大爆款,制造几个热点,让一波人都冲进来当韭菜。”

短剧黑马多方核实该新闻。
首先,《霍去病》的AI短片确实是存在的。该剧的作者是杨涵涵。
今年的2月3日,杨涵涵曾在短剧黑马的社群里,分享了该视频。当时,群里的从业者们满是赞美,夸赞其专业。
该剧时长4:39秒,只是一个短片的体量,远不是80集短剧的体量,微信视频号上的点赞是4274。我在抖音平台上搜索时,也只搜到了AI短片《霍去病》,该剧点赞4200。
杨涵涵还做了个AI短片的同名MV,发在了视频号上,点赞779。
在红果平台上,我搜到了一部叫做《封狼居胥》的短剧,只是15集,集数对不上。
此前,杨涵涵在媒体九干光年的采访中,回答该剧有5亿播放量时说,这剧是在海外火的。
Youtube上,我搜到了一部名为《霍去病》的短片,共有22:34分钟,播放量是13万次观看,主演的脸和AI短片《霍去病》不一样。80集的短剧,一般来说至少也需要80分钟,现在的时长和片子的数量,都对不上。

AI短片《霍去病》

X上最高一条是28万查看,245转发,69个评论。
现在真人短剧大爆款的数据是10亿是播放量。漫剧行业播放量唯一破5亿的作品,是第五说制作,酱油文化编剧《让你悟道,没让你起飞》。该剧改编自番茄小说作家“佛苦苦”的作品《让你悟道,没让你扛着天道起飞啊》,坐拥番茄小说的流量入口,有粉丝加持才过了5亿。
如今,我还暂时没有搜到AI短剧《霍去病》,也没有官方平台说,该剧播放量破5亿。
三、虚假新闻不应该发酵
据《霍去病》导演杨涵涵在微博公开透露,该作品的全部影片与图片均通过360纳米漫剧流水线平台生成,后期剪辑使用剪映完成。
通稿中写到,杨涵涵是过往履历是在高校从事行政教务工作,没做过影视剧。普通人的梦想,迎来了前所未有的机遇。
杨涵涵的这种身份设定,是在向大众传递一个有煽动性的核心信息:用纳米漫剧搞AI短剧,你也能成功。
现在年轻人干点活,赚点钱挺不容易的。别这样忽悠他们了。AI短剧是风口和未来,但没有那么容易,就能播放量过亿,赚到钱的。
在各类报道中被频繁提及的“火到海外社交媒体”,大概也是吃准了国内外的信息差,觉得大家很难去海外核实数据。
AI短剧《霍去病》的传播节奏很不错。
内容上,核心关键词是近期讨论度非常高的AI和短剧,5亿、3000元成本、3人团队5天产出,数字吸睛,反差拉满。主角杨涵涵一个人靠着短剧爆火,也很有人设上的戏剧性。
传播链路上,该新闻由青岛的官方媒体首发,增加权威性,营销号跟进,增加热度。纳米官方账号还有周鸿祎认证,植入纳米产品。这则极具煽动性的新闻上了热搜后,诸多官方媒体为了跟热度,纷纷主动展开报道,成功由行业事件转化成了社会事件。
从头到尾,AI短剧《霍去病》不缺热搜、不缺通稿、不缺大佬站台、不缺关注,唯独缺了一部80集短剧本该有的“剧”本身。
这出被编排的传播大戏里,AI短剧《霍去病》、杨涵涵、纳米漫剧流水线都收获了满满的热度;营销号获得了流量;打工的创业者被老板薅着问你看看人家怎么完成的;同类产品的眼看着自己的产品声音更小;看新闻的人获得了虚假的认知。
再进一步,当有创业者真的使用了漫剧工具,发现做不出这样的作品,获得不了5亿播放的时候,失望而归的人,还是会一批一批。
AI神话真诱人,但假的只会带来更假的东西,终究会有人买单。当城门失火时,鱼池也会遭殃。
今天这篇文章,是被很多粉丝催着写的。他们不希望自己在奋斗的AI短剧行业,充斥着这样虚假的声音。
本文来自微信公众号:短剧黑马,作者:短剧黑马
下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动
Generator 迭代器协议 & co 库底层原理+实战
这两个是理解 Generator 的「关键底层知识点」,也是面试中容易被追问的细节——迭代器协议是 Generator 能被遍历的基础,co 库是 Generator 实现异步流程控制的核心,我用最通俗的语言+代码拆解清楚。
一、迭代器协议(Iterator Protocol):Generator 能“暂停/遍历”的底层规则
1. 先搞懂:什么是“协议”?
协议就是「约定好的规则」——ES6 规定了两套和遍历相关的协议:
-
可迭代协议(Iterable Protocol) :一个对象只要有
[Symbol.iterator]()方法,且该方法返回一个「迭代器对象」,就称这个对象“可迭代”(比如 Array、Set、Map、Generator 对象都符合); -
迭代器协议(Iterator Protocol) :一个对象只要有
next()方法,且next()返回{ value: 产出值, done: 是否完成 }格式的对象,就称这个对象是“迭代器”。
2. Generator 与迭代器协议的关系
Generator 函数调用后返回的「生成器对象」,同时满足可迭代协议 + 迭代器协议——这是它能被 for...of 遍历、能暂停/恢复的核心原因。
验证:生成器对象的协议合规性
function* gen() {
yield 1;
yield 2;
}
const g = gen(); // 生成器对象
// 1. 验证迭代器协议:有next(),返回{value, done}
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: undefined, done: true }
// 2. 验证可迭代协议:有[Symbol.iterator](),且返回自身(迭代器)
console.log(typeof g[Symbol.iterator] === 'function'); // true
console.log(g[Symbol.iterator]() === g); // true(关键:返回自身)
// 3. 因此能被for...of遍历(for...of只遍历done: false的value,忽略return值)
for (let val of g) {
console.log(val); // 1、2(return的值不会被遍历)
}
3. 手动实现迭代器协议(理解 Generator 底层)
Generator 本质是 ES6 帮我们自动实现了迭代器协议,我们手动写一个迭代器,就能明白它的核心逻辑:
// 手动实现一个“模拟Generator”的迭代器
const myIterator = {
_step: 0, // 记录执行步骤
next() {
this._step++;
if (this._step === 1) {
return { value: 1, done: false }; // 对应yield 1
} else if (this._step === 2) {
return { value: 2, done: false }; // 对应yield 2
} else {
return { value: 3, done: true }; // 对应return 3
}
},
// 实现可迭代协议:返回自身
[Symbol.iterator]() {
return this;
}
};
// 调用方式和Generator完全一致
console.log(myIterator.next()); // { value: 1, done: false }
console.log(myIterator.next()); // { value: 2, done: false }
console.log(myIterator.next()); // { value: 3, done: true }
4. 面试高频:迭代器协议的核心考点
- Q:为什么 Generator 对象能被
for...of遍历?
A:因为 Generator 对象符合「可迭代协议」——有[Symbol.iterator]()方法且返回迭代器(自身),而for...of会自动调用迭代器的next()直到done: true。 - Q:迭代器协议和可迭代协议的区别?
A:迭代器协议是“对象有 next() 且返回 {value, done}”,可迭代协议是“对象有 Symbol.iterator 且返回迭代器”;前者是“能一步步取值”,后者是“能被遍历”。
二、co 库:Generator 异步流程的“自动执行器”
1. co 库的核心作用
在 async/await 出现前,Generator 处理异步的最大痛点是「需要手动调用 next()」——co 库的本质是一个自动执行器:它能自动调用 Generator 迭代器的 next(),并把异步操作(Promise)的结果作为参数传入下一个 next(),直到 Generator 执行完毕。
简单说:co 库 = 自动调用 next() + 处理 Promise 结果 + 异常捕获。
2. co 库的基本使用(先看效果)
先安装 co 库:
npm install co
使用示例(对比手动执行和 co 自动执行):
const co = require('co');
// 模拟异步请求
function fetchData(url) {
return new Promise((resolve) => {
setTimeout(() => resolve(`数据:${url}`), 1000);
});
}
// Generator异步函数
function* gen() {
const res1 = yield fetchData('https://api1.com');
const res2 = yield fetchData(`https://api2.com?data=${res1}`);
return res2;
}
// 方式1:手动执行(繁琐)
const g = gen();
g.next().value.then(res1 => {
g.next(res1).value.then(res2 => {
console.log(g.next(res2).value); // 数据:https://api2.com?data=数据:https://api1.com
});
});
// 方式2:co自动执行(简洁)
co(gen).then(res => {
console.log(res); // 数据:https://api2.com?data=数据:https://api1.com
});
3. co 库的核心原理(手写简化版)
co 库的源码不到 200 行,核心逻辑是「递归调用 next() + 处理 Promise」,我们手写一个简化版,吃透它的底层:
// 简化版co库:自动执行Generator
function co(genFunc) {
// 返回Promise,符合现代异步规范
return new Promise((resolve, reject) => {
const g = genFunc(); // 获取生成器迭代器
// 递归执行next的函数
function next(val) {
let result;
try {
result = g.next(val); // 恢复执行,传入上一次异步结果
} catch (e) {
return reject(e); // 捕获Generator内部异常
}
const { value, done } = result;
if (done) {
// 执行完毕,resolve最终值
return resolve(value);
}
// 核心:如果value是Promise,等待resolve后继续next
// 非Promise则直接传入下一个next
Promise.resolve(value).then(
(data) => next(data), // 异步成功:把结果传入下一个next
(err) => g.throw(err) // 异步失败:向Generator内部抛异常
);
}
// 启动执行器
next();
});
}
// 测试:和官方co库效果一致
co(gen).then(res => console.log(res)); // 数据:https://api2.com?data=数据:https://api1.com
4. co 库的核心规则(面试必知)
co 库能自动执行的前提是:Generator 中 yield 后面的值必须是以下类型之一(否则会直接传入 next()):
- Promise(最常用);
- 可迭代对象(Array、Set、Generator 对象等);
- 普通对象/函数(co 会尝试转换为 Promise)。
5. co 库 vs async/await(核心关系)
ES2017 引入的 async/await,本质是「Generator + co 库」的语法糖——浏览器/Node 内置了类似 co 的自动执行器,无需手动引入库。
| 维度 | co + Generator | async/await |
|---|---|---|
| 执行方式 | 手动引入co库 | 语言原生支持,自动执行 |
| 异常处理 | 需配合try/catch + co的catch | 原生try/catch即可 |
| 返回值 | Promise(co返回) | Promise(async函数返回) |
| 语法简洁性 | 稍繁琐(function* + yield) | 更简洁(async + await) |
| 兼容性 | 需ES6环境 + co库 | 需ES2017环境(或babel转译) |
6. 面试高频:co 库的考点
- Q:co 库的作用是什么?
A:co 库是 Generator 函数的自动执行器,核心解决 Generator 处理异步时需要手动调用 next() 的问题;它会自动递归调用 next(),并将 yield 后 Promise 的结果传入下一个 next(),最终返回一个 Promise,简化 Generator 异步流程控制。 - Q:async/await 是不是替代了 co 库?
A:是的。async/await 是 Generator + 自动执行器的语法糖,浏览器/Node 内置了类似 co 的执行逻辑,因此在现代开发中,co 库已几乎被 async/await 取代;但理解 co 库的原理,能更好地理解 async/await 的底层逻辑。
总结
迭代器协议核心
- 迭代器协议:对象有
next()且返回{ value, done };可迭代协议:对象有[Symbol.iterator]()且返回迭代器; - Generator 对象同时满足两套协议,因此能被
for...of遍历、能暂停/恢复; - 迭代器协议是 Generator 实现暂停/遍历的底层规则。
co 库核心
- co 库是 Generator 的自动执行器,核心逻辑是「递归调用 next() + 处理 Promise 结果」;
- co 库解决了 Generator 手动调用 next() 的痛点,是 async/await 的“前身”;
- 现代开发中 async/await 已替代 co 库,但理解 co 库原理是掌握 async/await 底层的关键。
这两个知识点是 Generator 进阶的核心,面试中只要能讲清「迭代器协议的规则」和「co 库的自动执行逻辑」,就能体现你对 Generator 不是只懂表面用法,而是理解底层设计。
逃离"Div汤":2026年,当AI写了75%的代码,前端开发者还剩什么?
引言:75%的代码由AI生成,但我的快乐去哪了?
2026年3月,某互联网大厂的前端开发会议上,一位资深工程师在白板上画出了一幅令人窒息的结构图。这个由AI生成的页面结构包含12层嵌套div,每个节点都裹挟着Tailwind的类名组合,像一锅熬煮过度的浓汤。"这代码连自己都看不懂,"他苦笑着摇头,"每天对着这些'Div汤'改bug,就像在拆炸弹。"
Gartner 2026软件工程报告显示,全球75%的企业级前端代码已由AI辅助生成。这个数字背后,是开发者们普遍面临的困境:当代码不再是自己亲手敲出来的,我们对它的掌控感、理解力、创造力还剩多少?这场由AI驱动的代码革命,正在重塑前端开发的生态,也迫使我们重新思考:在效率与创造力之间,如何找到平衡点?
什么是"Div汤":AI生成代码的隐忧
层层嵌套的HTML结构
AI生成的代码往往呈现出一种病态的嵌套结构。以下是一个典型的AI生成的页面结构示例:
<div class="flex flex-col md:flex-row">
<div class="md:w-1/2">
<div class="p-4">
<div class="bg-gray-100 rounded">
<div class="p-2">
<div class="text-sm">
<div class="flex justify-between">
<div class="font-medium">订单号</div>
<div class="text-gray-500">202603151234</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="md:w-1/2">
<div class="p-4">
<div class="bg-gray-100 rounded">
<div class="p-2">
<div class="text-sm">
<div class="flex justify-between">
<div class="font-medium">金额</div>
<div class="text-gray-500">¥128.00</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
这段代码呈现出明显的"Div汤"特征:12层嵌套的div结构,每个节点都添加了不必要的类名。这种结构不仅增加了页面的渲染开销,更让开发者难以理解页面的布局逻辑。
Tailwind类名的重复使用
AI生成的Tailwind类名组合往往呈现出"堆砌式"特征。以下是一个典型的类名组合示例:
<div class="flex flex-col md:flex-row justify-between items-center p-4 bg-white rounded shadow-md">
这种类名的堆砌不仅降低了代码的可读性,更让开发者难以进行有效的样式优化。当需要修改布局时,开发者往往需要逐个排查这些类名,耗时且低效。
代码遗产的不可逆性
AI生成的代码往往缺乏文档注释和结构说明。当开发者接手这样的代码时,常常面临"看不懂、不想改、不敢动"的困境。某电商网站的前端团队曾统计,其AI生成的代码中,有42%的代码存在"结构冗余"问题,但因缺乏文档,开发者不敢轻易重构。
AI带来的效率革命:从"写代码"到"审代码"
效率提升的量化数据
Gartner 2026报告显示,采用AI辅助开发的企业,其页面搭建时间平均缩短60%。某金融科技公司的数据表明,使用AI生成基础结构后,开发人员的编码效率提升了44%。这种效率提升主要体现在:
- 基础结构生成时间从2小时缩短至15分钟
- 常用组件的重复开发时间减少80%
- 布局调试时间降低55%
开发模式的转变
AI的介入正在改变前端开发的模式。开发者从"写代码"的执行者转变为"审代码"的架构师。某头部互联网公司的开发流程显示:
- AI根据需求文档生成基础结构
- 开发者审核并优化结构逻辑
- 手动实现核心交互逻辑
- 使用AI辅助进行样式优化
这种模式要求开发者具备更强的架构设计能力,但同时也带来了新的挑战:当代码不再是自己亲手敲出来的,我们对代码的理解力是否在退化?
隐形的代价:开发者能力的隐性流失
DOM结构直觉的退化
某高校的开发者能力评估报告显示,使用AI生成代码的开发者,其DOM结构理解能力下降了37%。以下是一个典型的调试困境:
document.querySelectorAll('.order-item').forEach(item => {
item.addEventListener('click', () => {
// 无法确定事件冒泡路径
console.log(item.closest('.order-list'));
});
});
当开发者面对这种嵌套结构时,往往需要依赖调试工具才能理解事件冒泡路径,这种依赖性正在削弱开发者对DOM结构的直觉。
调试能力的下降
AI生成的代码往往缺乏调试信息。某开发者社区的调查显示,78%的开发者在调试AI生成的代码时需要额外添加console.log语句。以下是一个典型的调试困境:
const container = document.querySelector('.container');
container.addEventListener('scroll', () => {
// 无法确定滚动事件的触发节点
console.log(container.scrollTop);
});
这种调试困难不仅增加了开发时间,更可能导致潜在的性能问题。
创造力的标准化输出
AI生成的代码往往呈现出"标准化输出"特征。某设计系统的统计数据显示,AI生成的组件样式与设计规范的匹配度仅为62%。这种标准化虽然提高了开发效率,但也抑制了设计师的创意表达。以下是一个典型的样式冲突案例:
/* AI生成的样式 */
.order-item {
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 手动修改的样式 */
.order-item {
padding: 1.5rem;
border-radius: 1rem;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
当开发者需要修改AI生成的样式时,往往需要重新计算所有相关样式,这种繁琐过程正在消磨开发者的创造力。
如何重获创造力:从"写代码"到"设计架构"
从执行到设计的转变
开发者需要重新定位自己的角色,从代码执行者转变为架构设计师。某大型电商公司的开发流程显示:
- 使用AI生成基础结构
- 手动设计布局逻辑
- 制定样式规范
- 使用AI辅助实现细节
// 设计布局逻辑
const layout = {
container: {
maxWidth: '1200px',
margin: '0 auto',
padding: '2rem'
},
header: {
height: '60px',
backgroundColor: '#fff'
}
};
// AI辅助生成代码
const code = generateCode(layout);
这种模式要求开发者具备更强的架构设计能力,但也能在AI辅助下实现更高效的开发。
规范驱动开发(SPEC模式)
SPEC(Specification)模式是一种通过规范驱动开发的方法。某开发团队采用的SPEC模式示例如下:
// SPEC规范
const spec = {
layout: {
type: 'flex',
direction: 'column',
gap: '1rem'
},
card: {
type: 'grid',
columns: 'repeat(3, 1fr)',
gap: '1rem'
}
};
// AI生成代码
const code = generateCode(spec);
这种模式让开发者能够通过规范定义布局,AI则负责将规范转化为实际代码,既保持了代码的可读性,又提高了开发效率。
定期"手写日":回归基础
某开发团队实施的"手写日"制度显示,定期手动编写代码的开发者,其代码质量提升了28%。以下是一个手写日的实践示例:
<!-- 手写日代码 -->
<div class="flex flex-col md:flex-row gap-4">
<div class="md:w-1/2 p-4 bg-white rounded shadow">
<div class="font-medium">订单号</div>
<div class="text-gray-500">202603151234</div>
</div>
<div class="md:w-1/2 p-4 bg-white rounded shadow">
<div class="font-medium">金额</div>
<div class="text-gray-500">¥128.00</div>
</div>
</div>
这种定期的手写实践不仅保持了开发者的代码直觉,也增强了对底层技术的理解。
关注CSS新特性:重获控制权
CSS的持续发展为开发者提供了新的控制手段。某开发团队采用的CSS新特性示例如下:
/* 使用CSS Grid实现响应式布局 */
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
/* 使用CSS Variables实现样式复用 */
:root {
--primary-color: #3b82f6;
}
.card {
background-color: var(--primary-color);
padding: 1rem;
border-radius: 0.5rem;
}
通过关注CSS新特性,开发者可以重新获得对样式控制的主动权,避免完全依赖AI生成的样式代码。
工具的选择:在AI时代保持掌控
文心快码的SPEC模式
文心快码的SPEC模式提供了白盒化流程,让开发者能够精确控制AI生成的代码。以下是一个SPEC模式的使用示例:
// 定义SPEC规范
const spec = {
layout: {
type: 'flex',
direction: 'column',
gap: '1rem'
},
card: {
type: 'grid',
columns: 'repeat(3, 1fr)',
gap: '1rem'
}
};
// 生成代码
const code = generateCode(spec);
这种模式让开发者能够通过规范定义布局,AI则负责将规范转化为实际代码,既保持了代码的可读性,又提高了开发效率。
Cursor的Composer:多文件编辑掌控感
Cursor的Composer功能允许开发者在多文件中进行协同编辑,保持对代码的掌控感。以下是一个Composer的使用示例:
// 项目结构
project/
├── index.html
├── styles.css
└── script.js
// Composer编辑流程
1. 打开index.html
2. 选择"Add CSS Class"操作
3. 选择"Add JS Event"操作
4. 保存并同步到其他文件
这种多文件协同编辑模式,让开发者能够在保持代码连贯性的前提下,充分发挥创造力。
结语:AI不是替代者,而是放大器
在AI时代,前端开发者的角色正在发生深刻变化。我们不再是单纯的代码书写者,而是架构设计师、规范制定者和创造力的守护者。AI带来的效率革命不可逆转,但开发者需要主动掌握这场变革的方向。
选择权始终在开发者手中:是让AI完全接管代码生成,还是通过SPEC模式保持对架构的控制?是放弃手写实践,还是定期回归基础?是被动接受AI生成的样式,还是主动探索CSS新特性?
真正的智慧在于,让AI成为我们的放大器,而不是替代者。当我们学会在效率与创造力之间找到平衡点,才能在AI时代保持前端开发的活力与魅力。记住,代码的温度,永远来自开发者的手指。
最基础的类型检测工具——typeof, instanceof
typeof 和 instanceof 是 JavaScript 中两个最基础但也最容易让人困惑的类型检测工具。要深入理解它们的原理,我们需要从 JavaScript 的底层数据存储、类型系统和原型链机制入手。
typeof
typeof 是一个一元运算符,它的核心任务是返回一个代表操作数类型的字符串。其原理深入到 JavaScript 引擎是如何在底层存储和标识变量的。
1. 核心原理:底层类型标签(Type Tagging)
在 JavaScript 的早期实现中,值在引擎内部是由一个**类型标签(Type Tag)**和实际的数据值来表示的。这个类型标签存储在变量的机器码低位中,用于标识该值的类型。
- 底层存储机制:JavaScript 引擎在存储变量时,会在内存中为变量分配空间,并用低位的1-3个比特位来存储其类型信息。
-
typeof的工作方式:当你对变量使用typeof操作符时,JavaScript 引擎并不会去回溯变量的创建过程,而是直接读取这个变量在内存中机器码低位的类型标签,然后将其映射为对应的类型字符串返回。
2. 机器码的类型映射
不同的类型对应着不同的低位标识。一个常见的类型标签映射如下:
-
000:对象(object)。 -
010:浮点数(number)。 -
100:字符串(string)。 -
110:布尔值(boolean)。 -
1:整数(number),整数类型标签是1,但会被归为number类型。 -
-2^30:undefined。
3. 著名的历史遗留问题:typeof null === 'object'
这是 JavaScript 中最著名的Bug之一,至今未被修复以保持兼容性。
-
原因:如前所述,对象的类型标签是
000。而**null在底层表示的是空指针,在大多数实现中,空指针的机器码全是0**。因此,当typeof读取null的类型标签时,发现是000,就错误地将其判断为object。
4. 特例:函数的识别
虽然函数在底层也是对象(类型标签是 000),但 typeof function(){} 返回的是 'function'。这是因为 JavaScript 引擎内部对可调用对象做了特殊处理。当 typeof 操作符检测到一个对象内部实现了 [[Call]] 方法时,它会特殊处理并返回 "function"。
instanceof
instanceof 是一个二元运算符,用于检测一个对象的原型链上是否存在另一个构造函数的 prototype 对象。它的核心是原型链查找。
1. 核心原理:原型链检查
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。其内部机制可以简单地用以下代码模拟:
function myInstanceof(instanceObj, constructorFunc) {
// 1. 获取实例对象的隐式原型(内部 [[Prototype]] 属性)
let proto = Object.getPrototypeOf(instanceObj);
// 2. 获取构造函数的显式原型
let prototype = constructorFunc.prototype;
// 3. 沿着原型链向上查找
while (true) {
// 4. 如果找到头(null)还没找到,返回 false
if (proto === null) {
return false;
}
// 5. 如果找到了匹配的原型,返回 true
if (proto === prototype) {
return true;
}
// 6. 继续向上一级查找
proto = Object.getPrototypeOf(proto);
}
}
2. 工作步骤详解
假设我们执行 obj instanceof Constructor,引擎会执行以下步骤:
-
获取隐式原型:获取左侧对象
obj的内部[[Prototype]]属性(在浏览器中可以通过非标准__proto__或标准Object.getPrototypeOf()访问)。 -
获取显式原型:获取右侧构造函数
Constructor的prototype属性。 -
循环比较:将
obj的隐式原型与Constructor的显式原型进行比较。- 如果相等,返回
true。 - 如果不相等,则将
obj的隐式原型的隐式原型(即原型链的上一级)取出来,再次与Constructor.prototype比较。 - 这个过程持续进行,直到原型链的末端(即
null)。如果一直没找到相等的对象,则返回false。
- 如果相等,返回
3. 重要特征
-
跨窗口问题:
instanceof依赖于原型链,因此它不能跨不同的全局执行环境(例如,来自iframe的数组在父页面中使用array instanceof parentWindow.Array会返回false),因为它们的原型链指向的是不同的Array.prototype对象。 -
只能用于对象:由于
instanceof的机制是查找原型链,对于原始类型(string,number,boolean等),它们不是对象,没有原型链,因此直接用instanceof检测原始类型会始终返回false。
总结对比
| 特征 | typeof |
instanceof |
|---|---|---|
| 本质原理 | 读取变量机器码低位的类型标签。 | 遍历左侧对象的原型链,查找右侧构造函数的 prototype。 |
| 返回值 | 字符串(如 "string", "object", "function") |
布尔值(true / false) |
| 适用场景 | 检测原始类型(除 null 外)和函数。 |
检测对象类型及其继承关系。 |
| 局限性 |
null 返回 "object";数组、日期等对象均返回 "object",无法细分。 |
不能跨窗口(iframe)使用;不能用于检测原始类型。 |