#星光不负 码向未来#从Vue到鸿蒙小记 原创
#星光不负 码向未来#从Vue到鸿蒙小记
前言:Vue开发者的鸿蒙转型踩坑记录
作为一名深耕Vue多年的前端开发者,接触鸿蒙生态后,我就一头扎进了前端转鸿蒙的学习里。起初我想得特简单:不就是换套语法嘛——Vue的组件化、响应式思想,跟鸿蒙ArkUI看着差不多。编写uI界面确实差不多,但是找上手写第一个稍微复杂一点的状态管理模块就栽了o(╥﹏╥)o:V1版本的装饰器规则绕得我头大,嵌套对象改完视图不刷新的问题,处理起来也比较难受,而且不仅仅是我一个难受,在后续逛鸿蒙开发者社区,以及参加一些线下沙龙和其他转型的开发者一聊才发现,这块处理起来都挺难受的【狗头】!
这篇文章就结合我的转型经历,聊聊鸿蒙状态管理从V1到V2的变化,把各阶段的坑和解决办法讲清楚,再顺带说说跨平台框架鸿蒙化的进展,给想转型的同学做个参考。
鸿蒙状态管理V1:探索与困境
(一)V1状态管理小试牛刀

刚接触V1时,我特意对比了Vue和鸿蒙的响应式机制:Vue2用Object.defineProperty、Vue3用Proxy,我们只需要定义状态变量即可实现数据的响应式;但鸿蒙V1得靠@State、@Prop、@Link等装饰器来实现,每个的作用域和传递规则都不一样,为了方便记忆,官方有一张完整的说明图示,如上图不同场景的装饰器是不同的。在业务不复杂不考虑嵌套数据的更新时,写起来和前端确实没啥区别。
先上个基础示例,看看V1怎么用装饰器实现父子组件状态传递,大家一看就懂:
// 父组件代码
@Entry
@Component
struct ParentComponent {
// 定义父组件内部状态,@State装饰器实现响应式
@State parentCount: number = 0;
build() {
Column() {
Text(`父组件计数:${this.parentCount}`)
.fontSize(18)
// 点击按钮修改状态
Button('父组件计数+1')
.onClick(() => {
this.parentCount++;
})
.margin(10)
// 向子组件传值,子组件用@Prop接收
ChildComponent({
childCount: this.parentCount,
changeCount: (newCount) => {
this.parentCount = newCount;
},
})
}
.padding(20)
}
}
// 子组件代码
@Component
struct ChildComponent {
// 通过@Prop接收传递的父组件状态
@Prop childCount: number = 0;
changeCount: (newCount: number) => void = (newCount) => {
}
build() {
Column() {
Text(`子组件接收的计数:${this.childCount}`)
.fontSize(16)
.fontColor('#0094ff')
.onClick(() => {
this.changeCount(Math.floor(Math.random() * 100))
})
}
.padding(10)
.border({ width: 1 })
}
}
这段代码核心就2点:
- 父-子:父组件用@State定义parentCount,子组件用@Prop接收。父组件的count一改,子组件就同步更新
- 子-父:点击子组件,通过回调函数传递随机数给父组件,父组件parentCount改变,重新传递子组件@Prop
这是一个非常简单的父子联动demo,咱们可以发现,虽然语法和Vue略有不同,但是步骤是一致的,当时的我,天真的认为其他的状态管理也和这个父子通讯类似,自己前端的经验可以全盘照搬。。。
对应官方文档:状态管理概述

