news 2026/10/22 11:35:25

鸿蒙状态管理V2 @Once与@Event装饰器实战:从原理到应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙状态管理V2 @Once与@Event装饰器实战:从原理到应用

写这篇笔记之前,先提醒一句:状态管理V2的内容在鸿蒙开发里属于“越早搞懂越省心”的类型。上一篇笔记我整理了 V2 的整体设计和 @Local、@Param 的用法,这一篇专门聊两个容易被忽略、但实际开发里很好用的装饰器——@Once和@Event。

如果你已经写过一阵子 V1 的状态管理(@State 配 @Prop、@Link 那套),再看 V2 会有种“终于有人讲道理了”的感觉。@Once 解决的是“初始化传一次,之后互不干扰”的问题,@Event 解决的是“子组件怎么优雅地告诉父组件发生了什么事”的问题。这两个组合在一起,基本能把页面上 80% 的父子通信场景都覆盖掉。适合正在从 V1 迁移到 V2、或者刚开始学 V2 的同学,看完能直接往项目里用。

1. 先说背景:状态管理V2到底改了啥

1.1 V1时代的痛点

老版本的状态管理里,最常用的是 @State 管本地状态、@Prop 接收父组件传入、@Link 做双向同步。用起来确实直接,但用久了会踩到几个挺难受的坑:

第一个坑是@Prop 的“全量同步”。只要父组件里那个传进去的变量变了,子组件里对应的 @Prop 就会跟着更新。初看没问题,但很多时候父组件的重新渲染并不代表子组件关心的数据真的变了。比如父组件有个计数器的值一直在变,每次变化都会触发父组件重新 build,子组件被整个重建或刷新一遍。子组件做的事情其实很简单,只是展示一个固定的商品名,结果也被拖着一起刷新。性能倒是小事,关键是状态被意外覆盖:子组件内部想维护一点本地状态,结果父组件一更新,本地状态就被重置了。

第二个坑是@Link 的双向绑定太“自由”。子组件改一下,父组件变;父组件变,子组件又变。链条一长,你根本说不清楚是哪里改的。排查问题的时候只能顺着调用栈一层层翻,很痛苦。

第三个坑是事件通信基本靠函数属性。子组件想通知父组件,常规做法就是传一个回调函数进去。但底层的刷新机制对这个回调并不友好,函数传进去之后,触发时机、this 指向、默认值各种细节全靠开发者自己注意,框架层面的约束很少。

V2 的出现,就是针对这些坑逐个下药。

1.2 V2的核心思路:把“自动同步”变成“按需流动”

V2 的设计理念,总结成一句话就是:数据流是单向的,谁的数据谁修改,谁的事件谁来发。

在这个理念下,V2 把装饰器分成了几类:

  • @Local:组件本地状态,替代 V1 的 @State
  • @Param:父组件向子组件传入的数据,替代 V1 的 @Prop 的一部分职责
  • @Once:@Param 的补充标记,让传入的数据只在初始化时生效一次
  • @Event:子组件向父组件抛出的事件回调,替代 V1 里“手写函数属性”的做法
  • @Provider / @Consumer:跨层级共享,替代 @Provide / @Consume
  • @ObservedV2 / @Trace:细粒度观察嵌套对象变化

这个分层很有意思,它把“数据”和“行为”彻底拆开了。数据用 @Param 往下传,行为用 @Event 往上抛,组件之间不再“互相绑定”,而是“各管一段”。对调试特别友好:数据流是单向的,你一眼就能看出某个值是从哪来的,某个动作是在哪触发的。

用生活里的例子来类比:旧版状态管理像两个人共用一个记事本,谁都能改,改完互相覆盖;V2 更像公司里的汇报关系,上级往下派任务(@Param),下级做完向上汇报(@Event),各司其职,出了事也找得到人。

理解了这套设计,再去看 @Once 和 @Event,思路就顺多了。

2. @Once:一次绑定,之后各管各

2.1 @Once 的语义与定位

@Once 不是独立使用的装饰器,它是 @Param 的“辅助标记”,作用就一句话:让被修饰的 @Param 变量只在组件首次初始化时接收父组件的传值,之后父组件哪怕反复更新这个值,也不会再同步到子组件。

为什么要这个东西?我们回到实际场景里看看。

父组件经常会有一些变化频繁的状态,比如购物车总件数、页面滚动偏移、搜索框的输入内容等等。这些状态一变,父组件就得重新 build。于是问题来了:父组件每次 build,都会给子组件重新传一遍参数。

