HarmonyOS NEXT应用开发之@Observed装饰器和\@ObjectLink装饰器:嵌套类对象属性变化

上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。

说明:

从API version 9开始,这两个装饰器支持在ArkTS卡片中使用。

概述

@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:

  • 被@Observed装饰的类,可以被观察到属性的变化;

  • 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。

  • 单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者 @Prop 使用。

限制条件

  • 使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。

  • @ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。

装饰器说明

@Observed类装饰器说明
装饰器参数
类装饰器装饰class。需要放在class的定义前,使用new创建类对象。
@ObjectLink变量装饰器说明
装饰器参数
允许装饰的变量类型必须为被@Observed装饰的class实例,必须指定类型。
不支持简单类型,可以使用 @Prop 。
支持继承Date、Array的class实例,API11及以上支持继承Map、Set的class实例。示例见 观察变化 。
API11及以上支持@Observed装饰类和undefined或null组成的联合类型,比如ClassA | ClassB, ClassA | undefined 或者 ClassA | null, 示例见 @ObjectLink支持联合类型 。
@ObjectLink的属性是可以改变的,但是变量的分配是不允许的,也就是说这个装饰器装饰变量是只读的,不能被改变。
被装饰变量的初始值不允许。

@ObjectLink装饰的数据为可读示例。

// 允许@ObjectLink装饰的数据属性赋值
this.objLink.a= ...
// 不允许@ObjectLink装饰的数据自身赋值
this.objLink= ...

说明:

@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用 @Prop 。

  • @Prop装饰的变量和数据源的关系是是单向同步,@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,@Prop装饰的变量本地的修改将被覆盖;

  • @ObjectLink装饰的变量和数据源的关系是双向同步,@ObjectLink装饰的变量相当于指向数据源的指针。禁止对@ObjectLink装饰的变量赋值,如果一旦发生@ObjectLink装饰的变量的赋值,则同步链将被打断。因为@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向数据同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TypeScript/JavaScript不能实现,会发生运行时报错。

变量的传递/访问规则说明

@ObjectLink传递/访问说明
从父组件初始化必须指定。
初始化@ObjectLink装饰的变量必须同时满足以下场景:
- 类型必须是@Observed装饰的class。
- 初始化的数值需要是数组项,或者class的属性。
- 同步源的class或者数组必须是@State,@Link,@Provide,@Consume或者@ObjectLink装饰的数据。
同步源是数组项的示例请参考 对象数组 。初始化的class的示例请参考 嵌套对象 。
与源对象同步双向。
可以初始化子组件允许,可用于初始化常规变量、@State、@Link、@Prop、@Provide

图1 初始化规则图示

image.png

观察变化和行为表现

观察变化

@Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被@Observed装饰,否则将观察不到其属性的变化。

class ClassA {public c: number;constructor(c: number) {this.c = c;}
}@Observed
class ClassB {public a: ClassA;public b: number;constructor(a: ClassA, b: number) {this.a = a;this.b = b;}
}

以上示例中,ClassB被@Observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于ClassA,没有被@Observed装饰,其属性的修改不能被观察到。

@ObjectLink b: ClassB// 赋值变化可以被观察到
this.b.a = new ClassA(5)
this.b.b = 5// ClassA没有被@Observed装饰,其属性的变化观察不到
this.b.a.c = 5

@ObjectLink:@ObjectLink只能接收被@Observed装饰class的实例,可以观察到:

  • 其属性的数值的变化,其中属性是指Object.keys(observedObject)返回的所有属性,示例请参考 嵌套对象 。

  • 如果数据源是数组,则可以观察到数组item的替换,如果数据源是class,可观察到class的属性的变化,示例请参考 对象数组 。

继承Date的class时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。

@Observed
class DateClass extends Date {constructor(args: number | string) {super(args)}
}@Observed
class ClassB {public a: DateClass;constructor(a: DateClass) {this.a = a;}
}@Component
struct ViewA {label: string = 'date';@ObjectLink a: DateClass;build() {Column() {Button(`child increase the day by 1`).onClick(() => {this.a.setDate(this.a.getDate() + 1);})DatePicker({start: new Date('1970-1-1'),end: new Date('2100-1-1'),selected: this.a})}}
}@Entry
@Component
struct ViewB {@State b: ClassB = new ClassB(new DateClass('2023-1-1'));build() {Column() {ViewA({ label: 'date', a: this.b.a })Button(`parent update the new date`).onClick(() => {this.b.a = new DateClass('2023-07-07');})Button(`ViewB: this.b = new ClassB(new DateClass('2023-08-20'))`).onClick(() => {this.b = new ClassB(new DateClass('2023-08-20'));})}}
}

继承Map的class时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。

继承Set的class时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见继承Set类。

框架行为

  1. 初始渲染:

    1. @Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法
    2. 子组件中@ObjectLink装饰的从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类会将自己注册给@Observed class。
  2. 属性更新:当@Observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的@ObjectLink包装类,通知数据更新。

使用场景

嵌套对象

以下是嵌套类对象的数据结构。

说明:

NextID是用来在 ForEach循环渲染 过程中,为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。

// objectLinkNestedObjects.ets
let NextID: number = 1;@Observed
class ClassA {public id: number;public c: number;constructor(c: number) {this.id = NextID++;this.c = c;}
}@Observed
class ClassB {public a: ClassA;constructor(a: ClassA) {this.a = a;}
}@Observed
class ClassD {public c: ClassC;constructor(c: ClassC) {this.c = c;}
}@Observed
class ClassC extends ClassA {public k: number;constructor(k: number) {// 调用父类方法对k进行处理super(k);this.k = k;}
}

以下组件层次结构呈现的是嵌套类对象的数据结构。

@Component
struct ViewC {label: string = 'ViewC1';@ObjectLink c: ClassC;build() {Row() {Column() {Text(`ViewC [${this.label}] this.a.c = ${this.c.c}`).fontColor('#ffffffff').backgroundColor('#ff3fc4c4').height(50).borderRadius(25)Button(`ViewC: this.c.c add 1`).backgroundColor('#ff7fcf58').onClick(() => {this.c.c += 1;console.log('this.c.c:' + this.c.c)})}.width(300)}}
}@Entry
@Component
struct ViewB {@State b: ClassB = new ClassB(new ClassA(0));@State child: ClassD = new ClassD(new ClassC(0));build() {Column() {ViewC({ label: 'ViewC #3',c: this.child.c })Button(`ViewC: this.child.c.c add 10`).backgroundColor('#ff7fcf58').onClick(() => {this.child.c.c += 10console.log('this.child.c.c:' + this.child.c.c)})}}
}

被@Observed装饰的ClassC类,可以观测到继承基类的属性的变化。

ViewB中的事件句柄:

  • this.child.c = new ClassA(0) 和this.b = new ClassB(new ClassA(0)): 对@State装饰的变量b和其属性的修改。

  • this.child.c.c = … :该变化属于第二层的变化,@State无法观察到第二层的变化,但是ClassA被@Observed装饰,ClassA的属性c的变化可以被@ObjectLink观察到。

ViewC中的事件句柄:

  • this.c.c += 1:对@ObjectLink变量a的修改,将触发Button组件的刷新。@ObjectLink和@Prop不同,@ObjectLink不拷贝来自父组件的数据源,而是在本地构建了指向其数据源的引用。

  • @ObjectLink变量是只读的,this.a = new ClassA(…)是不允许的,因为一旦赋值操作发生,指向数据源的引用将被重置,同步将被打断。

对象数组

对象数组是一种常用的数据结构。以下示例展示了数组对象的用法。

let NextID: number = 1;@Observed
class ClassA {public id: number;public c: number;constructor(c: number) {this.id = NextID++;this.c = c;}
}@Component
struct ViewA {// 子组件ViewA的@ObjectLink的类型是ClassA@ObjectLink a: ClassA;label: string = 'ViewA1';build() {Row() {Button(`ViewA [${this.label}] this.a.c = ${this.a ? this.a.c : "undefined"}`).onClick(() => {this.a.c += 1;})}}
}@Entry
@Component
struct ViewB {// ViewB中有@State装饰的ClassA[]@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 => item.id.toString())// 使用@State装饰的数组的数组项初始化@ObjectLink,其中数组项是被@Observed装饰的ClassA的实例ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })Button(`ViewB: reset array`).onClick(() => {this.arrA = [new ClassA(0), new ClassA(0)];})Button(`ViewB: push`).onClick(() => {this.arrA.push(new ClassA(0))})Button(`ViewB: shift`).onClick(() => {if (this.arrA.length > 0) {this.arrA.shift()} else {console.log("length <= 0")}})Button(`ViewB: chg item property in middle`).onClick(() => {this.arrA[Math.floor(this.arrA.length / 2)].c = 10;})Button(`ViewB: chg item property in middle`).onClick(() => {this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);})}}
}
  • this.arrA[Math.floor(this.arrA.length/2)] = new ClassA(…) :该状态变量的改变触发2次更新:

    1. ForEach:数组项的赋值导致ForEach的itemGenerator被修改,因此数组项被识别为有更改,ForEach的item builder将执行,创建新的ViewA组件实例。
    2. ViewA({ label: ViewA this.arrA[last], a: this.arrA[this.arrA.length-1] }):上述更改改变了数组中第二个元素,所以绑定this.arrA[1]的ViewA将被更新。
  • this.arrA.push(new ClassA(0)) : 将触发2次不同效果的更新:

    1. ForEach:新添加的ClassA对象对于ForEach是未知的 itemGenerator ,ForEach的item builder将执行,创建新的ViewA组件实例。
    2. ViewA({ label: ViewA this.arrA[last], a: this.arrA[this.arrA.length-1] }):数组的最后一项有更改,因此引起第二个ViewA的实例的更改。对于ViewA({ label: ViewA this.arrA[first], a: this.arrA[0] }),数组的更改并没有触发一个数组项更改的改变,所以第一个ViewA不会刷新。
  • this.arrA[Math.floor(this.arrA.length/2)].c:@State无法观察到第二层的变化,但是ClassA被@Observed装饰,ClassA的属性的变化将被@ObjectLink观察到。

二维数组

使用@Observed观察二维数组的变化。可以声明一个被@Observed装饰的继承Array的子类。

@Observed
class StringArray extends Array<String> {
}

使用new StringArray()来构造StringArray的实例,new运算符使得@Observed生效,@Observed观察到StringArray的属性变化。

声明一个从Array扩展的类class StringArray extends Array<String> {},并创建StringArray的实例。@Observed装饰的类需要使用new运算符来构建class实例。

@Observed
class StringArray extends Array<String> {
}@Component
struct ItemPage {@ObjectLink itemArr: StringArray;build() {Row() {Text('ItemPage').width(100).height(100)ForEach(this.itemArr,(item: string | Resource) => {Text(item).width(100).height(100)},(item: string) => item)}}
}@Entry
@Component
struct IndexPage {@State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()];build() {Column() {ItemPage({ itemArr: this.arr[0] })ItemPage({ itemArr: this.arr[1] })ItemPage({ itemArr: this.arr[2] })Divider()ForEach(this.arr,(itemArr: StringArray) => {ItemPage({ itemArr: itemArr })},(itemArr: string) => itemArr[0])Divider()Button('update').onClick(() => {console.error('Update all items in arr');if ((this.arr[0] as Array<String>)[0] !== undefined) {// 正常情况下需要有一个真实的ID来与ForEach一起使用,但此处没有// 因此需要确保推送的字符串是唯一的。this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`);this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`);this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`);} else {this.arr[0].push('Hello');this.arr[1].push('World');this.arr[2].push('!');}})}}
}

继承Map类

说明:

从API version 11开始,@ObjectLink支持@Observed装饰Map类型和继承Map类的类型。

在下面的示例中,myMap类型为MyMap<number, string>,点击Button改变myMap的属性,视图会随之刷新。

@Observed
class ClassA {public a: MyMap<number, string>;constructor(a: MyMap<number, string>) {this.a = a;}
}@Observed
export class MyMap<K, V> extends Map<K, V> {public name: string;constructor(name?: string, args?: [K, V][]) {super(args);this.name = name ? name : "My Map";}getName() {return this.name;}
}@Entry
@Component
struct MapSampleNested {@State message: ClassA = new ClassA(new MyMap("myMap", [[0, "a"], [1, "b"], [3, "c"]]));build() {Row() {Column() {MapSampleNestedChild({ myMap: this.message.a })}.width('100%')}.height('100%')}
}@Component
struct MapSampleNestedChild {@ObjectLink myMap: MyMap<number, string>build() {Row() {Column() {ForEach(Array.from(this.myMap.entries()), (item: [number, string]) => {Text(`${item[0]}`).fontSize(30)Text(`${item[1]}`).fontSize(30)Divider()})Button('set new one').onClick(() => {this.myMap.set(4, "d")})Button('clear').onClick(() => {this.myMap.clear()})Button('replace the first one').onClick(() => {this.myMap.set(0, "aa")})Button('delete the first one').onClick(() => {this.myMap.delete(0)})}.width('100%')}.height('100%')}
}

继承Set类

说明:

从API version 11开始,@ObjectLink支持@Observed装饰Set类型和继承Set类的类型。

在下面的示例中,mySet类型为MySet<number>,点击Button改变mySet的属性,视图会随之刷新。

@Observed
class ClassA {public a: MySet<number>;constructor(a: MySet<number>) {this.a = a;}
}@Observed
export class MySet<T> extends Set<T> {public name: string;constructor(name?: string, args?: T[]) {super(args);this.name = name ? name : "My Set";}getName() {return this.name;}
}@Entry
@Component
struct SetSampleNested {@State message: ClassA = new ClassA(new MySet("Set", [0, 1, 2, 3, 4]));build() {Row() {Column() {SetSampleNestedChild({ mySet: this.message.a })}.width('100%')}.height('100%')}
}@Component
struct SetSampleNestedChild {@ObjectLink mySet: MySet<number>build() {Row() {Column() {ForEach(Array.from(this.mySet.entries()), (item: number) => {Text(`${item}`).fontSize(30)Divider()})Button('set new one').onClick(() => {this.mySet.add(5)})Button('clear').onClick(() => {this.mySet.clear()})Button('delete the first one').onClick(() => {this.mySet.delete(0)})}.width('100%')}.height('100%')}
}

ObjectLink支持联合类型

@ObjectLink支持@Observed装饰类和undefined或null组成的联合类型,在下面的示例中,count类型为ClassA | ClassB | undefined,点击父组件Page2中的Button改变count的属性或者类型,Child中也会对应刷新。

@Observed
class ClassA {public a: number;constructor(a: number) {this.a = a;}
}@Observed
class ClassB {public b: number;constructor(b: number) {this.b = b;}
}@Entry
@Component
struct Page2 {@State count: ClassA | ClassB | undefined = new ClassA(10)build() {Column() {Child({ count: this.count })Button('change count property').onClick(() => {// 判断count的类型,做属性的更新if (this.count instanceof ClassA) {this.count.a += 1} else if (this.count instanceof ClassB) {this.count.b += 1} else {console.info('count is undefined, cannot change property')}})Button('change count to ClassA').onClick(() => {// 赋值为ClassA的实例this.count = new ClassA(100)})Button('change count to ClassB').onClick(() => {// 赋值为ClassA的实例this.count = new ClassB(100)})Button('change count to undefined').onClick(() => {// 赋值为undefinedthis.count = undefined})}.width('100%')}
}@Component
struct Child {@ObjectLink count: ClassA | ClassB | undefinedbuild() {Column() {Text(`count is instanceof ${this.count instanceof ClassA ? 'ClassA' : this.count instanceof ClassB ? 'ClassB' : 'undefined'}`).fontSize(30)Text(`count's property is  ${this.count instanceof ClassA ? this.count.a : this.count?.b}`).fontSize(15)}.width('100%')}
}

常见问题

在子组件中给@ObjectLink装饰的变量赋值

在子组件中给@ObjectLink装饰的变量赋值是不允许的。

【反例】

@Observed
class ClassA {public c: number = 0;constructor(c: number) {this.c = c;}
}@Component
struct ObjectLinkChild {@ObjectLink testNum: ClassA;build() {Text(`ObjectLinkChild testNum ${this.testNum.c}`).onClick(() => {// ObjectLink不能被赋值this.testNum = new ClassA(47);})}
}@Entry
@Component
struct Parent {@State testNum: ClassA[] = [new ClassA(1)];build() {Column() {Text(`Parent testNum ${this.testNum[0].c}`).onClick(() => {this.testNum[0].c += 1;})ObjectLinkChild({ testNum: this.testNum[0] })}}
}

点击ObjectLinkChild给@ObjectLink装饰的变量赋值:

this.testNum = new ClassA(47); 

这是不允许的,对于实现双向数据同步的@ObjectLink,赋值相当于要更新父组件中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。框架对于这种行为会发生运行时报错。

【正例】

@Observed
class ClassA {public c: number = 0;constructor(c: number) {this.c = c;}
}@Component
struct ObjectLinkChild {@ObjectLink testNum: ClassA;build() {Text(`ObjectLinkChild testNum ${this.testNum.c}`).onClick(() => {// 可以对ObjectLink装饰对象的属性赋值this.testNum.c = 47;})}
}@Entry
@Component
struct Parent {@State testNum: ClassA[] = [new ClassA(1)];build() {Column() {Text(`Parent testNum ${this.testNum[0].c}`).onClick(() => {this.testNum[0].c += 1;})ObjectLinkChild({ testNum: this.testNum[0] })}}
}

基础嵌套对象属性更改失效

在应用开发中,有很多嵌套对象场景,例如,开发者更新了某个属性,但UI没有进行对应的更新。

每个装饰器都有自己可以观察的能力,并不是所有的改变都可以被观察到,只有可以被观察到的变化才会进行UI更新。@Observed装饰器可以观察到嵌套对象的属性变化,其他装饰器仅能观察到第二层的变化。

【反例】

下面的例子中,一些UI组件并不会更新。

class ClassA {a: number;constructor(a: number) {this.a = a;}getA(): number {return this.a;}setA(a: number): void {this.a = a;}
}class ClassC {c: number;constructor(c: number) {this.c = c;}getC(): number {return this.c;}setC(c: number): void {this.c = c;}
}class ClassB extends ClassA {b: number = 47;c: ClassC;constructor(a: number, b: number, c: number) {super(a);this.b = b;this.c = new ClassC(c);}getB(): number {return this.b;}setB(b: number): void {this.b = b;}getC(): number {return this.c.getC();}setC(c: number): void {return this.c.setC(c);}
}@Entry
@Component
struct MyView {@State b: ClassB = new ClassB(10, 20, 30);build() {Column({ space: 10 }) {Text(`a: ${this.b.a}`)Button("Change ClassA.a").onClick(() => {this.b.a += 1;})Text(`b: ${this.b.b}`)Button("Change ClassB.b").onClick(() => {this.b.b += 1;})Text(`c: ${this.b.c.c}`)Button("Change ClassB.ClassC.c").onClick(() => {// 点击时上面的Text组件不会刷新this.b.c.c += 1;})}}
}
  • 最后一个Text组件Text(‘c: ${this.b.c.c}’),当点击该组件时UI不会刷新。 因为,@State b : ClassB 只能观察到this.b属性的变化,比如this.b.a, this.b.b 和this.b.c的变化,但是无法观察嵌套在属性中的属性,即this.b.c.c(属性c是内嵌在b中的对象classC的属性)。

  • 为了观察到嵌套于内部的ClassC的属性,需要做如下改变:

    • 构造一个子组件,用于单独渲染ClassC的实例。 该子组件可以使用@ObjectLink c : ClassC或@Prop c : ClassC。通常会使用@ObjectLink,除非子组件需要对其ClassC对象进行本地修改。
    • 嵌套的ClassC必须用@Observed装饰。当在ClassB中创建ClassC对象时(本示例中的ClassB(10, 20, 30)),它将被包装在ES6代理中,当ClassC属性更改时(this.b.c.c += 1),该代码将修改通知到@ObjectLink变量。

【正例】

以下示例使用@Observed/@ObjectLink来观察嵌套对象的属性更改。

class ClassA {a: number;constructor(a: number) {this.a = a;}getA(): number {return this.a;}setA(a: number): void {this.a = a;}
}@Observed
class ClassC {c: number;constructor(c: number) {this.c = c;}getC(): number {return this.c;}setC(c: number): void {this.c = c;}
}class ClassB extends ClassA {b: number = 47;c: ClassC;constructor(a: number, b: number, c: number) {super(a);this.b = b;this.c = new ClassC(c);}getB(): number {return this.b;}setB(b: number): void {this.b = b;}getC(): number {return this.c.getC();}setC(c: number): void {return this.c.setC(c);}
}@Component
struct ViewClassC {@ObjectLink c: ClassC;build() {Column({ space: 10 }) {Text(`c: ${this.c.getC()}`)Button("Change C").onClick(() => {this.c.setC(this.c.getC() + 1);})}}
}@Entry
@Component
struct MyView {@State b: ClassB = new ClassB(10, 20, 30);build() {Column({ space: 10 }) {Text(`a: ${this.b.a}`)Button("Change ClassA.a").onClick(() => {this.b.a += 1;})Text(`b: ${this.b.b}`)Button("Change ClassB.b").onClick(() => {this.b.b += 1;})ViewClassC({ c: this.b.c }) // Text(`c: ${this.b.c.c}`)的替代写法Button("Change ClassB.ClassC.c").onClick(() => {this.b.c.c += 1;})}}
}

复杂嵌套对象属性更改失效

【反例】

以下示例创建了一个带有@ObjectLink装饰变量的子组件,用于渲染一个含有嵌套属性的ParentCounter,用@Observed装饰嵌套在ParentCounter中的SubCounter。

let nextId = 1;
@Observed
class SubCounter {counter: number;constructor(c: number) {this.counter = c;}
}
@Observed
class ParentCounter {id: number;counter: number;subCounter: SubCounter;incrCounter() {this.counter++;}incrSubCounter(c: number) {this.subCounter.counter += c;}setSubCounter(c: number): void {this.subCounter.counter = c;}constructor(c: number) {this.id = nextId++;this.counter = c;this.subCounter = new SubCounter(c);}
}
@Component
struct CounterComp {@ObjectLink value: ParentCounter;build() {Column({ space: 10 }) {Text(`${this.value.counter}`).fontSize(25).onClick(() => {this.value.incrCounter();})Text(`${this.value.subCounter.counter}`).onClick(() => {this.value.incrSubCounter(1);})Divider().height(2)}}
}
@Entry
@Component
struct ParentComp {@State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];build() {Row() {Column() {CounterComp({ value: this.counter[0] })CounterComp({ value: this.counter[1] })CounterComp({ value: this.counter[2] })Divider().height(5)ForEach(this.counter,(item: ParentCounter) => {CounterComp({ value: item })},(item: ParentCounter) => item.id.toString())Divider().height(5)// 第一个点击事件Text('Parent: incr counter[0].counter').fontSize(20).height(50).onClick(() => {this.counter[0].incrCounter();// 每次触发时自增10this.counter[0].incrSubCounter(10);})// 第二个点击事件Text('Parent: set.counter to 10').fontSize(20).height(50).onClick(() => {// 无法将value设置为10,UI不会刷新this.counter[0].setSubCounter(10);})Text('Parent: reset entire counter').fontSize(20).height(50).onClick(() => {this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];})}}}
}

对于Text(‘Parent: incr counter[0].counter’)的onClick事件,this.counter[0].incrSubCounter(10)调用incrSubCounter方法使SubCounter的counter值增加10,UI同步刷新。

但是,在Text(‘Parent: set.counter to 10’)的onClick中调用this.counter[0].setSubCounter(10),SubCounter的counter值却无法重置为10。

incrSubCounter和setSubCounter都是同一个SubCounter的函数。在第一个点击处理时调用incrSubCounter可以正确更新UI,而第二个点击处理调用setSubCounter时却没有更新UI。实际上incrSubCounter和setSubCounter两个函数都不能触发Text(‘${this.value.subCounter.counter}’)的更新,因为@ObjectLink value : ParentCounter仅能观察其代理ParentCounter的属性,对于this.value.subCounter.counter是SubCounter的属性,无法观察到嵌套类的属性。

但是,第一个click事件调用this.counter[0].incrCounter()将CounterComp自定义组件中@ObjectLink value: ParentCounter标记为已更改。此时触发Text(‘${this.value.subCounter.counter}’)的更新。 如果在第一个点击事件中删除this.counter[0].incrCounter(),也无法更新UI。

【正例】

对于上述问题,为了直接观察SubCounter中的属性,以便this.counter[0].setSubCounter(10)操作有效,可以利用下面的方法:

@ObjectLink value:ParentCounter = new ParentCounter(0);
@ObjectLink subValue:SubCounter = new SubCounter(0);

该方法使得@ObjectLink分别代理了ParentCounter和SubCounter的属性,这样对于这两个类的属性的变化都可以观察到,即都会对UI视图进行刷新。即使删除了上面所说的this.counter[0].incrCounter(),UI也会进行正确的刷新。

该方法可用于实现“两个层级”的观察,即外部对象和内部嵌套对象的观察。但是该方法只能用于@ObjectLink装饰器,无法作用于@Prop(@Prop通过深拷贝传入对象)。详情参考@Prop与@ObjectLink的差异。

let nextId = 1;@Observed
class SubCounter {counter: number;constructor(c: number) {this.counter = c;}
}@Observed
class ParentCounter {id: number;counter: number;subCounter: SubCounter;incrCounter() {this.counter++;}incrSubCounter(c: number) {this.subCounter.counter += c;}setSubCounter(c: number): void {this.subCounter.counter = c;}constructor(c: number) {this.id = nextId++;this.counter = c;this.subCounter = new SubCounter(c);}
}@Component
struct CounterComp {@ObjectLink value: ParentCounter;build() {Column({ space: 10 }) {Text(`${this.value.counter}`).fontSize(25).onClick(() => {this.value.incrCounter();})CounterChild({ subValue: this.value.subCounter })Divider().height(2)}}
}@Component
struct CounterChild {@ObjectLink subValue: SubCounter;build() {Text(`${this.subValue.counter}`).onClick(() => {this.subValue.counter += 1;})}
}@Entry
@Component
struct ParentComp {@State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];build() {Row() {Column() {CounterComp({ value: this.counter[0] })CounterComp({ value: this.counter[1] })CounterComp({ value: this.counter[2] })Divider().height(5)ForEach(this.counter,(item: ParentCounter) => {CounterComp({ value: item })},(item: ParentCounter) => item.id.toString())Divider().height(5)Text('Parent: reset entire counter').fontSize(20).height(50).onClick(() => {this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];})Text('Parent: incr counter[0].counter').fontSize(20).height(50).onClick(() => {this.counter[0].incrCounter();this.counter[0].incrSubCounter(10);})Text('Parent: set.counter to 10').fontSize(20).height(50).onClick(() => {this.counter[0].setSubCounter(10);})}}}
}

@Prop与@ObjectLink的差异

在下面的示例代码中,@ObjectLink装饰的变量是对数据源的引用,即在this.value.subValue和this.subValue都是同一个对象的不同引用,所以在点击CounterComp的click handler,改变this.value.subCounter.counter,this.subValue.counter也会改变,对应的组件Text(this.subValue.counter: ${this.subValue.counter})会刷新。

let nextId = 1;@Observed
class SubCounter {counter: number;constructor(c: number) {this.counter = c;}
}@Observed
class ParentCounter {id: number;counter: number;subCounter: SubCounter;incrCounter() {this.counter++;}incrSubCounter(c: number) {this.subCounter.counter += c;}setSubCounter(c: number): void {this.subCounter.counter = c;}constructor(c: number) {this.id = nextId++;this.counter = c;this.subCounter = new SubCounter(c);}
}@Component
struct CounterComp {@ObjectLink value: ParentCounter;build() {Column({ space: 10 }) {CountChild({ subValue: this.value.subCounter })Text(`this.value.counter:increase 7 `).fontSize(30).onClick(() => {// click handler, Text(`this.subValue.counter: ${this.subValue.counter}`) will updatethis.value.incrSubCounter(7);})Divider().height(2)}}
}@Component
struct CountChild {@ObjectLink subValue: SubCounter;build() {Text(`this.subValue.counter: ${this.subValue.counter}`).fontSize(30)}
}@Entry
@Component
struct ParentComp {@State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];build() {Row() {Column() {CounterComp({ value: this.counter[0] })CounterComp({ value: this.counter[1] })CounterComp({ value: this.counter[2] })Divider().height(5)ForEach(this.counter,(item: ParentCounter) => {CounterComp({ value: item })},(item: ParentCounter) => item.id.toString())Divider().height(5)Text('Parent: reset entire counter').fontSize(20).height(50).onClick(() => {this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];})Text('Parent: incr counter[0].counter').fontSize(20).height(50).onClick(() => {this.counter[0].incrCounter();this.counter[0].incrSubCounter(10);})Text('Parent: set.counter to 10').fontSize(20).height(50).onClick(() => {this.counter[0].setSubCounter(10);})}}}
}

@ObjectLink图示如下:

【反例】

如果用@Prop替代@ObjectLink。点击第一个click handler,UI刷新正常。但是点击第二个onClick事件,@Prop 对变量做了一个本地拷贝,CounterComp的第一个Text并不会刷新。

this.value.subCounter和this.subValue并不是同一个对象。所以this.value.subCounter的改变,并没有改变this.subValue的拷贝对象,Text(this.subValue.counter: ${this.subValue.counter})不会刷新。

@Component
struct CounterComp {@Prop value: ParentCounter = new ParentCounter(0);@Prop subValue: SubCounter = new SubCounter(0);build() {Column({ space: 10 }) {Text(`this.subValue.counter: ${this.subValue.counter}`).fontSize(20).onClick(() => {// 1st click handlerthis.subValue.counter += 7;})Text(`this.value.counter:increase 7 `).fontSize(20).onClick(() => {// 2nd click handlerthis.value.incrSubCounter(7);})Divider().height(2)}}
}

@Prop拷贝的关系图示如下:

【正例】

可以通过从ParentComp到CounterComp仅拷贝一份@Prop value: ParentCounter,同时必须避免再多拷贝一份SubCounter。

  • 在CounterComp组件中只使用一个@Prop counter:Counter。

  • 添加另一个子组件SubCounterComp,其中包含@ObjectLink subCounter: SubCounter。此@ObjectLink可确保观察到SubCounter对象属性更改,并且UI更新正常。

  • @ObjectLink subCounter: SubCounter与CounterComp中的@Prop counter:Counter的this.counter.subCounter共享相同的SubCounter对象。

let nextId = 1;@Observed
class SubCounter {counter: number;constructor(c: number) {this.counter = c;}
}@Observed
class ParentCounter {id: number;counter: number;subCounter: SubCounter;incrCounter() {this.counter++;}incrSubCounter(c: number) {this.subCounter.counter += c;}setSubCounter(c: number): void {this.subCounter.counter = c;}constructor(c: number) {this.id = nextId++;this.counter = c;this.subCounter = new SubCounter(c);}
}@Component
struct SubCounterComp {@ObjectLink subValue: SubCounter;build() {Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`).onClick(() => {// 2nd click handlerthis.subValue.counter = 7;})}
}
@Component
struct CounterComp {@Prop value: ParentCounter;build() {Column({ space: 10 }) {Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`).fontSize(20).onClick(() => {// 1st click handlerthis.value.incrCounter();})SubCounterComp({ subValue: this.value.subCounter })Text(`this.value.incrSubCounter()`).onClick(() => {// 3rd click handlerthis.value.incrSubCounter(77);})Divider().height(2)}}
}
@Entry
@Component
struct ParentComp {@State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];build() {Row() {Column() {CounterComp({ value: this.counter[0] })CounterComp({ value: this.counter[1] })CounterComp({ value: this.counter[2] })Divider().height(5)ForEach(this.counter,(item: ParentCounter) => {CounterComp({ value: item })},(item: ParentCounter) => item.id.toString())Divider().height(5)Text('Parent: reset entire counter').fontSize(20).height(50).onClick(() => {this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];})Text('Parent: incr counter[0].counter').fontSize(20).height(50).onClick(() => {this.counter[0].incrCounter();this.counter[0].incrSubCounter(10);})Text('Parent: set.counter to 10').fontSize(20).height(50).onClick(() => {this.counter[0].setSubCounter(10);})}}}
}

拷贝关系图示如下:

在@Observed装饰类的构造函数中延时更改成员变量

在状态管理中,使用@Observed装饰类后,会给该类使用一层“代理”进行包装。当在组件中改变该类的成员变量时,会被该代理进行拦截,在更改数据源中值的同时,也会将变化通知给绑定的组件,从而实现观测变化与触发刷新。当开发者在类的构造函数中对成员变量进行赋值或者修改时,此修改不会经过代理(因为是直接对数据源中的值进行修改),也就无法被观测到。所以,如果开发者在类的构造函数中使用定时器修改类中的成员变量,即使该修改成功执行了,也不会触发UI的刷新。

【反例】

@Observed
class RenderClass {waitToRender: boolean = false;constructor() {setTimeout(() => {this.waitToRender = true;console.log("change waitToRender to " + this.waitToRender);}, 1000)}
}@Entry
@Component
struct Index {@State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass();@State textColor: Color = Color.Black;renderClassChange() {console.log("Render Class Change waitToRender is " + this.renderClass.waitToRender);}build() {Row() {Column() {Text("Render Class waitToRender is " + this.renderClass.waitToRender).fontSize(20).fontColor(this.textColor)Button("Show").onClick(() => {// 使用其他状态变量强行刷新UI的做法并不推荐,此处仅用来检测waitToRender的值是否更新this.textColor = Color.Red;})}.width('100%')}.height('100%')}
}

上文的示例代码中在RenderClass的构造函数中使用定时器在1秒后修改了waitToRender的值,但是不会触发UI的刷新。此时点击按钮,强行刷新Text组件可以看到waitToRender的值已经被修改成了true。

【正例】

@Observed
class RenderClass {waitToRender: boolean = false;constructor() {}
}@Entry
@Component
struct Index {@State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass();renderClassChange() {console.log("Render Class Change waitToRender is " + this.renderClass.waitToRender);}onPageShow() {setTimeout(() => {this.renderClass.waitToRender = true;console.log("change waitToRender to " + this.renderClass.waitToRender);}, 1000)}build() {Row() {Column() {Text("Render Class Wait To Render is " + this.renderClass.waitToRender).fontSize(20)}.width('100%')}.height('100%')}
}

上文的示例代码将定时器修改移入到组件内,此时界面显示时会先显示“Render Class Change waitToRender is false”。待定时器触发时,界面刷新显示“Render Class Change waitToRender is true”。

因此,更推荐开发者在组件中对@Observed装饰的类成员变量进行修改实现刷新。

在@Observed装饰的类内使用static方法进行初始化

在@Observed装饰的类内,尽量避免使用static方法进行初始化,在创建时会绕过Observed的实现,导致无法被代理,UI不刷新。

@Entry
@Component
struct MainPage {@State viewModel: ViewModel = ViewModel.build();build() {Column() {Button("Click").onClick((event) => {this.viewModel.subViewModel.isShow = !this.viewModel.subViewModel.isShow;})SubComponent({ viewModel: this.viewModel.subViewModel })}.padding({ top: 60 }).width('100%').alignItems(HorizontalAlign.Center)}
}@Component
struct SubComponent {@ObjectLink viewModel: SubViewModel;build() {Column() {if (this.viewModel.isShow) {Text("click to take effect");}}}
}class ViewModel {subViewModel: SubViewModel = SubViewModel.build(); //内部静态方法创建static build() {console.log("ViewModel build()")return new ViewModel();}
}@Observed
class SubViewModel {isShow?: boolean = false;static build() {//只有在SubViewModel内部的静态方法创建对象,会影响关联console.log("SubViewModel build()")let viewModel = new SubViewModel();return viewModel;}
}

上文的示例中,在自定义组件ViewModel中使用static方法进行初始化,此时点击Click按钮,页面中并不会显示click to take effect。

因此,不推荐开发者在自定义的类装饰器内使用static方法进行初始化。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/789580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

教育信创 | 云轴科技ZStack联合飞腾发布全场景教育信创白皮书

随着数字化时代的到来&#xff0c;教育行业正面临着前所未有的挑战与机遇。为了推动教育行业的数字化转型和信创人才培养&#xff0c;云轴科技ZStack联合飞腾于3月28日正式发布了《教育行业数字化自主创新飞腾生态解决方案白皮书》&#xff08;简称《教育白皮书》&#xff09;。…

新能源汽车充电桩主板产业链解析

新能源汽车充电桩主控制板&#xff0c;简称汽车充电桩主板&#xff0c;是充电桩设施的核心部件&#xff0c;主要负责控制充电桩的整体运行和管理充电过程。了解汽车充电桩主板的整体产业链是非常重要的&#xff0c;这可以帮助您更好地了解供应链、采购渠道以及行业发展趋势。 产…

抓住信号如此简单,WeTrade一个指标1分钟轻松解决

在交易中是不是有这样的困惑&#xff0c;没有清晰的计算逻辑还抓不住交易的信号&#xff0c;这样的投资者有福了&#xff0c;今天WeTrade众汇分享一个指标1分钟轻松解决这个困惑。 ROC全称Rate of Change&#xff0c;中文名为变动速度指标或变动率指标&#xff0c;它以百分比的…

Java就近原则和this关键字

Java 中的就近原则和 this 关键字有着密切的关系&#xff0c;特别是在处理成员变量与方法参数同名的情况下。就近原则指的是在同一作用域下&#xff0c;优先使用最近声明的变量或参数。 在 Java 中&#xff0c;如果一个方法的参数与类的成员变量同名&#xff0c;为了明确指示要…

大数据实验四-MapReduce编程实践

一&#xff0e;实验内容 MapReduce编程实践&#xff1a; 使用MapReduce实现多个文本文件中WordCount词频统计功能&#xff0c;实验编写Map处理逻辑、编写Reduce处理逻辑、编写main方法。 二&#xff0e;实验目的 1、通过实验掌握基本的MapReduce编程方法。 2、实现统计HDF…

【机器学习】数据探索(Data Exploration)---数据质量和数据特征分析

一、引言 在机器学习项目中&#xff0c;数据探索是至关重要的一步。它不仅是模型构建的基础&#xff0c;还是确保模型性能稳定、预测准确的关键。数据探索的过程中&#xff0c;数据质量和数据特征分析占据了核心地位。数据质量直接关系到模型能否从数据中提取有效信息&#xff…

Vue-Electron配置及踩坑

前言 大道至简。太复杂的教程不看。 本篇将记述我创建好Vue3项目之后&#xff0c;用Electron把页面呈现出来的整个过程。会记录一些踩坑。 首先&#xff0c;Electron官网可以参考。但是它只是作出了一个普通的html结构该如何用Electron呈现出来&#xff0c;vue的配置有一些变…

OC分层渲染详解,OC分层渲染与云渲染区别

​OC分层渲染通过分层处理场景来提升渲染效率&#xff0c;而云渲染借助云服务器进行远程高性能渲染。主要差异在于OC分层渲染优化了本地渲染过程&#xff0c;云渲染则依靠云计算资源执行。 OC分层渲染是指什么 OC分层渲染&#xff0c;即Object Channel分层渲染&#xff0c;是一…

C语言运算符和表达式——赋值中的自动类型转换(精度损失问题)

目录 自动类型转换 数值精度损失 自动类型转换 在不同类型数据间赋值时&#xff0c;会发生自动类型转换 *取值范围大的类型 → 取值范围小的类型&#xff0c;通常是不安全的 *数值溢出&#xff08;Overflow&#xff09; *反之&#xff0c;一定都是安全的吗&#xff1f;…

fastlio2 给 interactive-slam 保存每帧的点云和每帧的里程计为单独的文件做后端回环优化和手动回环优化

为了给 interactive-slam 提供数据做后端回环优化和手动回环优化,需要保存每帧的点云和每帧的里程计为单独的文件,并且需要保存的名字为ros时间戳。 效果很好,比我自己写的手动回环模块好用 // This is an advanced implementation of the algorithm described in the // fo…

Golang和Java对比

其实我是Javaer转的Golang&#xff0c;我谈谈自己对Java和Golang的体会 我先讲讲我认为Golang的优点 1、Golang是一门新语言&#xff0c;相比于Java&#xff0c;他的生态要小很多&#xff0c;优点很明显&#xff0c;自由度高&#xff0c;学习成本低&#xff0c;能快速拉起一个…

数据结构——二叉树链式结构

目录 前言 1. 二叉树的概念及结构 1.1概念 1.2 特殊的二叉树 1.3 二叉树的性质 1.4 二叉树的存储结构 2. 二叉树链式结构实现 2.1 手动创建二叉树 2.2 二叉树的遍历 2.2.1 前序、中序和后序遍历 2.2.2 层序遍历 2.3 节点个数以及高度 2.3.1 节点个数 2.3.2 求二叉…

【云呐】固定资产清查盘点报告模板

固定资产清查盘点报告的大致框架:一、前言说明本次清查盘点的背景和目的清晰表述清查盘点的责任与相关依据二、清查盘点范围按部门或区域明确清查范围口径明确被清查项目,如所有原值一定数额以上的固定资产三、清查盘点时间确定清查盘点实施的时间节点 四、清查盘点方法描述清查…

回文数-第15届蓝桥第5次STEMA测评Scratch真题精选

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第179讲。 如果想持续关注Scratch蓝桥真题解读&#xff0c;可以点击《Scratch蓝桥杯历年真题》并订阅合集&#xff0c;…

nginx与tomcat的区别?

关于nginx和tomcat的概念 网上有很多关于nginx和tomcat是什么东西的定义&#xff0c;我总结了一下: tomcat是Web服务器、HTTP服务器、应用服务器、Servlet容器、web容器。 Nginx是Web服务器、HTTP服务器、正向/反向代理服务器&#xff0c;。 这里有两个概念是交叉的&#xff…

【C++STL详解(二)】——string类模拟实现

目录 前言 一、接口总览 二、默认成员函数 1.构造函数 2.拷贝构造 写法一&#xff1a;传统写法 写法二&#xff1a;现代写法&#xff08;复用构造函数&#xff09; 3.赋值构造 写法一&#xff1a;传统写法 写法二&#xff1a;现代写法(复用拷贝构造) 4.析构函数 三、…

OSPF实验1

1,配置IP地址 [R1]dis ip interface brief Interface IP Address/Mask Physical Protocol GigabitEthernet0/0/0 200.1.1.1/24 up up GigabitEthernet0/0/1 10.1.1.1/24 up …

Oracle基础【7-Oracle中RMAN恢复管理器】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;Oracle从基础到进阶 本文对应Oracle实验报告源文件下载&#xff1a;公众号程序员刘同学回复oracle实验获取下载链接 实验七 RMAN恢复管理器一、实验目的二、实验环境三、实验内容1 为备份…

MySQL数据库(数据库连接池)

文章目录 1.批处理应用1.基本介绍2.批处理演示1.创建测试表2.修改url3.编写java代码 3.批处理源码分析 2.数据库连接池1.传统连接弊端分析2.数据库连接池基本介绍1.概念介绍2.数据库连接池示意图3.数据库连接池种类 3.C3P0连接池1.环境配置1.导入jar包2.将整个lib添加到项目中3…

AWS上面部署一台jenkins

问题 客户预算有限&#xff0c;需要在aws云上面搞一台EC2手动安装jenkins发版。 步骤 创建密钥对 在EC2服务里面创建密钥对&#xff0c;具体如下图&#xff1a; 设置密钥对&#xff0c;如下图&#xff1a; 保存好这个私钥文件&#xff0c;以便后续用这个私钥文件ssh登录j…