(二)较为繁琐的深度监听
然后我碰到了深层数据监听的问题o(╥﹏╥)o,当时做用户信息编辑模块,要改嵌套的地址信息。按Vue的习惯直接写this.userInfo.city.name,页面纹丝不动!仔细查阅官方文档才知道:“上文所述的装饰器(包括@State、@Prop、@Link、@Provide和@Consume装饰器)仅能观察到第一层的变化,嵌套属性默认是监听不到的。”
我写了个demo复现这个问题,大家一看就明白:
import { data } from '@kit.TelephonyKit'
class UserInfo {
name: string
age: number
city: City
constructor(
name: string,
age: number,
city: City
) {
this.name = name
this.age = age
this.city = city
}
}
class City {
name: string
constructor(
name: string
) {
this.name = name
}
}
@Entry
@Component
struct Index {
// 定义嵌套对象状态
@State userInfo: UserInfo = new UserInfo('张三', 25, new City('北京'))
build() {
Column({ space: 10 }) {
Text(`姓名:${this.userInfo.name}`)
.fontSize(18)
Text(`年龄:${this.userInfo.age}`)
.fontSize(18)
Text(`城市:${this.userInfo.city.name}`)
.fontSize(18)
// 修改第一层属性,正常更新
Button('修改姓名')
.onClick(() => {
this.userInfo.name = '李四'; // 视图会刷新
})
.margin(5)
// 修改嵌套属性,不触发更新(坑点!)
Button('修改城市')
.onClick(() => {
this.userInfo.city.name = '上海'; // 页面没反应,深度监听失效
})
.margin(5)
}
}
}
@Component
struct cityCom {
@ObjectLink city: City
build() {
Text(this.city.name)
}
}
实际测试就是这样:改userInfo.name这种第一层属性,视图正常更新;但改city.name这种嵌套属性,页面完全没反应。
如何解决呢?既然只能监听到第一层,那我直接对第一层属性进行修改:
// 其他略
Button('修改城市')
.onClick(() => {
// this.userInfo.city.name = '上海'; // 页面没反应,深度监听失效
this.userInfo.city = new City("北京")
})
使用上面的写法可以触发视图更新,但是这么做的话每次都需要创建新的实例化对象,性能上略有浪费,就没有更好的方法了吗?然后我就看到了:@Observed装饰器和@ObjectLink
- @Observed添加给Class
- 添加子组件,子组件通过@ObjectLink接收传递过来的被@Observed装饰的状态变量
具体代码如下
import { data } from '@kit.TelephonyKit'
class UserInfo {
name: string
age: number
city: City
constructor(
name: string,
age: number,
city: City
) {
this.name = name
this.age = age
this.city = city
}
}
@Observed
class City {
name: string
constructor(
name: string
) {
this.name = name
}
}
@Entry
@Component
struct Index {
// 定义嵌套对象状态
@State userInfo: UserInfo = new UserInfo('张三', 25, new City('北京'))
build() {
Column({ space: 10 }) {
Text(`姓名:${this.userInfo.name}`)
.fontSize(18)
Text(`年龄:${this.userInfo.age}`)
.fontSize(18)
// 这里依旧不会刷新
Text(`城市:${this.userInfo.city.name}`)
.fontSize(18)
// 添加子组件 传递 city
cityCom({ city: this.userInfo.city })
// 修改第一层属性,正常更新
Button('修改姓名')
.onClick(() => {
this.userInfo.name = '李四'; // 视图会刷新
})
.margin(5)
// 修改嵌套属性,不触发更新(坑点!)
Button('修改城市')
.onClick(() => {
this.userInfo.city.name = '上海'; // 页面没反应,深度监听失效
})
.margin(5)
}
}
}
@Component
struct cityCom {
// 使用ObjectLInk接收状态变量
@ObjectLink city: City
build() {
Text(this.city.name)
}
}
这么写完之后,当我们点击修改城市,城市就可以更新了。但为了实现这个效果,我们需要
- 使用新的装饰器
- 拆分子组件
但是我如果继续在父组件中,去访问那个嵌套属性,因为是@State装饰,依旧是无法监听到改变(42行代码),需要在子组件中,配合@ObjectLInk才可以监听到。