对于子组件来说,有些参数它其实只关心“第一次拿到的是什么”。比如商品卡片里的商品名称、单价、图片地址;比如详情页里某个只初始化一次的外部配置。这些信息本质上不会变,但父组件每次刷新都会带着它们跑一趟。用 @Once 标记之后,子组件只在装帧时接收一次,后续父组件再刷新,就与它无关了。

这等于是在“父组件频繁刷新”和“子组件不想被频繁刷新”之间,加了一道防火墙。

不过要留意,@Once 不是完全不刷新子组件。如果子组件里有 @Local 状态在变,它该刷新还是会刷新;如果 @Param 里还有其他没加 @Once 的字段变了,同步也仍然会发生。@Once 只针对它修饰的那一个字段做“切断后续同步”。

2.2 实操示例:购物清单的商品卡片

我写了一个很典型的例子,用商品卡片组件来展示 @Once 的用法。

先看子组件:

@ComponentV2 struct GoodsCard { // 商品基本信息:只在首次初始化的时点传一次,之后不再被父组件覆盖 @Param @Once name: string = ''; @Param @Once price: number = 0; // 本地可选择的数量 @Local count: number = 1; build() { Column({ space: 8 }) { Text(this.name).fontSize(20).fontWeight(FontWeight.Bold) Text(`单价:¥${this.price}`).fontSize(16) Row({ space: 12 }) { Button('减') .onClick(() => { if (this.count > 1) { this.count--; } }) Text(`${this.count}`).fontSize(18) Button('加') .onClick(() => { this.count++; }) } } .padding(16) .backgroundColor('#FFFFFF') .borderRadius(12) } }

再看父组件如何传入:

@Entry @ComponentV2 struct ShoppingListPage { @Local refreshFlag: number = 0; build() { Column({ space: 16 }) { Text(`触发父组件刷新的标记:${this.refreshFlag}`) Button('点击触发父组件刷新') .onClick(() => { this.refreshFlag++; }) GoodsCard({ name: '机械键盘', price: 399 }) } .padding(20) } }

注意一下:父组件我设了一个refreshFlag,点一次按钮它就会 +1,父组件整个 build 会重新执行。如果没有 @Once,GoodsCard每次都会收到一份新的name和price,组件可能会因此重新渲染。加了 @Once 之后,卡片里的商品名和单价就成了“一次性设定”,父组件刷新多少次都与它无关。

2.3 和 V1 的对比:为什么不是 @Prop

有人可能会想:V1 里不是有 @Prop 吗?直接拿来用不就好了?

这里面的差异很关键,我整理了一下:

对比项V1 @PropV2 @ParamV2 @Param + @Once
初始化传值支持支持支持
父组件后续更新自动同步到子组件自动同步到子组件不同步
子组件内修改不允许直接赋值不允许直接赋值允许自行管理
适用场景父组件数据频繁变化且需要子组件联动大部分父子传参仅初始化时需要,之后保持独立

尤其要记住最后一行:@Once 修饰的变量,初始化后子组件内部是可以自己改的,改完也不会反馈到父组件。因为父子之间的同步通道已经被切断了,它在语义上更像是“从父组件那里领了一份初始值,之后就是子组件自己的东西”。

2.4 用 @Once 的三个注意点

第一个注意点:默认值必须写。@Param 和 @Once 装饰的变量一定要给默认值,否则在父组件还没传值、或传值顺序异常的时候,子组件 build 阶段可能读到一个 undefined,直接白屏或者报错。习惯上给个空字符串、0、或者一个最小对象,都能保证组件在极端情况下也有能力渲染。

第二个注意点:不要给对象加 @Once 就以为万事大吉。@Once 只是切断了“后续参数同步”,但对于对象内部属性的变化,它的保护是有限的。如果父组件传入的是一个对象,而你在子组件里修改了这个对象的某个属性,这种修改依赖 Object 类型的传参机制。对于嵌套层级比较深的对象,官方推荐的做法是配合 @ObservedV2 和 @Trace 使用。如果不加这些,深层属性变化可能不会被 UI 感知,不是 @Once 的问题,而是整个响应式体系的边界。

第三个注意点:@Once 不是用来“优化性能”的银弹。它的真正价值在于语义清晰:哪些数据只在初始化时被关心,一眼就能看出来。不要为了减少一点刷新就把所有 @Param 都加上 @Once。父组件后续更新需要被子组件感知的字段,请保持普通 @Param 的默认行为。

3. @Event:让子组件把“事情”交还给父组件

3.1 为什么需要 @Event

讲完数据向下,再讲事件向上。

在一个正经的鸿蒙项目里,子组件很少只是显示出数据就完事了,更多时候它要做一些操作,然后把这些操作的结果告诉父组件。比如商品卡片的“加入购物车”、输入框内容的“实时上报”、列表项的“点击跳转”。

V1 时代的做法很朴素:直接在父组件里传一个函数属性给子组件:

// V1 老写法 GoodsCard({ name: '机械键盘', price: 399, onAddToCart: (count: number) => { // 父组件处理逻辑 } })

子组件内部调用this.onAddToCart?.(count)就行。这个写法能跑,但有两个小毛病:

  • 函数属性没有统一的声明约束,它可能是箭头函数、普通函数、甚至没传,子组件只能靠?.兜底,很容易在某个分支忘记判空就调用。
  • 函数属性和数据属性混在一起,看组件的 “props 列表”时,根本分不清哪些是数据、哪些是行为,维护成本高。

@Event 就是来解决这两个问题的。它就是 V2 体系里“事件”的正式公民。

3.2 @Event 的定义与调用

@Event 装饰的变量必须是函数类型,使用上分两步:

第一步在子组件里声明:

@ComponentV2 struct ChildComponent { // 声明一个事件,默认给一个空实现 @Event onCountChange: (value: number) => void = () => {}; build() { Button('点击') .onClick(() => { // 触发事件,把值带出去 this.onCountChange(42); }) } }

第二步在父组件里传入实现:

// 父组件 @Local count: number = 0; build() { Column() { ChildComponent({ onCountChange: (value: number) => { this.count = value; }}) } }

整个过程就是一个非常明确的“上报”流程:子组件不关心父组件收到之后会干什么,它只负责把“事情发生了”这个消息 + 相关参数抛出去。父组件也不关心子组件内部的状态细节,它只负责在收到消息时做该做的事。

我一开始不太理解为啥 @Event 要有个默认空实现,后来在真机上跑了一遍就懂了:如果没有空实现,当父组件还没传进来、或者某些条件分支漏传了,子组件调用this.onCountChange(...)就会直接崩掉。有了默认空实现,漏传的情况下调用也不会报错,最多就是“没人响应”。这对框架的健壮性是很实用的设计。

3.3 典型场景:商品卡片的加入购物车

还是拿刚才的 GoodsCard 来扩展。现在商品卡片上多了一个“加入购物车”按钮,点击之后要把当前选中的数量告诉父组件,让父组件去更新购物车总件数。

子组件改造如下:

@ComponentV2 struct GoodsCard { @Param @Once name: string = ''; @Param @Once price: number = 0; @Local count: number = 1; // 上报加购事件,参数是当前数量 @Event onAddToCart: (count: number, name: string) => void = () => {}; build() { Column({ space: 12 }) { Text(this.name).fontSize(20).fontWeight(FontWeight.Bold) Text(`单价:¥${this.price}`).fontSize(16) Row({ space: 12 }) { Button('减').onClick(() => { if (this.count > 1) { this.count--; } }) Text(`${this.count}`).fontSize(18) Button('加').onClick(() => { this.count++; }) } Button('加入购物车') .onClick(() => { this.onAddToCart(this.count, this.name); }) .width('100%') } .padding(16) .backgroundColor('#FFFFFF') .borderRadius(12) } }

父组件这边接收事件,更新自己的状态:

@Entry @ComponentV2 struct ShoppingListPage { @Local totalCount: number = 0; build() { Column({ space: 16 }) { Text(`购物车总件数:${this.totalCount}`) .fontSize(24) .fontWeight(FontWeight.Bold) GoodsCard({ name: '机械键盘', price: 399, onAddToCart: (count: number, name: string) => { this.totalCount += count; // 这里甚至可以继续上报全局埋点 } }) } .padding(20) } }

“加入购物车”这个动作,数据流的走向非常清晰:

  • 子组件里数量是 @Local 的,自己管理加减
  • 点击按钮通过 @Event 把count和商品名抛出去
  • 父组件收到后在回调里更新totalCount
  • totalCount变化后,父组件重绘,新的总数显示出来

上面例子在视觉上就是一个完整的闭环。

3.4 @Event 和 @Param 一起用的设计逻辑

实际项目里,@Event 跟 @Param 通常是成对出现的。@Param 往下传数据,@Event 往上抛事件,两者合起来就是一套标准的“受控组件”模式。

这个模式的关键点是:父组件是唯一的“数据源”。子组件要显示什么,由 @Param 决定;子组件要想改数据,不能自己直接改 @Param,而是通过 @Event 告诉父组件“该改了”,父组件改完之后,新的值又通过 @Param 传回来。

听起来绕,但这个设计能避开很多版本迭代时的坑:数据只在一个方向流动,父组件永远知道自己的数据是什么状态,不会出现“子组件偷偷改了值,父组件一脸懵”的情况。

这种方式跟 React 里的props + onChange是非常像的,如果你写过 React,理解 V2 这套几乎无成本。反过来,如果你一直写的是双向绑定风格,刚开始可能觉得繁琐,但跑几个复杂页面之后就会认同“显式比隐式好维护”这个道理。

一个实用建议:@Event 回调的参数尽量保持原子化,能传 count、id 这类基本类型,就别传一个整对象。回调参数越简单,父组件处理越灵活,代码也越好读。

4. 完整Demo:把 @Once 和 @Event 串起来

4.1 Demo 需求与分析

前面都是拆开讲,这节把两个装饰器放在同一个页面里跑一遍,看看它们在真实组合场景里的表现。

页面需求很简单:

  • 页面顶部显示购物车总件数,作为父组件的顶级状态
  • 中间是两张商品卡片,各自展示名称和价格
  • 卡片内部可以加减数量,初始数量从外部传入一次
  • 每张卡片有一个“加入购物车”按钮,点击后把数量上报父组件
  • 父组件的“刷新标记”会不断变化,测试卡片是否被无谓刷新

设计核心在于:卡片的基础信息属于“一次性初始数据”,用 @Once;卡片的操作动作属于“向上事件”,用 @Event;卡片内部的数量属于“自身状态”,用 @Local。

4.2 代码实现

先写商品卡片子组件:

@ComponentV2 struct GoodsCard { // 静态数据:初始化时传入一次,之后不受父组件刷新影响 @Param @Once id: number = 0; @Param @Once name: string = ''; @Param @Once price: number = 0; @Param @Once maxCount: number = 20; // 本地数量 @Local count: number = 1; // 上报事件 @Event onAddToCart: (id: number, name: string, count: number) => void = () => {}; build() { Column({ space: 10 }) { Row() { Text(this.name).fontSize(18).fontWeight(FontWeight.Bold) Text(`¥${this.price}`).fontSize(16).fontColor('#E84026') }.width('100%').justifyContent(FlexAlign.SpaceBetween) Row({ space: 10 }) { Button('−') .onClick(() => { if (this.count > 1) { this.count--; } }) Text(`${this.count}`).fontSize(18).width(40).textAlign(TextAlign.Center) Button('+') .onClick(() => { if (this.count < this.maxCount) { this.count++; } }) } Button(`加入购物车(${this.count}件)`) .width('100%') .onClick(() => { this.onAddToCart(this.id, this.name, this.count); }) } .padding(16) .backgroundColor('#FFFFFF') .borderRadius(12) .shadow({ radius: 6, color: '#11000000', offsetY: 4 }) } }

再写页面父组件:

@Entry @ComponentV2 struct ShoppingDemoPage { @Local totalCount: number = 0; @Local refreshFlag: number = 0; build() { Column({ space: 16 }) { Text(`购物车总件数:${this.totalCount}`) .fontSize(22) .fontWeight(FontWeight.Bold) Row() { Text(`父组件刷新标记:${this.refreshFlag}`) Button('触发刷新').onClick(() => { this.refreshFlag++; }) }.width('100%').justifyContent(FlexAlign.SpaceBetween) GoodsCard({ id: 1, name: '机械键盘', price: 399, maxCount: 10, onAddToCart: (id: number, name: string, count: number) => { this.totalCount += count; // 可在此追加日志 console.info(`AddToCart: id=${id}, name=${name}, count=${count}`); } }) GoodsCard({ id: 2, name: '显示器支架', price: 129, maxCount: 5, onAddToCart: (id: number, name: string, count: number) => { this.totalCount += count; // 可在此追加日志 console.info(`AddToCart: id=${id}, name=${name}, count=${count}`); } }) } .padding(20) .backgroundColor('#F1F3F5') .width('100%') .height('100%') } }

4.3 运行效果与数据流解读

把页面跑起来,操作一下,你会很直观地感受到这套设计:

  1. 点击两三次“触发刷新”,父组件的refreshFlag一直在涨,但商品卡片的名称、价格、数量都不会被重置。这就是 @Once 在起作用。
  2. 在一张卡片里把数量加到 3,点击“加入购物车”,父组件顶部的totalCount立刻变成 3。这是 @Event 在起作用。
  3. 点另一张卡片加购 2 件,totalCount变成 5,之前那张卡片内部的 count 还是 3,没有被父组件刷新干扰。

数据流全景是这样的:

父组件状态(totalCount) ↓ 通过 @Param/@Once 传入初始数据 子组件(GoodsCard) ↓ 通过 @Event 上报事件 父组件状态(totalCount)

这个环是闭合的,但每一环都是单向的。排查问题的时候,你只要沿着这个方向去查就行,不需要双向追踪。

5. 常见问题与避坑记录

这一节写写我在实际开发里遇到过的坑,以及身边朋友踩过的雷。有些问题官方文档提过但不够醒目,我按真实场景整理了一下。

5.1 @Once 修饰的变量被覆盖,找不到原因

现象:明明在子组件里把某个 @Param @Once 的变量改了,父组件一刷新,值又变回去了。

排查方向:先确认父组件是否对整个子组件做了重建,而不是仅仅调整了属性。在某些写法下,如果父组件用if/else包裹子组件,条件翻转会导致子组件销毁重建,这时候 @Once 的“只初始化一次”就不是遮挡了,而是新组件重新初始化了一次。这个坑严格来说不是 @Once 的锅,而是组件生命周期的问题。

经验:遇到“变化被复位”的情况,第一反应不是查装饰器,而是查这个组件到底有没有被重建。在 build 里加一行日志,看打印次数就能确认。

5.2 @Event 回调不执行,但又不报错

现象:点击按钮,页面没反应,控制台也没错误。

大多数情况是默认空实现被调用了,也就是说父组件并没有把真正的函数传进来。常见原因有:

  • 父组件里改名了回调参数,但传参时忘了同步
  • 子组件在某个分支重新实例化了,父组件没感知到
  • 在列表场景里,@Event 挂在 item 对象上,item 被复用或替换时回调没绑上

排查技巧:不要只在父组件的回调里打日志,在子组件触发前也打一行日志,确认子组件确实触发了this.onAddToCart(...)。如果子组件打了日志而父组件没有,问题就在传参链路上;如果子组件压根没打日志,先查按钮事件有没有绑定。

5.3 @Event 里使用普通函数,this 指向出错

父组件里传 @Event 时,写箭头函数是理智的做法。有人为了“整洁”写了普通函数,结果回调里访问this.totalCount时拿不到组件实例,直接报 undefined。

// 不推荐:普通函数里 this 指向会有问题 GoodsCard({ onAddToCart: function (id: number, count: number) { this.totalCount += count; // this 不是组件实例 } }) // 推荐:箭头函数 GoodsCard({ onAddToCart: (id: number, count: number) => { this.totalCount += count; } })

这个其实不是 @Event 特有的问题,而是 JS 里 this 的经典坑。但因为 @Event 的赋值点通常在 build 里,写起来很容易顺手写个普通函数,所以单独拎出来提醒一下。

5.4 @Param 对象类型配合 @Once 时,属性变化无感

现象:父组件传了一个对象给子组件,对象里面某个字段在子组件内部被修改了,UI 没刷新。

原因:@Param 传对象时,默认是深拷贝一份给子组件,子组件改的是副本。而 UI 刷新依赖响应式系统对这个字段建立观察。没有 @ObservedV2 + @Trace 加持的对象,属性级变化是感知不到的。

经验:如果子组件需要修改对象内部字段且要让 UI 感知,不要靠 @Once 或普通 @Param 扛,该上 @ObservedV2/@Trace 就上。@Once 只负责“初始化后不同步”,不负责“深层属性响应式”。

5.5 问题速查表

症状可能原因处理方向
@Once 变量值被重置子组件被销毁重建检查父组件 if/else 与 key 的变化
@Event 无响应父组件没传实现,空函数被调用检查传参名、列表 item 复用
@Event 回调里 this 异常用了普通函数改成箭头函数
对象属性改了 UI 不变缺少 @ObservedV2/@Trace补充细粒度观察
@Param 设置了默认值仍报错默认值与传入类型不一致保证同类型再传值
V1 和 V2 装饰器混用组件结构不一致检查是否混用 @Component 和 @ComponentV2

关于第六条多说一句:V1 的 @State、@Prop 不能和 V2 的 @Param、@Event 混在同一个组件里。一个组件要么走 V1 装饰器体系,要么走 V2 装饰器体系,别为了省事临时混搭,编译期可能不报错,运行期行为会很诡异。

6. 我这几次实践下来的延伸体会

@Once 和 @Event 单独看都不复杂,组合起来却改变了我写组件的方式。

以前写子组件,脑子里想的都是“我这个组件需要哪些外部数据”。现在写子组件,会先问两个问题:这些数据是只初始化需要,还是后续父组件变我也要跟着变?组件内部发生的动作,是应该自己消化,还是抛给父组件处理?这两个问题问完,组件接口基本就定了。接口定得清晰,代码就不会长歪。

另外强烈建议在项目初期就把 V2 这套统一起来用,不要新旧掺杂。V1 和 V2 虽然在同一个工程里能共存,但混用久了,新人看到概念各不相同、逻辑时隐时现的代码,基本就放弃维护了。如果团队正在从 V1 迁 V2,拿状态管理这块开刀是最值得的,因为它的收益最明显:数据流方向清楚、问题可追踪、组件边界清晰。

最后分享一个小技巧:调试 @Event 链路的时候,可以在子组件的 @Event 声明处加一个默认实现来打日志,比如:

@Event onAddToCart: (id: number, count: number) => void = (id: number, count: number) => { console.info(`[sub] onAddToCart called, id=${id}, count=${count}`); };

这样即使父组件漏传了实现,子组件触发时也能留下日志,排查效率直线提升。等确认链路没问题了,再把这个默认实现清空也不迟。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 11:35:04

古籍数字化重构:从拆书到三语知识库的完整工作流

去年春天&#xff0c;我拿到一批托名“贾子”的古代文献扫描件。六部&#xff0c;二三十万字&#xff0c;如果按传统路子来做&#xff0c;就是逐篇校注、白话翻译、加题解&#xff0c;最后攒成一套书。但我翻完第一遍就停住了——这套文献的问题根本不在“读不懂”&#xff0c;…

作者头像 李华
网站建设 2026/10/22 11:34:21

100行Python复刻AI Agent:大模型+工具调用的核心循环

最近社区里聊AI Agent的人越来越多&#xff0c;很多人入门的第一站是那个叫OpenClaw的开源Demo——一个看起来不大、但五脏俱全的个人助理原型。市面上把Agent讲得神乎其神的课不少&#xff0c;真正扒开看&#xff0c;核心只是一条循环&#xff1a;大模型决定做什么&#xff0c…

作者头像 李华
网站建设 2026/10/22 11:33:54

Python爬虫实战:抓取豆瓣短评生成个性化词云

爬虫这个词&#xff0c;在不少人眼里可能还带点“黑科技”滤镜&#xff0c;但拆开了其实就是三步&#xff1a;发一个请求、拿回一段页面、把我们要的信息抠出来。今天这篇教程&#xff0c;我从头写了一套完整的 Python 爬虫小项目&#xff1a;抓取豆瓣最新电影短评&#xff0c;…

作者头像 李华
网站建设 2026/10/22 11:33:40

Python爬虫实战:抓取豆瓣最新短评并生成中文词云

我最早写爬虫练手&#xff0c;就是拿豆瓣影评做词云&#xff0c;当时没想太多&#xff0c;纯粹想看看大家的评论里到底在聊什么。结果一上手才发现&#xff0c;从“能拿到数据”到“生成一张像样的词云图”&#xff0c;中间隔着一大堆细节&#xff1a;反爬怎么处理、网页结构怎…

作者头像 李华
网站建设 2026/10/22 11:32:06

fastbin_reverse_into_tcache:绕过 tcache 双重释放检查的堆利用深入解析

how2heap 的练习我前后刷过三遍&#xff0c;每次都会发现一些之前忽略的细节。今天想聊的是第二篇里一个非常经典的 2.31 考点&#xff1a; fastbin_reverse_into_tcache 。这个技巧本质上是把 fastbin 里的伪造节点反向塞进 tcache&#xff0c;从而绕过 tcache 的 double fr…

作者头像 李华
网站建设 2026/10/22 11:32:06

嵌入式C语言指针核心应用:内存模型、寄存器操作与野指针排查

指针这东西&#xff0c;C语言里绕不过去的坎。尤其是嵌入式开发&#xff0c;每天跟寄存器、缓冲区、硬件地址打交道&#xff0c;不懂指针可以说是寸步难行。我见过太多人刚开始学嵌入式&#xff0c;被指针搞得心态爆炸&#xff0c;其实指针没那么玄乎&#xff0c;你把它想明白了…

作者头像 李华