鸿蒙NEXT 实战开发之 如何优雅修改深层对象数据 和 对象数组数据让页面也能够同步刷新

899 阅读7分钟

前言

hello 大家好我是无言,最近也参与了不少鸿蒙项目的实际开发,中间也遇到了一些问题,今天来讲一讲鸿蒙中数据的处理过程。主要原因还是鸿蒙的装饰器@State 仅能观察到第一层的变化,但是我们实际开发过程中数据的复杂程度肯定远远不止一层数据。当然官方也提供了对应的解决方案@Observed和 @ObjectLink装饰器用来监听嵌套类对象属性变化。我觉得官方的解决方案其实有点繁琐,本文也会提供其他方案来实现。

目的

通过本篇文章小伙伴们能学到什么?我简单的总结了一下大概有以下几点。

正文

一、我们先来一个看起来非常奇怪的例子。

本例中数据模拟的是一个文章详情数据




interface levelType{
  level:number
  name?:string
}

interface userInfo{
  name:string
  age:number
  levelInfo:levelType
}

interface objType{
  title?:string;
  view?:number;
  user?:userInfo
}

@Entry
@Component
struct IndexPage {
  @State dataObj:objType = {  }

  aboutToAppear(): void {
    setTimeout(()=>{
      this.dataObj={
        title:'文章标题',
        view:1,//阅读数量
        user: {
          name: '小明', //作者
          age: 28, //年龄
          levelInfo: {
            //会员等级信息
            level: 1,
            name: '黄金会员'
          }
        }
      }
    },500)
  }
  build() {
    Flex({justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
      Column(){
          Text(this.dataObj?.title).width('100%').textAlign(TextAlign.Center)
          Text(this.dataObj?.user?.name).width('100%').textAlign(TextAlign.Center)
          Text(this.dataObj?.user?.levelInfo.name).width('100%').textAlign(TextAlign.Center)
          Button('单独修改标题')
          .onClick(()=>{
            this.dataObj.title='单独标题修改成功'
          })
            .margin({top:20})
        Button('单独修改昵称')
          .onClick(()=>{
            if(this.dataObj.user) {
              this.dataObj.user.name = '单独昵称修改成功'
            }
          })
          .margin({top:20})
        Button('单独修改昵称+修改第一层未使用的数据')
          .onClick(()=>{
            if(this.dataObj?.view){
              this.dataObj.view++
            }
            if(this.dataObj.user) {
              this.dataObj.user.name = '单独昵称修改成功'+this.dataObj.view
            }
          })
          .margin({top:20})
          Button('同时修改标题和昵称以及会员等级')
            .onClick(()=>{
              this.dataObj.title='同时标题修改成功'
              if(this.dataObj.user) {
                 this.dataObj.user.name = '同时昵称修改成功'
                 this.dataObj.user.levelInfo.name = '同时修改会员名称成功'
              }
            })
            .margin({top:20})

      }

    }.width('100%')
    .height('100%')


  }
}

我们来看一看实际运行效果

tutieshi_358x736_8s.gif

由此我们可以得出一个结论:

  • 修改对象的第一层数据,肯定会触发页面重新渲染。
  • 单独修改对象的第二层信息,是无法触发页面重新渲染的。
  • 当我们修改第二层第三或者 第N层对象数据的同时修改对象的第一层信息是可以触发页面重新渲染的,即使修改的第一层对象数据没有被页面使用。
二、用 @Observed装饰器和@ObjectLink装饰器 监听多层对象

我们先来看看官方的定义

  • @Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法

  • 子组件中@ObjectLink装饰的从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类会将自己注册给@Observed class。

  • 属性更新:当@Observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的@ObjectLink包装类,通知数据更新。

为了方便直观比较,我们修改一下上面上面内容



interface levelType{
  level:number
  name?:string
}


interface userInfo{
  name:string
  age:number
  levelInfo:levelType
}

interface objType{
  title?:string;
  view?:number;
  user?:userInfo
}


@Observed
class levelTypeObserved {
  public level: number;
  public name?: string;

  constructor(levelType:levelType) {
    this.level = levelType.level;
    this.name = levelType.name;
  }
}

@Observed
class userInfoObserved {
  public levelInfo: levelType;
  public name: string;
  public age: number;

  constructor(userInfo:userInfo) {
    this.levelInfo = userInfo.levelInfo;
    this.name = userInfo.name;
    this.age = userInfo.age;
  }
}


@Observed
class objTypeObserved {
  public user?: userInfo;
  public title?: string;
  public view?: number;

