普通视图

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

余永定:加大基础设施投资力度,可取得一举两得结果

2025年12月27日 14:18
12月27日,中国社会科学院学部委员余永定在三亚·财经国际论坛暨第五届三亚财富管理大会上表示,明年中国经济增速能否维持在5%水平,关键在于财政政策扩张力度。中国的中央财政政策空间较大,居民对国债需求强劲,完全可以明显提高赤字率。如果把赤字率提高到5%左右,中央政府完全可以进行大规模发债,以支持基建投资。(澎湃新闻)

韩束:旗下所有产品均未添加人表皮生长因子成分

2025年12月27日 14:16
36氪获悉,12月27日,韩束官方微博发文称,韩束旗下所有产品均未添加人表皮生长因子(EGF)成分。韩束在文中表示,近日,关于公司旗下产品“韩束丰盈紧致精华面膜”和“韩束嫩白透亮面膜”添加人表皮生长因子(EGF)的报道引发了部分消费者的担忧。报道发生后,公司已立即启动全面自查与追溯程序。早在2025年11月,上海市药品监督管理局稽查局已就该产品开展了现场核验与检测,并基于客观公允性进行了双向送检,结果均显示:送检样品中均未添加人表皮生长因子(EGF)成分。(韩束官方微博)

司美格鲁肽、替尔泊肽集体降价,2026年减重药竞争前移

2025年12月27日 14:10
曾在社交平台被神化为“减重神药”的GLP-1类药物,正在经历从高溢价创新药向大众普惠医疗品的身份转换。财联社记者近日注意到,2025年年末,一场由跨国药企诺和诺德与礼来主动发起的降价潮,在政府采购平台、电商平台悄然上演,实际成交价格较半年前近乎“腰斩”。“这并非简单的年终促销,而是一场精准的战略抢跑。”多位业内专家告诉记者,在2026年司美格鲁肽专利大限将至与国产创新药、生物类似药“抢滩战”开启前,巨头们正试图通过预设价格锚点,筑起防御高墙。(财联社)

AI 四格笑话爆火,我做了什么?

2025年12月27日 12:15

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通信方案详解:本地与跨域场景

作者 小杨梅君
2025年12月27日 11:42

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和图片主题色“提取”

作者 hello_Code
2025年12月27日 11:33

这个想法是来源于「性能优化」中的骨架屏: 在图片居多的站点中,这将是非常nice的体验 —— 图片加载通常是比较让人难受的,好的骨架中一般占位图就是低像素的图片,即大体配色和变化是和实际内容一致的。 有时候比如图片不固定的,那可以使用算法获取图片的主体颜色(至少得是同色系的吧),使用纯色块占位。

再进一步想到,在一些“轻松”的场景下,我们可以让背景色/页面主题色跟随轮播图改变。至于效果嘛......你们可以想一下网易云音乐滑动切歌时的背景效果。

因为是不固定图片,所以我想到了四种方法:

  • tensorflow.js 图像色彩分析
  • canvas对图片主基调进行分析,取大概值
  • css高斯模糊
  • 上传图片时后端对图片分析处理,返回时直接返回一张低像素图片

第一种方式目前还在我的实践中,以后会单独出一篇文章;最后一种方式个人不太建议首选:首先后端处理也需要时间,另一方面毕竟也是以图片进行传输的...yee~(而且后端可能也不太建议你首选🤣)

想看实际效果的推荐自己动手试下,因为我发现本文中用QQ截屏截取的图片怎么都这么暗啊,实际展示的还是挺漂亮的。

第三种方式看起来是纯css实现的,怎么获取呢?这就要说到css中的filter: blur(); 简单来说,利用模糊滤镜及进一步拉伸,可以近似地拿到一张图片的主题色:

<div></div>
div {
background: url(图片地址);
background-size: cover;
filter: blur(50px);
}

你看,通过比较大的一个模糊滤镜,将图片高斯模糊50px,模糊后的图片是不是有点内味了, ruawaba

不过还不行,存在一些模糊边缘,我们可以利用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;
}

ruawaba2

这样就拿到图片的主色调了。当然是要进行其他处理的。

再来说说第二种方法 —— 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

调用这个函数会拿到一个数组 —— 它是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常用配置

作者 小白x
2025年12月27日 11:25
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]  // 往左偏移更多
    },
    // 其他配置...
  },
  // 其他配置...
};
设置柱状图间隔(象形图)

image.png

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)

网易集团执行副总裁丁迎峰宣布退休,后续担任公司顾问

2025年12月27日 11:03

文 | 果脯

编辑 | 刘士武

北京时间12月27日凌晨(美股交易日期间),网易集团发布公告,宣布网易集团执行副总裁、互动娱乐事业部负责人丁迎峰(丁丁)将于2025年12月31日正式退休。后续他将继续担任公司顾问一职。

公告中,网易CEO丁磊表示:“我们衷心感谢丁迎峰先生的奉献和贡献。他为公司旗舰游戏的成功做出了卓越贡献,并在公司研发和运营能力的建设中发挥了关键作用。”

网易集团公告

丁迎峰于1998年进入游戏行业,曾担任过《古龙群侠传》的主策。之后他在2002年加入网易,参与集团多款标志性产品的设计与开发,而他参与的《大话西游 Online》也是网易旗下首款自研大型网游。

在众多知名游戏IP当中,丁迎峰在职期间参与推出的《梦幻西游》于2003年正式上线运营,截至2025年底,该游戏及其衍生手游产品系列已持续运营超过22年,是网易网络游戏业务历史上运营时间最长、累计营收最高的产品系列之一。

丁迎峰简历介绍

在担任互动娱乐事业群负责人期间,丁迎峰的管理范围覆盖了网易旗下包括《梦幻西游》《大话西游》等在内的多款产品,以及《燕云十六声》《第五人格》《明日之后》等来自不同品类的游戏产品。该事业群也是网易内部人员规模最大、产品数量最多的游戏研发与运营实体。

据网易历年财报披露,以MMO为主体玩法的网络游戏服务收入长期占据游戏业务总收入的七成以上,而互娱事业群是贡献该收入的核心单元。

网易集团内部与互娱事业群并行的重要游戏研发机构是雷火事业群。雷火事业群以研发《逆水寒》《永劫无间》等产品闻名,其风格侧重于高规格图形技术、开放世界与动作玩法。互娱和雷火两大事业群在MMO等核心赛道存在一定的业务差异化,这种结构被视为维持内部产品创新动力的机制之一。

在丁迎峰退休前夕,网易集团发布了2025年第三季度财务报告。财报显示,该季度网易净收入为人民币284亿元,游戏及相关增值服务净收入为233亿元,较2024年同期增长11.8%,并占公司总营收比例达82%‌。增长部分主要得益于《燕云十六声》《破碎之地》等新游的上线贡献,以及《梦幻西游》系列、《蛋仔派对》等既有产品的收入稳定性。

丁迎峰

而这份财报所反映的业务状况,是丁迎峰退休前参与交出的最后一份阶段性成绩单。

丁迎峰退休后,网易官方暂未披露接任者信息以及关于此人事变动的更多细节。

随着网易互娱事业部告别丁迎峰时代,其管理层的具体变动以及对网易游戏业务中长期战略与产品线规划的影响,将在2026年及之后逐步显现。

张军扩:刺激消费政策要在总量不减、持续用力的前提下,不断优化商品性消费的支持范围和结构

2025年12月27日 11:03
“2025三亚·财经国际论坛暨第五届三亚财富管理大会”12月27日召开,第十四届全国政协委员、中国发展研究基金会理事长、国务院发展研究中心原副主任张军扩发表演讲时表示,“十五五”充分有效释放居民消费需求潜力需要标本兼治,更加突出结构性对策。一是逆周期性消费刺激政策,要在总量不减、持续用力的前提下,不断优化商品性消费的支持范围和结构,并以更大力度支持服务性消费,特别是要按照投资于人的要求,针对群众急难愁盼的教育、医疗、养老、生育等方面问题,通过加大补贴力度,提振消费意愿,增强消费能力。二是要适应我国发展阶段和水平变化需要,以较大力度提高低收入人群的社会保障和公共服务水平,有效提升我国居民的安全保障预期和消费信心。三是加大供给侧政策力度,要通过减少准入限制、优化监管政策、加大国内标准、品牌、环境建设力度等举措,扩大优质服务供给,特别是要针对国内急需而供给难以满足的服务消费领域,加大对内放开和对外开放力度,尽快通过市场力量引入优质服务供给资源更好满足需求。(财联社)

