鸿蒙应用开发系列文章:第五天 - 组件化开发思想开发鸿蒙案例(详解父子组件传值)

鸿蒙应用开发从入门到入行

第五天 - 组件化开发思想开发鸿蒙案例(详解父子组件传值)

导读:在本篇文章里,您将掌握组件化开发、组件传值等相关知识。并能彻底弄懂鸿蒙父子组件数据的同步机制。本篇干货很多,特别是有些关于组件的细节,需要好好掌握。

本次整体学习目标介绍

  • 最近比较忙,不过好在本文也是紧赶慢赶的弄出来了。

  • 话不多说,我们先回顾一下我们需要做的案例

  • 我们发现,这是一个综合性比较强的案例,涉及了布局、状态切换、列表渲染、数据新增、侧滑删除等功能。非常适合入门时的综合练手。

  • 接下来,我们分析一下这个案例布局大致的划分

Progress的使用

image-20240812175525224

  • 咱们本次案例中需要用到一个进度缓,如上图

  • 这种效果ArkUI已为我们提供了内置组件,即 Progress

  • 使用方法

    Progress( { value: 当前值, total: 总值 } )
    
  • Progress( { value: 4, total: 10 } ) // 总量10,已完成4
    
  • 默认情况下,Progress产生的是水平进度条,因此,上述代码会得到如下图效果

    image-20240812175905752

  • 一些细节:

    • 默认情况下,value为0,total为100

    • 当Progress的宽度大于或等于高度时水平显示(如上图)

    • 当Progress的宽度小于高度时(不包含等于,必须小于高度)垂直显示(如下图)

      image-20240812180157289

  • type参数

    • 用法

      Progress( { value: 5, total: 10, type: ProgressType.Ring } )
      
    • 通过使用Progress传入type属性,可以修改Progress的样式

    • 该参数需要传入ProgressType类型的枚举,一共有5种样式,分别如下

      • ProgressType.Linear:线性样式,默认值。样式为上边两张图效果。

      • ProgressType.Ring:环形样式(无刻度),样式如下

        img

      • ProgressType.ScaleRing:环形样式(有刻度),样式如下

        img

      • ProgressType.Eclipse:圆形样式

        img

      • 和ProgressType.Capsule:胶囊样式(通过宽高设置水平或水平样式,参照ProgressType.Linear)

        img

  • 修改颜色,默认情况下,进度条底色为灰色,前景色(进度值的色)为渐变蓝色。如果需要改,可以分别通过backgroundColor(背景色)与 color(前景)改

            Progress({ value: 3, total: 10, type: ProgressType.Ring })
              .width(80)
              .height(80)
              .backgroundColor(Color.Red)
              .color(Color.Pink)
    
    • 这里是把背景改为红,进度值为粉色,如下图

      image-20240812233332356

组件化开发

  • 组件:组成页面的一部分
  • 组件化开发:把页面的每一部分当成一个组件,然后把这些组件像搭积木一样搭在一起即为组件化开发
  • 组件化开发优势:代码分门别类,页面与逻辑内聚,方便阅读与维护、方便复用,等等,总之好多好多优点,这就不说了。