  constructor(objType?:objType) {
    this.user = objType?.user;
    this.title = objType?.title;
    this.view = objType?.view;
  }
}


// 子组件
@Component
struct levelInfoCom {
  @ObjectLink levelInfo: levelTypeObserved;
  build() {
    Column() {
      Text('子组件等级'+this.levelInfo.name).width('100%').textAlign(TextAlign.Center)
      Button('单独修改等级')
        .onClick(()=>{
          if(this.levelInfo) {
            this.levelInfo.name = '单独等级修改成功'
          }
        })
    }
  }
}

@Component
struct userInfoCom {
  @ObjectLink user: userInfoObserved;
  build() {
    Column() {
      Text('子组件昵称:'+this.user.name).width('100%').textAlign(TextAlign.Center)
      Button('单独修改昵称')
        .onClick(()=>{
          if(this.user) {
            this.user.name = '单独昵称修改成功'
          }
        })
    }
  }
}



@Entry
@Component
struct IndexPage {
  @State dataObj:objTypeObserved =  new objTypeObserved();


  aboutToAppear(): void {
    setTimeout(()=>{
      const levelInfo:levelTypeObserved =new levelTypeObserved({level: 1, name: '黄金会员'})
      const user:userInfoObserved= new userInfoObserved({levelInfo,name:'小明',age:28})
      this.dataObj=new objTypeObserved({user,title:'文章标题',view:1})
    },500)
  }
  build() {
    Flex({justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
      Column(){
        Text('父组件等级:'+this.dataObj?.user?.levelInfo.name).width('100%').textAlign(TextAlign.Center)
        Text('父组件昵称:'+this.dataObj?.user?.name).width('100%').textAlign(TextAlign.Center)
        Button('父组件修改昵称')
          .onClick(()=>{
            if( this.dataObj.user){
              this.dataObj.user.name = '父组件单独昵称修改成功'
            }
          })
        //用户相关组件
        if(this.dataObj?.user){
          userInfoCom({user:this.dataObj.user}).margin({top:20})
        }
        // 等级相关组件

        if(this.dataObj.user?.levelInfo){
          levelInfoCom({levelInfo:this.dataObj.user?.levelInfo}).margin({top:20})
        }

      }

    }.width('100%')
    .height('100%')


  }
}

看看运行效果

tutieshi_354x724_7s.gif

由此我们可以得出一个结论:

  • 使用 @Observed 装饰包裹的对象修改可以触发页面更新。
  • 子组件修改对象属性会触发子组件更新,但是父组件不会更新。
  • 父组件修改对象属性也不会触发父组件自身更新,但是会触发子组件页面更新。
三、用 @Observed装饰器和@ObjectLink装饰器 监听对象数组

下面例子模拟的是一个文章列表信息


export  interface articleType {
 is_liked: boolean;
 id: number;
 votes: number;
 title: string;
}


@Observed  class CategoryCardItem {
 public id: number;
 public votes: number;
 public title: string;
 public is_liked: boolean;

 constructor(listItem: articleType) {
   this.id = listItem.id;
   this.votes = listItem.votes;
   this.is_liked = listItem.is_liked;
   this.title = listItem.title;
 }
}



@Component
struct ItemPage {
 @ObjectLink item: CategoryCardItem;
 build() {
   Column(){
     Row(){
       Text(this.item.title)
     }.width('100%')
     Row() {
       Text( this.item.is_liked?'已点赞':'未点赞')
         .fontColor(this.item.is_liked?'red':'#333')
         .onClick(()=> {
           this.item.is_liked=! this.item.is_liked
         })
       Text('阅读数量'+this.item?.votes)
         .margin({left:15})
         .onClick(() => {
           this.item.votes++;
         })
     }.width('100%')
     .margin({top:15})
   }.padding(16)
 }
}

@Entry
@Component
struct IndexPage {
 @State arr: Array<CategoryCardItem> = [];

 aboutToAppear(): void {
   setTimeout(()=>{
     const list:articleType[] =[{id:1,votes:5,is_liked:false,title:"这是文章标题文章标题这是文章标题文章标题"},{id:2,votes:5,is_liked:false,title:"这是文章标题文章标题这是文章标题文章标题"}]

     list.forEach(element => {
       this.arr.push(new CategoryCardItem(element))
     });
   },500)
 }

 build() {
   Flex({justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
   Column() {
     ForEach(this.arr,(item:CategoryCardItem)=>{
       ItemPage({ item: item})
     })
   }
   }

 }
}

看看运行效果

tutieshi_352x732_6s.gif

四、用一些前端的传统方法来实现

回到我们最初的例子,我们来看看如何修改多层对象数据,其实核心原理用到了第一个例子的相关原理。

为了方便快速处理,网上找了一个对象数组深克隆的方法后面会用到

export  function deepCopy<T>(obj: T): T {
  if (obj === null || typeof obj!== 'object') {
    return obj;
  }

  let copy: T;

  if (Array.isArray(obj)) {
    copy = [] as T;
    for (let i = 0; i < (obj as any[]).length; i++) {
      (copy as any[])[i] = deepCopy((obj as any[])[i]);
    }
  } else {
    copy = {} as T;
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        (copy as { [key: string]: any })[key] = deepCopy((obj as { [key: string]: any })[key]);
      }
    }
  }

  return copy;
}

修改一下最初的例子


import {deepCopy} from "../common/utils"

interface levelType{
  level:number
  name?:string
}


interface userInfo{
  name:string
  age:number
  levelInfo:levelType
}

interface objType{
  title?:string;
  view?:number;
  user?:userInfo
}


@Entry
@Component
struct IndexPage {
  @State dataObj:objType = {  }

  aboutToAppear(): void {
    setTimeout(()=>{
      this.dataObj={
        title:'文章标题',
        view:1,//阅读数量
        user: {
          name: '小明', //作者
          age: 28, //年龄
          levelInfo: {
            //会员等级信息
            level: 1,
            name: '黄金会员'
          }
        }
      }
    },500)
  }
  // 修改数据


  build() {
    Flex({justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
      Column(){
        Text(this.dataObj?.title).width('100%').textAlign(TextAlign.Center)
        Text(this.dataObj?.user?.name).width('100%').textAlign(TextAlign.Center)
        Text(this.dataObj?.user?.levelInfo.name).width('100%').textAlign(TextAlign.Center)
        Button('单独修改用户昵称')
          .onClick(()=>{
            if(this.dataObj.user){
              const user =deepCopy(this.dataObj.user)//克隆数据
              user.name='单独昵称修改成功'
              this.dataObj.user=user
            }
          })
        Button('单独修改等级')
          .onClick(()=>{
            if(this.dataObj.user){
              const user =deepCopy(this.dataObj.user)// 克隆数据
              user.levelInfo.name='单独修改等级成功'
              this.dataObj.user=user
            }
          }).margin({top:20})

      }

    }.width('100%')
    .height('100%')


  }
}

看看运行效果

tutieshi_352x728_6s.gif

重点:其实核心原理很简单就是利用了@State只能能观察到第一层的变化,所以我们就赋值修改第一层数据

再来看看数组对象的处理

export  interface articleType {
  is_liked: boolean;
  id: number;
  votes: number;
  title: string;
}


@Entry
@Component
struct IndexPage {
  @State arr: Array<articleType> = [];

  aboutToAppear(): void {
    setTimeout(()=>{
      const list:articleType[] =[{id:1,votes:5,is_liked:false,title:"这是文章标题文章标题这是文章标题文章标题"},{id:2,votes:5,is_liked:false,title:"这是文章标题文章标题这是文章标题文章标题"}]

      list.forEach(element => {
        this.arr.push(element)
      });
    },500)
  }
  changeItemliked(item:articleType,index:number){
    item.is_liked=!item.is_liked
    this.arr.splice(index,1, item)
  }

  changeItemVotes(item:articleType,index:number){
    item.votes++
    this.arr.splice(index,1, item)
  }

  build() {
    Flex({justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
    Column() {
      ForEach(this.arr,(item:articleType,index:number)=>{
        Column(){
          Row(){
            Text(item.title)
          }.width('100%')
          Row() {
            Text( item.is_liked?'已点赞':'未点赞')
              .fontColor(item.is_liked?'red':'#333')
              .onClick(()=> {
                 this.changeItemliked(item,index)
              })
            Text('阅读数量'+item?.votes)
              .margin({left:15})
              .onClick(() => {
                this.changeItemVotes(item,index)
              })
          }.width('100%')
          .margin({top:15})
        }.padding(16)
      }) //这里需要注意不要设置 key 如果设置为 Id 会无效,如果要设置就需要设置为你改变的值
    }
    }

  }
}

运行效果如下

tutieshi_352x732_6s.gif

重点: 主要运用到了 数组的 splice 方法 。缺陷 :这里需要注意ForEach 渲染组件 不能设置固定key例如Id 等

总结

本文是关于鸿蒙 NEXT 中处理深层对象数据和对象数组数据以实现页面同步刷新的实战开发分享。 核心重点是鸿蒙的@State 装饰器仅能观察到第一层数据变化,详细讲解了如何使用@Observed 装饰器和@ObjectLink 装饰器监听多层对象和对象数组的方法,本文还分享了一些前端的传统方法来处理此类问题,包括对象深克隆和利用数组的 splice 方法。