新修订的民用航空法自2026年7月1日起施行

2025年12月27日 11:02
全国人大常委会会议12月27日表决通过新修订的民用航空法,自2026年7月1日起施行。加强对民用无人驾驶航空器的管理,新修订的民用航空法明确,从事民用无人驾驶航空器的设计、生产、进口、维修和飞行活动,应当按照国家有关规定向国务院民用航空主管部门申请取得适航许可,按照规定无需取得适航许可的除外。从事民用无人驾驶航空器生产的机构应当按照国家有关规定为其生产的无人驾驶航空器设置唯一产品识别码。进一步强化民用航空安全保障,新修订的民用航空法明确,禁止设置影响机场目视助航设施使用的“激光”,对影响机场电磁环境的禁止行为作出具体列举,规定航空运输企业、民用机场安全运营保障能力不足时可以采取相应措施。此外,新修订的民用航空法还对法律责任等有关条款予以完善。 (新华社)

国家数据局:引导金融机构支持数据科技创新,带动长期资本、耐心资本、优质资本更多投向数据科技领域

2025年12月27日 10:58
36氪获悉,国家数据局发布关于加强数据科技创新的实施意见,意见提到,加大财税金融支持力度。加强政府投资牵引作用,引导金融机构支持数据科技创新,带动长期资本、耐心资本、优质资本更多投向数据科技领域,鼓励投早、投小、投长期、投硬科技,形成政府、市场、社会协同联动的持续稳定投入机制。发挥高新技术企业税收优惠、科技创新专项担保等政策激励作用,降低数据科技企业创新成本。鼓励有条件的地方加大数据科技创新的支持力度。(国家数据局公众号)

国家数据局:加强关键数据技术攻关突破

2025年12月27日 10:56
36氪获悉,国家数据局发布关于加强数据科技创新的实施意见,意见提到,加强关键数据技术攻关突破。将数据科技研发纳入国家科技计划体系,加快攻关数据供给、流通、利用、安全等关键技术,以及促进人工智能、具身智能等技术创新发展的高质量数据集构建和评测等技术,研制一批数据领域关键软硬件设备。依托国家自然科学基金,强化数据科技基础研究和应用基础研究。利用现有资金渠道支持相关机构开展技术攻关和设备研发。定期发布数据科技领域前沿研究方向,引导相关机构承担科技创新任务。鼓励地方结合区域特色,支持数据科技研发。(国家数据局公众号)

国家统计局:高技术制造业利润增速加快

2025年12月27日 10:50
36氪获悉,国家统计局工业司首席统计师于卫宁解读2025年1—11月份工业企业利润数据,高技术制造业利润增速加快。1—11月份,规模以上高技术制造业利润同比增长10.0%,较1—10月份加快2.0个百分点,增速高于全部规模以上工业平均水平9.9个百分点。从行业看,“人工智能+”行动深入实施带动相关设备制造行业利润向好,电子工业专用设备制造行业利润同比增长57.4%,其中半导体器件专用设备制造、电子元器件与机电组件设备制造行业利润分别增长97.2%、46.0%;航空航天产业快速发展推动行业利润增长较快,航空、航天器及设备制造行业利润同比增长13.3%,其中航天相关设备制造、航空相关设备制造行业利润分别增长192.9%、36.3%;智能化产品助力数智化转型,智能消费设备制造行业利润同比增长54.0%,其中智能车载设备制造、智能无人飞行器制造、其他智能消费设备制造行业利润分别增长105.7%、76.6%、58.1%。(国家统计局官网)

国家统计局:装备制造业利润带动作用明显

2025年12月27日 10:48
36氪获悉,国家统计局工业司首席统计师于卫宁解读2025年1—11月份工业企业利润数据,装备制造业利润带动作用明显。1—11月份,规模以上装备制造业利润同比增长7.7%,拉动全部规模以上工业企业利润增长2.8个百分点,是对规模以上工业企业利润增长拉动作用最强的板块。从行业看,装备制造业的8个大类行业中有7个行业利润实现同比增长,其中,铁路船舶航空航天、电子行业利润两位数增长,增速分别达27.8%、15.0%;汽车行业利润增长7.5%,较1—10月份加快3.1个百分点;通用设备、专用设备、电气机械行业利润继续增长,增速分别为4.8%、4.6%、4.2%。(国家统计局官网)

