HarmonyOS NEXT應(yīng)用開發(fā)之MVVM模式

應(yīng)用通過狀態(tài)去渲染更新UI是程序設(shè)計中相對復(fù)雜电湘,但又十分重要的每庆,往往決定了應(yīng)用程序的性能筐带。程序的狀態(tài)數(shù)據(jù)通常包含了數(shù)組、對象缤灵,或者是嵌套對象組合而成伦籍。在這些情況下,ArkUI采取MVVM = Model + View + ViewModel模式腮出,其中狀態(tài)管理模塊起到的就是ViewModel的作用帖鸦,將數(shù)據(jù)與視圖綁定在一起,更新數(shù)據(jù)的時候直接更新視圖胚嘲。

  • Model層:存儲數(shù)據(jù)和相關(guān)邏輯的模型作儿。它表示組件或其他相關(guān)業(yè)務(wù)邏輯之間傳輸?shù)臄?shù)據(jù)。Model是對原始數(shù)據(jù)的進一步處理馋劈。

  • View層:在ArkUI中通常是@Components修飾組件渲染的UI攻锰。

  • ViewModel層:在ArkUI中,ViewModel是存儲在自定義組件的狀態(tài)變量妓雾、LocalStorage和AppStorage中的數(shù)據(jù)口注。

    • 自定義組件通過執(zhí)行其build()方法或者@Builder裝飾的方法來渲染UI,即ViewModel可以渲染View君珠。
    • View可以通過相應(yīng)event handler來改變ViewModel,即事件驅(qū)動ViewModel的改變娇斑,另外ViewModel提供了@Watch回調(diào)方法用于監(jiān)聽狀態(tài)數(shù)據(jù)的改變策添。
    • 在ViewModel被改變時,需要同步回Model層毫缆,這樣才能保證ViewModel和Model的一致性唯竹,即應(yīng)用自身數(shù)據(jù)的一致性。
    • ViewModel結(jié)構(gòu)設(shè)計應(yīng)始終為了適配自定義組件的構(gòu)建和更新苦丁,這也是將Model和ViewModel分開的原因浸颓。

目前很多關(guān)于UI構(gòu)造和更新的問題,都是由于ViewModel的設(shè)計并沒有很好的支持自定義組件的渲染旺拉,或者試圖去讓自定義組件強行適配Model層产上,而中間沒有用ViewModel來進行分離。例如蛾狗,一個應(yīng)用程序直接將SQL數(shù)據(jù)庫中的數(shù)據(jù)讀入內(nèi)存晋涣,這種數(shù)據(jù)模型不能很好的直接適配自定義組件的渲染,所以在應(yīng)用程序開發(fā)中需要適配ViewModel層沉桌。

根據(jù)上面涉及SQL數(shù)據(jù)庫的示例谢鹊,應(yīng)用程序應(yīng)設(shè)計為:

  • Model:針對數(shù)據(jù)庫高效操作的數(shù)據(jù)模型算吩。

  • ViewModel:針對ArkUI狀態(tài)管理功能進行高效的UI更新的視圖模型。

  • 部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互轉(zhuǎn)換佃扼。

    • converters/adapters可以轉(zhuǎn)換最初從數(shù)據(jù)庫讀取的Model偎巢,來創(chuàng)建并初始化ViewModel。
    • 在應(yīng)用的使用場景中兼耀,UI會通過event handler改變ViewModel压昼,此時converters/adapters需要將ViewModel的更新數(shù)據(jù)同步回Model。

雖然與強制將UI擬合到SQL數(shù)據(jù)庫模式(MV模式)相比翠订,MVVM的設(shè)計比較復(fù)雜巢音,但應(yīng)用程序開發(fā)人員可以通過ViewModel層的隔離,來簡化UI的設(shè)計和實現(xiàn)尽超,以此來收獲更好的UI性能官撼。

ViewModel的數(shù)據(jù)源

ViewModel通常包含多個頂層數(shù)據(jù)源。@State和@Provide裝飾的變量以及LocalStorage和AppStorage都是頂層數(shù)據(jù)源似谁,其余裝飾器都是與數(shù)據(jù)源做同步的數(shù)據(jù)傲绣。裝飾器的選擇取決于狀態(tài)需要在自定義組件之間的共享范圍。共享范圍從小到大的排序是:

  • @State:組件級別的共享巩踏,通過命名參數(shù)機制傳遞秃诵,例如:CompA: ({ aProp: this.aProp }),表示傳遞層級(共享范圍)是父子之間的傳遞塞琼。

  • @Provide:組件級別的共享菠净,可以通過key和@Consume綁定,因此不用參數(shù)傳遞彪杉,實現(xiàn)多層級的數(shù)據(jù)共享毅往,共享范圍大于@State。

  • LocalStorage:頁面級別的共享派近,可以通過@Entry在當前組件樹上共享LocalStorage實例攀唯。

  • AppStorage:應(yīng)用全局的UI狀態(tài)存儲,和應(yīng)用進程綁定渴丸,在整個應(yīng)用內(nèi)的狀態(tài)數(shù)據(jù)的共享侯嘀。

@State裝飾的變量與一個或多個子組件共享狀態(tài)數(shù)據(jù)

