@Watch狀態監聽機制:響應式數據變化處理

文章簡介

在HarmonyOS應用開發中,狀態管理是構建響應式應用的核心。@Watch裝飾器作為ArkUI的重要特性,能夠監聽狀態變量的變化並執行相應的回調函數。本文將深入講解@Watch的使用方法、原理和最佳實踐。

官方參考資料:

  • @Watch裝飾器

一、@Watch基礎概念

1.1 什麼是@Watch裝飾器

@Watch是ArkTS語言中的裝飾器,用於監聽狀態變量的變化。當被裝飾的狀態變量發生改變時,@Watch裝飾的方法會被自動調用。

核心特性:

  • 響應式數據監聽
  • 自動觸發回調函數
  • 支持同步和異步操作
  • 可監聽多個狀態變量

1.2 基本語法結構

@State @Watch('onCountChange') count: number = 0;

onCountChange(): void {
  // 當count變化時執行
  console.log('Count changed to: ' + this.count);
}

二、@Watch裝飾器詳解

2.1 裝飾器參數説明

@Watch裝飾器接受一個字符串參數,指定要調用的回調方法名。

參數配置表:

參數

類型

必填

説明

callback

string


狀態變化時調用的方法名

-

-

-

方法必須為無參數或單參數形式

2.2 支持的數據類型

@Watch可以監聽多種數據類型的狀態變化:

  • 基本類型:number、string、boolean
  • 對象類型:class實例、interface實現
  • 數組類型:Array
  • 嵌套對象:多層嵌套的數據結構

三、@Watch實戰應用

3.1 基本使用示例

@Component
struct WatchBasicExample {
  @State @Watch('onUserNameChange') userName: string = 'HarmonyOS';
  @State logMessages: string[] = [];

  onUserNameChange(): void {
    this.logMessages.push(`用户名變為: ${this.userName}`);
  }

  build() {
    Column() {
      TextInput({ placeholder: '請輸入用户名' })
        .onChange((value: string) => {
          this.userName = value;
        })
        
      Text('當前用户名: ' + this.userName)
        .fontSize(20)
        .margin(10)

      // 顯示變化日誌
      ForEach(this.logMessages, (message: string) => {
        Text(message)
          .fontSize(14)
          .fontColor(Color.Gray)
      })
    }
    .padding(20)
  }
}

3.2 監聽多個狀態變量

@Component
struct MultipleWatchExample {
  @State @Watch('onFormChange') firstName: string = '';
  @State @Watch('onFormChange') lastName: string = '';
  @State @Watch('onFormChange') age: number = 0;
  @State fullName: string = '';
  @State formChangeCount: number = 0;

  onFormChange(): void {
    this.fullName = `${this.firstName} ${this.lastName}`;
    this.formChangeCount++;
    
    console.log(`表單第${this.formChangeCount}次變化:`);
    console.log(`- 姓: ${this.firstName}`);
    console.log(`- 名: ${this.lastName}`);
    console.log(`- 年齡: ${this.age}`);
  }

  build() {
    Column() {
      TextInput({ placeholder: '姓' })
        .onChange((value: string) => {
          this.firstName = value;
        })
        
      TextInput({ placeholder: '名' })
        .onChange((value: string) => {
          this.lastName = value;
        })
        
      Slider({
        value: this.age,
        min: 0,
        max: 100
      })
      .onChange((value: number) => {
        this.age = value;
      })

      Text(`全名: ${this.fullName}`)
        .fontSize(18)
        .margin(10)
        
      Text(`表單變化次數: ${this.formChangeCount}`)
        .fontSize(14)
        .fontColor(Color.Blue)
    }
    .padding(20)
  }
}

3.3 對象屬性監聽

class UserProfile {
  name: string = '';
  email: string = '';
  level: number = 1;
}

@Component
struct ObjectWatchExample {
  @State @Watch('onProfileChange') profile: UserProfile = new UserProfile();
  @State changeHistory: string[] = [];

  onProfileChange(): void {
    const timestamp = new Date().toLocaleTimeString();
    this.changeHistory.push(`${timestamp}: ${this.profile.name} - ${this.profile.email}`);
    
    // 限制歷史記錄數量
    if (this.changeHistory.length > 5) {
      this.changeHistory = this.changeHistory.slice(-5);
    }
  }

  build() {
    Column() {
      TextInput({ placeholder: '姓名' })
        .onChange((value: string) => {
          this.profile.name = value;
        })
        
      TextInput({ placeholder: '郵箱' })
        .onChange((value: string) => {
          this.profile.email = value;
        })

      Text('變更歷史:')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })

      ForEach(this.changeHistory, (history: string) => {
        Text(history)
          .fontSize(12)
          .fontColor(Color.Gray)
      })
    }
    .padding(20)
  }
}

四、@Watch高級用法

4.1 條件觸發與防抖處理

@Component
struct DebounceWatchExample {
  @State @Watch('onSearchKeywordChange') searchKeyword: string = '';
  @State searchResults: string[] = [];
  @State isLoading: boolean = false;

  // 模擬搜索函數(帶防抖)
  onSearchKeywordChange(): void {
    if (this.searchKeyword.length < 2) {
      this.searchResults = [];
      return;
    }

    this.isLoading = true;
    
    // 模擬API調用延遲
    setTimeout(() => {
      this.performSearch();
    }, 300);
  }

  performSearch(): void {
    // 模擬搜索結果
    this.searchResults = [
      `結果1: ${this.searchKeyword}相關`,
      `結果2: ${this.searchKeyword}教程`,
      `結果3: ${this.searchKeyword}示例`
    ];
    this.isLoading = false;
  }

  build() {
    Column() {
      TextInput({ placeholder: '輸入關鍵詞搜索...' })
        .onChange((value: string) => {
          this.searchKeyword = value;
        })
        
      if (this.isLoading) {
        LoadingProgress()
          .color(Color.Blue)
      }

      ForEach(this.searchResults, (result: string) => {
        Text(result)
          .fontSize(14)
          .margin(5)
      })
    }
    .padding(20)
  }
}

4.2 數組變化監聽

@Component
struct ArrayWatchExample {
  @State @Watch('onTodoListChange') todoList: string[] = [];
  @State completedCount: number = 0;
  @State totalCount: number = 0;

  onTodoListChange(): void {
    this.totalCount = this.todoList.length;
    console.log(`待辦事項數量: ${this.totalCount}`);
  }

  addTodoItem(): void {
    const newItem = `任務 ${this.todoList.length + 1}`;
    this.todoList.push(newItem);
    // 需要重新賦值觸發監聽
    this.todoList = [...this.todoList];
  }

  removeTodoItem(index: number): void {
    this.todoList.splice(index, 1);
    this.todoList = [...this.todoList];
  }

  build() {
    Column() {
      Button('添加任務')
        .onClick(() => {
          this.addTodoItem();
        })
        
      Text(`總任務數: ${this.totalCount}`)
        .fontSize(16)
        .margin(10)

      ForEach(this.todoList, (item: string, index?: number) => {
        Row() {
          Text(item)
            .fontSize(14)
            
          Button('刪除')
            .onClick(() => {
              this.removeTodoItem(index!);
            })
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
        .margin(5)
      })
    }
    .padding(20)
  }
}

五、@Watch與其它裝飾器配合

5.1 與@Link配合使用

@Component
struct ParentComponent {
  @State parentCount: number = 0;

  build() {
    Column() {
      Text(`父組件計數: ${this.parentCount}`)
        .fontSize(20)
        
      Button('父組件增加')
        .onClick(() => {
          this.parentCount++;
        })
        
      ChildComponent({ count: $parentCount })
    }
    .padding(20)
  }
}

@Component
struct ChildComponent {
  @Link @Watch('onCountChange') count: number;
  @State changeLog: string[] = [];

  onCountChange(): void {
    this.changeLog.push(`計數變化: ${this.count}`);
  }

  build() {
    Column() {
      Text(`子組件計數: ${this.count}`)
        .fontSize(18)
        
      Button('子組件增加')
        .onClick(() => {
          this.count++;
        })
        
      ForEach(this.changeLog, (log: string) => {
        Text(log)
          .fontSize(12)
          .fontColor(Color.Green)
      })
    }
    .padding(15)
    .backgroundColor(Color.White)
  }
}

5.2 與@Prop和@Provide/@Consume配合

@Entry
@Component
struct WatchWithProvideExample {
  @Provide @Watch('onThemeChange') theme: string = 'light';

