普通视图
韩束:旗下所有产品均未添加人表皮生长因子成分
疯狂动物城2实时票房破39亿
司美格鲁肽、替尔泊肽集体降价,2026年减重药竞争前移
AI 四格笑话爆火,我做了什么?
0. 前言
在 2025年的尾巴上,发生了一件非常有趣的事,我在微信公众号上的 AI 四格漫画 意外爆火。之前公众号上发的技术文章,基本上阅读量不过 300,每天广告收益也就几毛钱。目前最火的美杜莎,浏览量已经达到惊人的 5W。这样让我不禁感叹:
十年技术无人问,一曲漫笑广人闻。
![]()
火爆之后,带来的最直接价值就是迎来了泼天富贵。从未想过有一天,我的日广告收益能达到 250+ ,目前已经连续三天高位。除了金钱,自己的作品受到欢迎,以及大家在评论区的吐槽、讨论,也为我带来了很大的情绪价值。
| - | - |
|---|---|
1. 缘起
先简单介绍一下:我是一个籍籍无名的编程技术小博主,全网统一名号 张风捷特烈 。 编程之王 是我维护的公众号,一直是输出编程技术的文章,主要以 Flutter 技术为主。
但技术文章更新的不是非常频繁,而公众号每天有一篇发文的机会。本着 不想浪费 的优良传统,在 AI 重塑一切的浪潮中,我想用 AI 画些四格漫画的笑话试试。于是开启了 慧心一笑 专栏, 《小火柴的倒霉日常》 就是第一篇,现在还没火。大家也可以点开看看,内容非常精简,就是一幅图+提示词。
![]()
这个系列整体是诙谐幽默的,下面是第一篇的内容:
![]()
一开始我是用自然语言的提示词,感觉效果并不是太好,四格漫画有着连续的信息和一致性的人物、场景等。由于编程出身,在 结构一致性 方面有着天然的敏锐嗅觉。于是基于 yaml 文件来定义统一的场景、角色、样式、色调等信息:
comic_info:
type: "四格漫画"
style: "手绘简笔画、柔软线条、轻松冷幽默、统一角色"
color_scheme: "暖黄主色调,红橙色点缀,柔和明暗层次"
character:
name: "小火柴"
appearance: "细长圆柱身体、红色火柴头、两根短竖眉毛、圆点眼睛、呆萌可爱"
personality: "迷糊、天真、略倒霉"
background_style: "白色简约背景,搭配少量手绘街景或物件增强生活感"
面板列表放在 panels 节点下,每个宫格由 panel[x] 固定场景内容。包括描述、场景、动作、表情、细节、文本等:
panels:
panel1:
description: "第一格:日常铺垫"
scene: "温暖的手绘街道:地面为淡黄色纹理,简单的路灯、几株小草、远处一座小房子,空气里飘着幾颗小亮点"
action: "小火柴双手背在身后,踩着轻快的小步子前进"
expression: "轻松微笑,眼睛微弯"
details: "路灯用细线勾勒,小草三两稀疏点缀,天空加几朵柔软的白云"
text: "今天天气真好呀~"
定义完结构,一个 yaml 文件就对应了一个四格故事,把这个内容丢给 AI 生图的工具,就能得到对应的图片。
![]()
2. 关于 AI 生图工具与质量
我的理念是: 文本是一种序列的约定:
它可以视为一个四格漫画的 基因,而 AI 工具会将基因
实例化为个体。
所以,生成图的好坏取决于两个因素:基因序列 和 成长环境。也就是提示词好不好,以及 AI 工具厉不厉害。 AI 生图的工具有很多,单目前大多数,对于标准的四格漫画都无法准确输出,下面列举几个:
- 即梦 AI
![]()
- 豆包
![]()
- Nano Banana
![]()
目前来看,国产的 AI 仍有很大的进步空间,Nano Banana 能符合我对图片产品的预期。但是 AI 正在蓬勃发展中, AI 生图也是最近一两年才逐渐可用的,我对他们的未来持有乐观的态度,包括我们国产的大模型。所以如果 成长环境 将会越来越好,那么 基因序列 本身将会成为非常重要的因素。
目前我只是简单设计了一下 yaml,按照版本控制,称为 v0.0.1 吧,后续随着创作需求的升级,我也会逐步迭代整体结构,设计更合理的 DNA 结构 😁
3. 选定方向? Flow Heart
有人问我,你是怎么想到这些稀奇古怪的方向的,而且你是怎么坚持下来的。
对于一个创作者来说,拓宽自己的边界是一个很必要的事。特别是对一个编程创作者,广泛涉猎是家常便饭。使用一切手段,解决自己遇到的问题;没有问题时就去发展自己,在新的领域中寻找问题。至于坚持嘛,遵循内心的指引,做自己喜欢的事,是不需要坚持的,就像你每天都要喝水一样自然。
可能有人会问,如果 AI 的笑话漫画没有火,你还会坚持下去吗?刚做前两个漫画文章时,还没有火,一天收入 1 块钱,我已经觉得很美滋滋了。投入的产出符合我的预期,毕竟只需要准备个笑话雏形,其他都交给 AI 写就行了。我还和女朋友炫耀:
| - | - |
|---|---|
最后还是想强调一点:如果一件事,对社会、对他人没有危害,自己做着觉得开心,起来没有负担和压力,就会大胆去做。反之,可以在其他方面继续延伸,找到自己喜欢的那个领域。AI 工具的加持,让个体拥有了前所未有的能力,个人的边界可以极度拓宽。
4. 为什么会火?
第一次感觉会火,是因为擎天柱 这篇,浏览量异常上升:
![]()
从数据统计来看,发布第一天只有 102 个浏览量,和往常没什么区别。持续一周,没有任何波澜,突然在 12-20 号,增加了近 5000 的浏览量,第二天持续上涨过万,然后逐渐平息:
![]()
在第一篇爆火的后一天,慧心一笑#03 | 爸爸去钓鱼~ 数据开始上升,感觉像是连带效应:
![]()
![]()
为了验证一下是不是偶然火爆,我在 20号和 21 号又发表了两篇小笑话。结果不温不火,似乎感觉也不是必然的。 在 23 号,我发布了 慧心一笑#06 | 被美杜莎石化...,这篇在当晚直接火爆,
![]()
从数据来看,第二天浏览量直接过 2.6W,后面还有持续几天的流量:
![]()
至于为什么火爆,从阅读渠道构成来看 98.7% 的阅读量来自于公众号推荐。只能说是老天喂饭吃 ~
![]()
5. 小结一下
接下来几天的 慧心一笑#07 | 爸爸回来了... 和 慧心一笑#09 | 农夫与蛇 也阅读过 3万。目前慧心一笑系列发布了 9 篇,阅读量超过 2.5W 的爆款有 5 篇,比例算是很高了。
感觉微信公众号的推荐阅读机制应该有所变化。另外也不是每篇都会火爆,应该和作品本身质量、流传度也有关系。这个有趣的现象让我非常欣喜,后续我还会继续创作更有意思的四格漫画,来继续验证数据。大家也可以关注 《编程之王》 公众号和我一起见证。等到第 30 篇后,我会再写一个复盘报告,和大家分享。
另外可能会有人问,你发这个就不怕别人也抄你的模式,跟你竞争吗。我只想说:
![]()
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。让我们一起成长,变得更强。我们下次再见~
Vue3与iframe通信方案详解:本地与跨域场景
ps:本项目使用的vue3技术栈
Vue3与iframe通信方案详解:本地与跨域场景
本文详细介绍了在Vue3项目中,与内嵌iframe(包括本地HTML文件和服务端跨域HTML)进行双向通信的完整解决方案。核心通信方式为postMessage API,并针对不同场景提供了安全可靠的代码示例。
1. iframe加载本地HTML文件
1.1 Vue端通信代码
<template>
...
<iframe
ref="iframe"
name="iframe-html"
src="./index.html"
width="100%"
height="100%"
frameborder="0"
></iframe>
...
</template
如何在vue端跟iframe端加载的.html文件进行通讯呢,看下面的代码
// vue端
...
const sendMsg2iframe = (msg) => {
window["iframe-html"].sendMsg2iframe(msg);
}
...
// index.html
...
window.sendMsg2iframe = function (msg) {
// 接收到vue端发来的消息
}
...
1.2 iframe端(index.html)通信代码
// index.html
function sendMessageToVue(messageData) {
// 发送消息到父窗口
window.parent.postMessage(messageData, window.location.origin);
}
// vue端
// 组件挂载时开始监听消息
onMounted(() => {
window.addEventListener('message', handleReceiveMessage);
});
// 组件卸载时移除监听,防止内存泄漏
onUnmounted(() => {
window.removeEventListener('message', handleReceiveMessage);
});
// 接收来自iframe消息的处理函数
const handleReceiveMessage = (event) => {
// 重要:在实际应用中,应验证event.origin以确保安全
// if (event.origin !== '期望的源') return;
console.log('Vue组件收到来自iframe的消息:', event.data);
// 在这里处理接收到的数据
};
2. iframe加载服务器HTML(跨域场景)
其实还是通过window的postMessage进行通讯,只不过是涉及到了跨域问题,下面是具体的代码,关键在于postMessage的第二个参数上
2.1 html端通信代码
// .html
...
// 获取url并解析出父窗口的origin
const urlParams = new URLSearchParams(window.location.search);
const parentOrigin = urlParams.get('parentOrigin') || window.location.origin;
// 监听来自父窗口的消息
window.addEventListener('message', function (event) {
if (event.origin === parentOrigin) {
console.log('收到来自父窗口的消息:', event.data);
if(event.data.type === 'sendJSON2Unity'){
window.SendJSON2Unity(event.data.data);
}
}
});
function sendMessageToVue(messageData) {
// 发送消息到父窗口
window.parent.postMessage(messageData, parentOrigin);
}
...
2.2 Vue端通信代码
// .vue
...
<iframe
ref="iframeRef"
name="unity-home"
:src="violationDocumentURL"
width="100%"
height="100%"
frameborder="0"
@load="onIframeLoad">
</iframe>
...
// 这里把自己的origin通过URL参数传给iframe
const violationDocumentURL = import.meta.env.VITE_U3D_SERVICE + "具体路径" + "?parentOrigin=" + encodeURIComponent(window.location.origin);
const iframeRef = ref(null);
const iframeOrigin = ref(import.meta.env.VITE_U3D_SERVICE.replace(/\/$/, "")); // iframe加载的资源的origin
const sendToUnity = (data) => {
iframeRef.value.contentWindow.postMessage(
data,
iframeOrigin.value
);
};
// 组件挂载时开始监听消息
onMounted(() => {
window.addEventListener('message', handleReceiveMessage);
});
// 组件卸载时移除监听,防止内存泄漏
onUnmounted(() => {
window.removeEventListener('message', handleReceiveMessage);
});
// 接收来自iframe的消息
const handleMessageFromIframe = (event) => {
// 确保消息来自可信的来源
if (event.origin === iframeOrigin.value) {
if (event.data) {
// do something
}
}
};
ok基本就是这样的
3 服务器HTML端(Unity WebGL示例)
因为我们是加载的unity的webgl包,所以最后附赠一下打出的webgl包的index.html的代码(ps:是不压缩版的)
<!DOCTYPE html>
<html lang="en-us" style="width: 100%; height: 100%">
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Unity WebGL Player | NanDingGDS</title>
</head>
<body id="unity3d-body" style="text-align: center; padding: 0; border: 0; margin: 0; width: 100%; height: 100%; overflow: hidden">
<canvas id="unity-canvas" style="background: #231f20"></canvas>
<script>
/** unity的web包加载逻辑开始 */
const canvas = document.getElementById("unity-canvas");
const body = document.getElementById("unity3d-body");
const { clientHeight, clientWidth } = body;
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
var meta = document.createElement("meta");
meta.name = "viewport";
meta.content = "width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes";
document.getElementsByTagName("head")[0].appendChild(meta);
container.className = "unity-mobile";
canvas.className = "unity-mobile";
} else {
canvas.width = clientWidth;
canvas.height = clientHeight;
}
const baseUrl = "Build/webgl";
var loaderUrl = baseUrl + ".loader.js";
var myGameInstance = null;
var script = document.createElement("script");
script.src = loaderUrl;
var config = {
dataUrl: baseUrl + ".data",
frameworkUrl: baseUrl + ".framework.js",
codeUrl: baseUrl + ".wasm",
streamingAssetsUrl: "StreamingAssets",
companyName: "DefaultCompany",
productName: "FanWeiZhang",
productVersion: "0.1.0",
};
script.onload = () => {
createUnityInstance(canvas, config, (progress) => {}).then((unityInstance) => {
myGameInstance = unityInstance;
sendMessageToVue({
type: "unityLoaded",
message: "Unity3D加载完成",
});
});
};
document.body.appendChild(script);
/** unity的web包加载逻辑结束 */
// 获取url并解析出父窗口的origin
const urlParams = new URLSearchParams(window.location.search);
const parentOrigin = urlParams.get("parentOrigin") || window.location.origin;
// 监听来自父窗口的消息
window.addEventListener("message", function (event) {
if (event.origin === parentOrigin) {
console.log("收到来自父窗口的消息:", event.data);
if (event.data.type === "sendJSON2Unity") {
window.SendJSON2Unity(event.data.data);
}
}
});
function sendMessageToVue(messageData) {
// 发送消息到父窗口
window.parent.postMessage(messageData, parentOrigin);
}
window.SendJSON2Unity = function (str) {
console.log("发送到Unity的JSON字符串:", str);
myGameInstance.SendMessage("WebController", "receiveJSONByWeb", str);
};
window.QuiteUnity = function () {
console.log("退出Unity3D");
sendMessageToVue({
type: "quitUnity",
message: "退出Unity3D",
});
};
// window.js2Unity = function (str) {
// // 第一个参数是unity中物体的名称,第二是要调用的方法名称,第三个参数是unity中接收到的参数
// // myGameInstance.SendMessage('Main Camera', 'TestRotation', '')
// console.log(str);
// }
</script>
</body>
</html>
css和图片主题色“提取”
这个想法是来源于「性能优化」中的骨架屏: 在图片居多的站点中,这将是非常nice的体验 —— 图片加载通常是比较让人难受的,好的骨架中一般占位图就是低像素的图片,即大体配色和变化是和实际内容一致的。 有时候比如图片不固定的,那可以使用算法获取图片的主体颜色(至少得是同色系的吧),使用纯色块占位。
再进一步想到,在一些“轻松”的场景下,我们可以让背景色/页面主题色跟随轮播图改变。至于效果嘛......你们可以想一下网易云音乐滑动切歌时的背景效果。
因为是不固定图片,所以我想到了四种方法:
- tensorflow.js 图像色彩分析
- canvas对图片主基调进行分析,取大概值
- css高斯模糊
- 上传图片时后端对图片分析处理,返回时直接返回一张低像素图片
第一种方式目前还在我的实践中,以后会单独出一篇文章;最后一种方式个人不太建议首选:首先后端处理也需要时间,另一方面毕竟也是以图片进行传输的...yee~(而且后端可能也不太建议你首选🤣)
想看实际效果的推荐自己动手试下,因为我发现本文中用QQ截屏截取的图片怎么都这么暗啊,实际展示的还是挺漂亮的。
第三种方式看起来是纯css实现的,怎么获取呢?这就要说到css中的filter: blur(); 简单来说,利用模糊滤镜及进一步拉伸,可以近似地拿到一张图片的主题色:
<div></div>
div {
background: url(图片地址);
background-size: cover;
filter: blur(50px);
}
你看,通过比较大的一个模糊滤镜,将图片高斯模糊50px,模糊后的图片是不是有点内味了,
![]()
不过还不行,存在一些模糊边缘,我们可以利用overflow进行剪裁。
接下来,我们需要去掉模糊的边角,以及通过transform: scale()放大效果,将颜色进一步聚焦:
这里就很推荐使用伪元素进行操作了
div {
position: relative;
width: xx;
height: xx;
overflow: hidden;
}
div::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url(图片地址);
background-size: cover;
filter: blur(50px);
transform: scale(2); //自行更改
transform-origin: center center;
}
![]()
这样就拿到图片的主色调了。当然是要进行其他处理的。
再来说说第二种方法 —— canvas。其实也不建议,因为本身就是JS操作,而在图片又不固定又有些多的情况下单线程的js处理这种“一级事件”造成的性能和体验感的损失是不可想象的。但本文笔者还是要分享一下,因为这是我当初研究的第一个被应用的成果(有情怀了嘿嘿)
首先,canvas中的getImageData()方法可以获取图片的像素集合:
function getImagePixel(canvas, img) {
const context = canvas.getContext("2d");
context.drawImage(img, 0, 0);
return context.getImageData(0, 0, canvas.width, canvas.height).data;
}
这里对使用canvas不熟悉的同学提个醒:img是异步加载的,所有对图片的操作都要放在 img 的 onload 中进行 —— 你可以考虑用 promise 做这件事。
![]()
调用这个函数会拿到一个数组 —— 它是rgba值,也就是说,处理时四个数据为“一组”,更通俗地说,for循环中i+=4!来处理一下数据:
function getCountArr(pData) {
let colorList = [], rgba = [], rgbaStr = '';
for(let i=0; i<pData.length; i+=4) {
rgba[0] = pData[i];
rgba[1] = pData[i+1];
rgba[2] = pData[i+2];
rgba[3] = pData[i+3];
if(rgba.indexOf(undefined)!==-1 || pData[i+3] === 0) {
continue;
}
rgbaStr = rgba.join(',');
if(rgbaStr in colorList) {
++colorList[rgbaStr];
}else {
colorList[rgbaStr] = 1;
}
}
return colorList;
}
这个时候,得到的就是每组数据(色值)出现的次数了。 然后改写刚刚的getImagePixel函数:
return getCountArr(pixelData);
至此,我们将其排序并取出第一个值/或者取出某些标志项的平均值,基本上就可以将其作为 background 值了!
峰回路转!
你难道真觉得canvas的这种方法只是鸡肋?那试想这样一种场景:在弱网情况下,图片必定贼慢才能加载出来。这时候我们通过js拿到图片的主色调并填充到图片的位置中。这是不是一个“模糊渐变加载”的绝佳场景! 而且,笔者曾经遇到这样一个场景:往图片上添加文字。这时候你就需要注意一个问题,图片主色调。用canvas分析图片的主要颜色或平均色可以在深色调时添加白色文字在浅色调时添加黑色文字!
笔者前段时间弄了一个微信公众号:前端Code新谈。里面暂时有webrtc、前端面试和用户体验系列文章,欢迎关注!希望能够帮到大家,也希望能互相交流!共同进步
Echarts常用配置
title设置字体
textStyle
option = {
title: {
text: "Main Title",
subtext: "Sub Title",
left: "center",
top: "center",
textStyle: {
fontSize: 30,
fontWeight:'bolder'
},
subtextStyle: {
fontSize: 20
}
}
}
控制图表边距
grid: { top: '20%',botton:'20%',left:'10%',right:'10%' },
X轴坐标系标签,旋转角度
xAxis: [
{
type: 'category',
data: data,
axisPointer: {
type: 'shadow'
},
axisLabel: { // 坐标轴刻度标签的相关设置。
rotate: '20' // x轴数据标签旋转角度
}
}
],
限制柱状图最大宽度
series: [
{
name: '数量',
type: 'bar',
barMaxWidth: 50, // 最大宽度
data: data
}]
柱状图渐变色
series里面
itemStyle: {
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1, // 渐变方向从左上角到右下角
[
{ offset: 0, color: 'rgb(128,100,162)' }, // 0% 处的颜色
{ offset: 1, color: '#fff' } // 100% 处的颜色
]
)
},
柱状图文字显示
直接在取消柱子上方显示具体数据信息,以及自定义信息,比如100%,数字后面加一个百分号 1)show,显示节点上的文本信息 2)position,文本位置,可以根据需要调整为 ‘top’, ‘bottom’, ‘inside’, ‘insideTop’, 等 top,表示在节点上方
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'bar',
label:{
show:true,
position:'top',
formatter:function(data){
return data.value+'件'
}
}
}
]
折线图变平滑
series属性中使用smooth: true语句让折线图变成平滑折线图
echart柱状图最小间隔
var option = {
// ... 其他配置项
yAxis: {
type: 'value',
// 设置Y轴的最小间隔
minInterval: 1 // 示例值,根据实际需求进行调整
},
// ... 其他配置项
};
立体柱状图
var xData2 = ['容城谷庄']
var data1 = [50]
option = {
backgroundColor: 'rgba(0,0,0,0)',
grid: {
left: 0,
bottom: 15,
top: 15,
right: 80
},
xAxis: {
data: xData2,
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
show: false
}
},
yAxis: {
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
// textStyle: {
// color: '#fff',
// fontSize: 20,
// },
// 不显示Y轴数值
formatter: function () {
return ''
}
}
},
series: [
// 数据低下的圆片
{
name: '',
type: 'pictorialBar',
symbolSize: [41, 15],
symbolOffset: [0, 8],
z: 12,
symbol: 'circle', // 修改为圆形
itemStyle: {
opacity: 1,
color: function (params) {
return new echarts.graphic.LinearGradient(
1,
// 深色#2BA9ED 浅色 #34EDF2
0,
0,
0,
[
{
offset: 0,
color: '#E1DC53' // 0% 处的颜色
},
{
offset: 1,
color: '#E1DC53' // 100% 处的颜色
}
],
false
)
}
// color: 'transparent'
},
data: [1]
},
// 数据的柱状图
{
name: '',
type: 'bar',
barWidth: 41,
itemStyle: {
// lenged文本
opacity: 1, // 这个是 透明度
color: function (params) {
return new echarts.graphic.LinearGradient(
0,
1,
0,
0,
[
{
offset: 0,
color: '#E1DC53' // 0% 处的颜色
},
{
offset: 1,
color: '#E8AE62' // 100% 处的颜色
}
],
false
)
}
},
data: data1
},
// 替代柱状图 默认不显示颜色,是最下方柱图(邮件营销)的value值 - 20
{
type: 'bar',
symbol: 'circle', // 修改为圆形
barWidth: 43,
itemStyle: {
color: 'transparent'
},
data: data1
},
// 数据顶部的样式
{
name: '',
type: 'pictorialBar',
symbol: 'circle', // 修改为圆形
symbolSize: [41, 15],
symbolOffset: [0, -8],
z: 12,
itemStyle: {
normal: {
opacity: 1,
color: function (params) {
return new echarts.graphic.LinearGradient(
0,
0,
1,
0,
[
{
offset: 0,
color: '#E1DC53' // 0% 处的颜色
},
{
offset: 1,
color: '#E8AE62' // 100% 处的颜色
}
],
false
)
},
label: {
show: true, // 开启显示
position: 'top', // 在上方显示
textStyle: {
// 数值样式
color: '#FFFFFF',
fontSize: 20,
top: 50
},
formatter: function (param) {
return param.data + '%'
}
}
}
},
symbolPosition: 'end',
data: data1
},
// 阴影的顶部
{
name: '', // 头部
type: 'pictorialBar',
symbol: 'circle', // 修改为圆形
symbolSize: [41, 15],
symbolOffset: [0, -8],
z: 17,
symbolPosition: 'end',
itemStyle: {
color: 'rgba(24,78,134,0.3)',
opacity: 0.3,
borderWidth: 1,
borderColor: '#526558'
},
data: [100]
},
// 后面的背景
{
name: '2019',
type: 'bar',
barWidth: 41,
barGap: '-100%',
z: 0,
itemStyle: {
color: 'rgba(24,78,134,0.1)'
},
data: [100]
}
]
}
Echarts给柱状图上面增加小横杠
option = {
title: {
text: '世界人口统计',
left: 'center',
textStyle: {
fontSize: 24,
fontWeight: 'bold',
color: '#333'
}
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
name: '人口 (万)',
nameLocation: 'middle',
nameGap: 30,
axisLabel: {
formatter: function(value) {
if (value >= 10000) {
return (value / 10000) + '亿';
}
return value;
}
}
},
yAxis: {
type: 'category',
data: ['巴西', '中国', '世界'],
axisLabel: {
color: '#666',
fontSize: 14
}
},
series: [
{
name: '2011',
type: 'bar',
data: [10,20],
itemStyle: {
borderRadius: [0, 4, 4, 0],
color: '#36A2EB'
},
label: {
show: true,
position: 'right',
formatter: function(params) {
return params.value.toLocaleString();
}
},
markPoint: {
symbol: 'rect',
symbolSize: [4, 20],
data: [
{
// 标记巴西 (2011)
name: '',
coord: [10, '巴西'],
itemStyle: { color: '#FF6384' }
},
{
// 标记中国 (2011)
name: '',
coord: [20, '中国'],
itemStyle: { color: '#FF6384' }
}
]
}
}
]
};
javascript
markPoint: { symbol: 'rect', // 标记点形状为矩形 symbolSize: [4, 20], // 标记点大小 data: [ { name: '', coord: [2, 36], value: 36 } // 关键配置 ] }
假设这是一个**折线图或柱状图**(直角坐标系):
- **`coord: [2, 36]`** 的含义:
- **`2`**:在 X 轴上,对应**第 3 个类目**(索引从 0 开始,例如 `['一月', '二月', '三月', ...]` 中的 `'三月'`)。
- **`36`**:在 Y 轴上的数值位置(例如 Y 轴范围是 0~100,标记点位于 Y=36 的高度)。
- **效果**:在 X 轴第 3 个类目(`'三月'`)与 Y 轴数值 36 的交叉点处,绘制一个 4×20 大小的矩形标记
柱状图文字太多不显示
优化 X 轴标签显示
若标签文字过长或过多,即使调整柱子间距仍可能显示不全,需进一步配置 axisLabel。
1. 强制显示所有标签(避免省略)
xAxis: {
axisLabel: {
interval: 0, // 强制显示所有标签(默认自动隐藏部分标签)
// 或使用 formatter 换行(适用于长标签)
formatter: function (value) {
return value.split('').join('\n'); // 按字符换行(示例)
// 或根据字数换行:return value.substr(0, 4) + '\n' + value.substr(4);
}
}
}
2. 旋转标签文字
通过 rotate 调整文字角度,避免重叠:
xAxis: {
axisLabel: {
rotate: 45, // 旋转角度(建议 30°~60°,避免垂直显示)
margin: 10 // 标签与轴的间距,防止被柱子遮挡
}
}
3. 自适应隐藏部分标签
若必须显示部分标签,可通过 interval 控制显示间隔(如每隔 N 个显示 1 个):
xAxis: {
axisLabel: {
interval: 1 // 0=全部显示,1=隔 1 个显示 1 个,2=隔 2 个显示 1 个,依此类推
}
}
取消Y轴分割线
yAxis: {
type: 'value',
splitLine: {
show: false
}
},
y轴上方标题
option = {
yAxis: {
name: '数量\n(个)', // 名称和单位分行
nameLocation: 'end',
nameGap: 5,
nameTextStyle: {
color: '#333', // 深色文本
align: 'left', // 左对齐
lineHeight: 16, // 行高控制间距
padding: [0, 0, 0, -8] // 往左偏移更多
},
// 其他配置...
},
// 其他配置...
};
设置柱状图间隔(象形图)
![]()
echart分组柱状图没数组不展示
// 原始数据源(模拟有缺失数据的场景)
let tufang = [100, 200, 150, 80, 70, 110, 10];
let qiaoliang = [100, 80, 90, 0, 60, 0, 150];
let suidao = [0, 90, 150, 80, 70, 0, 10];
let lumian = [0, 0, 10, 80, 70, 0, 0];
let jidian = [90, 190, 150, 0, 70, 0, 10];
const option = {
tooltip: {},
title: {
show: true,
text: '不符合常理的柱状图表实现',
textStyle: {
fontSize: 14,
lineHeight: 18,
width: 10
}
},
xAxis: [
{
type: 'category',
axisLabel: {
align: 'center',
hideOverlap: true
},
data: this.specificKeys
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'custom',
renderItem: function (params, api) {
return getRect(params, api);
},
data: tufang
},
{
type: 'custom',
renderItem: function (params, api) {
return getRect(params, api);
},
data: qiaoliang
},
{
type: 'custom',
renderItem: function (params, api) {
return getRect(params, api);
},
data: suidao
},
{
type: 'custom',
renderItem: function (params, api) {
return getRect(params, api);
},
data: lumian
},
{
type: 'custom',
renderItem: function (params, api) {
return getRect(params, api);
},
data: jidian
}
]
}
function getRect (params, api) {
let dataSeries = [
tufang,
qiaoliang,
suidao,
lumian,
jidian
]; // 确保这里有5个数据系列
const { seriesIndex } = params;
let categoryIndex = api.value(0); // x轴序列
let vald = api.value(1); // 数据值
// 如果数据为0,则不渲染柱子
if (vald === 0) {
return;
}
let start = api.coord([categoryIndex, vald]);
let height = api.size([0, vald]);
// 柱子宽度和间距
let barWidth = 30; // 单个柱子的固定宽度
let barGap = 3; // 柱子之间的间距
// 计算当前系列的偏移量
let xOffset = dataSeries.slice(0, seriesIndex).reduce((sum, currentSeries, index) => {
return sum + (currentSeries[categoryIndex] !== undefined && currentSeries[categoryIndex] !== 0 ? barWidth + barGap : 0);
}, 0);
// 计算当前系列的x位置
let x = start[0] - barWidth / 2 + xOffset - 10; // 柱子的中心位置 再减20是因为让其起点靠中间左边点
return {
type: 'rect',
shape: {
x: x, // 当前柱子的x位置
y: start[1],
width: barWidth,
height: height[1]
},
style: api.style()
};
}
option && this[myChart].setOption(option, true)
网易集团执行副总裁丁迎峰宣布退休,后续担任公司顾问
文 | 果脯
编辑 | 刘士武
北京时间12月27日凌晨(美股交易日期间),网易集团发布公告,宣布网易集团执行副总裁、互动娱乐事业部负责人丁迎峰(丁丁)将于2025年12月31日正式退休。后续他将继续担任公司顾问一职。
公告中,网易CEO丁磊表示:“我们衷心感谢丁迎峰先生的奉献和贡献。他为公司旗舰游戏的成功做出了卓越贡献,并在公司研发和运营能力的建设中发挥了关键作用。”
![]()
网易集团公告
丁迎峰于1998年进入游戏行业,曾担任过《古龙群侠传》的主策。之后他在2002年加入网易,参与集团多款标志性产品的设计与开发,而他参与的《大话西游 Online》也是网易旗下首款自研大型网游。
在众多知名游戏IP当中,丁迎峰在职期间参与推出的《梦幻西游》于2003年正式上线运营,截至2025年底,该游戏及其衍生手游产品系列已持续运营超过22年,是网易网络游戏业务历史上运营时间最长、累计营收最高的产品系列之一。
![]()
丁迎峰简历介绍
在担任互动娱乐事业群负责人期间,丁迎峰的管理范围覆盖了网易旗下包括《梦幻西游》《大话西游》等在内的多款产品,以及《燕云十六声》《第五人格》《明日之后》等来自不同品类的游戏产品。该事业群也是网易内部人员规模最大、产品数量最多的游戏研发与运营实体。
据网易历年财报披露,以MMO为主体玩法的网络游戏服务收入长期占据游戏业务总收入的七成以上,而互娱事业群是贡献该收入的核心单元。
网易集团内部与互娱事业群并行的重要游戏研发机构是雷火事业群。雷火事业群以研发《逆水寒》《永劫无间》等产品闻名,其风格侧重于高规格图形技术、开放世界与动作玩法。互娱和雷火两大事业群在MMO等核心赛道存在一定的业务差异化,这种结构被视为维持内部产品创新动力的机制之一。
在丁迎峰退休前夕,网易集团发布了2025年第三季度财务报告。财报显示,该季度网易净收入为人民币284亿元,游戏及相关增值服务净收入为233亿元,较2024年同期增长11.8%,并占公司总营收比例达82%。增长部分主要得益于《燕云十六声》《破碎之地》等新游的上线贡献,以及《梦幻西游》系列、《蛋仔派对》等既有产品的收入稳定性。
![]()
丁迎峰
而这份财报所反映的业务状况,是丁迎峰退休前参与交出的最后一份阶段性成绩单。
丁迎峰退休后,网易官方暂未披露接任者信息以及关于此人事变动的更多细节。
随着网易互娱事业部告别丁迎峰时代,其管理层的具体变动以及对网易游戏业务中长期战略与产品线规划的影响,将在2026年及之后逐步显现。
张军扩:刺激消费政策要在总量不减、持续用力的前提下,不断优化商品性消费的支持范围和结构
新修订的民用航空法自2026年7月1日起施行
国家数据局:引导金融机构支持数据科技创新,带动长期资本、耐心资本、优质资本更多投向数据科技领域
国家数据局:加强关键数据技术攻关突破
国家统计局:高技术制造业利润增速加快
国家统计局:装备制造业利润带动作用明显
React 19 源码揭秘(二):useState 的实现原理
React 19 源码揭秘(二):useState 的实现原理
本文深入 React 源码,带你彻底搞懂 useState 从调用到更新的完整流程。
前言
useState 可能是你用得最多的 Hook,但你知道它背后是怎么工作的吗?
const [count, setCount] = useState(0);
setCount(count + 1); // 这行代码背后发生了什么?
本文将从源码角度,完整解析 useState 的实现原理。
一、Hook 的数据结构
首先,我们需要了解 Hook 在 React 内部是如何存储的。
Hook 节点
每个 Hook 调用都会创建一个 Hook 对象:
type Hook = {
memoizedState: any, // 存储的状态值
baseState: any, // 基础状态(用于更新计算)
baseQueue: Update | null, // 基础更新队列
queue: UpdateQueue | null, // 更新队列
next: Hook | null, // 指向下一个 Hook
};
Hook 链表
多个 Hook 以链表形式存储在 Fiber 节点的 memoizedState 上:
Fiber.memoizedState
│
▼
┌───────┐ ┌───────┐ ┌───────┐
│ Hook1 │ ──► │ Hook2 │ ──► │ Hook3 │ ──► null
│useState│ │useEffect│ │useMemo│
└───────┘ └───────┘ └───────┘
这就是为什么 Hook 不能在条件语句中调用——React 依赖调用顺序来匹配 Hook。
二、首次渲染:mountState
当组件首次渲染时,useState 会调用 mountState:
// 源码位置:react-reconciler/src/ReactFiberHooks.js
function mountState(initialState) {
// 1. 创建 Hook 节点,加入链表
const hook = mountWorkInProgressHook();
// 2. 处理初始值(支持函数式初始化)
if (typeof initialState === 'function') {
initialState = initialState();
}
// 3. 保存初始状态
hook.memoizedState = hook.baseState = initialState;
// 4. 创建更新队列
const queue = {
pending: null, // 待处理的更新
lanes: NoLanes,
dispatch: null, // setState 函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
// 5. 绑定 dispatch 函数(就是 setState)
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;
// 6. 返回 [state, setState]
return [hook.memoizedState, dispatch];
}
mountWorkInProgressHook
这个函数负责创建 Hook 节点并维护链表:
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 第一个 Hook,挂载到 Fiber
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 追加到链表末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
三、触发更新:dispatchSetState
当你调用 setCount(1) 时,实际执行的是 dispatchSetState:
function dispatchSetState(fiber, queue, action) {
// 1. 获取更新优先级
const lane = requestUpdateLane(fiber);
// 2. 创建更新对象
const update = {
lane,
action, // 新值或更新函数
hasEagerState: false,
eagerState: null,
next: null,
};
// 3. 性能优化:Eager State(提前计算)
if (fiber.lanes === NoLanes) {
const currentState = queue.lastRenderedState;
const eagerState = basicStateReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 如果新旧状态相同,跳过更新!
if (Object.is(eagerState, currentState)) {
return; // Bailout!
}
}
// 4. 将更新加入队列
enqueueConcurrentHookUpdate(fiber, queue, update, lane);
// 5. 调度更新
scheduleUpdateOnFiber(root, fiber, lane);
}
Eager State 优化
这是一个重要的性能优化:
const [count, setCount] = useState(0);
// 点击按钮
setCount(0); // 状态没变,React 会跳过这次更新!
React 会在调度之前就计算新状态,如果和旧状态相同(通过 Object.is 比较),直接跳过整个更新流程。
四、更新渲染:updateState
当组件重新渲染时,useState 会调用 updateState:
function updateState(initialState) {
// useState 本质上是预设了 reducer 的 useReducer
return updateReducer(basicStateReducer, initialState);
}
// 基础 reducer:支持值或函数
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
updateReducer
这是处理更新的核心逻辑:
function updateReducer(reducer, initialArg) {
// 1. 获取当前 Hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 2. 获取待处理的更新
const pending = queue.pending;
// 3. 计算新状态
let newState = hook.baseState;
if (pending !== null) {
let update = pending.first;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
}
// 4. 保存新状态
hook.memoizedState = newState;
// 5. 返回新状态和 dispatch
return [hook.memoizedState, queue.dispatch];
}
updateWorkInProgressHook
更新时,需要从 current 树复制 Hook:
function updateWorkInProgressHook() {
// 从 current Fiber 获取对应的 Hook
let nextCurrentHook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = currentHook.next;
}
currentHook = nextCurrentHook;
// 复制 Hook 到 workInProgress
const newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 加入链表...
return newHook;
}
五、完整流程图
┌─────────────────────────────────────────────────────────┐
│ 首次渲染 (Mount) │
├─────────────────────────────────────────────────────────┤
│ useState(0) │
│ │ │
│ ▼ │
│ mountState(0) │
│ │ │
│ ├──► 创建 Hook 节点 │
│ ├──► 初始化 memoizedState = 0 │
│ ├──► 创建 UpdateQueue │
│ ├──► 绑定 dispatch = dispatchSetState │
│ │ │
│ ▼ │
│ 返回 [0, setCount] │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 触发更新 │
├─────────────────────────────────────────────────────────┤
│ setCount(1) │
│ │ │
│ ▼ │
│ dispatchSetState(fiber, queue, 1) │
│ │ │
│ ├──► 获取优先级 lane │
│ ├──► 创建 Update 对象 │
│ ├──► Eager State: 计算新状态 │
│ ├──► 比较新旧状态,相同则 Bailout │
│ ├──► 入队更新 │
│ │ │
│ ▼ │
│ scheduleUpdateOnFiber() ──► 调度重新渲染 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 重新渲染 (Update) │
├─────────────────────────────────────────────────────────┤
│ useState(0) // 初始值被忽略 │
│ │ │
│ ▼ │
│ updateState(0) │
│ │ │
│ ▼ │
│ updateReducer(basicStateReducer, 0) │
│ │ │
│ ├──► 获取对应的 Hook │
│ ├──► 处理 UpdateQueue 中的更新 │
│ ├──► 计算新状态 = 1 │
│ │ │
│ ▼ │
│ 返回 [1, setCount] │
└─────────────────────────────────────────────────────────┘
六、Dispatcher 切换
React 如何区分 mount 和 update?答案是 Dispatcher 切换:
// renderWithHooks 中
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 首次渲染
: HooksDispatcherOnUpdate; // 更新渲染
// 两个 Dispatcher 的 useState 指向不同函数
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
// ...
};
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
// ...
};
渲染完成后,切换到 ContextOnlyDispatcher,禁止在组件外调用 Hook:
// 渲染完成后
ReactSharedInternals.H = ContextOnlyDispatcher;
const ContextOnlyDispatcher = {
useState: throwInvalidHookError, // 抛出错误
// ...
};
七、为什么 Hook 不能条件调用?
现在你应该明白了:
// ❌ 错误
if (condition) {
const [a, setA] = useState(0); // Hook 1
}
const [b, setB] = useState(0); // Hook 2 或 Hook 1?
// ✅ 正确
const [a, setA] = useState(0); // 始终是 Hook 1
const [b, setB] = useState(0); // 始终是 Hook 2
React 通过遍历链表来匹配 Hook,如果顺序变了,状态就乱了。
八、调试技巧
想要亲自验证?在这些位置打断点:
// 首次渲染
mountState // react-reconciler/src/ReactFiberHooks.js
// 触发更新
dispatchSetState // react-reconciler/src/ReactFiberHooks.js
// 重新渲染
updateReducer // react-reconciler/src/ReactFiberHooks.js
用 Counter 组件测试:
const Counter = () => {
const [count, setCount] = useState(0); // 断点这里
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
小结
本文深入分析了 useState 的实现原理:
- 数据结构:Hook 以链表形式存储在 Fiber.memoizedState
- 首次渲染:mountState 创建 Hook 和 UpdateQueue
- 触发更新:dispatchSetState 创建 Update,调度渲染
- Eager State:提前计算,相同状态跳过更新
- 重新渲染:updateReducer 处理更新队列,计算新状态
- Dispatcher:通过切换实现 mount/update 的区分
下一篇我们将分析 useEffect 的实现原理,看看副作用是如何被调度和执行的。
📦 配套源码:github.com/220529/reac…
上一篇:React 19 源码全景图
下一篇:useEffect 的实现原理
如果觉得有帮助,欢迎点赞收藏 👍
React 19 源码全景图:从宏观到微观
React 19 源码全景图:从宏观到微观
本文是 React 源码系列的总览篇,帮你建立完整的知识框架,后续文章将逐一深入。
一、React 是什么?
一句话:React 是一个将状态映射为 UI 的函数。
UI = f(state)
当状态变化时,React 会:
- 计算新的 UI(Reconciler)
- 调度更新任务(Scheduler)
- 将变化应用到 DOM(Renderer)
二、三大核心模块
┌─────────────────────────────────────────────────────────┐
│ 你的代码 │
│ <App /> → useState → setState │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Scheduler 调度器 │
│ │
│ • 优先级管理(用户交互 > 动画 > 数据请求) │
│ • 时间切片(5ms 一片,避免卡顿) │
│ • 任务队列(最小堆实现) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Reconciler 协调器 │
│ │
│ • Fiber 架构(可中断的链表结构) │
│ • Diff 算法(最小化 DOM 操作) │
│ • Hooks 系统(状态和副作用管理) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Renderer 渲染器 │
│ │
│ • ReactDOM(Web) │
│ • React Native(移动端) │
│ • React Three Fiber(3D) │
└─────────────────────────────────────────────────────────┘
三、核心概念速览
1. Fiber
Fiber 是 React 的核心数据结构,每个组件对应一个 Fiber 节点:
FiberNode {
// 类型信息
tag, // 组件类型(函数组件=0,类组件=1,DOM=5)
type, // 组件函数或 DOM 标签
// 树结构
return, // 父节点
child, // 第一个子节点
sibling, // 兄弟节点
// 状态
memoizedState, // Hooks 链表
memoizedProps, // 上次的 props
// 副作用
flags, // 标记(插入、更新、删除)
// 双缓冲
alternate, // 另一棵树的对应节点
}
2. Lane(优先级)
React 19 使用 31 位二进制数表示优先级:
SyncLane = 0b0000000000000000000000000000010 // 同步(最高)
InputContinuousLane = 0b0000000000000000000000000001000 // 连续输入
DefaultLane = 0b0000000000000000000000000100000 // 默认
TransitionLane = 0b0000000000000000000000010000000 // 过渡
IdleLane = 0b0010000000000000000000000000000 // 空闲(最低)
3. 双缓冲
React 维护两棵 Fiber 树:
- current:当前屏幕显示的
- workInProgress:正在构建的
更新完成后一行代码切换:root.current = workInProgress
四、渲染流程
完整流程图
setState()
│
▼
scheduleUpdateOnFiber() ← 标记更新
│
▼
ensureRootIsScheduled() ← 确保调度
│
▼
scheduleCallback() ← Scheduler 调度
│
▼
performConcurrentWorkOnRoot() ← 开始渲染
│
├─────────────────────────────────────┐
│ Render 阶段(可中断) │
│ │
│ workLoopConcurrent() │
│ │ │
│ ▼ │
│ performUnitOfWork() ←──┐ │
│ │ │ │
│ ▼ │ │
│ beginWork() │ 循环 │
│ │ │ │
│ ▼ │ │
│ completeWork() ────────┘ │
│ │
└─────────────────────────────────────┘
│
▼
├─────────────────────────────────────┐
│ Commit 阶段(不可中断) │
│ │
│ commitBeforeMutationEffects() │
│ │ │
│ ▼ │
│ commitMutationEffects() ← DOM操作 │
│ │ │
│ ▼ │
│ root.current = finishedWork │
│ │ │
│ ▼ │
│ commitLayoutEffects() │
│ │
└─────────────────────────────────────┘
│
▼
flushPassiveEffects() ← useEffect(异步)
Render 阶段
beginWork(向下递归):
- 根据组件类型处理(函数组件、类组件、DOM 元素)
- 调用组件函数,执行 Hooks
- Diff 子节点,创建/复用 Fiber
completeWork(向上回溯):
- 创建 DOM 节点
- 收集副作用标记
- 冒泡 subtreeFlags
Commit 阶段
三个子阶段:
| 阶段 | 时机 | 主要工作 |
|---|---|---|
| Before Mutation | DOM 操作前 | getSnapshotBeforeUpdate |
| Mutation | DOM 操作 | 增删改 DOM |
| Layout | DOM 操作后 | useLayoutEffect、componentDidMount |
五、Hooks 原理
Hooks 以链表形式存储在 Fiber 的 memoizedState 上:
Fiber.memoizedState → useState → useEffect → useMemo → null
useState 流程
Mount: mountState() → 创建 Hook → 初始化状态 → 返回 [state, setState]
Update: updateState() → 获取 Hook → 处理更新队列 → 返回 [newState, setState]
useEffect 流程
Mount: mountEffect() → 创建 Effect → 标记 Passive
Commit: flushPassiveEffects() → 执行销毁函数 → 执行创建函数
六、Diff 算法
React Diff 的三个策略:
- 同层比较:不跨层级移动节点
- 类型判断:类型不同直接替换
- key 标识:通过 key 识别节点
单节点 Diff
key 相同 && type 相同 → 复用
key 相同 && type 不同 → 删除重建
key 不同 → 删除,继续找
多节点 Diff(三轮遍历)
第一轮:从左到右,处理更新
第二轮:处理新增或删除
第三轮:处理移动(Map 查找)
七、Scheduler 调度
优先级
ImmediatePriority // -1ms,立即执行
UserBlockingPriority // 250ms,用户交互
NormalPriority // 5000ms,普通更新
LowPriority // 10000ms,低优先级
IdlePriority // 永不过期,空闲执行
时间切片
function workLoop() {
while (task && !shouldYield()) { // 5ms 检查一次
task = performTask(task);
}
if (task) {
scheduleCallback(task); // 还有任务,继续调度
}
}
八、源码目录
packages/
├── react/ # React API
│ └── src/ReactHooks.js # Hooks 入口
│
├── react-reconciler/ # 协调器(核心)
│ └── src/
│ ├── ReactFiber.js # Fiber 定义
│ ├── ReactFiberWorkLoop.js # 工作循环
│ ├── ReactFiberBeginWork.js # 递阶段
│ ├── ReactFiberCompleteWork.js # 归阶段
│ ├── ReactFiberHooks.js # Hooks 实现
│ ├── ReactFiberCommitWork.js # Commit
│ ├── ReactFiberLane.js # 优先级
│ └── ReactChildFiber.js # Diff 算法
│
├── react-dom/ # DOM 渲染器
│
└── scheduler/ # 调度器
└── src/
├── Scheduler.js # 调度逻辑
└── SchedulerMinHeap.js # 最小堆
九、系列文章导航
| 序号 | 主题 | 核心内容 |
|---|---|---|
| 00 | 调试环境搭建 | 项目介绍、快速开始 |
| 01 | 架构总览(本文) | 三大模块、核心概念 |
| 02 | useState 原理 | Hook 链表、更新队列 |
| 03 | useEffect 原理 | Effect 链表、执行时机 |
| 04 | Fiber 工作循环 | beginWork、completeWork |
| 05 | Diff 算法 | 单节点、多节点 Diff |
| 06 | Scheduler 调度器 | 优先级、时间切片 |
| 07 | Commit 阶段 | 三个子阶段、DOM 操作 |
十、学习建议
- 先跑起来:clone react-debug,打断点调试
- 从 useState 开始:最简单也最核心
- 画流程图:边看边画,加深理解
- 写测试组件:验证你的理解
📦 配套源码:github.com/220529/reac…
上一篇:React 源码调试环境搭建
下一篇:useState 的实现原理
Cursor Visual Editor:前端样式调试的新利器
作为前端开发者,你一定经历过这样的场景:为了调整一个渐变的角度、修改一个元素的行高,反复在代码和浏览器之间切换,改一行代码、保存、刷新、看效果、再改……
这种"盲调"的方式效率低下,尤其是在调试 CSS 动画这类需要精细控制的效果时,更是让人抓狂。
好消息是,Cursor 2.2 带来了一个令人兴奋的新功能——Visual Editor。它将你的 Web 应用、代码库和可视化编辑工具整合到同一个窗口中,让界面调试变得前所未有的直观。
今天,我们就通过两个炫酷的 CSS 动画案例,来体验一下这个可视化编辑器的强大之处。
认识 Visual Editor
首先,在 Cursor 中选择 Open Browser 即可打开内置浏览器小窗口:
![]()
根据 Cursor 官方博客 的介绍,Visual Editor 提供了四大核心能力:
1. 拖拽重排(Drag-and-drop)
直接在渲染好的页面上拖动元素,调整布局结构。你可以交换按钮顺序、旋转区块位置、测试不同的网格配置——所有操作都不需要切换上下文。当视觉设计符合预期后,让 Agent 帮你更新底层代码。
2. 组件状态测试(Test component states)
对于 React 应用,Visual Editor 可以在侧边栏直接显示组件的 props,让你方便地切换不同的组件状态变体。
3. 属性可视化调整(Visual controls)
这是最实用的功能之一。侧边栏提供了滑块、颜色选择器等可视化控件,支持实时预览。你可以精确调整颜色、布局、字体等属性,所有改动即时生效。
4. 点击 + 提示(Point and prompt)
选中界面上的任意元素,用自然语言描述你想要的修改。比如点击一个元素说"把这个变大",点击另一个说"改成红色"——多个 Agent 会并行执行,几秒钟内完成修改。
实战案例一:渐变流动文字
让我们用一个渐变流动文字效果来体验 Visual Editor 的威力。
效果展示
先来看看最终效果:
![]()
可视化调试体验
在 Visual Editor 中打开这个页面后,点击文字元素即可选中它:
![]()
选中后,侧边栏会显示该元素的所有可调整属性。比如我们想调整渐变的角度,只需要拖动滑块即可实时预览效果:
![]()
想象一下,如果用传统方式调试这个角度参数:修改代码 → 保存 → 等待热更新 → 查看效果 → 不满意再改……而现在只需要拖动滑块,所见即所得!
核心原理
这个渐变流动效果的实现原理其实很简单,核心代码如下:
.text {
/* 多色线性渐变 */
background: linear-gradient(
90deg,
rgba(48, 207, 208, 1) 0%,
rgba(102, 166, 255, 1) 22%,
rgba(136, 136, 136, 1) 40%,
rgba(255, 154, 139, 1) 60%,
rgba(51, 8, 103, 1) 81%,
rgba(48, 207, 208, 1) 100%
);
/* 背景宽度设为元素的 2 倍 */
background-size: 200% auto;
/* 将背景裁剪到文字形状 */
-webkit-background-clip: text;
background-clip: text;
/* 文字颜色透明,露出背景 */
color: transparent;
/* 应用流动动画 */
animation: gradient-flow 3s linear infinite;
}
@keyframes gradient-flow {
0% {
background-position: 0% center;
}
100% {
background-position: 200% center;
}
}
原理解析:
-
linear-gradient:创建一个多色渐变背景,首尾颜色相同以实现无缝循环 -
background-size: 200%:让背景宽度是元素的两倍,为动画提供移动空间 -
background-clip: text:将背景裁剪到文字轮廓内 -
animation:通过改变background-position从 0% 到 200%,让渐变"流动"起来
实战案例二:立体透视文字
接下来看一个更有意思的效果——立体透视文字。
效果展示
![]()
可视化调试体验
这个效果的视觉呈现高度依赖于 line-height 和 clip-height 等参数的精确配合。使用 Visual Editor,我们可以直观地调整这些数值:
![]()
通过可视化调整,你可以直观地看到参数变化对立体效果的影响,快速找到最佳的视觉平衡点。
核心原理
这个立体透视效果的核心在于 CSS 变换的巧妙组合:
:root {
--clip-height: 90px;
--line-height: 85px;
--left-offset: 50px;
}
.Words-line {
height: var(--clip-height);
overflow: hidden;
position: relative;
}
/* 奇数行:倾斜 + 压缩 */
.Words-line:nth-child(odd) {
transform: skew(60deg, -30deg) scaleY(0.66667);
}
/* 偶数行:倾斜 + 拉伸 */
.Words-line:nth-child(even) {
transform: skew(0deg, -30deg) scaleY(1.33333);
}
/* 每行递增的左偏移,形成阶梯效果 */
.Words-line:nth-child(1) { left: calc(var(--left-offset) * 1); }
.Words-line:nth-child(2) { left: calc(var(--left-offset) * 2); }
.Words-line:nth-child(3) { left: calc(var(--left-offset) * 3); }
/* ... */
原理解析:
-
skew()倾斜变换:通过不同的倾斜角度,让奇偶行形成视觉上的"折叠"效果 -
scaleY()垂直缩放:奇数行压缩(0.66667),偶数行拉伸(1.33333),配合倾斜创造 3D 透视错觉 -
递增的
left偏移:每行向右偏移,形成阶梯状的立体层次 -
overflow: hidden:裁剪超出的内容,确保每行只显示固定高度
hover 时的文字切换动画则通过 translate3d 实现:
p {
transition: all 0.4s ease-in-out;
}
.Words:hover p {
transform: translate3d(0, calc(var(--clip-height) * -1), 0);
}
总结
Cursor Visual Editor 的出现,真正实现了"设计即代码"的理念:
- 所见即所得:告别反复保存刷新的低效循环,样式调整即时生效
- 降低心智负担:不再需要脑补参数变化的效果,可视化控件让调试更直观
- 设计与代码统一:在同一窗口完成视觉调整和代码修改,无缝衔接
这个功能特别适合以下场景:
- 样式微调:颜色、间距、字体大小等参数的精细调整
- 布局实验:快速测试不同的布局方案
- 动画调试:实时预览动画参数的变化效果
正如 Cursor 官方所说,他们看到了一个未来:Agent 与 Web 应用开发深度融合,人们通过更直观的界面将想法转化为代码。Visual Editor 正是朝着这个方向迈出的重要一步。
如果你还没有尝试过这个功能,强烈建议打开 Cursor,用你自己的项目体验一下——相信你会爱上这种"所见即所得"的开发方式!
Tailwind CSS:原子化 CSS 的现代开发实践
Tailwind CSS:原子化 CSS 的现代开发实践
在当今快速迭代的前端开发环境中,如何高效、一致且可维护地构建用户界面,成为每个团队必须面对的核心问题。传统 CSS 的命名困境、样式冗余和复用难题,催生了一种新的解决方案——原子化 CSS(Atomic CSS)。而 Tailwind CSS,正是这一理念最成功的实践者。本文将结合实际代码与开发场景,深入解析 Tailwind CSS 的核心思想、优势及最佳实践。
一、什么是原子化 CSS?
传统 CSS 倾向于“语义化命名”:我们为组件起一个名字(如 .card-title),然后为其编写样式。这种方式常被称为“面向对象 CSS”(OOCSS),它试图通过封装基类、组合多态来提升复用性。例如:
.btn { padding: 8px 16px; border-radius: 6px; }
.btn-primary { background: skyblue; color: white; }
但实践中,样式往往带有太多业务属性,导致在一个或少数类名下,样式几乎无法跨项目复用,最终演变为“一次性 CSS”。
原子化 CSS 则反其道而行之:它将样式拆解为最小、单一职责的“原子类”,每个类只控制一个具体的样式属性。例如:
-
p-4→padding: 1rem -
bg-blue-500→background-color: #3b82f6 -
text-center→text-align: center
这些类名直接描述样式本身,而非内容语义。通过组合这些原子类,我们可以在 HTML 中直接构建 UI,无需离开模板文件。
将我们的 CSS 规则拆分成原子 CSS,会有大量的基类,好复用、好维护,不会重复。
二、Tailwind CSS:原子化理念的集大成者
Tailwind CSS 是一个功能优先(Utility-First)的 CSS 框架,它不提供预设组件(如 Bootstrap 的 .btn),而是提供一套完整的工具类系统。
示例:构建一个按钮
<button className="px-4 py-2 bg-[skyblue] text-white rounded-lg hover:bg-blue-200">
提交
</button>
-
px-4 py-2:设置内边距,表示水平方向内边距1rem,垂直方向内边距0.5rem; -
bg-[skyblue]:背景色; -
rounded-lg:圆角,lg为large大号圆角(0.5rem = 8px); -
hover:bg-blue-200:悬停效果,鼠标悬停时背景色变为蓝色系200深度颜色,hover为悬停伪类前缀。
所有样式一目了然,无需查阅 CSS 文件。
🤖 LLM 时代的理想搭档
随着大语言模型(LLM)的普及,用自然语言生成 UI 代码成为可能。而 Tailwind 的类名具有高度语义化、结构化、可预测的特点:
- 开发者只需描述:“一个带圆角、蓝色背景、白色文字的按钮”
- LLM 即可输出:
<button class="px-4 py-2 bg-blue-500 text-white rounded">
相比之下,传统 CSS 需要模型同时生成 HTML 和 CSS,并保证类名匹配,难度更高。Tailwind 让“Prompt → UI” 的路径更短、更可靠。
三、快速上手:基于 Vite 的项目配置
要在项目中使用 Tailwind,只需几步:
-
创建 Vite 项目:
npm init vite -
安装 Tailwind 及官方 Vite 插件:
npm install -D tailwindcss @tailwindcss/vite npx tailwindcss init -
配置
vite.config.js:import { defineConfig } from 'vite' import tailwindcss from '@tailwindcss/vite' // tailwind插件 import react from '@vitejs/plugin-react' // react插件 // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], }) -
在入口 CSS 文件(如
index.css)中引入:@import "tailwindcss";
至此,所有原子类即可在 JSX/HTML 中直接使用,几乎无需再手写 CSS。
四、性能与工程化:DocumentFragment 与 React Fragment
高效的 UI 不仅关乎视觉,也涉及性能。在动态渲染大量 DOM 节点时,减少重排/重绘至关重要。
原生优化:DocumentFragment
const fragment = document.createDocumentFragment();
fragment.appendChild(p1);
fragment.appendChild(p2);
container.appendChild(fragment); // 仅触发一次 DOM 更新
通过 DocumentFragment,我们将多个节点在内存中组装后一次性插入,显著提升性能。
React 场景:Fragment 解决单根限制
React 要求组件返回单一根节点。若需返回多个同级元素,传统做法是包裹一个无意义的 <div>,但这会污染 DOM 结构。
Tailwind + React 的最佳实践是使用 Fragment:
export default function App() {
return (
<>
<h1>111</h1>
<h2>222</h2>
<ArticleCard /> {/* 自定义卡片组件 */}
</>
)
}
<>...</>(即 <React.Fragment>)允许我们返回多个元素,不产生额外 DOM 节点,保持结构纯净,同时也便于一次性插入整个 UI 片段,提升渲染性能。
五、响应式设计:Mobile First 的优雅实现
Tailwind 内置响应式前缀,完美支持“移动端优先”开发策略。
基础布局(移动端垂直堆叠):
<div className="flex flex-col gap-4">
<main className="bg-blue-100 p-4">主内容</main>
<aside className="bg-green-100 p-4">侧边栏</aside>
</div>
增强至桌面端(水平排列):
<div className="flex flex-col md:flex-row gap-4">
<main className="bg-blue-100 p-4 md:w-2/3">主内容</main>
<aside className="bg-green-100 p-4 md:w-1/3">侧边栏</aside>
</div>
- 小屏:
flex-col(垂直) - 中屏及以上:
md:flex-row(水平)
这种“渐进增强”的方式,确保了在所有设备上都有良好体验。
六、为什么选择 Tailwind?
- 开发效率高:样式即代码,无需上下文切换;
- 设计一致性:基于预设的设计系统(间距、颜色、字体等);
-
高度可定制:通过
tailwind.config.js扩展主题、断点、插件; - 极致性能:JIT 模式仅生成用到的 CSS,体积极小;
- 未来友好:与 React、Vue、Svelte 等现代框架无缝集成;
- AI 友好:类名结构清晰,易于 LLM 理解与生成。
七、结语
Tailwind CSS 不仅仅是一个 CSS 框架,更是一种UI 开发哲学。它通过原子化、功能优先的设计,将 CSS 从“命名的艺术”转变为“组合的科学”。正如我们在文章中所见,无论是简单的按钮、复杂的卡片,还是响应式布局,Tailwind 都能以简洁、直观的方式实现。
更重要的是,在 AI 编程时代,Tailwind 的结构化、语义化类名使其成为自然语言生成 UI 的理想载体。对于追求效率、一致性和可维护性的现代前端团队而言,Tailwind CSS 无疑是值得拥抱的利器。
“不用离开 HTML 写 CSS 了,所有的样式都在类名中。”
—— 这或许是对 Tailwind 最精炼的赞美。
参考资料: