基于状态变量实现复杂对象的状态监听

场景一:简单对象监听

对象在我们开发过程中是很常见的数据类型,我们在进行UI渲染的时候经常会用到对象,这里提供简单对象(所有字段均为基本数据类型)的监听效果。

方案一:状态管理V1实现

简单对象可以直接使用@State观测,这里使用起来比较简单,但是需要注意的是如果某个Class在页面中渲染中使用的字段较多的时候,可以结合@Track修饰器来进行观测。

实现代码:

@Builder
export function SimpleObjectBuilder(name: string, param: Object) {
  SimpleObject()
}
 
class LogTrack {
  // 这里使用@Track可以达到最小化更新的效果
  @Track str1: string;
  @Track str2: string;
 
  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = 'World';
  }
}
 
class LogNotTrack {
  str1: string;
  str2: string;
 
  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = '世界';
  }
}
 
@Component
export struct SimpleObject {
  pathStack: NavPathStack = new NavPathStack()
  @State logTrack: LogTrack = new LogTrack('Hello');
  @State logNotTrack: LogNotTrack = new LogNotTrack('你好');
 
  isRender(index: number) {
    console.log(`Text ${index} is rendered`);
    return 50;
  }
 
  build() {
    NavDestination() {
      Column() {
        Column() {
          Text(this.logTrack.str1)// UINode1
            .fontSize(this.isRender(1))
            .fontWeight(FontWeight.Bold)
          Text(this.logTrack.str2)// UINode2
            .fontSize(this.isRender(2))
            .fontWeight(FontWeight.Bold)
          Button('change logTrack.str1')
            .onClick(() => {
              // 点击更新str1 字段,观察页面日志变化,会发现使用@Track修饰的变量只会对改变的变量做最小化更新
              this.logTrack.str1 = 'Bye';
            })
          Text(this.logNotTrack.str1)// UINode3
            .fontSize(this.isRender(3))
            .fontWeight(FontWeight.Bold)
          Text(this.logNotTrack.str2)// UINode4
            .fontSize(this.isRender(4))
            .fontWeight(FontWeight.Bold)
          Button('change logNotTrack.str1')
            .onClick(() => {
              // 点击更新str1字段,观察页面日志变化,会发现在不使用@Track修饰即使只改变了一个值,但是对象中所有的字段全都会触发UI更新那么值没有发生改变
              this.logNotTrack.str1 = '再见';
            })
        }
      }.width('100%').height('100%')
    }.onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

实现效果如下:

观察日志发现第一种在更新UI的时候只会对操作的UI进行重绘渲染,而第二种在更新UI的时候所有在页面中用到了的组件都会进行重绘,所以为了解释性能,我们更建议在Class中使用@Track对字段进行精准修饰。

方案二:状态管理V2实现

在状态管理V2中对于对象的观察只有一套框架@ObservedV2+@Trace,它们具备深度观测对象的能力,简单对象也同样可以观测。

需要注意的是使用@ObservedV2修饰的类,如果字段需要观测变化的时候需要对该字段使用@Trace进行修饰,否则UI不会刷新。

实现代码:

@Builder
export function SimpleObjectBuilderV2(name: string, param: Object) {
  SimpleObject()
}
 
@ObservedV2
class LogTrack {
  @Trace str1: string;
  str2: string;
 
  constructor(str1: string) {
    this.str1 = str1;
    this.str2 = 'World';
  }
}
 
@ComponentV2
struct SimpleObject {
  pathStack: NavPathStack = new NavPathStack()
  logTrack: LogTrack = new LogTrack('hello');
 
  isRender(index: number) {
    console.log(`Text ${index} is rendered`);
    return 50;
  }
 
  build() {
    NavDestination() {
      Column() {
        Text(this.logTrack.str1)// UINode1
          .fontSize(this.isRender(1))
          .fontWeight(FontWeight.Bold)
        Text(this.logTrack.str2)// UINode2
          .fontSize(this.isRender(2))
          .fontWeight(FontWeight.Bold)
        Button('change logTrack.str1')
          .onClick(() => {
            // 点击更新Str1和str2,str1使用了@Trace修饰,UI界面正常刷新,Str2没有修饰,UI界面不刷新
            this.logTrack.str1 = 'Hi';
            this.logTrack.str2 = '世界';
          })
      }
    }.onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

并且使用@Trace修饰的字段也具备最小化更新的能力,具体见下方日志:

另外还需要注意的是,被@ObservedV2与@Trace装饰的类对象实例,虽然具有深度观测对象属性的能力。但当对对象整体赋值时,UI却无法刷新。使用@Local装饰对象,可以达到观测对象本身变化的效果。

实现代码:

@ObservedV2
class Info {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
@Entry
@ComponentV2
struct Index {
  info1: Info = new Info('Tom', 25);
  @Local info2: Info = new Info('Tom', 25);
  build() {
    Column() {
      Text(`info1: ${this.info1.name}-${this.info1.age}`) // Text1
      Text(`info2: ${this.info2.name}-${this.info2.age}`) // Text2
      Button('change info1&info2')
        .onClick(() => {
          this.info1 = new Info('Lucy', 18); // Text1不会刷新
          this.info2 = new Info('Lucy', 18); // Text2会刷新
        })
    }
  }
}

场景二:复杂对象监听

复杂对象一般是指在对象字段中还存在其他对象(嵌套对象)的场景。

方案一:状态管理V1实现

@State装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套对象的情况,它们的第二层的属性变化是无法观察到的。我们先看数据模型类图:

在上面数据模型中,Class User的字段bag是Class Bag类型,Class Book的字段bookName是Class BookName类型,这种情况都是一个对象的字段中还含有一个对象,这一类数据我们叫复杂对象,这类数据在使用状态变量监听的时候会比较麻烦,@State/@Prop等修饰器只能监听一层,也就是对象下的基本数据类型字段,对于多次嵌套的对象,在深度监听的时候需要用@Observed/@ObjectLink装饰器来进行深度观测,请看下面实现;总结下面代码,我们不难发现在状态管理V1中一层组件永远只能监听一层对象,如果涉及到多层对象,我们就需要分子组件来进行监听同步。

@Builder
export function ComplexObjectBuilderV1(name: string, param: Object) {
  ComplexObject()
}
 
// objectLinkNestedObjects.ets
let NextID: number = 1;
 
// 这里的对象我们都需要深度监听,Class类先使用@Observed修饰
@Observed
class Bag {
  public id: number;
  public size: number;
 
  constructor(size: number) {
    this.id = NextID++;
    this.size = size;
  }
}
 
@Observed
class User {
  public bag: Bag;
 
  constructor(bag: Bag) {
    this.bag = bag;
  }
}
 
@Observed
class Book {
  public bookName: BookName;
 
  constructor(bookName: BookName) {
    this.bookName = bookName;
  }
}
 
@Observed
class BookName extends Bag {
  public nameSize: number;
 
  constructor(nameSize: number) {
    // 调用父类方法对nameSize进行处理
    super(nameSize);
    this.nameSize = nameSize;
  }
}
 
@Component
export struct ComplexObject {
  pathStack: NavPathStack = new NavPathStack()
  // 初始化状态变量,因为@ObjectLink修饰器的入参类型必须也是状态变量,所以我们第一层使用@State修饰
  @State user: User = new User(new Bag(0));
  @State child: Book = new Book(new BookName(0));
 
  build() {
    NavDestination() {
      Column() {
        // @ObjectLink是子组件与父组件双向同步,对于这类复杂对象无法做到在一个组件中实现深度监听,需要分层级来实现,嵌套多少层就需要分多少层组件
        ViewA({ label: 'ViewA #1', bag: this.user.bag })
          .width(320)
        ViewC({ label: 'ViewC #3', bookName: this.child.bookName })
          .width(320)
        Button(`ViewC: this.child.bookName.size add 10`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // 使用@State修饰并且子组件使用了@ObjectLink修饰,因此能达到父组件状态子组件同步的效果,在父组件更新可以生效,如果子组件没有使用@ObjectLink修饰,则不生效
            this.child.bookName.size += 10
            console.log('this.child.bookName.size:' + this.child.bookName.size)
          })
        Button(`ViewB: this.user.bag = new Bag(10)`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // 直接替换user.bag,此时相当于是bag是整个值替换,因为@State能够监听一层,所以UI会改变,这里的原理与简单对象监听原理一致
            this.user.bag = new Bag(10);
          })
        Button(`ViewB: this.user = new User(new Bag(20))`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // 直接替换整个user使用@State能够监听到user的变化,这里就相当于基本数据类型使用@State监听原理一致
            this.user = new User(new Bag(20));
          })
 
      }.width('100%').height('100%')
    }.onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}
 
@Component
struct ViewA {
  label: string = 'ViewA';
  // 接收父级对象的Bag对象
  @ObjectLink bag: Bag;
 
  build() {
    Column() {
      Text(`ViewC [${this.label}] this.bag.size = ${this.bag.size}`)
        .fontColor('#ffffffff')
        .backgroundColor('#ff3d9dba')
        .width(320)
        .height(50)
        .margin(10)
        .textAlign(TextAlign.Center)
      Button(`ViewA: this.bag.size add 1`)
        .width(320)
        .backgroundColor('#ff17a98d')
        .margin(10)
        .onClick(() => {
          // 更新子组件bag的字段,因为使用了@ObjeckLink修饰父组件传入的对象,可以达到父子组件同步的效果,因此UI会刷新
          this.bag.size += 1;
        })
    }
  }
}
 
@Component
struct ViewC {
  label: string = 'ViewC1';
  @ObjectLink bookName: BookName;
 
  build() {
    Row() {
      Column() {
        Text(`ViewC [${this.label}] this.bookName.size = ${this.bookName.size}`)
          .fontColor('#ffffffff')
          .backgroundColor('#ff3d9dba')
          .width(320)
          .height(50)
          .margin(10)
          .textAlign(TextAlign.Center)
        Button(`ViewC: this.bookName.size add 1`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // 与上面同理,因为使用了@ObjectLink修饰,能达到父子组件同步的效果,因此子组件状态变化UI会刷新
            this.bookName.size += 1;
            console.log('this.bookName.size:' + this.bookName.size)
          })
      }
      .width(320)
    }
  }
}

实现效果图如下:

方案二:状态管理V2实现

同样场景下,类图见状态管理V1,在状态管理V2中实现起来就要方便的多,我们在场景一中介绍了,状态管理V2对于对象的监听只有@ObservedV2+@Trace修饰器,复杂对象我们也是使用这个修饰器,具体实现见下面代码。

在使用状态管理V2进行观测的时候,我们无须考虑当前对象是否是多层嵌套对象,需要需要分子组件来进行观测,只需要关注Class中的字段是否需要展示在UI中即可。

@Builder
export function ComplexObjectBuilderV2(name: string, param: Object) {
  ComplexObject()
}
 
let NextID: number = 1;
 
@ObservedV2
class Bag {
  public id: number;
  // 在UI中展示的字段需要使用@Trace修饰,否则无法观测,UI无法刷线
  @Trace public size: number;
 
  constructor(size: number) {
    this.id = NextID++;
    this.size = size;
  }
}
 
@ObservedV2
class User {
  @Trace public bag: Bag;
 
  constructor(bag: Bag) {
    this.bag = bag;
  }
}
 
@ObservedV2
class Book {
  @Trace public bookName: BookName;
 
  constructor(bookName: BookName) {
    this.bookName = bookName;
  }
}
 
@ObservedV2
class BookName extends Bag {
  public nameSize: number;
 
  constructor(nameSize: number) {
    // 调用父类方法对nameSize进行处理
    super(nameSize);
    this.nameSize = nameSize;
  }
}
 
@ComponentV2
export struct ComplexObject {
  pathStack: NavPathStack = new NavPathStack()
  // 初始化状态变量,在简单对象修饰中讲解过了,@ObservedV2+@Trace能实现对象的深度观测,但是当自身被替换的时候无法观测到,我们需要使用@Local观测
  @Local user: User = new User(new Bag(0));
  @Local child: Book = new Book(new BookName(0));
 
  build() {
    NavDestination() {
      Column() {
        Column() {
          Text(`this.user.bag.size = ${this.user.bag.size}`)
            .fontColor('#ffffffff')
            .backgroundColor('#ff3d9dba')
            .width(320)
            .height(50)
            .margin(10)
            .textAlign(TextAlign.Center)
          Button(`ViewA: this.bag.size add 1`)
            .width(320)
            .backgroundColor('#ff17a98d')
            .margin(10)
            .onClick(() => {
              // @Trace修饰的字段具备深度观测能力,可以直接修改
              this.user.bag.size += 1;
            })
        }.width(320)
 
        Column() {
          Text(`this.child.bookName.size = ${this.child.bookName.size}`)
            .fontColor('#ffffffff')
            .backgroundColor('#ff3d9dba')
            .width(320)
            .height(50)
            .margin(10)
            .textAlign(TextAlign.Center)
          Button(`ViewC: this.bookName.size add 1`)
            .width(320)
            .backgroundColor('#ff17a98d')
            .margin(10)
            .onClick(() => {
              // @Trace修饰的字段具备深度观测能力,可以直接修改
              this.child.bookName.size += 1;
              console.log('this.bookName.size:' + this.child.bookName.size)
            })
        }.width(320)
 
        Button(`ViewC: this.child.bookName.size add 10`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // @Trace修饰的字段具备深度观测能力,可以直接修改
            this.child.bookName.size += 10
            console.log('this.child.bookName.size:' + this.child.bookName.size)
          })
        Button(`ViewB: this.user.bag = new Bag(10)`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // @Trace修饰的字段具备深度观测能力,可以直接修改
            this.user.bag = new Bag(10);
          })
        Button(`ViewB: this.user = new User(new Bag(20))`)
          .width(320)
          .backgroundColor('#ff17a98d')
          .margin(10)
          .onClick(() => {
            // @Trace修饰的字段具备深度观测能力,可以直接修改
            this.user = new User(new Bag(20));
          })
      }.width('100%').height('100%')
    }.onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

实现效果与状态管理V1一致。

场景三:对象数组监听

对象数组是指该数组的每一项元素都是对象的场景,该数据类型属于我们开发中最场景的一种场景,下面实现一个双层对象数据的场景示例,具体数据模型见下图。

AllList{}-->Outer[]--->Inner[]

方案一:状态管理V1实现

在场景二中我们介绍了,状态管理V1在一层组件中只能监听对象下面的一层字段,因此这里需要分多层子组件来实现,具体结构是与数据模型保持一致,组件关系与数据模型对应图示如下:

数据分层代码如下:

前面介绍了,因为无法观测多层对象的变化,因此我们只能将数据拆分成一层一层的来观测。

// model.ets
/**
 * 列表数据结构模型
 */
export class AllList {
  // 列表总数据源
  items: Outer[] = []
  // 内层列表勾选状态下输入总金额
  allAmount: string = ''
 
  constructor(items: Outer[], allAmount: string) {
    this.items = items
    this.allAmount = allAmount
  }
}
 
@Observed
export class Outer {
  // 外层label  类似于视频中的居民、企事业
  public label: string = ''
  //外层label展开状态,点击显示里层inners,再次点击隐藏inners
  public expand: boolean = false
  //内部数据源  类似视频中的每个用点户号
  public inners: Inner[] = []
 
  constructor(label: string, inners: Inner[], expand?: boolean) {
    this.label = label
    this.inners = inners
    this.expand = expand ? expand : false
  }
}
 
@Observed
export class Inner {
  public id: string
  // 户号名称
  public innerName: string = ''
  // 当前户号的勾选状态
  public checkStatus: boolean = false
  // 推荐金额 类似于视频重的按钮金额  ¥20/¥50/¥100/¥200
  public btnMoney: string = '0'
  // 输入框输入的金额  只能是数字、两位小数
  public inputContent: string = ''
 
  constructor(innerName: string) {
    this.innerName = innerName
    this.id = Math.random().toString()
  }
}

UI实现

然后在页面中使用的时候也是一层组件对应一层数据结构。

// index.ets
import { Inner, Outer } from '../model/Model'
import { OuterComponent } from './components/ListComponent'
 
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'allAmount': 0 };
let storage: LocalStorage = new LocalStorage(para);
 
@Entry(storage)
@Component
struct Index {
  // 首页Index对应AllList,这里没有直接使用上面的AllList对象,而是直接使用状态变量将两个字段分别观测了
  @Provide AllList: Outer[] = []
  @LocalStorageLink('allAmount') allAmount: number = 0
 