  onThemeChange(): void {
    console.log(`主題切換為: ${this.theme}`);
  }

  build() {
    Column() {
      Text('主題設置')
        .fontSize(24)
        
      Button('切換主題')
        .onClick(() => {
          this.theme = this.theme === 'light' ? 'dark' : 'light';
        })
        
      ThemeConsumerComponent()
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct ThemeConsumerComponent {
  @Consume theme: string;

  build() {
    Column() {
      Text(`當前主題: ${this.theme}`)
        .fontSize(18)
        .fontColor(this.theme === 'light' ? Color.Black : Color.White)
    }
    .width('100%')
    .height(200)
    .backgroundColor(this.theme === 'light' ? Color.White : Color.Black)
  }
}

六、性能優化與最佳實踐

6.1 避免不必要的監聽

@Component
struct OptimizedWatchExample {
  @State essentialData: string = '';
  @State nonEssentialData: string = '';
  
  // 只監聽必要的數據
  @State @Watch('onEssentialChange') criticalValue: number = 0;

  onEssentialChange(): void {
    // 只處理關鍵業務邏輯
    this.performCriticalOperation();
  }

  performCriticalOperation(): void {
    // 關鍵業務操作
    console.log('執行關鍵操作...');
  }

  build() {
    Column() {
      // 界面構建...
    }
  }
}

6.2 批量更新策略

@Component
struct BatchUpdateExample {
  @State @Watch('onDataUpdate') dataSet: number[] = [1, 2, 3];
  @State updateCount: number = 0;

  onDataUpdate(): void {
    this.updateCount++;
    console.log(`數據更新次數: ${this.updateCount}`);
  }

  // 批量更新方法
  batchUpdateData(): void {
    const newData = [...this.dataSet];
    
    // 多次修改
    newData.push(4);
    newData.push(5);
    newData[0] = 100;
    
    // 一次性賦值,只觸發一次@Watch
    this.dataSet = newData;
  }

  build() {
    Column() {
      Text(`更新次數: ${this.updateCount}`)
        .fontSize(18)
        
      Button('批量更新')
        .onClick(() => {
          this.batchUpdateData();
        })
        
      ForEach(this.dataSet, (item: number) => {
        Text(`數據: ${item}`)
          .fontSize(14)
      })
    }
    .padding(20)
  }
}

七、常見問題與解決方案

7.1 @Watch不觸發的常見原因

問題排查清單:

  • 狀態變量未使用@State裝飾
  • 回調方法名拼寫錯誤
  • 對象引用未改變(需創建新對象)
  • 數組操作後未重新賦值
  • 在構造函數中修改狀態

7.2 循環監聽問題

@Component
struct CircularWatchExample {
  @State @Watch('onValueAChange') valueA: number = 0;
  @State @Watch('onValueBChange') valueB: number = 0;

  // 錯誤的循環監聽
  onValueAChange(): void {
    // 這會導致循環調用!
    // this.valueB = this.valueA * 2;
  }

  onValueBChange(): void {
    // 這也會導致循環調用!
    // this.valueA = this.valueB / 2;
  }

  // 正確的解決方案
  safeUpdateB(): void {
    this.valueB = this.valueA * 2;
  }

  build() {
    Column() {
      // 界面構建...
    }
  }
}

八、版本兼容性説明

8.1 API版本要求

HarmonyOS版本

@Watch支持

備註

4.0.0+

✅ 完全支持

推薦使用

3.1.0-3.2.0

✅ 基本支持

部分高級特性不可用

3.0.0及以下

❌ 不支持

需使用其他狀態管理方案

8.2 開發環境配置

重要提示: 確保開發環境滿足以下要求:

  • DevEco Studio 4.0+
  • SDK API 10+
  • 編譯工具鏈最新版本

拓展學習推薦

@Monitor裝飾器 v2狀態管理中可以使用@Monitor更加優化的實現類似效果