@State可以初始化多種狀態(tài)變量,@Prop谱轨、@Link和@ObjectLink可以和其建立單向或雙向同步戒幔。

  1. 使用Parent根節(jié)點中@State裝飾的testNum作為ViewModel數(shù)據(jù)項。將testNum傳遞給其子組件LinkChild和Sibling碟嘴。

    // xxx.ets
    @Entry
    @Component
    struct Parent {
      @State @Watch("testNumChange1") testNum: number = 1;
    
      testNumChange1(propName: string): void {
        console.log(`Parent: testNumChange value ${this.testNum}`)
      }
    
      build() {
        Column() {
          LinkChild({ testNum: $testNum })
          Sibling({ testNum: $testNum })
        }
      }
    }
    
  2. LinkChild和Sibling中用@Link和父組件的數(shù)據(jù)源建立雙向同步溪食。其中LinkChild中創(chuàng)建了LinkLinkChild和PropLinkChild。

    @Component
    struct Sibling {
      @Link @Watch("testNumChange") testNum: number;
    
      testNumChange(propName: string): void {
        console.log(`Sibling: testNumChange value ${this.testNum}`);
      }
    
      build() {
        Text(`Sibling: ${this.testNum}`)
      }
    }
    
    @Component
    struct LinkChild {
      @Link @Watch("testNumChange") testNum: number;
    
      testNumChange(propName: string): void {
        console.log(`LinkChild: testNumChange value ${this.testNum}`);
      }
    
      build() {
        Column() {
          Button('incr testNum')
            .onClick(() => {
              console.log(`LinkChild: before value change value ${this.testNum}`);
              this.testNum = this.testNum + 1
              console.log(`LinkChild: after value change value ${this.testNum}`);
            })
          Text(`LinkChild: ${this.testNum}`)
          LinkLinkChild({ testNumGrand: $testNum })
          PropLinkChild({ testNumGrand: this.testNum })
        }
        .height(200).width(200)
      }
    }
    
  3. LinkLinkChild和PropLinkChild聲明如下娜扇,PropLinkChild中的@Prop和其父組件建立單向同步關(guān)系错沃。

    @Component
    struct LinkLinkChild {
      @Link @Watch("testNumChange") testNumGrand: number;
    
      testNumChange(propName: string): void {
        console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`);
      }
    
      build() {
        Text(`LinkLinkChild: ${this.testNumGrand}`)
      }
    }
    
    
    @Component
    struct PropLinkChild {
      @Prop @Watch("testNumChange") testNumGrand: number = 0;
    
      testNumChange(propName: string): void {
        console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
      }
    
      build() {
        Text(`PropLinkChild: ${this.testNumGrand}`)
          .height(70)
          .backgroundColor(Color.Red)
          .onClick(() => {
            this.testNumGrand += 1;
          })
      }
    }
    

當LinkChild中的@Link testNum更改時栅组。

  1. 更改首先同步到其父組件Parent,然后更改從Parent同步到Sibling枢析。

  2. LinkChild中的@Link testNum更改也同步給子組件LinkLinkChild和PropLinkChild玉掸。

@State裝飾器與@Provide、LocalStorage醒叁、AppStorage的區(qū)別:

  • @State如果想要將更改傳遞給孫子節(jié)點司浪,需要先將更改傳遞給子組件,再從子節(jié)點傳遞給孫子節(jié)點把沼。
  • 共享只能通過構(gòu)造函數(shù)的參數(shù)傳遞啊易,即命名參數(shù)機制CompA: ({ aProp: this.aProp })。

完整的代碼示例如下:

@Component
struct LinkLinkChild {
  @Link @Watch("testNumChange") testNumGrand: number;

  testNumChange(propName: string): void {
    console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`);
  }

  build() {
    Text(`LinkLinkChild: ${this.testNumGrand}`)
  }
}


@Component
struct PropLinkChild {
  @Prop @Watch("testNumChange") testNumGrand: number = 0;

  testNumChange(propName: string): void {
    console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
  }

  build() {
    Text(`PropLinkChild: ${this.testNumGrand}`)
      .height(70)
      .backgroundColor(Color.Red)
      .onClick(() => {
        this.testNumGrand += 1;
      })
  }
}


@Component
struct Sibling {
  @Link @Watch("testNumChange") testNum: number;

  testNumChange(propName: string): void {
    console.log(`Sibling: testNumChange value ${this.testNum}`);
  }

  build() {
    Text(`Sibling: ${this.testNum}`)
  }
}

@Component
struct LinkChild {
  @Link @Watch("testNumChange") testNum: number;

  testNumChange(propName: string): void {
    console.log(`LinkChild: testNumChange value ${this.testNum}`);
  }

  build() {
    Column() {
      Button('incr testNum')
        .onClick(() => {
          console.log(`LinkChild: before value change value ${this.testNum}`);
          this.testNum = this.testNum + 1
          console.log(`LinkChild: after value change value ${this.testNum}`);
        })
      Text(`LinkChild: ${this.testNum}`)
      LinkLinkChild({ testNumGrand: $testNum })
      PropLinkChild({ testNumGrand: this.testNum })
    }
    .height(200).width(200)
  }
}


@Entry
@Component
struct Parent {
  @State @Watch("testNumChange1") testNum: number = 1;

  testNumChange1(propName: string): void {
    console.log(`Parent: testNumChange value ${this.testNum}`)
  }

  build() {
    Column() {
      LinkChild({ testNum: $testNum })
      Sibling({ testNum: $testNum })
    }
  }
}

@Provide裝飾的變量與任何后代組件共享狀態(tài)數(shù)據(jù)

@Provide裝飾的變量可以與任何后代組件共享狀態(tài)數(shù)據(jù)饮睬,其后代組件使用@Consume創(chuàng)建雙向同步租谈,詳情見@Provide和@Consume。

因此捆愁,@Provide-@Consume模式比使用@State-@Link-@Link從父組件將更改傳遞到孫子組件更方便割去。@Provide-@Consume適合在單個頁面UI組件樹中共享狀態(tài)數(shù)據(jù)。

使用@Provide-@Consume模式時昼丑,@Consume和其祖先組件中的@Provide通過綁定相同的key連接呻逆,而不是在組件的構(gòu)造函數(shù)中通過參數(shù)來進行傳遞。

以下示例通過@Provide-@Consume模式菩帝,將更改從父組件傳遞到孫子組件咖城。

@Component
struct LinkLinkChild {
  @Consume @Watch("testNumChange") testNum: number;

  testNumChange(propName: string): void {
    console.log(`LinkLinkChild: testNum value ${this.testNum}`);
  }

  build() {
    Text(`LinkLinkChild: ${this.testNum}`)
  }
}

@Component
struct PropLinkChild {
  @Prop @Watch("testNumChange") testNumGrand: number = 0;

  testNumChange(propName: string): void {
    console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
  }

  build() {
    Text(`PropLinkChild: ${this.testNumGrand}`)
      .height(70)
      .backgroundColor(Color.Red)
      .onClick(() => {
        this.testNumGrand += 1;
      })
  }
}

@Component
struct Sibling {
  @Consume @Watch("testNumChange") testNum: number;

  testNumChange(propName: string): void {
    console.log(`Sibling: testNumChange value ${this.testNum}`);
  }

  build() {
    Text(`Sibling: ${this.testNum}`)
  }
}

@Component
struct LinkChild {
  @Consume @Watch("testNumChange") testNum: number;

  testNumChange(propName: string): void {
    console.log(`LinkChild: testNumChange value ${this.testNum}`);
  }

  build() {
    Column() {
      Button('incr testNum')
        .onClick(() => {
          console.log(`LinkChild: before value change value ${this.testNum}`);
          this.testNum = this.testNum + 1
          console.log(`LinkChild: after value change value ${this.testNum}`);
        })
      Text(`LinkChild: ${this.testNum}`)
      LinkLinkChild({ /* empty */ })
      PropLinkChild({ testNumGrand: this.testNum })
    }
    .height(200).width(200)
  }
}

@Entry
@Component
struct Parent {
  @Provide @Watch("testNumChange1") testNum: number = 1;

  testNumChange1(propName: string): void {
    console.log(`Parent: testNumChange value ${this.testNum}`)
  }

  build() {
    Column() {
      LinkChild({ /* empty */ })
      Sibling({ /* empty */ })
    }
  }
}

給LocalStorage實例中對應(yīng)的屬性建立雙向或單向同步

通過@LocalStorageLink和@LocalStorageProp,給LocalStorage實例中的屬性建立雙向或單向同步呼奢【品保可以將LocalStorage實例視為@State變量的Map。

LocalStorage對象可以在ArkUI應(yīng)用程序的幾個頁面上共享控妻。因此,使用@LocalStorageLink揭绑、@LocalStorageProp和LocalStorage可以在應(yīng)用程序的多個頁面上共享狀態(tài)弓候。

以下示例中:

  1. 創(chuàng)建一個LocalStorage實例,并通過@Entry(storage)將其注入根節(jié)點他匪。

  2. 在Parent組件中初始化@LocalStorageLink("testNum")變量時菇存,將在LocalStorage實例中創(chuàng)建testNum屬性,并設(shè)置指定的初始值為1邦蜜,即@LocalStorageLink("testNum") testNum: number = 1依鸥。

  3. 在其子組件中,都使用@LocalStorageLink或@LocalStorageProp綁定同一個屬性名key來傳遞數(shù)據(jù)悼沈。

LocalStorage可以被認為是@State變量的Map贱迟,屬性名作為Map中的key姐扮。

@LocalStorageLink和LocalStorage中對應(yīng)的屬性的同步行為,和@State和@Link一致衣吠,都為雙向數(shù)據(jù)同步茶敏。

以下為組件的狀態(tài)更新圖:

@Component
struct LinkLinkChild {
  @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;

  testNumChange(propName: string): void {
    console.log(`LinkLinkChild: testNum value ${this.testNum}`);
  }

  build() {
    Text(`LinkLinkChild: ${this.testNum}`)
  }
}

@Component
struct PropLinkChild {
  @LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;

  testNumChange(propName: string): void {
    console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
  }

  build() {
    Text(`PropLinkChild: ${this.testNumGrand}`)
      .height(70)
      .backgroundColor(Color.Red)
      .onClick(() => {
        this.testNumGrand += 1;
      })
  }
}

@Component
struct Sibling {
  @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;

  testNumChange(propName: string): void {
    console.log(`Sibling: testNumChange value ${this.testNum}`);
  }

  build() {
    Text(`Sibling: ${this.testNum}`)
  }
}

@Component
struct LinkChild {
  @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1;

  testNumChange(propName: string): void {
    console.log(`LinkChild: testNumChange value ${this.testNum}`);
  }

  build() {
    Column() {
      Button('incr testNum')
        .onClick(() => {
          console.log(`LinkChild: before value change value ${this.testNum}`);
          this.testNum = this.testNum + 1
          console.log(`LinkChild: after value change value ${this.testNum}`);
        })
      Text(`LinkChild: ${this.testNum}`)
      LinkLinkChild({ /* empty */ })
      PropLinkChild({ /* empty */ })
    }
    .height(200).width(200)
  }
}

// create LocalStorage object to hold the data
const storage = new LocalStorage();
@Entry(storage)
@Component
struct Parent {
  @LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;

  testNumChange1(propName: string): void {
    console.log(`Parent: testNumChange value ${this.testNum}`)
  }

  build() {
    Column() {
      LinkChild({ /* empty */ })
      Sibling({ /* empty */ })
    }
  }
}

給AppStorage中對應(yīng)的屬性建立雙向或單向同步

AppStorage是LocalStorage的單例對象,ArkUI在應(yīng)用程序啟動時創(chuàng)建該對象缚俏,在頁面中使用@StorageLink和@StorageProp為多個頁面之間共享數(shù)據(jù)惊搏,具體使用方法和LocalStorage類似。

也可以使用PersistentStorage將AppStorage中的特定屬性持久化到本地磁盤的文件中忧换,再次啟動的時候@StorageLink和@StorageProp會恢復(fù)上次應(yīng)用退出的數(shù)據(jù)恬惯。

示例如下:

@Component
struct LinkLinkChild {
  @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;

  testNumChange(propName: string): void {
    console.log(`LinkLinkChild: testNum value ${this.testNum}`);
  }

  build() {
    Text(`LinkLinkChild: ${this.testNum}`)
  }
}

@Component
struct PropLinkChild {
  @StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1;

  testNumChange(propName: string): void {
    console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`);
  }

  build() {
    Text(`PropLinkChild: ${this.testNumGrand}`)
      .height(70)
      .backgroundColor(Color.Red)
      .onClick(() => {
        this.testNumGrand += 1;
      })
  }
}

@Component
struct Sibling {
  @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;

  testNumChange(propName: string): void {
    console.log(`Sibling: testNumChange value ${this.testNum}`);
  }

  build() {
    Text(`Sibling: ${this.testNum}`)
  }
}

@Component
struct LinkChild {
  @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1;

  testNumChange(propName: string): void {
    console.log(`LinkChild: testNumChange value ${this.testNum}`);
  }

  build() {
    Column() {
      Button('incr testNum')
        .onClick(() => {
          console.log(`LinkChild: before value change value ${this.testNum}`);
          this.testNum = this.testNum + 1
          console.log(`LinkChild: after value change value ${this.testNum}`);
        })
      Text(`LinkChild: ${this.testNum}`)
      LinkLinkChild({ /* empty */
      })
      PropLinkChild({ /* empty */
      })
    }
    .height(200).width(200)
  }
}


@Entry
@Component
struct Parent {
  @StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1;

  testNumChange1(propName: string): void {
    console.log(`Parent: testNumChange value ${this.testNum}`)
  }

  build() {
    Column() {
      LinkChild({ /* empty */
      })
      Sibling({ /* empty */
      })
    }
  }
}

ViewModel的嵌套場景

大多數(shù)情況下,ViewModel數(shù)據(jù)項都是復(fù)雜類型的亚茬,例如酪耳,對象數(shù)組、嵌套對象或者這些類型的組合才写。對于嵌套場景葡兑,可以使用@Observed搭配@Prop或者@ObjectLink來觀察變化。

@Prop和@ObjectLink嵌套數(shù)據(jù)結(jié)構(gòu)

推薦設(shè)計單獨的@Component來渲染每一個數(shù)組或?qū)ο笤薏荨4藭r讹堤,對象數(shù)組或嵌套對象(屬性是對象的對象稱為嵌套對象)需要兩個@Component,一個@Component呈現(xiàn)外部數(shù)組/對象厨疙,另一個@Component呈現(xiàn)嵌套在數(shù)組/對象內(nèi)的類對象洲守。 @Prop、@Link沾凄、@ObjectLink修飾的變量只能觀察到第一層的變化梗醇。

  • 對于類:

    • 可以觀察到賦值的變化:this.obj=new ClassObj(...)
    • 可以觀察到對象屬性的更改:this.obj.a=new ClassA(...)
    • 不能觀察更深層級的屬性更改:this.obj.a.b = 47
  • 對于數(shù)組:

    • 可以觀察到數(shù)組的整體賦值:this.arr=[...]
    • 可以觀察到數(shù)據(jù)項的刪除、插入和替換:this.arr[1] = new ClassA()撒蟀、this.arr.pop()叙谨、 this.arr.push(new ClassA(...))、this.arr.sort(...)
    • 不能觀察更深層級的數(shù)組變化:this.arr[1].b = 47

如果要觀察嵌套類的內(nèi)部對象的變化保屯,可以使用@ObjectLink或@Prop手负。優(yōu)先考慮@ObjectLink,其通過嵌套對象內(nèi)部屬性的引用初始化自身姑尺。@Prop會對嵌套在內(nèi)部的對象的深度拷貝來進行初始化竟终,以實現(xiàn)單向同步。在性能上@Prop的深度拷貝比@ObjectLink的引用拷貝慢很多切蟋。

@ObjectLink或@Prop可以用來存儲嵌套內(nèi)部的類對象统捶,該類必須用@Observed類裝飾器裝飾,否則類的屬性改變并不會觸發(fā)更新,UI并不會刷新喘鸟。@Observed為其裝飾的類實現(xiàn)自定義構(gòu)造函數(shù)匆绣,此構(gòu)造函數(shù)創(chuàng)建了一個類的實例,并使用ES6代理包裝(由ArkUI框架實現(xiàn))迷守,攔截修飾class屬性的所有“get”和“set”犬绒。“set”觀察屬性值兑凿,當發(fā)生賦值操作時凯力,通知ArkUI框架更新±窕“get”收集哪些UI組件依賴該狀態(tài)變量咐鹤,實現(xiàn)最小化UI更新。

如果嵌套場景中圣絮,嵌套數(shù)據(jù)內(nèi)部是數(shù)組或者class時祈惶,需根據(jù)以下場景使用@Observed類裝飾器。

  • 如果嵌套數(shù)據(jù)內(nèi)部是class扮匠,直接被@Observed裝飾捧请。

  • 如果嵌套數(shù)據(jù)內(nèi)部是數(shù)組,可以通過以下方式來觀察數(shù)組變化棒搜。

    @Observed class ObservedArray<T> extends Array<T> {
        constructor(args: T[]) {
            if (args instanceof Array) {
              super(...args);
            } else {
              super(args)
            }
        }
        /* otherwise empty */
    }
    

    ViewModel為外層class疹蛉。

    class Outer {
      innerArrayProp : ObservedArray<string> = [];
      ...
    }
    

嵌套數(shù)據(jù)結(jié)構(gòu)中@Prop和@ObjectLink之的區(qū)別

以下示例中:

  • 父組件ViewB渲染@State arrA:Array<ClassA>。@State可以觀察新數(shù)組的分配力麸、數(shù)組項插入可款、刪除和替換。

  • 子組件ViewA渲染每一個ClassA的對象克蚂。

  • 類裝飾器@Observed ClassA與@ObjectLink a: ClassA闺鲸。

    • 可以觀察嵌套在Array內(nèi)的ClassA對象的變化。

    • 不使用@Observed時:
      ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10將不會被觀察到埃叭,相應(yīng)的ViewA組件也不會更新摸恍。

      對于數(shù)組中的第一個和第二個數(shù)組項,每個數(shù)組項都初始化了兩個ViewA的對象赤屋,渲染了同一個ViewA實例误墓。在一個ViewA中的屬性賦值this.a.c += 1;時不會引發(fā)另外一個使用同一個ClassA初始化的ViewA的渲染更新。

let NextID: number = 1;

// 類裝飾器@Observed裝飾ClassA
@Observed
class ClassA {
  public id: number;
  public c: number;

  constructor(c: number) {
    this.id = NextID++;
    this.c = c;
  }
}

@Component
struct ViewA {
  @ObjectLink a: ClassA;
  label: string = "ViewA1";

  build() {
    Row() {
      Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
        .onClick(() => {
          // 改變對象屬性
          this.a.c += 1;
        })
    }
  }
}

@Entry
@Component
struct ViewB {
  @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];

  build() {
    Column() {
      ForEach(this.arrA,
        (item: ClassA) => {
          ViewA({ label: `#${item.id}`, a: item })
        },
        (item: ClassA): string => { return item.id.toString(); }
      )

      Divider().height(10)

      if (this.arrA.length) {
        ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
        ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
      }

      Divider().height(10)

      Button(`ViewB: reset array`)
        .onClick(() => {
          // 替換整個數(shù)組益缎,會被@State this.arrA觀察到
          this.arrA = [new ClassA(0), new ClassA(0)];
        })
      Button(`array push`)
        .onClick(() => {
          // 數(shù)組中插入數(shù)據(jù),會被@State this.arrA觀察到
          this.arrA.push(new ClassA(0))
        })
      Button(`array shift`)
        .onClick(() => {
          // 數(shù)組中移除數(shù)據(jù)然想,會被@State this.arrA觀察到
          this.arrA.shift()
        })
      Button(`ViewB: chg item property in middle`)
        .onClick(() => {
          // 替換數(shù)組中的某個元素莺奔,會被@State this.arrA觀察到
          this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
        })
      Button(`ViewB: chg item property in middle`)
        .onClick(() => {
          // 改變數(shù)組中某個元素的屬性c,會被ViewA中的@ObjectLink觀察到
          this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
        })
    }
  }
}

在ViewA中,將@ObjectLink替換為@Prop令哟。

@Component
struct ViewA {

  @Prop a: ClassA = new ClassA(0);
  label : string = "ViewA1";

  build() {
     Row() {
        Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
        .onClick(() => {
            // change object property
            this.a.c += 1;
        })
     }
  }
}

與用@Prop修飾不同恼琼,用@ObjectLink修飾時,點擊數(shù)組的第一個或第二個元素屏富,后面兩個ViewA會發(fā)生同步的變化晴竞。

@Prop是單向數(shù)據(jù)同步,ViewA內(nèi)的Button只會觸發(fā)Button自身的刷新狠半,不會傳播到其他的ViewA實例中噩死。在ViewA中的ClassA只是一個副本,并不是其父組件中@State arrA : Array<ClassA>中的對象神年,也不是其他ViewA的ClassA已维,這使得數(shù)組的元素和ViewA中的元素表面是傳入的同一個對象,實際上在UI上渲染使用的是兩個互不相干的對象已日。

需要注意@Prop和@ObjectLink還有一個區(qū)別:@ObjectLink裝飾的變量是僅可讀的垛耳,不能被賦值;@Prop裝飾的變量可以被賦值飘千。

  • @ObjectLink實現(xiàn)雙向同步堂鲜,因為它是通過數(shù)據(jù)源的引用初始化的。

  • @Prop是單向同步护奈,需要深拷貝數(shù)據(jù)源缔莲。

  • 對于@Prop賦值新的對象,就是簡單地將本地的值覆寫逆济,但是對于實現(xiàn)雙向數(shù)據(jù)同步的@ObjectLink酌予,覆寫新的對象相當于要更新數(shù)據(jù)源中的數(shù)組項或者class的屬性,這個對于 TypeScript/JavaScript是不能實現(xiàn)的奖慌。

MVVM應(yīng)用示例

以下示例深入探討了嵌套ViewModel的應(yīng)用程序設(shè)計抛虫,特別是自定義組件如何渲染一個嵌套的Object,該場景在實際的應(yīng)用開發(fā)中十分常見简僧。

開發(fā)一個電話簿應(yīng)用建椰,實現(xiàn)功能如下:

  • 顯示聯(lián)系人和設(shè)備("Me")電話號碼 。

  • 選中聯(lián)系人時岛马,進入可編輯態(tài)“Edit”棉姐,可以更新該聯(lián)系人詳細信息,包括電話號碼啦逆,住址伞矩。

  • 在更新聯(lián)系人信息時,只有在單擊保存“Save Changes”之后夏志,才會保存更改乃坤。

  • 可以點擊刪除聯(lián)系人“Delete Contact”,可以在聯(lián)系人列表刪除該聯(lián)系人。

ViewModel需要包括:

  • AddressBook(class)
    • me(設(shè)備): 存儲一個Person類湿诊。
    • contacts(設(shè)備聯(lián)系人):存儲一個Person類數(shù)組狱杰。

AddressBook類聲明如下:

export class AddressBook {
  me: Person;
  contacts: ObservedArray<Person>;
  
  constructor(me: Person, contacts: Person[]) {
    this.me = me;
    this.contacts = new ObservedArray<Person>(contacts);
  }
}
  • Person (class)
    • name : string
    • address : Address
    • phones: ObservedArray<string>
    • Address (class)
      • street : string
      • zip : number
      • city : string

Address類聲明如下:

@Observed
export class Address {
  street: string;
  zip: number;
  city: string;

  constructor(street: string,
              zip: number,
              city: string) {
    this.street = street;
    this.zip = zip;
    this.city = city;
  }
}

Person類聲明如下:

let nextId = 0;

@Observed
export class Person {
  id_: string;
  name: string;
  address: Address;
  phones: ObservedArray<string>;

  constructor(name: string,
              street: string,
              zip: number,
              city: string,
              phones: string[]) {
    this.id_ = `${nextId}`;
    nextId++;
    this.name = name;
    this.address = new Address(street, zip, city);
    this.phones = new ObservedArray<string>(phones);
  }
}

需要注意的是,因為phones是嵌套屬性厅须,如果要觀察到phones的變化仿畸,需要extends array,并用@Observed修飾它朗和。ObservedArray類的聲明如下错沽。

@Observed
export class ObservedArray<T> extends Array<T> {
  constructor(args: T[]) {
    console.log(`ObservedArray: ${JSON.stringify(args)} `)
    if (args instanceof Array) {
      super(...args);
    } else {
      super(args)
    }
  }
}
  • selected : 對Person的引用。

更新流程如下:

  1. 在根節(jié)點PageEntry中初始化所有的數(shù)據(jù)例隆,將me和contacts和其子組件AddressBookView建立雙向數(shù)據(jù)同步甥捺,selectedPerson默認為me,需要注意镀层,selectedPerson并不是PageEntry數(shù)據(jù)源中的數(shù)據(jù)镰禾,而是數(shù)據(jù)源中,對某一個Person的引用唱逢。

PageEntry和AddressBookView聲明如下:

@Component
struct AddressBookView {

    @ObjectLink me : Person;
    @ObjectLink contacts : ObservedArray<Person>;
    @State selectedPerson: Person = new Person("", "", 0, "", []);

    aboutToAppear() {
        this.selectedPerson = this.me;
    }

    build() {
        Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) {
            Text("Me:")
            PersonView({
             person: this.me,
             phones: this.me.phones,
             selectedPerson: this.selectedPerson
           })

            Divider().height(8)

           ForEach(this.contacts, (contact: Person) => {
             PersonView({
               person: contact,
               phones: contact.phones as ObservedArray<string>,
               selectedPerson: this.selectedPerson
             })
           },
             (contact: Person): string => { return contact.id_; }
           )

            Divider().height(8)

            Text("Edit:")
            PersonEditView({ 
             selectedPerson: this.selectedPerson, 
             name: this.selectedPerson.name, 
             address: this.selectedPerson.address, 
             phones: this.selectedPerson.phones 
           })
        }
            .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5)
    }
}

@Entry
@Component
struct PageEntry {
  @Provide addrBook: AddressBook = new AddressBook(
    new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]),
    [
      new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
      new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
      new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]),
    ]);

  build() {
    Column() {
      AddressBookView({ 
       me: this.addrBook.me, 
       contacts: this.addrBook.contacts, 
       selectedPerson: this.addrBook.me 
     })
    }
  }
}
  1. PersonView吴侦,即電話簿中聯(lián)系人姓名和首選電話的View,當用戶選中坞古,即高亮當前Person备韧,需要同步回其父組件AddressBookView的selectedPerson,所以需要通過@Link建立雙向同步痪枫。
    PersonView聲明如下:

    // 顯示聯(lián)系人姓名和首選電話
    // 為了更新電話號碼织堂,這里需要@ObjectLink person和@ObjectLink phones,
    // 顯示首選號碼不能使用this.person.phones[0]奶陈,因為@ObjectLink person只代理了Person的屬性易阳,數(shù)組內(nèi)部的變化觀察不到
    // 觸發(fā)onClick事件更新selectedPerson
    @Component
    struct PersonView {
    
        @ObjectLink person : Person;
        @ObjectLink phones :  ObservedArray<string>;
    
        @Link selectedPerson : Person;
    
        build() {
            Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
              Text(this.person.name)
              if (this.phones.length > 0) {
                Text(this.phones[0])
              }
            }
            .height(55)
            .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff")
            .onClick(() => {
                this.selectedPerson = this.person;
            })
        }
    }
    
  2. 選中的Person會在PersonEditView中顯示詳細信息,對于PersonEditView的數(shù)據(jù)同步分為以下三種方式:

    • 在Edit狀態(tài)通過Input.onChange回調(diào)事件接受用戶的鍵盤輸入時吃粒,在點擊“Save Changes”之前潦俺,這個修改是不希望同步回數(shù)據(jù)源的,但又希望刷新在當前的PersonEditView中徐勃,所以@Prop深拷貝當前Person的詳細信息事示;

    • PersonEditView通過@Link seletedPerson: Person和AddressBookView的``selectedPerson建立雙向同步,當用戶點擊“Save Changes”的時候僻肖,@Prop的修改將被賦值給@Link seletedPerson: Person肖爵,這就意味這,數(shù)據(jù)將被同步回數(shù)據(jù)源臀脏。

    • PersonEditView中通過@Consume addrBook: AddressBook和根節(jié)點PageEntry建立跨組件層級的直接的雙向同步關(guān)系劝堪,當用戶在PersonEditView界面刪除某一個聯(lián)系人時法挨,會直接同步回PageEntry,PageEntry的更新會通知AddressBookView刷新contracts的列表頁幅聘。 PersonEditView聲明如下:

     // 渲染Person的詳細信息
     // @Prop裝飾的變量從父組件AddressBookView深拷貝數(shù)據(jù)眷柔,將變化保留在本地, TextInput的變化只會在本地副本上進行修改肋杖。
     // 點擊 "Save Changes" 會將所有數(shù)據(jù)的復(fù)制通過@Prop到@Link, 同步到其他組件
     @Component
     struct PersonEditView {
     
         @Consume addrBook : AddressBook;
     
         /* 指向父組件selectedPerson的引用 */
         @Link selectedPerson: Person;
     
         /*在本地副本上編輯,直到點擊保存*/
         @Prop name: string = "";
         @Prop address : Address = new Address("", 0, "");
         @Prop phones : ObservedArray<string> = [];
     
         selectedPersonIndex() : number {
             return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_);
         }
     
         build() {
             Column() {
                 TextInput({ text: this.name})
                     .onChange((value) => {
                         this.name = value;
                       })
                 TextInput({text: this.address.street})
                     .onChange((value) => {
                         this.address.street = value;
                     })
     
                 TextInput({text: this.address.city})
                     .onChange((value) => {
                         this.address.city = value;
                     })
     
                 TextInput({text: this.address.zip.toString()})
                     .onChange((value) => {
                         const result = Number.parseInt(value);
                         this.address.zip= Number.isNaN(result) ? 0 : result;
                     })
     
                 if (this.phones.length > 0) {
                   ForEach(this.phones,
                     (phone: ResourceStr, index?:number) => {
                       TextInput({ text: phone })
                         .width(150)
                         .onChange((value) => {
                           console.log(`${index}. ${value} value has changed`)
                           this.phones[index!] = value;
                         })
                     },
                     (phone: ResourceStr, index?:number) => `${index}`
                   )
                 }

                 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
                     Text("Save Changes")
                         .onClick(() => {
                             // 將本地副本更新的值賦值給指向父組件selectedPerson的引用
                             // 避免創(chuàng)建新對象宋舷,在現(xiàn)有屬性上進行修改
                             this.selectedPerson.name = this.name;
                             this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city)
                             this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } );
                         })
                     if (this.selectedPersonIndex()!=-1) {
                         Text("Delete Contact")
                             .onClick(() => {
                                 let index = this.selectedPersonIndex();
                                 console.log(`delete contact at index ${index}`);
     
                                 // 刪除當前聯(lián)系人
                                 this.addrBook.contacts.splice(index, 1);
     
                                 // 刪除當前selectedPerson巷怜,選中態(tài)前移一位
                                 index = (index < this.addrBook.contacts.length) ? index : index-1;
     
                                 // 如果contract被刪除完葛超,則設(shè)置me為選中態(tài)
                                 this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me;
                             })
                     }
                 }
     
             }
         }
     }

