鸿蒙app开发中的数据驱动ui渲染问题
# 有趣的同事
我有一个年轻的同事,小哥帅的一批,他最近在学习鸿蒙手机app开发,据他据说,他已经参照华为官网的教程学习了有半个月了,可以说是已经大成,我听此很为他感到开心。但是他说在开发的过程中遇到一个问题,就是使用数组数据循环渲染页面时,改动某个元素的属性,页面不会跟着发生变化。他在在网上找了两天了,一直没有找到解决方案,最终他听从了网友大神的建议,某个元素的属性需要变化时,就将这个元素删掉,然后重新添加一个元素进去,因为定义数组的时候用了@State来修饰,所以页面是可以跟着变化的,他照做了,完事后功能已经非常完美了,但美中不足的是,每次操作元素时,列表项中有个图标,它都会闪一下,这是他的问题。
我听完,心想,原因大概也不难猜,毕竟他删除了元素重新插入的,顺序没有乱就已经很不错了。毕竟我也是精通vue的大佬,这不就是双向绑定、数据驱动页面元素嘛,按理说华子出品的开发语言不能连这种基础的功能都做不到吧,我的同事告诉我,现在全网并没有解决方案。我说:“我不信,我给你写个demo吧。”此时,我历史学习鸿蒙的总时长累计应该有1个小时左右,其中40分钟应该是安装环境。
# 他的问题
他的写法是这样的(我没仔细看过他的代码,但大致是这样的结构)
@Entry
@Component
struct Index {
// 假装这是接口请求来的数据
@State myList: Array<any> = [
{id: 1, imgUrl: $r('app.media.bg_1'), title: '标题标题', brief: '内容内容内容内容内容', num: 0},
{id: 2, imgUrl: $r('app.media.bg_1'), title: '标题标题', brief: '内容内容内容内容内容', num: 0},
{id: 3, imgUrl: $r('app.media.bg_1'), title: '标题标题', brief: '内容内容内容内容内容', num: 0}
]
build() {
Column() {
ForEach(
this.myList,
(item: any, index: number) => {
Row() {
Image(item.imgUrl)
// ...
Column() {
Text(item.id + ": " + item.title)
// ...
Text(item.brief)
// ...
}
// ...
Text(item.num+"")
}
// ...
.onClick(() => {
item.num++;
console.log("触发了点击,第" + index + "个元素的num-->" + this.myList[index].num + "");
})
},
(index: number) => index+""
)
}
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
所以这段代码显示的页面应该是这个样子的

当点击每一行的时候,日志也有输出
08-02 12:13:41.605 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第0个元素的num-->1
08-02 12:13:42.824 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第0个元素的num-->2
08-02 12:13:43.820 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第0个元素的num-->3
08-02 12:13:49.187 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第1个元素的num-->1
08-02 12:13:49.888 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第1个元素的num-->2
08-02 12:13:51.887 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第2个元素的num-->1
08-02 12:13:52.655 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第2个元素的num-->2
08-02 12:16:03.355 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第2个元素的num-->3
08-02 12:16:03.556 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第2个元素的num-->4
08-02 12:16:03.755 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第2个元素的num-->5
08-02 12:16:03.922 32241-6026747 I A0c0d0/JSApp: app Log: 触发了点击,第2个元素的num-->6
2
3
4
5
6
7
8
9
10
11
但是页面是并没有发生任何变化,我们期望的是第一行后面的那个数据能发生变化,毕竟我们在onclick中修改了num的值。
这个现象让我想到了vue中的ref()和reactive()的区别,那么这里可能也是相同的问题,我写demo的时候,是这样写的
@Entry
@Component
struct Index {
// 假装这是接口请求来的数据
@State myList: Array<any> = [ ... ]
build() {
Column() {
ForEach(
this.myList,
(item: any, index: number) => {
DemoItem({item: item, index: index})
},
(index: number) => index+""
)
}
// ...
}
}
@Component
struct DemoItem {
@Prop item: any;
@Prop index: number;
Row() {
Image(item.imgUrl)
Column() {
Text(item.id + ": " + item.title)
Text(item.brief)
}
Text(item.num+"")
}
.onClick(() => {
item.num++;
console.log("触发了点击,第" + index + "个元素的num-->" + this.myList[index].num + "");
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
现在这样的写法已经满足我的期望了:

我将这种方式告诉了我的同事,他也按我这种方式,将每一行拆分成独立的Component,但是他那边似乎并不好使,于是我又改了一版:
@Component
struct DemoItem {
@Prop item: any;
@Prop index: number;
// 将复杂的对象解构出简单的属性变量
@State title: string = this.item.title;
@State brief: string = this.item.brief;
@State num: number = this.item.num;
// 在页面中引用基础数据类型的变量
}
2
3
4
5
6
7
8
9
10
11
12
到此,他的问题已经解决了。
# 复盘一下
我觉得他的问题并不在于没有学习明白,本质的问题应该是在于写代码时的代码布局习惯,他写的页面代码是一整大坨,页面中不管大的小的功能块都在一坨中,导到了不得不用更复杂的数据结构体来封装数据,而恰好@State的变量复杂对象属性驱动页面元素变化并不理想。
我写的代码,一开始就把列表按行来封装@Component了,每个@Component中不需要使用太复杂的对象结构,甚至直接用基本数据类型都行,因为足够小,每个块中不需要太多的属性变量,我没遇到他那样的问题,在我看来完全就是代码书写习惯的区别。好的代码书写习惯可以很自然的写出好的代码结构,同时也可以避免很多不必要的坑,修改起来也更容易。