“答开发者问”之HarmonyOS 鸿蒙Next技术问题解析 第4期
“答开发者问”之HarmonyOS 鸿蒙Next技术问题解析 第4期
HarmonyOS开发者小伙伴们
这里是你们强大的后盾——鸿蒙官方技术支持团队!我们深知在探索鸿蒙开发的旅途中,每一个疑问都可能是前行的绊脚石。因此,我们特别推出了“答开发者问”系列帖,定期筛选并解答大家的问题,旨在为大家答疑解惑,扫清前行路上的障碍。
我们诚挚邀请大家积极发帖,无论是技术上的疑惑,还是开发过程中的瓶颈,都可以在社区中提问交流。同时,我们也鼓励大家互帮互助,如果你有过类似问题的解决经验,不妨慷慨分享,让这份知识和力量传递给更多的开发者。你的每一个提问,每一次解答,都将为鸿蒙生态的发展贡献一份力量。
请持续关注我们的“答开发者问”系列帖,我们会定期更新内容,助开发者一臂之力。让我们携手共进,共创鸿蒙开发的辉煌未来!
本期问题如下:
1、父组件的点击事件,如何让子组件进行响应?
2、子组件的点击事件,如何正确让父组件进行响应?
3、父子组件使用@State/@Prop交互,子组件中UI成功更新,但promise中无法获取到最新的值,原因是什么?
4、同样使用@State装饰器,有时第三层数据可以更新UI,有时不可以,是什么原因?
5、@ObservedV2和@Trace装饰的单例class数据,修改后为什么UI不更新?
往期问题回顾:
“答开发者问”之HarmonyOS技术问题解析 第1期-华为开发者问答 | 华为开发者联盟 (huawei.com)
更多关于“答开发者问”之HarmonyOS 鸿蒙Next技术问题解析 第4期的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
问题五:[@ObservedV2](/user/ObservedV2)和[@Trace](/user/Trace)装饰的单例class数据,修改后为什么UI不更新?
问题描述:
想要更新项目中一个单例数据源,同时刷新多个组件的UI,使用[@ObservedV2](/user/ObservedV2)和[@Trace](/user/Trace)装饰器修改单例数据后界面不会刷新,是否有其他方式实现?
@ObservedV2
class Arr {
id: number = 0;
@Trace numberArr: number[] = [];
static shared: Arr = new Arr()
constructor() {
this.numberArr = [0, 1, 2];
}
}
@Entry
@ComponentV2
struct Index {
arr: Arr = Arr.shared // UI不能刷新
// arr: Arr = new Arr() // UI可以刷新
build() {
Column() {
Text(length: <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.arr.numberArr.length}</span>
)
.fontSize(40)
Divider()
ForEach(this.arr.numberArr, (item: number, index: number) => {
Text(<span class="hljs-subst">${index}</span> <span class="hljs-subst">${item}</span>
)
.fontSize(40)
})
<span class="hljs-title class_">Button</span>(<span class="hljs-string">'push'</span>)
.<span class="hljs-title function_">onClick</span>(<span class="hljs-function">() =></span> {
<span class="hljs-variable language_">this</span>.<span class="hljs-property">arr</span>.<span class="hljs-property">numberArr</span>.<span class="hljs-title function_">push</span>(<span class="hljs-number">50</span>);
})
<span class="hljs-title class_">Button</span>(<span class="hljs-string">'pop'</span>)
.<span class="hljs-title function_">onClick</span>(<span class="hljs-function">() =></span> {
<span class="hljs-variable language_">this</span>.<span class="hljs-property">arr</span>.<span class="hljs-property">numberArr</span>.<span class="hljs-title function_">pop</span>();
})
}
}
}
解决方案:
原因是static修饰的静态属性属于类本身,而不是类的某个实例,框架的观察者机制通常监听实例属性的变化。
可以改用闭包单例模式,参考如下:
@ObservedV2
class Arr {
id: number = 0;
@Trace numberArr: number[] = [];
constructor() {
this.numberArr = [0, 1, 2];
}
}
// 创建单例实例的闭包函数
const createSingleton = (() => {
let instance: Arr | null = null;
return () => {
if (!instance) {
instance = new Arr();
}
return instance;
};
})();
// 获取单例实例
const sharedArr = createSingleton();
@Entry
@ComponentV2
struct Index {
// 获取单例实例
arr: Arr = sharedArr; // 使用闭包单例实例
build() {
Column() {
Text(length: <span class="hljs-subst">${<span class="hljs-variable language_">this</span>.arr.numberArr.length}</span>
)
.fontSize(40)
Divider()
ForEach(this.arr.numberArr, (item: number, index: number) => {
Text(<span class="hljs-subst">${index}</span> <span class="hljs-subst">${item}</span>
)
.fontSize(40)
})
<span class="hljs-title class_">Button</span>(<span class="hljs-string">'push'</span>)
.<span class="hljs-title function_">onClick</span>(<span class="hljs-function">() =></span> {
<span class="hljs-variable language_">this</span>.<span class="hljs-property">arr</span>.<span class="hljs-property">numberArr</span>.<span class="hljs-title function_">push</span>(<span class="hljs-number">50</span>);
})
<span class="hljs-title class_">Button</span>(<span class="hljs-string">'pop'</span>)
.<span class="hljs-title function_">onClick</span>(<span class="hljs-function">() =></span> {
<span class="hljs-variable language_">this</span>.<span class="hljs-property">arr</span>.<span class="hljs-property">numberArr</span>.<span class="hljs-title function_">pop</span>();
})
}
}
}
原链接:
更多关于“答开发者问”之HarmonyOS 鸿蒙Next技术问题解析 第4期的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
问题四:同样使用[@State](/user/State)装饰器,有时第三层数据可以更新UI,有时不可以,是什么原因?
问题描述:
如下两段代码,逻辑基本相同,但第三层属性this.parent.son.son
重新赋值后,代码1的UI更新成功,代码2的UI并未更新。只是将this.parent.son = this.son;
换了位置,结果便不同了。不太明白原因,可以解答下我的疑惑吗?
代码1如下:
class Son {
son: string = '000';
constructor(son: string) {
this.son = son;
}
}
class Parent {
son: Son = new Son(‘111’);
}
@Entry
@Component
struct Test {
@State son: Son = new Son(‘222’);
@State parent: Parent = new Parent();
aboutToAppear(): void {
this.parent.son = this.son;
}
build() {
Column() {
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.son.son}</span>
);
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.parent.son.son}</span>
);
Button(‘change’)
.onClick(() => {
this.parent.son.son = ‘333’;
})
}
}
}
执行结果如下:
代码2如下:
class Son {
son: string = ‘000’;
constructor(son: string) {
this.son = son;
}
}
class Parent {
son: Son = new Son(‘111’);
}
@Entry
@Component
struct Test {
@State son: Son = new Son(‘222’);
@State parent: Parent = new Parent();
aboutToAppear(): void {
}
build() {
Column() {
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.son.son}</span>
);
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.parent.son.son}</span>
);
Button(‘change’)
.onClick(() => {
this.parent.son = this.son;
this.parent.son.son = ‘333’;
})
}
}
}
执行结果如下:
解决方案:
@State装饰器,当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,第三层变化是无法观察的。
第一段代码中,this.parent.son = this.son是直接给parent的属性赋值,可观察;而this.parent.son.son = '333’是给第三层的属性赋值,不可观察,故UI中的this.parent.son.son不会更新。UI中this.son.son更新是因为class是引用数据类型,this.parent.son和this.son指向的是同一个堆内存中的对象,该对象的值已发生变化,框架观察到son的属性发生变化触发UI更新。
第二段代码中,this.parent.son = this.son时框架观察到parent的属性发生变化触发UI更新,这是一个异步过程,在UI更新时this.parent.son.son = '333’赋值已执行完毕,所以最终UI中的this.parent.son.son显示的是最新值。
将两次赋值分开结果就与第一段代码相同,如下:
struct Test {
@State son: Son = new Son(‘222’);
@State parent: Parent = new Parent();
build() {
Column() {
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.son.son}</span>
);
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.parent.son.son}</span>
);
Button(‘change’)
.onClick(() => {
// 点击第一个按钮执行二层属性的赋值
this.parent.son = this.son;
})
Button(‘change2’)
.onClick(() => {
// 点击第二个按钮执行三层属性的赋值
this.parent.son.son = ‘333’;
})
}
}
}
参考文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-state-V5#观察变化和行为表现
原链接:
@State的观测问题求解,各位大佬能解答一下小弟的疑惑吗,真的有点懵。。。-华为开发者问答 | 华为开发者联盟 (huawei.com)
问题三:父子组件使用[@State](/user/State)/[@Prop](/user/Prop)交互,子组件中UI成功更新,但promise中无法获取到最新的值,原因是什么?
问题描述:
现象如下:
- 目前首次点击,弹出提示框文案为:【父组件初始值】,我的期望是弹出【改值了!!!】
- 后续再进行点击没问题的,因为此时 msg 确实已经变为【改值了!!!】
排查及猜想:
- 首次点击与期望不符合,猜想可能有类似于 nextTick 之类的东西,应当等待子组件的 Prop 更新了之后,再去执行 showToast 。
- 查找文档没找到符合的 API。必须用 setTimeout 这种方式去延迟 showToast 吗?
- 尝试了用 setTimeout 延迟 showToast,发现只有 10ms 以上,才能达到期望结果;
请问该现象的原因是什么?怎么修改?
代码如下:
import { promptAction } from '@kit.ArkUI';
@Component
struct Child {
@Prop msg: string = “原始数据”
cb: () => Promise<void> = () => {
return Promise.resolve()
}
build() {
Button(this.msg).onClick(() => {
this.cb().then(() => {
console.log(“from Child”, this.msg)
setTimeout(() => {
promptAction.showToast({ message: this.msg })
}, 10)
})
})
}
}
@Entry
@Component
struct Parent {
@State msg: string = “父组件初始值”
parentFunc = () => {
const pFunc: Promise<void> = new Promise((resolve: Function) => {
this.msg = “改值了!!!”
console.log(“from Parent”, this.msg)
resolve()
})
return pFunc
}
build() {
Column() {
Child({
msg: this.msg,
cb: this.parentFunc
})
}
}
}
解决方案:
该现象是装饰器的使用问题,@State/@Prop的实现本质上是深拷贝,同步数据时不是和@State/@Link、@Provide/@Consume一样同步更新。当同步父组件数据时,需要走组件更新函数,也就是会晚于父组件一帧的时间识别数据变化,进行后续处理,那么刷新的时间就需要两帧。而@State/@Link、@Provide/@Consume是双向数据同步,只需要当前帧即可识别数据变化,下一帧刷新。
所以可以根据需求,使用@State/@Link或@Provide/@Consume代替@State/@Prop,即可实现期望的效果。@Provide/@Consume与@State/@Link的不同点在于:前者可以在多层级的父子组件之间传递。
另外还有一种方案,可以用@Watch来监听@Prop来实现。
方案一:使用@State/@Link。把原代码中@Prop改为@Link即可。
方案二:使用@Provide/@Consume。代码如下:
import { promptAction } from ‘@kit.ArkUI’;
@Component
struct Child {
@Consume(“pMsg”) msg: string
cb: () => Promise<void> = () => {
return Promise.resolve()
}
build() {
Button(this.msg).onClick(() => {
this.cb().then(() => {
console.log(“from Child”, this.msg)
promptAction.showToast({ message: this.msg })
})
})
}
}
@Entry
@Component
struct Parent {
@Provide(“pMsg”) msg: string = “父组件初始值”
parentFunc = () => {
const pFunc: Promise<void> = new Promise((resolve: Function) => {
this.msg = “改值了!!!”
console.log(“from Parent”, this.msg)
resolve()
})
return pFunc
}
build() {
Column() {
Child({
cb: this.parentFunc
})
}
}
}
import { promptAction } from ‘@kit.ArkUI’;
@Component
struct Child {
@Prop @Watch(‘chlTest’) msg: string = “原始数据”
cb: () => Promise<void> = () => {
return Promise.resolve()
}
chlTest(propName: string): void {
console.log(“from Child”, this.msg)
promptAction.showToast({ message: this.msg })
}
build() {
Button(this.msg).onClick(() => {
this.cb().then(() => {
})
})
}
}
@Entry
@Component
struct Parent {
@State msg: string = “父组件初始值”
parentFunc = () => {
const pFunc: Promise<void> = new Promise((resolve: Function) => {
this.msg = “改值了!!!”
console.log(“from Parent”, this.msg)
resolve()
})
return pFunc
}
build() {
Column() {
Child({
msg: this.msg,
cb: this.parentFunc
})
}
}
}
原链接:
传入的 Prop 已经修改了,UI 层已经更改了,但是在 promise中无法获取到最新的值,这种应该如何处理?(急急急!)求助各位大佬!-华为开发者问答 | 华为开发者联盟 (huawei.com)
问题二:子组件的点击事件,如何正确让父组件进行响应?
问题描述:
在 web 的 vue.js 中,可以通过 emit 将子组件的事件通知给父组件,父组件@监听 emit 的 key 值就可以处理子组件的事件响应了。
ArkTS中有类似方法吗?除了官网提供的透传方式(如下图)以外,还有别的方式可以在父组件中,处理子组件内点击事件吗?
解决方案:
以下几种方案可供参考:
方案一:楼主提到的官网中的透传方案,将父组件的事件传到子组件,即可在子组件调用,在父组件响应处理。参考文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-arkui-99-V5
方案二:使用事件订阅Emitter进行通信,不仅可以实现父子组件之间通信,还可以实现线程间通信。参考文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/itc-with-emitter-V5
方案三:可以在子组件中用一个[@Link](/user/Link)修饰的变量标志该事件发生,父组件中用[@State](/user/State)修饰 [@Watch](/user/Watch)监听,在监听事件中响应。参考以下demo:
@Entry
@Component
struct EntryComponent {
@State @Watch('test') count: number = 7;
test() {
console.log(‘testTag test in my component’ + this.count);
}
build() {
Column() {
MyComponent({
title: ‘Hello World 2’,
count: this.count
})
}
}
}
@Component
struct MyComponent {
@State title: string = ‘Hello World’
@Link count: number;
private increaseBy: number = 1
build() {
Column() {
Text(<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.title}</span>
).fontSize(30)
Button(Click to increase count=<span class="hljs-subst">${<span class="hljs-variable language_">this</span>.count}</span>
)
.margin(20)
.onClick(() => {
// 修改变量count,触发父组件test方法
this.count += this.increaseBy
})
}
}
}
原链接:
问题一:父组件的点击事件,如何让子组件进行响应?
问题描述:
CenterScreen是封装的自定义组件,在CenterView中引入使用。我想给CenterView的Image($r('app.media.startIcon'))设置点击事件,在CenterScreen中响应并处理逻辑,该怎样处理呢?
关键代码如下:
import CenterScreen from "./CenterScreen"
@Component
export struct CenterView {
build() {
Column({space: 8}) {
CenterScreen()
.layoutWeight(0.95)
.height(‘100%’)
.padding({
left: 12,
top: 12,
bottom: 12
})
......
<span class="hljs-title class_">Image</span>($r(<span class="hljs-string">'app.media.startIcon'</span>))
.<span class="hljs-title function_">width</span>(<span class="hljs-number">100</span>)
.<span class="hljs-title function_">height</span>(<span class="hljs-number">100</span>)
.<span class="hljs-title function_">draggable</span>(<span class="hljs-literal">true</span>)
.<span class="hljs-title function_">onClick</span>(<span class="hljs-function">() =></span> {
<span class="hljs-comment">// 需要调用在子组件CenterScreen中定义的方法</span>
})
}
.<span class="hljs-title function_">width</span>(<span class="hljs-string">'80%'</span>)
.<span class="hljs-title function_">height</span>(<span class="hljs-string">'100%'</span>)
.<span class="hljs-title function_">flexGrow</span>(<span class="hljs-number">1</span>)
.<span class="hljs-title function_">padding</span>({
<span class="hljs-attr">left</span>: <span class="hljs-number">8</span>,
<span class="hljs-attr">bottom</span>: <span class="hljs-number">8</span>
})
}
}
解决方案:
方案一:可以定义一个controller类,在controller类中定义和子组件中类型相同的方法,在子组件中将实际封装的方法给到controller。父组件在使用时,new一个controller对象传入子类中,在父组件中调用controller对应的方法。参考demo:
@Component
struct Child {
@State private text: string = ‘初始值’
private controller: ChildController = new ChildController();
aboutToAppear() {
if(this.controller) {
//给controller对应的方法赋值
this.controller.changeText = this.changeText
}
}
//封装的能力
private changeText = (value: string) =>{
this.text = value
}
build() {
Column() {
Text(this.text)
}
}
}
//定义controller对象
class ChildController {
changeText = (value: string) => {}
}
@Entry
@Component
struct Parent {
private ChildRef = new ChildController()
build() {
Column() {
Text(‘调用Child的changeText’).fontSize(‘18vp’).fontColor(Color.Gray)
Divider()
Child({ controller:this. ChildRef })
Button(‘Parent调用childer的changeText’).onClick(() => {
this.ChildRef.changeText(‘Parent调用childer的changeText’)
})
}
.justifyContent(FlexAlign.Center)
.width(“100%”)
.height(“100%”)
}
}
方案二:可以使用事件订阅实现事件传递,HarmonyOS提供了Emitter和EventHub两种方式。
- 两者本质上都是通过发布订阅方式来实现事件的传递。
- 使用场景有所不同,主线程内通信用EventHub,主线程与worker间通信用Emitter。
- 使用方式有所不同,EventHub仅Stage模型可用,通过context直接获取,作用范围与使用的context等价。Emitter在FA与Stage模型都可以用,直接搭配worker就可以。
- Emitter初衷是用于线程间通信,只是使用方法采用事件的发布与订阅。团队开发若都需要使用到Emitter,建议将Emitter的ID进行分段,不同的团队使用不同的ID分段。
- 如果是在单个模块范围内进行事件发布与订阅的功能,可以考虑使用EventHub的功能,它通过context进行事件的发布和订阅。使用示例可参考:使用eventhub进行数据通信
Emitter参考文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-emitter-V5
EventHub参考文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-inner-application-eventhub-V5
原链接: