@Watch装饰器:状态变量更改通知
developer.huawei.com/consumer/cn…
@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范
观察变化和行为表现
- 当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的@Watch的回调方法将被触发;
- @Watch方法在自定义组件的属性变更之后同步执行;
- 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
- 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法
转换前代码
/**
*
* WatchChildCmpt.ets
* Created by unravel on 2024/5/3
* @abstract
*/
class PurchaseItem {
static NextId: number = 0;
public id: number;
public price: number;
constructor(price: number) {
this.id = PurchaseItem.NextId++;
this.price = price;
}
}
@Component
struct BasketViewer {
@Link @Watch('onBasketUpdated') shopBasket: PurchaseItem[];
@State totalPurchase: number = 0;
updateTotal(): number {
let total = this.shopBasket.reduce((sum, i) => sum + i.price, 0);
// 超过100欧元可享受折扣
if (total >= 100) {
total = 0.9 * total;
}
return total;
}
// @Watch 回调
onBasketUpdated(propName: string): void {
this.totalPurchase = this.updateTotal();
}
build() {
Column() {
ForEach(this.shopBasket,
(item: PurchaseItem) => {
Text(`Price: ${item.price.toFixed(2)} €`)
},
(item: PurchaseItem) => item.id.toString()
)
Text(`Total: ${this.totalPurchase.toFixed(2)} €`)
}
}
}
@Component
export struct WatchChildCmpt {
@State shopBasket: PurchaseItem[] = [];
build() {
Column() {
Button('Add to basket')
.onClick(() => {
this.shopBasket.push(new PurchaseItem(Math.round(100 * Math.random())))
})
BasketViewer({ shopBasket: $shopBasket })
}
}
}
转换后代码
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface WatchChildCmpt_Params {
shopBasket?: PurchaseItem[];
}
interface BasketViewer_Params {
shopBasket?: PurchaseItem[];
totalPurchase?: number;
}
/**
*
* WatchChildCmpt.ets
* Created by unravel on 2024/5/3
* @abstract
*/
class PurchaseItem {
static NextId: number = 0;
public id: number;
public price: number;
constructor(price: number) {
this.id = PurchaseItem.NextId++;
this.price = price;
}
}
class BasketViewer extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__shopBasket = new SynchedPropertyObjectTwoWayPU(params.shopBasket, this, "shopBasket");
this.__totalPurchase = new ObservedPropertySimplePU(0, this, "totalPurchase");
this.setInitiallyProvidedValue(params);
this.declareWatch("shopBasket", this.onBasketUpdated);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: BasketViewer_Params) {
if (params.totalPurchase !== undefined) {
this.totalPurchase = params.totalPurchase;
}
}
updateStateVars(params: BasketViewer_Params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__shopBasket.purgeDependencyOnElmtId(rmElmtId);
this.__totalPurchase.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__shopBasket.aboutToBeDeleted();
this.__totalPurchase.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private __shopBasket: SynchedPropertySimpleOneWayPU<PurchaseItem[]>;
get shopBasket() {
return this.__shopBasket.get();
}
set shopBasket(newValue: PurchaseItem[]) {
this.__shopBasket.set(newValue);
}
private __totalPurchase: ObservedPropertySimplePU<number>;
get totalPurchase() {
return this.__totalPurchase.get();
}
set totalPurchase(newValue: number) {
this.__totalPurchase.set(newValue);
}
updateTotal(): number {
let total = this.shopBasket.reduce((sum, i) => sum + i.price, 0);
// 超过100欧元可享受折扣
if (total >= 100) {
total = 0.9 * total;
}
return total;
}
// @Watch 回调
onBasketUpdated(propName: string): void {
this.totalPurchase = this.updateTotal();
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
ForEach.create();
const forEachItemGenFunction = _item => {
const item = _item;
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create(`Price: ${item.price.toFixed(2)} €`);
}, Text);
Text.pop();
};
this.forEachUpdateFunction(elmtId, this.shopBasket, forEachItemGenFunction, (item: PurchaseItem) => item.id.toString(), false, false);
}, ForEach);
ForEach.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create(`Total: ${this.totalPurchase.toFixed(2)} €`);
}, Text);
Text.pop();
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
export class WatchChildCmpt extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__shopBasket = new ObservedPropertyObjectPU([], this, "shopBasket");
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: WatchChildCmpt_Params) {
if (params.shopBasket !== undefined) {
this.shopBasket = params.shopBasket;
}
}
updateStateVars(params: WatchChildCmpt_Params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__shopBasket.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__shopBasket.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private __shopBasket: ObservedPropertyObjectPU<PurchaseItem[]>;
get shopBasket() {
return this.__shopBasket.get();
}
set shopBasket(newValue: PurchaseItem[]) {
this.__shopBasket.set(newValue);
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Button.createWithLabel('Add to basket');
Button.onClick(() => {
this.shopBasket.push(new PurchaseItem(Math.round(100 * Math.random())));
});
}, Button);
Button.pop();
{
this.observeComponentCreation2((elmtId, isInitialRender) => {
if (isInitialRender) {
let componentCall = new BasketViewer(this, { shopBasket: this.__shopBasket }, undefined, elmtId, () => { }, { page: "entry/src/main/ets/pages/WatchChildCmpt.ets", line: 61 });
ViewPU.create(componentCall);
let paramsLambda = () => {
return {
shopBasket: this.shopBasket
};
};
componentCall.paramsGenerator_ = paramsLambda;
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {});
}
}, { name: "BasketViewer" });
}
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
@Watch
代码转换前后
可以看到转换之后生成了一个SynchedPropertyObjectTwoWayPU实例,同时调用了this.declareWatch方法。
这两部分分别对应@Link和@Watch各自的作用。关于@Link的分析可以看 鸿蒙ACE-V1状态分析@Link
@Watch转换后,只是在组件的contructor里面调用了 this.declareWatch
ViewPU
protected declareWatch(propStr: string, callback: (propName: string) => void): void
declareWatch方法将watch变量名和对应的函数映射存储到了watchedProps中。
关于 watchedProps 可以看 鸿蒙ACE-ArkUI构建(二)、渲染控制和构建过程
watchedProps是一个Map
Watch 使用的地方
ViewPU
viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set): void
我们知道 viewPropertyHasChanged 在状态变量变化之后会调用。这一块可以回忆一下鸿蒙ACE-状态分析@State
小结 见 观察变化和行为表现 1. 2. 3.
private performDelayedUpdate(): void
我们还看到一个performDelayedUpdate方法,这个函数在组件设置了 freezeWhenInactive 为true,并且从未活跃变为活跃时调用
这个方法内也会调用watch设置的回调函数
public setActiveInternal(active: boolean): void
这个函数是调用performDelayedUpdate的复函数,在这个函数内部,会判断组件有没有设置freezeWhenInactive,没有设置就什么也不做,否则会根据活跃不活跃调用相应的方法
小结
- @Watch声明的观测函数,在编译后会被以kv对的形式存储在组件的watchedProps属性里
- 状态变量变化的时候会更新组件,同时执行对应属性的观测函数