(三)组件输入输出不明确
接下来这个问题,主要是因为自己粗心,但是也侧面反映了V1中组件通信装饰器的职能不明确的问题,直接上代码,子组件用错装饰器导致状态不同步:
// 错误示例:子组件用@State接收父组件状态,导致后续的修改UI不刷新
@Component
struct WrongDecoratorDemo {
// 错误!子组件接父组件值该用@Prop,我误用了@State
@State wrongProp: string = '';
build() {
Text(`子组件错误接收:${this.wrongProp}`)
}
}
@Entry
@Component
struct Index {
@State parentMsg: string = '父组件消息';
build() {
Column() {
WrongDecoratorDemo({ wrongProp: this.parentMsg })
Button('修改父组件消息')
.onClick(() => {
this.parentMsg = '修改后的父组件消息';
// 子组件用了@State,这里修改同步不过去!
})
}
}
}
子组件中错误的选用了@State来接收父组件传递的数据,导致后续父组件修改状态变量页面不更新,首先主要原因在我,但是这么写在传递数据的时候不会有任何的错误提示,让我在编码阶段一度认为自己是没错的。
不好好在随着熟练度的替身,这些小问题倒也容易规避,只是依旧会期待,期待有更好的写法。
鸿蒙状态管理V2:突破与展望
(一)痛点击破-深度监听
鸿蒙状态管理V2发布时,我第一时间就打开文档进行确认,看到了这段介绍之后,立马打开编辑器进行测试,因为之前我遇到的痛点全部都已经解决了。
- 深度监听,支持属性级更新
- <font style=“color:rgb(36, 39, 40);”>在组件中明确输入与输出</font>
- <font style=“color:rgb(36, 39, 40);”>新增计算属性支持</font>
- <font style=“color:rgb(36, 39, 40);”>…</font>
咱们逐个上代码演示
直接上代码,看看V2怎么解决深度监听问题,跟V1对比差距很明显:
@ObservedV2
class UserInfo {
@Trace
name: string
age: number
city: City
constructor(
name: string,
age: number,
city: City
) {
this.name = name
this.age = age
this.city = city
}
}
@ObservedV2
class City {
@Trace
name: string
constructor(
name: string
) {
this.name = name
}
}
@Entry
@ComponentV2
struct Index {
// 定义嵌套对象状态
userInfo: UserInfo = new UserInfo('张三', 25, new City('北京'))
build() {
Column({ space: 10 }) {
Text(`姓名:${this.userInfo.name}`)
.fontSize(18)
Text(`年龄:${this.userInfo.age}`)
.fontSize(18)
Text(`城市:${this.userInfo.city.name}`)
.fontSize(18)
// 修改第一层属性,正常更新
Button('修改姓名')
.onClick(() => {
this.userInfo.name = '李四'; // 视图会刷新
})
.margin(5)
// 修改嵌套属性,不触发更新(坑点!)
Button('修改城市')
.onClick(() => {
this.userInfo.city.name = '上海'; // 页面更新,深度监
})
.margin(5)
}
}
}
需要监听的class用@ObservedV2修饰,需要监听的属性用@Trace修饰,首层属性,或者深沉次属性都可以被监听到,相比于V1的写法更加简单便捷。
(二)痛点击破-明确输入输出
新增的装饰器在语义上更加明确,比如用@Local代替V1的@State,一看就知道是组件本地状态;父向子传值用@Param,依旧直接上代码
@ComponentV2
struct WrongDecoratorDemo {
// 正确写法,子组件用@Param接收父组件状态
@Param wrongProp: string = '';
// 直接报错,子组件用@Local定义状态变量,外部无法传递属性
// @Local wrongProp: string = '';
// 直接报错,子组件定义普通属性,无法从外部传递属性值
// wrongProp: string = '';
build() {
Text(`子组件错误接收:${this.wrongProp}`)
}
}
@Entry
@ComponentV2
struct Index {
@Local parentMsg: string = '父组件消息';
build() {
Column() {
WrongDecoratorDemo({ wrongProp: this.parentMsg })
Button('修改父组件消息')
.onClick(() => {
this.parentMsg = '修改后的父组件消息';
// 子组件用了@State,这里修改同步不过去!
})
}
}
}
用@Local,或者不写装饰器无法从外部传递数据,做到了明确的输入输出,这个改变我很喜欢。
(三)性能优化-计算属性
<font style=“color:rgba(0, 0, 0, 0.9);”>“当开发者使用相同的计算逻辑重复绑定在UI上时,为了防止重复计算,可以使用@Computed计算属性。”这是官网解释,Vue中也是一样的作用呢。</font>
@Computed
get sum() {
return this.count1 + this.count2 + this.count3;
}
Text(`${this.count1 + this.count2 + this.count3}`) // 计算this.count1 + this.count2 + this.count3
Text(`${this.count1 + this.count2 + this.count3}`) // 重复计算this.count1 + this.count2 + this.count3
Text(`${this.sum}`) // 读取@Computed sum的缓存值,节省上述重复计算
Text(`${this.sum}`) // 读取@Computed sum的缓存值,节省上述重复计算
重复取值会读取缓存值,性能消耗会更低一些V1这个可视没有的呢。
除了我罗列的这些以外,还有更多的新特性,稍后大伙可以自行探索,相信大伙可能还有疑惑:
- V2是从api12推出,现在已经非常稳定了,可以放心使用
- 新项目无脑直接V2即可
- V2和V1可以混用
如果你之前用的是V1,正好试试V2,如果还没有接触鸿蒙,直接从V2开始就是啦~~
跨平台框架的鸿蒙化进程
转型过程中,我还试了不同跨平台框架的鸿蒙化方案,也让我更了解鸿蒙生态的包容性。对Vue开发者来说,转型不一定非得硬啃原生,用熟悉的跨平台框架也是条好路。现在uni-app、Flutter、RN这几个主流框架都在搞鸿蒙化,进度还不慢,给我们不少选择。
总结与展望
鸿蒙从V1到V2的进化,不仅解决了我们开发者的痛点,更让我看到了国产操作系统生态的快速成长。在版本的更迭中,类似的优化层出不穷。
在学习过程中,鸿蒙开发者社区的Codelabs案例、线下沙龙的专家答疑,还有其他转型开发者的经验分享,都给了我很大帮助。大家都在踊跃的分享自己的经验心得,就像当初别人帮我那样,把经验传递下去。
更值得高兴的是,uni-app、Flutter、RN这些跨平台框架的鸿蒙化也在稳步推进,给我们提供了多样化的转型路径,如果你有对应的经验,同样可以快速切入
最后想对刚入门鸿蒙的前端同行说:转型路上难免踩坑,但每解决一个问题都是成长。跟着官方文档学基础,在社区里找同伴,多动手做实战项目,你会发现鸿蒙开发并没有那么难。星光不负,我们都能在鸿蒙生态里实现自己的价值。



