项目准备

  • 我们来看看,这个年度待办案例,我们本次分几个区域(几个组件)

    image-20240809093615731

  • 通过上图发现,我们需要三大区域,分别对应头部区(统计)输入区列表展现区,因此新建三个组件,然后集中到Index.ets来用

  • 步骤

    • 新建项目,过程略

    • 把侧滑显示的删除图标放到entry/src/main/resources/base/media文件夹里(点我下载

    • pages同级目录,新建view文件夹

    • view文件夹,新建三个ets文件,分别起名叫TodoHeaderTodoInputTodoMain,里面,每个组件里放一个Text作为暂时的显示,且导出这个组件,这里仅贴TodoHeader代码,其他两个组件类似

       
      export struct TodoHeader {
        build () {
          Text('TodoHeader')
        }
      }
      
    • 然后来到pages/Index.ets,导入这三个组件,并依次写到Column里(因为这三个组件如上图所示,就是从上到下依次排列)

      • 技巧:上篇说过,不用导入,只要在组件写了export的情况下,直接写组件名出提示后按回车,会自动生成导入代码
      • Index.ets代码如下
      import { TodoHeader } from '../view/TodoHeader'
      import { TodoInput } from '../view/TodoInput'
      import { TodoMain } from '../view/TodoMain'
      
      
      
      struct Index {
        build() {
          Column({ space: 20 }) {
      
            TodoHeader()
            TodoInput()
            TodoMain()
          }
          .height('100%')
          .width('100%')
          .backgroundColor('#f2f3f5')
        }
      }
      
    • 注意:根据效果图发现整个页面背景是灰色的,因此记得给整个Column加背景色,每个区域之间有间距,因此加space

    • 至此,项目准备工作完成

TodoHeader - 布局实现

  • 分析布局如图

    image-20240812174007588

  • 根据上图很明显能发现,就是一个Row(水平布局),里面一个Text显示标题,一个Stack放层叠布局(里面再放Progress与Text,具体后面说)

    • 记得给Row宽度、高度与背景色、Text要加粗、内容居中等
  • 基于此,代码如下

    
    export struct TodoHeader {
      build() {
        Row() {
          Text('今年目标')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
            .margin({ right: 20 })
    
    
          Stack() {
            Progress({ value: 0, total: 10, type: ProgressType.Ring })
              .width(80)
              .height(80)
            Text(`0 / 10`)
          }
        }
        .width('100%')
        .height(110)
        .justifyContent(FlexAlign.Center)
        .backgroundColor(Color.White)
      }
    }
    
  • 这里其他属性都很容易,如果有不懂的可以评论区留言

  • 主要解释下Stack环形进度条那里

    image-20240812234111654

    • 如上图解释的:这里其实有两个组件Progress用来显示环形进度条,但是它没有文字,所以还要搭一个Text

    • 而如果不加堆叠,则他们会并排显示,如下图

      image-20240812233916354

    • 所以此时我们是期望能让 3 / 10 这个Text能叠在Progress上面,所以这种层叠效果,需要套一个Stack来实现(注意,默认情况下后面的在最上层,因此Text必须放在最后。除此外,默认都是居中堆叠,所以刚好Text就在环形中间了)

  • 注意:本代码里的宽高颜色都可以由各位根据预览效果自行设置,不必强求与猫林老师一致

TodoInput - 布局实现

image-20240813000951665

  • 如上图所示,Row里放TextInput即可,记得让TextInput不用铺满Row。因为将来必然会需要拿到输入框里的内容,因此声明个变量,并与之双向绑定

  • 代码如下

    
    export struct TodoInput {
       newFlag: string = ''
    
      build() {
    
        Row() {
          TextInput({ placeholder: '请输入新目标', text: $$this.newFlag })
            .width('90%')
        }
        .width('100%')
        .height(80)
        .backgroundColor(Color.White)
        .justifyContent(FlexAlign.Center)
      }
    }
    

TodoMain - 布局实现

image-20240813001656133

  • 发现这个区域就是一堆上一篇文章里教大家封装的组件,直接拿来用即可

    • 注:上一篇里猫林老师不小心起错名字。其实规范的名字应该叫TodoItem,上一篇写成了ToDoItem,这里正好勘误,用更规范的写法
  • 步骤

    • 来到view新建TodoItem.ets文件,然后写如下代码(分析过程略,见上篇)

      
      export struct TodoItem {
        build() {
          Row() {
            Checkbox()
              .margin({ left: 20, right: 20 })
            Text('跟猫林学鸿蒙')
          }
          .width('100%')
          .height(40)
          .backgroundColor(Color.White)
          .borderRadius(20)
        }
      }
      
    • 然后来到TodoMain组件,进行导入与使用,为了暂时能看到一堆内容,我们用ForEach先循环生成10个, TodoMain代码如下

      import { TodoItem } from './TodoItem'
      
      
      export struct TodoMain {
         todoList: number[] = [1, 2, 3, 4, 5, 6, 7]
      
        build() {
          Column({ space: 10 }) {
            ForEach(this.todoList, (item: number) => {
              TodoItem()
            })
          }
          .width('100%')
        }
      }
      
      • 这里TodoMain的根容器是Column,因为它需要一行一行从上到下来显示

初始数据与说明

  • 初始数据如下

     class TodoModel {
       text: string = ''
       finished: boolean = false
     }
    
     @State totalFlags: Array<TodoModel> = [
        { text: "月入5万", finished: false },
        { text: "中彩票500万", finished: false },
        { text: "找个富婆", finished: false },
        { text: "买套别墅", finished: false },
        { text: "改掉爱做梦的习惯", finished: false },
      ];
    
  • 解释

    • 每个新年目标都是个对象,有两种属性:text(目标文字)、finished(是否完成)
    • 以下是对语法的解释,会TS的可跳过下面这三段
      • 因为ArkTS是一种类型严谨的语言,因此需要对这种对象做一个类型定义,即声明一个类叫TodoModel,它里面有两个这样的属性
      • totalFlags即为这种对象类型的数组,例如Array代表数组每个元素都是数值类型的数组,所以上面写的Array代表数组每个元素都是TodoModel类型的数组
      • 数组也可以简写为 number[] 、本例中的Array<TodoModel>可简写为 TodoModel[]
  • 按照官方的编码规范指导,这种项目中用到的数据对应的类,要写到viewsmodel文件夹(与pages平级),我们新建好文件夹后,也在此文件夹再新建TodoModel.ets文件,声明这个类,并导出

    image-20240813003652455

    • 文件内代码如下
    export class TodoModel {
      text: string = ''
      finished: boolean = false
    }
    
  • 根据功能分析,本案例的年度目标必然是一个数组,且可对其增、删、改。但是这个数组在本案例中三大组件里都需要用:

    • TodoHeader 需要数组来统计总目标和已完成目标
    • TodoInput 需要给数组添加新内容
    • TodoMain 需要展示数组内容,并且将来侧滑时需要能删除数组内容
  • 基于上面需求,提问:数组应该放在哪?

    • 没错,应该放到他们共同的父组件,本案例即为Index.ets里。
    • 来到Index.ets,导入上面声明的类,并声明一个数组
    ......
    
    import { TodoModel } from '../viewmodel/TodoModel'
    
    
    
    struct Index {
      
       totalFlags: Array<TodoModel> = [
        { text: "月入5万", finished: false },
        { text: "中彩票500万", finished: false },
        { text: "找个富婆", finished: false },
        { text: "买套别墅", finished: false },
        { text: "改掉爱做梦的习惯", finished: false },
      ];
    
      build() {
        ......
      }
    }
    

组件传参 - 父传子

  • 此时数据有了,但还没交给TodoMain去显示,此时相当于要把Index里的数据给TodoMain,且因为Index是父组件,TodoMain是子组件,因此这种数据传递方式叫父传子,即把父的数据传递给子组件

    • 注:本章节主要是为了学习语法,最终部分代码不会出现于年度待办案例,因此此时先把TodoMainTodoItem代码做一个备份。然后再开始练下面语法
  • 语法步骤:

    • 子里声明一个成员变量
    • 父里使用组件时,在小括号里传入
  • 例如,本案例中我们有 TodoMainTodoItem,因为TodoMain包含了TodoItem。所以TodoMain是父TodoItem是子。我们就用这两个组件试试父传子

  • 代码步骤:

    • TodoItem里声明一个变量叫name,并在Text里使用,代码如下

      export struct TodoItem {
        // 声明个成员变量,等待父传,注意:此时没加任意装饰器
        name: string = ''
      
        build() {
      		.......
          Text(this.name)
        }
      }
      
    • TodoMain里声明一个变量name,并传递给TodoItem

      
      export struct TodoMain {
        .......
         name: string = 'abc'
      
      
        build() {
          Column({ space: 10 }) {
            ........
            ForEach(this.todoList, (item: number) => {
              // 这里是传参,把父的name传递给了子里的name
              TodoItem({ name: this.name })
            })
          }
          .width('100%')
        }
      }
      
    • 此时会发现,正因为把父的name,也即数据为abc,传递给了子,所以此时TodoItem显示的即为abc,如下图

      image-20240813102012962

  • 注意,此时虽能成功父传子,但是父的数据一旦改变,子并不会跟着改变

  • 我们此时可以测试一下,在TodoMain里,添加一个Button,并在Button里修改掉name的值,如下代码

    export struct TodoMain {
    	....
       name: string = 'abc'
    
      build() {
        Column({ space: 10 }) {
          Button('我改').onClick((event: ClickEvent) => {
            this.name = 'Good猫林'
          })
           
         .........
         }
    		......
      }
    }
    
    • 点击按钮后会发现,子组件也即TodoItem上没有任何变化
  • 如果希望实现父的数据改变,子的数据能随着改变,需要在子的变量前加@Prop装饰器

    • 修改TodoItem里的name,前面加上@Prop,再来点击按钮看变化

      export struct TodoItem {
        // 此时加了@Prop修饰
         name: string = ''
      
        build() {
          Row() {
            Checkbox()
              .margin({ left: 20, right: 20 })
            Text(this.name)
          }
         
         .......
        }
      }
      
    • 此时点击按钮,让父的数据改变,子里的也跟着变

      image-20240813102604724

  • 到目前,我们已经学了两个用来修饰成员变量的装饰器,分别是@State@Prop,我们对它总结一下

    • @State: 主要是装饰给组件自己使用的数据,效果:能让成员变量的值改变后,界面也能刷新
    • @Prop: 主要是用在作为子组件时,用来装饰由父传递过来的数据,效果:能让父的数据改变子也能接收到
      • 注意:在ArkTS中,即使父传递的是引用类型的数据,若不加@Prop修饰,一样会导致父的数据改变子里不会变,同学们有兴趣可以自行测试

组件传参 - 父传子双向同步

  • 上面我们讲到,子里的成员变量加@Prop后,即可让父的数据改变,子随之改变,也即父的数据自动同步到子

  • 但是,目前无法实现子同步到父,也即子里改变了这个父传进来的数据,子里自身能改变,但是父的无法改变。也即Vue框架里的单向数据流

    • 例:在TodoMain里用一个Text显示name的值,并在TodoItem里给Row加点击事件并修改name的值,我们可观察效果

      • TodoMain代码

        export struct TodoMain {
        	....
           name: string = 'abc'
        
          build() {
            Column({ space: 10 }) {
              Button('我改').onClick((event: ClickEvent) => {
                this.name = 'Good猫林'
              })
                
              Row() {
                // 显示name
                Text(this.name)
              }
               
             .........
             }
        		......
          }
        }
        
      • TodoItem代码

        export struct TodoItem {
          // 此时加了@Prop修饰
           name: string = ''
        
          build() {
            Row() {
             ......
            }
            .onClick(() => {
              this.name = '学鸿蒙'
            })
           
           .......
          }
        }
        
    • 发现当我们点击TodoItem时,TodoItem自身的name数据了学鸿蒙,但是父里的还是abc,如图

      image-20240813153206161

  • 如果希望实现:父改了数据,自动同步到子。以及子改了数据,也同步到父,需要把子里的@Prop修改成@Link

  • 注意:如果用@Link修饰了成员变量,则成员变量不可初始化,例

    export struct TodoItem {
      // 此时加了@Link,不用初始化
       name: string
    
      build() {
        .......
      }
    }
    
  • 虽然仅仅只是把@Prop改成了@Link,且把初始化值的部分删了,但此时已经完成了双向同步,我们来点击一下Row测试一下效果,会发现如下图所示,子和父都变成了学鸿蒙

    image-20240813153338538

  • 总结@Prop@Link

    • 相同点:
      • 都是用在子组件,用来接收父传递过来的数据,
      • 都可以实现父改变数据后同步给子
    • 不同点:
      • 初始化值不同:
        • @Prop需要初始化值,相当于给默认值。可以实现,父如果传了就用父的数据,如果没传则用默认值
        • @Link不能初始化,相当于必须要父传递数据了才有数据
      • 同步给父不同
        • @Prop修饰的数据,子里改变了不会同步给父
        • @Link修饰的数据,子里改变了会同步给父

年度目标案例 - 数据展示

  • 回到我们之前最初的需求:要把Index里的数据给TodoMain,经历了上面组件传参的学习后,我们能很快完成。

  • 步骤:

    • 注意:先把之前备份的TodoMain与TodoItem恢复

    • TodoMain里,声明成员变量接收Index传递过来的数组,之前备份的文件里其实已经声明过,只不过是number类型的,改成TodoModel即可,代码如下

       todoList: TodoModel[] = []
      
      • 解释为什么用 @Link修饰符
        • 因为在TodoMain里将来需要改变数组(例如侧滑删除等),需要同步给父组件(即Index),因此用@Link
    • Index里传递(这里Index的数据在上面初始数据与说明章节已声明过)

      TodoMain({ todoList: this.totalFlags })
      
    • 此时已经把数组从Index传递到TodoMain,但是TodoMain里又需要遍历这个数组去产生TodoItem,并且需要把数组每一项交给TodoItem去渲染,所以这里又要父传子,思路示意图如下

      image-20240813154431601

    • 来到TodoItem,声明一个TodoModel的对象,用来接收数组每一项,并且把item里的finished属性绑定给Checkboxtext属性绑定给Text去显示

      import { TodoModel } from '../viewmodel/TodoModel'
      
      
      export struct TodoItem {
      
         item: TodoModel = new TodoModel() // 本次代码
      
        build() {
          Row() {
            Checkbox()
              .select(this.item.finished) // 本次代码
              .margin({ left: 20, right: 20 })
            Text(this.item.text) // 本次代码
          }
          .width('100%')
          .height(40)
          .backgroundColor(Color.White)
          .borderRadius(20)
        }
      }
      
    • TodoMain里使用ForEach渲染

      import { TodoItem } from './TodoItem'
      import { TodoModel } from '../viewmodel/TodoModel'
      
      
      export struct TodoMain {
         todoList: TodoModel[]
      
        build() {
          Column({ space: 10 }) {
           
              // 本次代码
              ForEach(this.todoList, (item: TodoModel, index: number) => {
                  // 这里用了ES6简写,完整写法是 TodoItem({ item: item })代表把ForEach里的item传递给TodoItem需要的item
                  TodoItem({ item })
              })
            // 以上是本次代码改动
          }
          .width('100%')
        }
      }
      
  • 此时便完成了数组的渲染

年度目标案例 - 目标完成打勾

  • 需求如下:

    image-20240813164303085

  • 根据上面数据说明,打勾与否需要与每一项的finished属性绑定,因此先来到TodoItemCheckboxselect属性做双向绑定

          Checkbox()
            .select($$this.item.finished)
    
    • 注:
      • Checkbox的select属性方法是用来设置它是否打勾的。传true代表打勾,传false代表不打勾
      • 这里我们除了要数据能影响Checkbox以外,也需要当我们操作组件后结果能影响到数据,因此加$$做双向绑定
  • 然后根据finished的值,做不同的处理

    • 如果是true,给文字加删除线,否则不加任何线

    • 如果是true,给文字加opacity为半透明,否则不透明

      • 解释:通过需求图可以看到,要让文字和删除线都变灰,最快的方式是给整个Text加透明度,配合白色底就会呈现灰色。而如果单独知识改文字颜色也即fontColor只会让文字变灰,删除线不变,还得在decoration里加color才有用,不方便
    • 代码如下

      Text(this.item.text)// 本次代码
        .opacity(this.item.finished ? 0.4 : 1)
        .decoration({ type: this.item.finished ? TextDecorationType.LineThrough : TextDecorationType.None })
      
      • 注:decoration就是Text设置文字线条的样式,可以设置删除线、下划线等,具体可查官方文档

插播 - 目前鸿蒙开发的缺陷

  • 目前虽然实现了数据展现与目标完成的打勾,但此时存在数据传递的问题

  • 通过最终效果图我们知道,我们需要统计出已完成的目标数量展现到进度条里。但此时,我们在TodoItem里打勾了后,finished的值已经改变(通过界面有变化能证明),但是没有传入到父,导致父里的finished还是false

    • 我们在父里(TodoMain)里写一个Row输出数组里下标0的元素的finished

      .......
      
        build() {
          Column({ space: 10 }) {
      
            Row() {
              Text(this.todoList[0].finished + '')
            }
      
            ..........
          }
        }
      .....
      
    • 注:这里的+ '' 是为了把finished转为字符串类型,因为Text只能用字符串

    • 然后给第一项打勾,会发现如下图效果

      image-20240813171303886

  • 大家思考一下:为什么会这样呢?

    • 请思考
    • 请思考
    • 请思考
    • 请思考
    • 请思考
    • 没错,就是因为子里用的是@Prop,我们上面说过,@Prop只能实现父数据同步到子,子里变了无法同步到父,此时就是这原因导致
  • 如何解决?

    • 相信同学们都能想到用@Link,父子双向同步。但此时,鸿蒙的缺陷体现出来了!当你把TodoItem里的item改成@Link装饰后,惊讶地发现:在TodoMain里报错了!如下图

      image-20240813171812681

  • 原因:语法限制,@Link只能接收父组件里声明的第一层成员变量

  • 什么叫第一层?

    • 就好比一个数组,数组里全是对象。对于数组而言即为第一层,数组里的每个对象称之为第二层,以此类推
    • 再好比一个嵌套对象。即对象里有个属性又是对象,那么外层的称之为第一层,里面的属性即为第二层,以此类推
  • 所以上述报错里写的item相当于就是数组里的对象,也即第二层,所以报错

  • 出现这个语法限制的根本原因是:目前的鸿蒙开发中,默认情况下无法监听到第二层的改动。而@Link又要实现双向同步,你都无法监听到改动,又如何完成双向同步呢?

  • 所以鸿蒙也给了解决方案:使用@Observed@ObjectLink来解决。但是,猫林老师这里不打算讲它。因为这个解决方案其实用起来也很麻烦繁琐,非常不人性化。

    • 有兴趣的同学可以自行去学习Observed与ObjectLink,猫林老师这里只讲最实用的技术,帮助你更快开发鸿蒙!
    • 题外话,从API12开始,鸿蒙提供了@ObservedV2,力求解决监听这种监听属性的问题。但猫林老师尝鲜过,也不是很方便,这里赞不推荐
    • 题外话2:不要一听猫林老师吐槽一句鸿蒙开发中存在这种不太方便的点,就觉得鸿蒙不行。任何语言刚推出时总有些坑或者不太方便的地方需要等待后续更新。当初苹果的Swift刚开始推出时也有许多地方有待完善,苹果也是慢慢更新使之越来越好。所以大家要保持好信心,未来可期。而且,华为官方也确实不断的对鸿蒙开发做着改进,就像上面提到的@ObservedV2。所以鸿蒙开发未来必然会越来越方便,越来越容易好用。可是到那时越容易就会越多人涌进,但是不要怕。你们在此刻开始进入鸿蒙的人,都算得上是鸿蒙的元老级程序员。(毕竟现在连纯血鸿蒙的正式版都还没发布,仅仅是测试版呢),所以对你们,更是抓住风口的弄潮儿,必将成为吃到红利的一波人!

待办列表 - 解决缺陷 - 实现打勾改动同步给父

  • 那,猫林老师这里怎么解决上述缺陷呢?首先,因为@Link目前不能用,那咱们就把它换回@Prop

    .....
    
    export struct TodoItem {
       item: TodoModel
      .....
    }
    
  • 可是@Prop又确实无法让父的数据同步改变,该怎么办呢?

    点击查看图片来源

  • 既然子里无法改动到父,那就换个思路。让父,自己改!!

  • 整体思路是:

    • 让父提供一个修改数据的方法, 子里要修改时调用父的方法即可修改!

    • 如图

      image-20240814001242310

  • 实现:

    • 来到TodoMain,声明一个方法如下

        changeStatus(item: TodoModel, index: number) {
          this.todoList[index] = {
            text: item.text,
            finished: !item.finished
          }
        }
      
      • 解释:
        • 本方法需要传入被点的item以及被点的item的索引
        • 通过索引的方式改掉数组中这一项,文字不变,但是完成状态取反即可
        • 这时候可能有老铁有疑问:
          • 为什么不直接 item = !item.finished
          • 还是那个问题:目前不支持监听第二层数据改变,直接改item还是第二层。
          • 但数组是第一层,因此你用数组[索引]的方式,就是在改第一层数据,这是能被监听到的
    • 此时需要把这个方法传递给TodoItem,因此TodoItem需要声明一个成员方法来接收

      export struct TodoItem {
        ......
        onChange: () => void = () => {}
        .....
      }
      
      • 解释:
        • 方法名叫onChange,它的类型是一个无参数无返回值的函数,初始化值是一个空函数
        • 一般情况下,这种由外界传入的方法不需要加装饰器
    • 然后给Select组件加onChange 事件,这个事件是当Select发生勾选状态改变就会调用的事件,在里面调用传入的onChange方法

           Checkbox()
              .select(this.item.finished)
              .margin({ left: 20, right: 20 })
              .onChange(() => {
                // 本次代码
                this.onChange()
              })
      
    • 回到TodoMain做方法传递:此时调用TodoItem除了之前要传入的item,现在还要多一个onChange

            ForEach(this.todoList, (item: TodoModel, index: number) => {
              // 此时调用TodoItem
              TodoItem({
                item, onChange: () => {
                  this.changeStatus(item, index)
                }
              })
            })
      
      • 解释参数:item即为当前变动的数据,index即为当前数据对应的下标(都是changeStatus需要用到的内容)
      • 注意看:这里我没有直接把this.changeStatus 这个方法传递给onChange,而是声明了一个新的箭头函数,只不过在箭头函数里的函数体里调用了this.changeStatus,这么做的原因是this.changeStatus方法里有this.todoList这样的代码,我们都知道this是指当前环境上下文,它在TodoMain里,就代表TodoMain这个上下文,所以它修改它的todoList没毛病。但如果你是直接把this.changeStatus传递给onChange,那它相当于是在todoItem里调用,同样的this也会变成todoItem上下文,此时它是没有todoList数组的,所以这里利用箭头函数保留当前上下文的特点,在todoMain里用箭头函数再包一层,即可保证this依然指向todoMain ------- 这段请可能理解,因为文字有其局限性,实在不理解等以后猫林老师出视频教程再来看
      • 附:有懂JS的同学可能会问:那可以 onChange: this.changeStatus.bind(.....)这样的方式传递吗?似乎也能绑定当前this指向呀?
        • 答:不能!ArkTS中不允许使用callapplybind
        • 具体可查看当前最新文档,点我进入,打开页面后搜:不支持Function.bind
  • 至此,状态改变同步给父已完成。

  • 鉴于篇幅关系以及一篇文章颗粒度太大也影响大家阅读。所以年度目标这个案例的剩余部分我在下一篇文章讲完,其中下一篇也会以需求驱动的方式为大家讲到很多干货新知识,请拭目以待!

总结今日内容

  • 组件

  • 组件化开发思想

  • 父子组件数据传递与同步

  • 本篇干活与细节略多,需要多认真思考学习。

  • 忠告:我们是以需求驱动学习知识点。特别是本篇含有部分需要理解与实操的内容。鸿蒙零基础的同学,一定要好好的跟着敲本案例代码才能理解

课后练习

  • 判断题
    • 一个Progress的宽度是100,高度是150,此时环形图一定是垂直方向的线性样式滚动条

互动简答题

  • 思考:本案例中,最终TodoItem里的数据打勾变化后(完成状态变化),TodoMain已经能成功收到改动了。那么它的父组件,最早持有数组的Index有收到改动吗?请说出你的理由,打在评论区

判断题答案

  • 错。原因自己思考,实在不懂可以评论区留言找一起学的同学帮助
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值