React 19 源码揭秘(二):useState 的实现原理

作者 借个火er
2025年12月27日 10:42

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 的实现原理:

  1. 数据结构:Hook 以链表形式存储在 Fiber.memoizedState
  2. 首次渲染:mountState 创建 Hook 和 UpdateQueue
  3. 触发更新:dispatchSetState 创建 Update,调度渲染
  4. Eager State:提前计算,相同状态跳过更新
  5. 重新渲染:updateReducer 处理更新队列,计算新状态
  6. Dispatcher:通过切换实现 mount/update 的区分

下一篇我们将分析 useEffect 的实现原理,看看副作用是如何被调度和执行的。


📦 配套源码:github.com/220529/reac…

上一篇:React 19 源码全景图

下一篇:useEffect 的实现原理

如果觉得有帮助,欢迎点赞收藏 👍

React 19 源码全景图:从宏观到微观

作者 借个火er
2025年12月27日 10:41

React 19 源码全景图:从宏观到微观

本文是 React 源码系列的总览篇,帮你建立完整的知识框架,后续文章将逐一深入。

一、React 是什么?

一句话:React 是一个将状态映射为 UI 的函数

UI = f(state)

当状态变化时,React 会:

  1. 计算新的 UI(Reconciler)
  2. 调度更新任务(Scheduler)
  3. 将变化应用到 DOM(Renderer)

二、三大核心模块

┌─────────────────────────────────────────────────────────┐
│                        你的代码                          │
│            <App /> → useState → setState                │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                   Scheduler 调度器                       │
│                                                         │
│  • 优先级管理(用户交互 > 动画 > 数据请求)               │
│  • 时间切片(5ms 一片,避免卡顿)                        │
│  • 任务队列(最小堆实现)                                │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                  Reconciler 协调器                       │
│                                                         │
│  • Fiber 架构(可中断的链表结构)                        │
│  • Diff 算法(最小化 DOM 操作)                          │
│  • Hooks 系统(状态和副作用管理)                        │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                   Renderer 渲染器                        │
│                                                         │
│  • ReactDOM(Web)                                      │
│  • React Native(移动端)                               │
│  • React Three Fiber3D)                              │
└─────────────────────────────────────────────────────────┘

三、核心概念速览

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 的三个策略:

  1. 同层比较:不跨层级移动节点
  2. 类型判断:类型不同直接替换
  3. 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 操作

十、学习建议

  1. 先跑起来:clone react-debug,打断点调试
  2. 从 useState 开始:最简单也最核心
  3. 画流程图:边看边画,加深理解
  4. 写测试组件:验证你的理解

📦 配套源码:github.com/220529/reac…

上一篇:React 源码调试环境搭建

下一篇:useState 的实现原理

Cursor Visual Editor:前端样式调试的新利器

作者 bytemanx
2025年12月27日 03:05

作为前端开发者,你一定经历过这样的场景:为了调整一个渐变的角度、修改一个元素的行高,反复在代码和浏览器之间切换,改一行代码、保存、刷新、看效果、再改……

这种"盲调"的方式效率低下,尤其是在调试 CSS 动画这类需要精细控制的效果时,更是让人抓狂。

好消息是,Cursor 2.2 带来了一个令人兴奋的新功能——Visual Editor。它将你的 Web 应用、代码库和可视化编辑工具整合到同一个窗口中,让界面调试变得前所未有的直观。

今天,我们就通过两个炫酷的 CSS 动画案例,来体验一下这个可视化编辑器的强大之处。

认识 Visual Editor

首先,在 Cursor 中选择 Open Browser 即可打开内置浏览器小窗口:

20251227024633_rec_-convert.gif

根据 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 中打开这个页面后,点击文字元素即可选中它:

image.png

选中后,侧边栏会显示该元素的所有可调整属性。比如我们想调整渐变的角度,只需要拖动滑块即可实时预览效果:

20251226200440_rec_.gif