  aboutToAppear(): void {
    // 初始化数据
    this.AllList = [
      new Outer('居民', [new Inner('光谷理想城x栋x单元xxx'), new Inner('保利时代x栋x单元xxx'), new Inner('南湖名都x栋x单元xxx')]),
      new Outer('企事业', [new Inner('光谷软件园x栋x楼xxxx'), new Inner('光谷金融港x栋x楼xxx'), new Inner('光谷智慧园x栋x楼xxx')])
    ]
  }
 
  build() {
    Column() {
      ...
      List() {
        ForEach(this.AllList, (outer: Outer) => {
          if (outer.inners.length) {
            // 遍历数组将对应的Outer对象传递给OuterComponent子组件,在子组件完成Outer对象的观测并同步给父组件
            OuterComponent({ outer: outer })
          }
        }, (item: Outer) => item.label)
      }
      ...
    }.backgroundColor('#e9ecef').width('100%').height('100%')
  }
}

// ListComponent.ets
import { Inner, Outer } from '../../model/Model'
 
@Component
export struct OuterComponent {
  // 接收Index传进来的Outer对象,并使用@ObjectLink观测实现父子组件双向同步
  @ObjectLink outer: Outer
  @State arrowImg: Resource = $r('sys.media.ohos_ic_public_arrow_up')
  ...
  // 只展示核心代码,完整demo见最后附件
  build() {
    ListItemGroup({ header: this.itemHead(this.outer.label), style: ListItemGroupStyle.NONE }) {
      ForEach(this.outer.inners, (inner: Inner, index: number) => {
        ListItem({ style: ListItemStyle.NONE }) {
          // 因为当前的Outer对象中还存在Inner对象数组,因此继续分层
          InnerComponent({ inner: inner })
        }
      }, (inner: Inner) => inner.innerName)
    }.backgroundColor('#e9ecef')
    .margin({ top: 16 })
  }
}
 
@Component
struct InnerComponent {
  // 接受OuterComponent传进来的Inner对象,并使用@ObjectLink观测实现父子组件双向同步
  @ObjectLink inner: Inner
 
