九、页面级变量的状态管理 原创
状态管理概述
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。
- View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
- State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
基本概念
- 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。
- 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。以下示例中increaseBy变量为常规变量。
- 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。以下示例中数据源为count: 1。
- 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA: ({ aProp: this.aProp })。
- 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。示例:
@Component
struct MyComponent {
//状态变量
@State count: number = 0;
//常规变量
increaseBy: number = 1;
build() {
}
}
@Component
struct Parent {
build() {
Column() {
// 从父组件初始化,覆盖本地定义的默认值
MyComponent({ count: 1, increaseBy: 2 })
}
}
}
- 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。示例同上。
- 本地初始化:在变量声明的时候赋值,作为变量的默认值。示例:@State count: number = 0。
装饰器总览
ArkUI提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为:
- 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
- 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
:::success 咱们来看一张完整的装饰器说明图,咱们后续的学习就围绕着这张图来展开
- 管理组件状态:小框中
- 管理应用状态:大框中
@State自己的状态
@State 装饰器咱们已经学习过了,所以就不从头讲解,而是说2 个使用的注意点
:::success 观察变化注意点:
并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。
- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
- 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。声明ClassA和Model类。
:::
**<font style="color:rgb(27, 27, 27);">Object.keys()</font>**
静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组。
基本数据类型
// for simple type
@State count: number = 0;
// value changing can be observed
this.count = 1;
interface Chicken {
name: string
age: number
color: string
}
const c: Chicken = {
name: '1',
age: 2,
color: '黄绿色'
}
// 获取对象的属性名,返回字符串数组
console.log('', Object.keys(c))
复杂数据类型且嵌套
interface Dog {
name: string
}
interface Person {
name: string
dog: Dog
}
@Component
export struct HelloComponent {
// 状态变量
@State message: string = 'Hello, World!';
@State person: Person = {
name: 'jack',
dog: {
name: '柯基'
}
}
sayHi() {
console.log('你好呀')
}
build() {
Column() {
Text(this.message)
Button('修改 message')
.onClick(() => {
this.message = 'Hello,ArkTS'
})
Text(JSON.stringify(this.person))
Button('修改title外层属性')
.onClick(() => {
this.person.name = '666'
})
Button('修改title嵌套属性')
.onClick(() => {
this.person.dog.name = '内部的 666'
// this.person.dog = {
// name: '阿拉斯加'
// }
})
}
}
}
@Prop父传子_单向传递数据
@Prop 装饰的变量可以和父组件建立单向的同步关系。@Prop
@Component
struct SonCom {
build() {
}
}
@Entry
@Component
// FatherCom 父组件
struct FatherCom {
build() {
Column() {
// 子组件
SonCom()
}
}
}
简单类型
简单类型 string、number、boolean、enum
:::success 注意:
- 修改父组件数据,会同步更新子组件
- 修改子组件@Prop 修饰的数据,子组件 UI 更新,但会被父组件状态更新被覆盖
- 通过回调函数的方式修改父组件的数据,然后触发@Prop数据的更新
:::
基础模版:
@Component
struct SonCom {
@Prop info: string
build() {
Button('info:' + this.info)
.onClick(() => {
this.info = '子组件修改'
})
}
}
@Entry
@Component
struct FatherCom {
@State info: string = '么么哒'
build() {
Column() {
Text(this.info)
.onClick(() => {
this.info = '父组件修改'
})
SonCom({
info: this.info,
})
}
.padding(20)
.backgroundColor(Color.Orange)
}
}
:::success 步骤:
- 子组件定义修改数据的函数(箭头函数)
- 父组件,传入回调函数(必须是箭头函数),
- 回调函数中修改数据,并修改
- 如果要传递参数,定义形参,并传入实参即可
:::
@Component
struct SonCom {
@Prop info: string
changeInfo = (newInfo: string) => {
}
build() {
Button('info:' + this.info)
.onClick(() => {
this.changeInfo('改啦')
})
}
}
@Entry
@Component
struct FatherCom {
@State info: string = '么么哒'
build() {
Column() {
Text(this.info)
SonCom({
info: this.info,
changeInfo: (newInfo: string) => {
this.info = newInfo
}
})
}
.padding(20)
.backgroundColor(Color.Orange)
}
}
复杂类型
复杂类型的做法类似,通过回调函数将需要修改的数据传递给父组件即可
基础模版:
interface User {
name: string
age: number
}
@Entry
@Component
struct Index {
@State
userInfo: User = {
name: 'jack',
age: 18
}
build() {
Column({ space: 20 }) {
Text('父组件')
.fontSize(30)
Text('用户名:' + this.userInfo.name)
.white()
.onClick(() => {
this.userInfo.name = 'rose'
})
Text('年龄:' + this.userInfo.age)
.white()
.onClick(() => {
this.userInfo.age++
})
Child({
user: this.userInfo,
})
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Pink)
}
}
@Component
struct Child {
@Prop
user: User
build() {
Text('子组件:' + JSON.stringify(this.user))
.padding(10)
.backgroundColor('#0094ff')
.fontColor(Color.White)
.onClick(() => {
})
}
}
@Extend(Text)
function white() {
.fontSize(20)
.fontColor(Color.White)
}
:::success 步骤:
- 子组件定义修改数据的函数(箭头函数)
- 父组件,传入回调函数(必须是箭头函数),
- 函数中接收最新的数据,进行修改,改完之后影响子组件
- 根据需求决定是否需要添加参数
:::
参考代码:
interface User {
name: string
age: number
}
@Entry
@Component
struct Index {
@State
userInfo: User = {
name: 'jack',
age: 18
}
build() {
Column({ space: 20 }) {
Text('父组件')
.fontSize(30)
Text('用户名:' + this.userInfo.name)
.white()
.onClick(() => {
this.userInfo.name = 'rose'
})
Text('年龄:' + this.userInfo.age)
.white()
.onClick(() => {
this.userInfo.age++
})
Child({
user: this.userInfo,
userChange: (newUser: User) => {
this.userInfo.name = newUser.name
this.userInfo.age = newUser.age
}
})
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Pink)
}
}
@Component
struct Child {
@Prop
user: User
userChange: (newUser: User) => void = (newUser: User) => {
}
build() {
Text('子组件:' + JSON.stringify(this.user))
.padding(10)
.backgroundColor('#0094ff')
.fontColor(Color.White)
.onClick(() => {
this.userChange({
name: '路飞',
age: 26
})
})
}
}
@Extend(Text)
function white() {
.fontSize(20)
.fontColor(Color.White)
}
@Link双向同步
父子双向同步, 对硬件消耗大
使用@Link 可以实现父组件和子组件的双向同步:
:::info 使用步骤:
- 将父组件的状态属性传递给子组件
- 子组件通过@Link修饰即可
- 分别测试基础,和复杂类型
注意:
- api9 之前是$语法,现在支持this语法了,比如**@State info:string='123'**
- api9 之前:子组件({xx:$info})
- api9 及之后:子组件({xx:this.info})
:::
@Entry
@Component
// 父组件
struct KnowledgePage {
@State count: number = 0
build() {
Column() {
Text('父组件')
.fontSize(30)
Text(this.count.toString())
Button('修改数据')
.onClick(() => {
})
SonComponent()
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 子组件
struct SonComponent {
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是子组件')
.fontSize(20)
Column() {
Button('修改count')
.onClick(() => {
})
}
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('80%')
.margin({ top: 100 })
.padding(10)
.borderRadius(10)
}
}
interface Person {
name: string
age: number
}
@Entry
@Component
// 父组件
struct KnowledgePage {
@State count: number = 0
@State p: Person = {
name: 'jack',
age: 18,
}
build() {
Column() {
Text('父组件')
.fontSize(30)
Text(this.count.toString())
Text(JSON.stringify(this.p))
Button('修改数据')
.onClick(() => {
this.count--
})
SonComponent({
count: this.count,
p: this.p
})
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 子组件
struct SonComponent {
@Link count: number
@Link p: Person
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是子组件')
.fontSize(20)
Text(this.count.toString())
Text(JSON.stringify(this.p))
Column() {
Button('修改count')
.onClick(() => {
this.count++
})
Button('修改Person')
.onClick(() => {
this.p.age++
})
}
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('80%')
.margin({ top: 100 })
.padding(10)
.borderRadius(10)
}
}
@Provide、@Consume后代组件
将数据传递给后代和后代的数据进行双向同步
// 写法 1:通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 写法 2通过相同的变量别名绑定
@Provide b: number = 0;
@Consume('b') c: number;
:::info 使用步骤:
- 将父组件的状态属性使用@Provide修饰
- 子组件通过@Consume修饰
- 分别测试基础,和复杂类型
:::
@Entry
@Component
// 顶级组件
struct RootComponent {
build() {
Column() {
Text('顶级组件')
.fontSize(30)
.fontWeight(900)
ParentComponent()
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 二级组件
struct ParentComponent {
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是二级组件')
.fontSize(22)
.fontWeight(900)
SonComponent()
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
@Component
// 内层组件
struct SonComponent {
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是内层组件')
.fontSize(20)
.fontWeight(900)
}
.backgroundColor('#bf94e4')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
interface Person {
name: string
age: number
}
@Entry
@Component
// 顶级组件
struct RootComponent {
@Provide
p: Person = {
name: 'jack',
age: 18
}
@Provide
food: string = '西兰花炒蛋'
build() {
Column() {
Text('顶级组件')
.fontSize(30)
.fontWeight(900)
Text(JSON.stringify(this.p))
Text(this.food)
ParentComponent()
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 二级组件
struct ParentComponent {
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是二级组件')
.fontSize(22)
.fontWeight(900)
SonComponent()
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
@Component
// 内层组件
struct SonComponent {
// 相同变量名
@Consume p: Person
@Consume('food') f: string
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是内层组件')
.fontSize(20)
.fontWeight(900)
Text(JSON.stringify(this.p))
Text(this.f)
Button('修改')
.onClick(() => {
this.p.name = 'rose'
this.p.age = 99
this.f += '!'
})
}
.backgroundColor('#bf94e4')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
@Observed、@ObjectLink
通过@Link
和给对象进行整体重新赋值的方法可以实现修改嵌套对象的写法和让页面刷新,但是会出现闪屏的情况,会降低用户体验。为了解决闪屏问题,咱们可以通过 @Observed 与 @ObjectLink
// @Observed 装饰类
@Observed
class ClassA{
// 略
}
// 子组件
@Component
struct Com{
@ObjectLink
数据:ClassA
}
基本使用
:::info 使用步骤:
- interface 改为 类 class 并使用 @Observed 修饰
- 通过 new 的方式完成数据的创建(通过 new 的方式来监测数据的改变)
- 状态修饰符改为 @ObjectLink
- 在父组件修改数据:不需要 splice
- 在子组件修改数据
:::
interface Person {
id: number
name: string
age: number
}
@Entry
@Component
struct ObservedAndLink {
@State personList: Person[] = [
{
id: 1,
name: '张三',
age: 18
},
{
id: 2,
name: '李四',
age: 19
},
{
id: 3,
name: '王五',
age: 20
}
]
build() {
Column({ space: 20 }) {
Text('父组件')
.fontSize(30)
List({ space: 10 }) {
ForEach(this.personList, (item: Person, index: number) => {
ItemCom({
info: item,
addAge: () => {
item.age++
this.personList.splice(index, 1, item)
}
})
})
}
}
.backgroundColor('#cbe69b')
.width('100%')
.height('100%')
.padding(20)
}
}
@Component
struct ItemCom {
@Prop info: Person
addAge = () => {
}
build() {
ListItem() {
Row({ space: 10 }) {
Text('姓名:' + this.info.name)
Text('年龄:' + this.info.age)
Blank()
Button('修改数据')
.onClick(() => {
this.addAge()
})
}
.backgroundColor(Color.Pink)
.padding(10)
.width('100%')
}
}
}
@Observed
class Person {
id: number
name: string
age: number
constructor(id: number, name: string, age: number) {
this.id = id
this.name = name
this.age = age
}
}
@Entry
@Component
struct ObservedAndLink {
@State personList: Person[] = [
new Person(1, '张三', 18),
new Person(2, '李四', 18),
new Person(3, '王五', 18),
]
build() {
Column({ space: 20 }) {
Text('父组件')
.fontSize(30)
List({ space: 10 }) {
ForEach(this.personList, (item: Person, index: number) => {
ItemCom({
info: item,
addAge: () => {
item.age++
}
})
})
}
}
.backgroundColor('#cbe69b')
.width('100%')
.height('100%')
.padding(20)
}
}
@Component
struct ItemCom {
@ObjectLink info: Person
addAge = () => {
}
build() {
ListItem() {
Row({ space: 10 }) {
Text('姓名:' + this.info.name)
Text('年龄:' + this.info.age)
Blank()
Button('修改数据')
.onClick(() => {
this.addAge()
// 这种写法也可以
// this.info.age++
})
}
.backgroundColor(Color.Pink)
.padding(10)
.width('100%')
}
}
}
:::info 注意:
- @Prop 修改之后无法同步给父组件(页面可以更新,父组件数据不更新)
- @ObjectLink,数据的更新会同步到父组件(推荐)
:::
知乎案例优化
使用刚刚学习的内容,来优化知乎案例
:::success 核心步骤:
- interface 改为 类 class 并使用 @Observed 修饰
- 通过 new
- 状态修饰符改为 @ObjectLink
- 在父组件修改数据:不需要 splice
- 在子组件修改数据:直接修改
:::
补充一个写法:
:::info 接口转类,构造函数只需要写一个参数:
:::
// 1. 增加接口
interface IReply {
id: number
avatar: ResourceStr
author: string
content: string
time: string
area: string
likeNum: number
likeFlag: boolean
}
// 2. 定义类,实现接口
class ReplyItem implements IReply {
id: number
avatar: ResourceStr
author: string
content: string
time: string
area: string
likeNum: number
likeFlag: boolean
// 构造函数,接收接口类型的参数,内部通过点语法完成赋值
constructor(item: IReply) {
this.id = item.id
this.avatar = item.avatar
this.author = item.author
this.content = item.content
this.time = item.time
this.area = item.area
this.likeNum = item.likeNum
this.likeFlag = item.likeFlag
}
}
基础模板:
interface ReplyItem {
id: number
avatar: ResourceStr
author: string
content: string
time: string
area: string
likeNum: number
likeFlag: boolean
}
class ReplyData {
// 全部评论数组
static getCommentList(): ReplyItem[] {
return [
{
id: 2,
avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
author: '偏执狂-妄想家',
content: '更何况还分到一个摩洛哥[惊喜]',
time: '11-30',
area: '海南',
likeNum: 34,
likeFlag: false
},
{
id: 3,
avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
author: 'William',
content: '当年希腊可是把1:0发挥到极致了',
time: '11-29',
area: '北京',
likeNum: 58,
likeFlag: false
},
]
}
// 获取顶部 评论
static getRootComment(): ReplyItem {
return {
id: 1,
avatar: $r('app.media.avatar'),
author: '周杰伦',
content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
time: '11-30',
area: '海南',
likeNum: 98,
likeFlag: true
}
}
}
@Entry
@Component
struct ZhiHu {
// 全部评论
@State commentList: ReplyItem[] = ReplyData.getCommentList()
// 顶部评论
@State rootComment: ReplyItem = ReplyData.getRootComment()
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
Scroll() {
Column() {
// 顶部组件(子组件,没功能)
HmNavBar()
// 顶部评论(子组件)
CommentItem({
replay: this.rootComment,
// 切换点赞状态~
changeLike: () => {
this.rootComment.likeFlag = !this.rootComment.likeFlag
if (this.rootComment.likeFlag == true) {
// likeFlag true +1
this.rootComment.likeNum++
} else {
// likeFlag false -1
this.rootComment.likeNum--
}
}
})
// 分割线
Divider()
.strokeWidth(6)
.color("#f4f5f6")
// 回复数 (子组件)
ReplyCount({ num: this.commentList.length })
// 回复评论列表 (子组件)
ForEach(this.commentList, (item: ReplyItem, index: number) => {
CommentItem({
replay: item,
changeLike: () => {
item.likeFlag = !item.likeFlag
if (item.likeFlag == true) {
item.likeNum++
} else {
item.likeNum--
}
this.commentList.splice(index, 1, item)
}
})
})
}
.width('100%')
.backgroundColor(Color.White)
}
.padding({
bottom: 60
})
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Off)
}
.height('100%')
// 自定义组件(子组件)
ReplyInput({
addComment: (message: string) => {
this.commentList.unshift({
id: Date.now(), // 当前的时间戳
avatar: $r('app.media.avatar'),
author: '花姐',
content: message,
time: `${new Date().getMonth() + 1}-${new Date().getDate()}`, // 月-日
area: '江西',
likeNum: 999,
likeFlag: true
})
}
})
}
.height('100%')
}
}
@Component
struct HmNavBar {
build() {
Row() {
Row() {
Image($r('app.media.ic_public_arrow_left'))
.width(20)
.height(20)
}
.borderRadius(20)
.backgroundColor('#f6f6f6')
.justifyContent(FlexAlign.Center)
.width(30)
.aspectRatio(1)
.margin({
left: 15
})
Text("评论回复")
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({
right: 35
})
}
.width('100%')
.height(50)
.border({
width: {
bottom: 1
},
color: '#f6f6f6',
})
}
}
@Component
struct CommentItem {
// 定义 Prop 接收评论信息
@Prop replay: ReplyItem
// 点赞 函数
changeLike = () => {
}
build() {
Row() {
Image(this.replay.avatar)// 头像
.width(32)
.height(32)
.borderRadius(16)
Column({ space: 10 }) {
Text(this.replay.author)// 作者
.fontWeight(600)
Text(this.replay.content)// 内容
.lineHeight(20)
.fontSize(14)
.fontColor("#565656")
Row() {
Text(`${this.replay.time} . IP属地 ${this.replay.area}`)
.fontColor("#c3c4c5")
.fontSize(12)
Row() {
Image($r('app.media.like'))
.width(14)
.aspectRatio(1)
.fillColor(this.replay.likeFlag ? Color.Red : Color.Green)// "#c3c4c5" 或 red
.onClick(() => {
// 修改点赞状态
this.changeLike()
})
Text(this.replay.likeNum.toString())
.fontSize(12)
.margin({
left: 5
})
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.padding({
left: 15,
right: 5
})
}
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Top)
.width('100%')
.padding(15)
}
}
@Component
struct ReplyCount {
@Prop num: number
build() {
Text() {
Span('回复')
Span(`${this.num}`)
}
.padding(15)
.fontWeight(700)
.alignSelf(ItemAlign.Start)
}
}
@Component
struct ReplyInput {
@State message: string = ''
addComment = (message: string) => {
}
build() {
Row() {
TextInput({ placeholder: '回复', text: $$this.message })
.layoutWeight(1)
.backgroundColor("#f4f5f6")
.height(40)
Text('发布')
.fontColor("#6ecff0")
.fontSize(14)
.margin({
left: 10
})
.onClick(() => {
// console.log(this.message)
// 将输入的文本,传递给 父组件
this.addComment(this.message)
// 清空
this.message = ''
})
}
.padding(10)
.backgroundColor(Color.White)
.border({
width: { top: 1 },
color: "#f4f5f6"
})
}
}
参考代码:
// 1. 调整数据的定义,方便后续使用 new 进行实例化
interface IReply {
id: number
avatar: ResourceStr
author: string
content: string
time: string
area: string
likeNum: number
likeFlag: boolean
}
@Observed
class ReplyItem implements IReply {
id: number = 0
avatar: ResourceStr = ""
author: string = ""
content: string = ""
time: string = ""
area: string = ""
likeNum: number = 0
likeFlag: boolean = false
constructor(item: IReply) {
this.id = item.id
this.avatar = item.avatar
this.author = item.author
this.content = item.content
this.time = item.time
this.area = item.area
this.likeNum = item.likeNum
this.likeFlag = item.likeFlag
}
}
class ReplyData {
// 全部评论数组
static getCommentList(): ReplyItem[] {
return [
new ReplyItem({
id: 1,
avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
author: '偏执狂-妄想家',
content: '更何况还分到一个摩洛哥[惊喜]',
time: '11-30',
area: '海南',
likeNum: 34,
likeFlag: false
}),
new ReplyItem({
id: 2,
avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
author: 'William',
content: '当年希腊可是把1:0发挥到极致了',
time: '11-29',
area: '北京',
likeNum: 58,
likeFlag: false
}),
new ReplyItem({
id: 3,
avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
author: 'Andy Garcia',
content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
time: '11-28',
area: '上海',
likeNum: 10,
likeFlag: false
}),
new ReplyItem({
id: 4,
avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
author: '正宗好鱼头',
content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
time: '11-27',
area: '香港',
likeNum: 139,
likeFlag: false
}),
new ReplyItem({
id: 5,
avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
author: '柱子哥',
content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
time: '11-27',
area: '旧金山',
likeNum: 29,
likeFlag: false
}),
new ReplyItem({
id: 6,
avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
author: '飞轩逸',
content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
time: '11-26',
area: '里约',
likeNum: 100,
likeFlag: false
})
]
}
// 获取顶部 评论
static getRootComment(): ReplyItem {
return new ReplyItem({
id: 1,
avatar: $r('app.media.avatar'),
author: '周杰伦',
content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
time: '11-30',
area: '海南',
likeNum: 98,
likeFlag: true
})
}
}
@Entry
@Component
struct ZhiHu {
// 全部评论
@State
@Watch('commentListChange')
commentList: ReplyItem[] = ReplyData.getCommentList()
// 顶部评论
@State rootComment: ReplyItem = ReplyData.getRootComment()
commentListChange() {
console.log('123')
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
Scroll() {
Column() {
// 顶部组件(子组件,没功能)
HmNavBar()
// 顶部评论(子组件)
CommentItem({
replay: this.rootComment,
// 切换点赞状态~
changeLike: () => {
this.rootComment.likeFlag = !this.rootComment.likeFlag
if (this.rootComment.likeFlag == true) {
// likeFlag true +1
this.rootComment.likeNum++
} else {
// likeFlag false -1
this.rootComment.likeNum--
}
}
})
// 分割线
Divider()
.strokeWidth(6)
.color("#f4f5f6")
// 回复数 (子组件)
ReplyCount({ num: this.commentList.length })
// 回复评论列表 (子组件)
ForEach(this.commentList, (item: ReplyItem, index: number) => {
CommentItem({
replay: item,
changeLike: () => {
// 可以在父组件直接修改 item
item.likeFlag = !item.likeFlag
if (item.likeFlag == true) {
item.likeNum++
} else {
item.likeNum--
}
// 移除 splice
// this.commentList.splice(index, 1, item)
}
})
})
}
.width('100%')
.backgroundColor(Color.White)
}
.padding({
bottom: 60
})
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Off)
}
.height('100%')
// 自定义组件(子组件)
ReplyInput({
addComment: (message: string) => {
this.commentList.unshift(new ReplyItem({
id: Date.now(), // 当前的时间戳
avatar: $r('app.media.avatar'),
author: '花姐',
content: message,
time: `${new Date().getMonth() + 1}-${new Date().getDate()}`, // 月-日
area: '江西',
likeNum: 999,
likeFlag: true
}))
}
})
}
.height('100%')
}
}
@Component
struct HmNavBar {
build() {
Row() {
Row() {
Image($r('app.media.ic_public_arrow_left'))
.width(20)
.height(20)
}
.borderRadius(20)
.backgroundColor('#f6f6f6')
.justifyContent(FlexAlign.Center)
.width(30)
.aspectRatio(1)
.margin({
left: 15
})
Text("评论回复")
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({
right: 35
})
}
.width('100%')
.height(50)
.border({
width: {
bottom: 1
},
color: '#f6f6f6',
})
}
}
@Component
struct CommentItem {
// 定义 Prop 接收评论信息
@ObjectLink replay: ReplyItem
// 点赞 函数
changeLike = () => {
}
build() {
Row() {
Image(this.replay.avatar)// 头像
.width(32)
.height(32)
.borderRadius(16)
Column({ space: 10 }) {
Text(this.replay.author)// 作者
.fontWeight(600)
Text(this.replay.content)// 内容
.lineHeight(20)
.fontSize(14)
.fontColor("#565656")
Row() {
Text(`${this.replay.time} . IP属地 ${this.replay.area}`)
.fontColor("#c3c4c5")
.fontSize(12)
Row() {
Image($r('app.media.like'))
.width(14)
.aspectRatio(1)
.fillColor(this.replay.likeFlag ? Color.Red : Color.Green)// "#c3c4c5" 或 red
.onClick(() => {
// 修改点赞状态
this.changeLike()
// 也可以直接在子组件修改数据,都可以正常检测
// this.replay.likeFlag = !this.replay.likeFlag
// if (this.replay.likeFlag == true) {
// this.replay.likeNum++
// } else {
// this.replay.likeNum--
// }
})
Text(this.replay.likeNum.toString())
.fontSize(12)
.margin({
left: 5
})
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.padding({
left: 15,
right: 5
})
}
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Top)
.width('100%')
.padding(15)
}
}
@Component
struct ReplyCount {
@Prop num: number
build() {
Text() {
Span('回复')
Span(`${this.num}`)
}
.padding(15)
.fontWeight(700)
.alignSelf(ItemAlign.Start)
}
}
@Component
struct ReplyInput {
@State message: string = ''
addComment = (message: string) => {
}
build() {
Row() {
TextInput({ placeholder: '回复', text: $$this.message })
.layoutWeight(1)
.backgroundColor("#f4f5f6")
.height(40)
Text('发布')
.fontColor("#6ecff0")
.fontSize(14)
.margin({
left: 10
})
.onClick(() => {
// console.log(this.message)
// 将输入的文本,传递给 父组件
this.addComment(this.message)
// 清空
this.message = ''
})
}
.padding(10)
.backgroundColor(Color.White)
.border({
width: { top: 1 },
color: "#f4f5f6"
})
}
}
@Watch监听
- @State:修饰之后,数据改变,页面改变
- @Prop:父子组件通讯
- @Observed,@ObjectLink
- 嵌套数据,可以监测到数据的改变
- 数据:interface->class ,@Observed修饰
- 子组件接受数据:@ObjectLink
- 对象数组循环,更改对象的属性(知乎,评论,购物车。。)
- @Link
- 父子双向同步
- @Provide,@Consume
祖先和后台同步
- @Watch
- 监视数据,数据改变-》执行方法
- 可以执行自定义的逻辑,比如弹框,比如和服务器通讯。。。