想象一下,如果用传统方式调试这个角度参数:修改代码 → 保存 → 等待热更新 → 查看效果 → 不满意再改……而现在只需要拖动滑块,所见即所得!

核心原理

这个渐变流动效果的实现原理其实很简单,核心代码如下:

.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;
    }
}

原理解析:

  1. linear-gradient:创建一个多色渐变背景,首尾颜色相同以实现无缝循环
  2. background-size: 200%:让背景宽度是元素的两倍,为动画提供移动空间
  3. background-clip: text:将背景裁剪到文字轮廓内
  4. animation:通过改变 background-position 从 0% 到 200%,让渐变"流动"起来

实战案例二:立体透视文字

接下来看一个更有意思的效果——立体透视文字。

效果展示

可视化调试体验

这个效果的视觉呈现高度依赖于 line-heightclip-height 等参数的精确配合。使用 Visual Editor,我们可以直观地调整这些数值:

20251227021832_rec_-convert.gif

通过可视化调整,你可以直观地看到参数变化对立体效果的影响,快速找到最佳的视觉平衡点。

核心原理

这个立体透视效果的核心在于 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); }
/* ... */

原理解析:

  1. skew() 倾斜变换:通过不同的倾斜角度,让奇偶行形成视觉上的"折叠"效果
  2. scaleY() 垂直缩放:奇数行压缩(0.66667),偶数行拉伸(1.33333),配合倾斜创造 3D 透视错觉
  3. 递增的 left 偏移:每行向右偏移,形成阶梯状的立体层次
  4. 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 的出现,真正实现了"设计即代码"的理念:

  • 所见即所得:告别反复保存刷新的低效循环,样式调整即时生效
  • 降低心智负担:不再需要脑补参数变化的效果,可视化控件让调试更直观
  • 设计与代码统一:在同一窗口完成视觉调整和代码修改,无缝衔接

这个功能特别适合以下场景:

  1. 样式微调:颜色、间距、字体大小等参数的精细调整
  2. 布局实验:快速测试不同的布局方案
  3. 动画调试:实时预览动画参数的变化效果

正如 Cursor 官方所说,他们看到了一个未来:Agent 与 Web 应用开发深度融合,人们通过更直观的界面将想法转化为代码。Visual Editor 正是朝着这个方向迈出的重要一步。

如果你还没有尝试过这个功能,强烈建议打开 Cursor,用你自己的项目体验一下——相信你会爱上这种"所见即所得"的开发方式!

Tailwind CSS:原子化 CSS 的现代开发实践

作者 Tzarevich
2025年12月27日 00:26

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-4padding: 1rem
  • bg-blue-500background-color: #3b82f6
  • text-centertext-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,只需几步:

  1. 创建 Vite 项目:

    npm init vite
    
  2. 安装 Tailwind 及官方 Vite 插件:

    npm install -D tailwindcss @tailwindcss/vite
    npx tailwindcss init
    
  3. 配置 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()],
    })
    
  4. 在入口 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?

  1. 开发效率高:样式即代码,无需上下文切换;
  2. 设计一致性:基于预设的设计系统(间距、颜色、字体等);
  3. 高度可定制:通过 tailwind.config.js 扩展主题、断点、插件;
  4. 极致性能:JIT 模式仅生成用到的 CSS,体积极小;
  5. 未来友好:与 React、Vue、Svelte 等现代框架无缝集成;
  6. AI 友好:类名结构清晰,易于 LLM 理解与生成。

七、结语

Tailwind CSS 不仅仅是一个 CSS 框架,更是一种UI 开发哲学。它通过原子化、功能优先的设计,将 CSS 从“命名的艺术”转变为“组合的科学”。正如我们在文章中所见,无论是简单的按钮、复杂的卡片,还是响应式布局,Tailwind 都能以简洁、直观的方式实现。

更重要的是,在 AI 编程时代,Tailwind 的结构化、语义化类名使其成为自然语言生成 UI 的理想载体。对于追求效率、一致性和可维护性的现代前端团队而言,Tailwind CSS 无疑是值得拥抱的利器。

“不用离开 HTML 写 CSS 了,所有的样式都在类名中。”
—— 这或许是对 Tailwind 最精炼的赞美。

参考资料

❌
❌