鸿蒙ACE-V1状态分析@Watch

88 阅读3分钟

@Watch装饰器:状态变量更改通知

developer.huawei.com/consumer/cn…

@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范

观察变化和行为表现

  1. 当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的@Watch的回调方法将被触发;
  2. @Watch方法在自定义组件的属性变更之后同步执行
  3. 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
  4. 在第一次初始化的时候,@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

image.png

ViewPU

protected declareWatch(propStr: string, callback: (propName: string) => void): void

declareWatch方法将watch变量名和对应的函数映射存储到了watchedProps中。

image.png

关于 watchedProps 可以看 鸿蒙ACE-ArkUI构建(二)、渲染控制和构建过程

watchedProps是一个Map

image.png

Watch 使用的地方

ViewPU

viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set): void

我们知道 viewPropertyHasChanged 在状态变量变化之后会调用。这一块可以回忆一下鸿蒙ACE-状态分析@State

image.png

小结 见 观察变化和行为表现 1. 2. 3.

private performDelayedUpdate(): void

我们还看到一个performDelayedUpdate方法,这个函数在组件设置了 freezeWhenInactive 为true,并且从未活跃变为活跃时调用

这个方法内也会调用watch设置的回调函数

image.png

public setActiveInternal(active: boolean): void

这个函数是调用performDelayedUpdate的复函数,在这个函数内部,会判断组件有没有设置freezeWhenInactive,没有设置就什么也不做,否则会根据活跃不活跃调用相应的方法

image.png

小结

  1. @Watch声明的观测函数,在编译后会被以kv对的形式存储在组件的watchedProps属性里
  2. 状态变量变化的时候会更新组件,同时执行对应属性的观测函数

参考资料

  1. gitee.com/openharmony…
  2. 声明式范式的语法编译转换,语法验证 https://gitee.com/openharmony/developtools_ace_ets2bundle