其中關(guān)于@ObjectLink和@Link的區(qū)別要注意以下幾點:

  1. 在AddressBookView中實現(xiàn)和父組件PageView的雙向同步,需要用@ObjectLink me : Person和@ObjectLink contacts : ObservedArray<Person>延塑,而不能用@Link绣张,原因如下:
    - @Link需要和其數(shù)據(jù)源類型完全相同,且僅能觀察到第一層的變化关带;
    - @ObjectLink可以被數(shù)據(jù)源的屬性初始化侥涵,且代理了@Observed裝飾類的屬性,可以觀察到被裝飾類屬性的變化宋雏。
  2. 當 聯(lián)系人姓名 (Person.name) 或者首選電話號碼 (Person.phones[0]) 發(fā)生更新時芜飘,PersonView也需要同步刷新,其中Person.phones[0]屬于第二層的更新磨总,如果使用@Link將無法觀察到嗦明,而且@Link需要和其數(shù)據(jù)源類型完全相同。所以在PersonView中也需要使用@ObjectLink蚪燕,即@ObjectLink person : Person和@ObjectLink phones : ObservedArray<string>娶牌。

在這個例子中,我們可以大概了解到如何構(gòu)建ViewModel馆纳,在應(yīng)用的根節(jié)點中诗良,ViewModel的數(shù)據(jù)可能是可以巨大的嵌套數(shù)據(jù),但是在ViewModel和View的適配和渲染中厕诡,我們盡可能將ViewModel的數(shù)據(jù)項和View相適配累榜,這樣的話在針對每一層的View,都是一個相對“扁平”的數(shù)據(jù)灵嫌,僅觀察當前層就可以了壹罚。

在應(yīng)用實際開發(fā)中,也許我們無法避免去構(gòu)建一個十分龐大的Model寿羞,但是我們可以在UI樹狀結(jié)構(gòu)中合理地去拆分數(shù)據(jù)猖凛,使得ViewModel和View更好的適配,從而搭配最小化更新來實現(xiàn)高性能開發(fā)绪穆。

完整應(yīng)用代碼如下:

// ViewModel classes
let nextId = 0;

@Observed
export class ObservedArray<T> extends Array<T> {
  constructor(args: T[]) {
    console.log(`ObservedArray: ${JSON.stringify(args)} `)
    if (args instanceof Array) {
      super(...args);
    } else {
      super(args)
    }
  }
}

@Observed
export class Address {
  street: string;
  zip: number;
  city: string;

  constructor(street: string,
              zip: number,
              city: string) {
    this.street = street;
    this.zip = zip;
    this.city = city;
  }
}

@Observed
export class Person {
  id_: string;
  name: string;
  address: Address;
  phones: ObservedArray<string>;

  constructor(name: string,
              street: string,
              zip: number,
              city: string,
              phones: string[]) {
    this.id_ = `${nextId}`;
    nextId++;
    this.name = name;
    this.address = new Address(street, zip, city);
    this.phones = new ObservedArray<string>(phones);
  }
}

export class AddressBook {
  me: Person;
  contacts: ObservedArray<Person>;

  constructor(me: Person, contacts: Person[]) {
    this.me = me;
    this.contacts = new ObservedArray<Person>(contacts);
  }
}

// 渲染出Person對象的名稱和Observed數(shù)組<string>中的第一個號碼
// 為了更新電話號碼辨泳,這里需要@ObjectLink person和@ObjectLink phones虱岂,
// 不能使用this.person.phones,內(nèi)部數(shù)組的更改不會被觀察到菠红。
// 在AddressBookView第岖、PersonEditView中的onClick更新selectedPerson
@Component
struct PersonView {
  @ObjectLink person: Person;
  @ObjectLink phones: ObservedArray<string>;
  @Link selectedPerson: Person;

  build() { 
    Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
      Text(this.person.name)
      if (this.phones.length) {
        Text(this.phones[0])
      }
    }
    .height(55)
    .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff")
    .onClick(() => {
      this.selectedPerson = this.person;
    })
  }
}

@Component
struct phonesNumber {
  @ObjectLink phoneNumber: ObservedArray<string>

  build() {
    Column() {

      ForEach(this.phoneNumber,
        (phone: ResourceStr, index?: number) => {
          TextInput({ text: phone })
            .width(150)
            .onChange((value) => {
              console.log(`${index}. ${value} value has changed`)
              this.phoneNumber[index!] = value;
            })
        },
        (phone: ResourceStr, index: number) => `${this.phoneNumber[index] + index}`
      )
    }
  }
}


// 渲染Person的詳細信息
// @Prop裝飾的變量從父組件AddressBookView深拷貝數(shù)據(jù),將變化保留在本地, TextInput的變化只會在本地副本上進行修改试溯。
// 點擊 "Save Changes" 會將所有數(shù)據(jù)的復(fù)制通過@Prop到@Link, 同步到其他組件
@Component
struct PersonEditView {
  @Consume addrBook: AddressBook;
  /* 指向父組件selectedPerson的引用 */
  @Link selectedPerson: Person;
  /*在本地副本上編輯蔑滓,直到點擊保存*/
  @Prop name: string = "";
  @Prop address: Address = new Address("", 0, "");
  @Prop phones: ObservedArray<string> = [];

  selectedPersonIndex(): number {
    return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_);
  }

  build() {
    Column() {
      TextInput({ text: this.name })
        .onChange((value) => {
          this.name = value;
        })
      TextInput({ text: this.address.street })
        .onChange((value) => {
          this.address.street = value;
        })

      TextInput({ text: this.address.city })
        .onChange((value) => {
          this.address.city = value;
        })

      TextInput({ text: this.address.zip.toString() })
        .onChange((value) => {
          const result = Number.parseInt(value);
          this.address.zip = Number.isNaN(result) ? 0 : result;
        })

      if (this.phones.length > 0) {
        phonesNumber({ phoneNumber: this.phones })
      }

      Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) {
        Text("Save Changes")
          .onClick(() => {
            // 將本地副本更新的值賦值給指向父組件selectedPerson的引用
            // 避免創(chuàng)建新對象,在現(xiàn)有屬性上進行修改
            this.selectedPerson.name = this.name;
            this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city)
            this.phones.forEach((phone: string, index: number) => {
              this.selectedPerson.phones[index] = phone
            });
          })
        if (this.selectedPersonIndex() != -1) {
          Text("Delete Contact")
            .onClick(() => {
              let index = this.selectedPersonIndex();
              console.log(`delete contact at index ${index}`);

              // 刪除當前聯(lián)系人
              this.addrBook.contacts.splice(index, 1);

              // 刪除當前selectedPerson遇绞,選中態(tài)前移一位
              index = (index < this.addrBook.contacts.length) ? index : index - 1;

              // 如果contract被刪除完键袱,則設(shè)置me為選中態(tài)
              this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me;
            })
        }
      }

    }
  }
}

@Component
struct AddressBookView {
  @ObjectLink me: Person;
  @ObjectLink contacts: ObservedArray<Person>;
  @State selectedPerson: Person = new Person("", "", 0, "", []);

  aboutToAppear() {
    this.selectedPerson = this.me;
  }

  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) {
      Text("Me:")
      PersonView({
        person: this.me,
        phones: this.me.phones,
        selectedPerson: this.selectedPerson
      })

      Divider().height(8)

      ForEach(this.contacts, (contact: Person) => {
        PersonView({
          person: contact,
          phones: contact.phones as ObservedArray<string>,
          selectedPerson: this.selectedPerson
        })
      },
        (contact: Person): string => {
          return contact.id_;
        }
      )

      Divider().height(8)

      Text("Edit:")
      PersonEditView({
        selectedPerson: this.selectedPerson,
        name: this.selectedPerson.name,
        address: this.selectedPerson.address,
        phones: this.selectedPerson.phones
      })
    }
    .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5)
  }
}

@Entry
@Component
struct PageEntry {
  @Provide addrBook: AddressBook = new AddressBook(
    new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]),
    [
      new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["11*********", "12*********"]),
      new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["13*********", "14*********"]),
      new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["15*********", "168*********"]),
    ]);

  build() {
    Column() {
      AddressBookView({
        me: this.addrBook.me,
        contacts: this.addrBook.contacts,
        selectedPerson: this.addrBook.me
      })
    }
  }
}

寫在最后

  • 如果你覺得這篇內(nèi)容對你還蠻有幫助,我想邀請你幫我三個小忙:
  • 點贊摹闽,轉(zhuǎn)發(fā)蹄咖,有你們的 『點贊和評論』,才是我創(chuàng)造的動力付鹿。
  • 關(guān)注小編澜汤,同時可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識倘屹。
  • 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識點银亲,請移步前往小編:https://gitee.com/MNxiaona/733GH/blob/master/jianshu
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纽匙,隨后出現(xiàn)的幾起案子务蝠,更是在濱河造成了極大的恐慌,老刑警劉巖烛缔,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馏段,死亡現(xiàn)場離奇詭異,居然都是意外死亡践瓷,警方通過查閱死者的電腦和手機院喜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晕翠,“玉大人喷舀,你說我怎么就攤上這事×苌觯” “怎么了硫麻?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長樊卓。 經(jīng)常有香客問我拿愧,道長,這世上最難降的妖魔是什么碌尔? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任浇辜,我火速辦了婚禮券敌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘柳洋。我一直安慰自己待诅,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布熊镣。 她就那樣靜靜地躺著咱士,像睡著了一般。 火紅的嫁衣襯著肌膚如雪轧钓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天锐膜,我揣著相機與錄音毕箍,去河邊找鬼。 笑死道盏,一個胖子當著我的面吹牛而柑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荷逞,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼媒咳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了种远?” 一聲冷哼從身側(cè)響起涩澡,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坠敷,沒想到半個月后妙同,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡膝迎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年粥帚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片限次。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡芒涡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卖漫,到底是詐尸還是另有隱情费尽,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布懊亡,位于F島的核電站依啰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏店枣。R本人自食惡果不足惜速警,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一叹誉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闷旧,春花似錦长豁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至该园,卻和暖如春酸舍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背里初。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工啃勉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人双妨。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓淮阐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刁品。 傳聞我的和親對象是個殘疾皇子泣特,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容