  build() {
    ...
    // 只展示核心代码,完整demo见最后附件
  }
}

方案二:状态管理V2实现

这种方式对比方案一,无需对组件分层,只用在需要UI展示的字段上使用@Trace修饰器修饰即可,具体实现如下:

/* 
 列表数据结构模型
 */
@ObservedV2
class AllList {
  // 列表总数据源
  @Trace items: Outer[] = []
  // 内层列表勾选状态下输入总金额
  @Trace allAmount: string = ''
 
  constructor(items: Outer[], allAmount: string) {
    this.items = items
    this.allAmount = allAmount
  }
}
 
@ObservedV2
class Outer {
  // 外层label  类似于视频中的居民、企事业
  @Trace label: string = ''
  //外层label展开状态,点击显示里层inners,再次点击隐藏inners
  @Trace expand: boolean = false
  //内部数据源  类似视频中的每个用点户号
  @Trace inners: Inner[] = []
 
  constructor(label: string, inners: Inner[], expand?: boolean) {
    this.label = label
    this.inners = inners
    this.expand = expand ? expand : false
  }
}
 
@ObservedV2
class Inner {
  @Trace id: string
  // 户号名称
  @Trace innerName: string = ''
  // 当前户号的勾选状态
  @Trace checkStatus: boolean = false
  // 推荐金额 类似于视频重的按钮金额  ¥20/¥50/¥100/¥200
  @Trace btnMoney: string = '0'
  // 输入框输入的金额  只能是数字、两位小数
  @Trace inputContent: string = ''
 
  constructor(innerName: string) {
    this.innerName = innerName
    this.id = Math.random().toString()
  }
}
 
@Entry
@Component
struct IndexV2 {
  // 数据初始化
  AllList: AllList = new AllList([
    new Outer('居民',
      [new Inner('xx小区x栋x单元xxx'), new Inner('xx小区x栋x单元xxx'), new Inner('xx小区x栋x单元xxx')]),
    new Outer('企事业',
      [new Inner('xx小区x栋x单元xxx'), new Inner('xx小区栋x单元xxx'), new Inner('xx小区x栋x单元xxx')])
  ], '0')
 
  // ...
  // 只展示数据监听核心代码,详细实现请看文章结尾附件
 
  build() {
    Column() {
      List() {
        // 因为@ObservedV2具备深入观测能力,使用时直接使用数据对象往下.即可
        ForEach(this.AllList.items, (outer: Outer) => {
          ListItemGroup({ header: this.itemHead(outer), style: ListItemGroupStyle.NONE }) {
            ForEach(outer.inners, (inner: Inner, index: number) => {
              ListItem({ style: ListItemStyle.NONE }) {
                // ...
                // 只展示数据监听核心代码,详细实现请看文章结尾附件
              }
            }, (inner: Inner) => inner.innerName)
          }.backgroundColor('#e9ecef')
          .margin({ top: 16 })
        }, (item: Outer) => item.label)
      }
      .width('90%').height('100%')
    }.backgroundColor('#e9ecef').width('100%').height('100%')
  }
}

汇总对比

场景

状态管理V1

状态管理V2

简单对象

使用@State观测,在字段过多时建议结合@Track做最小化观测

使用@ObservedV2+@Trace观测,需要UI展示的字段使用@Trace修饰,同时如果涉及到整个对象替换的时候需要在组件中使用@Local修饰

复杂对象

使用@Observed+@ObjectLink观测,但是对应多层数据需要数据分层的同时还需要组件分层,通过这种方式虽然能够实现对嵌套类中属性变化的观测,但是当嵌套层级较深时,代码将会变得十分复杂,易用性差

@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力,对比V1的实现更符合开发逻辑,易用性与代码可读性都更好

以上是针对上面三个场景对V1与V2做到一个对比,下面针对V1与V2的各类装饰器做一个完整对比:

 

状态管理V1

状态管理V2

对比

组件内状态

@State

@Local

状态管理V1使用@State定义类中的状态变量,@State装饰器能够从外部初始化,因此@State无法准确表达组件内部状态不能被外面修改的语义,而状态管理V2中的@Local无法从外传入初始化,因此能更准确的表达组件的内部状态

组件外部输入

@State/@Prop/@Link/@ObjectLink

@Param

状态管理V1存在多种可接受外部传入的装饰器,常用的有@State、@Prop、@Link、@ObjectLink。这些装饰器使用各有限制,不易区分,当使用不当时,还会导致性能问题。而状态管理V2只有@Param装饰器表示组件从外部传入的状态,并且可以实现与父组件@Local修饰的变量进行同步

状态变量修改监听

@Watch

@Monitor

@Watch无法实现对对象、数组中某一单个属性或数组项变化的监听,且无法获取变化之前的值。@Monitor装饰器实现对对象、数组中某一单个属性或数组项变化的监听,并且能够获取到变化之前的值

更多资料,请参考文档:状态管理V2

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值