• vue3的单组件的编写(二)--通过对比vue2来讲解


    🐯 单组件的编写(二)

    主要讲了

    在这里插入图片描述

    🌈 响应式数据的变化

    响应式数据是MVVM数据变驱动编程的特色, VUE的设计也是受 MVVM模型的启发,大部分开发者选择MVVM框架都是因为数据驱动编程比传统的事件驱动编程来的方便。而选择vue,则是方便中的方便。

    ⭐️ Model-View-ViewModel 简称MVVM 是一种软件架构模式,将视图UI 和 业务逻辑分开,通过逻辑数据的修改驱动视图UI的更新,因此称为 “数据驱动”,与之对应是操作DOM完成视图更新的编程方式“事件驱动”。

    🚀 原理上的变化

    作为最重要的一个亮点,Vue3 的响应式数据在设计上和Vue2 有很大的不同。

    🐳 回顾vue2的原理

    vue2 是使用了 Object.defineProperty API 的getter / setter 来实现数据的响应式,这个方法的具体用法可以参考

    MDN 的文档: Object.defineProperty - MDN

    下面使用 Object.defineProperty 实现一个简单的双向绑定 demo ,亲自敲代码试一下可以有更多的理解:

    DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>DefineProperty Demotitle>
    head>
    
    <body>
      
      <div>
        <input type="text" id="input" />
        <button onclick="vm.text = 'Hello World'"> 设置为 hello world button>
      div>
      
    
      
      <div id="output">div>
      
    
      <script>
        // 声明一个响应式数据
        let vm = {};
        Object.defineProperty(vm, 'text', {
          set(value) {
            console.log("set事件触发了");
            document.querySelector("#input").value = value;
            document.querySelector("#output").innerText = value;
          }
        })
    
        document.querySelector("#input").oninput = function(e){
          vm.text = e.target.value;
        }
    
      script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    这个小 demo 实现了这两个功能:

    1. 输入框的输入行为只修改 vm.text 的数据,但会同时更新 output 标签的文本内容
    2. 点击按钮修改 vm.text 的数据,也会触发输入框和 output 文本的更新

    当然 Vue 做了非常多的工作,而非只是简单的调用了 Object.defineProperty ,可以在官网 深入 Vue 2 的响应式原理 一章了解更多 Vue 2 的响应式原理。

    🐳 了解vue3的原理

    vue3 是使用了 Proxy API 的getter/setter 来实现数据的响应式,这个方法的具体用法参考 MDN 的文档: Proxy - MDN

    同样的,也来实现一个简单的双向绑定 demo ,这次使用 Proxy 来实现:

    DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Proxy Demotitle>
      head>
      <body>
        
        <div>
          <input type="text" id="input" />
          <button onclick="vm.text = 'Hello World'">设置为 Hello Worldbutton>
        div>
        
    
        
        <div id="output">div>
        
    
        <script>
          // 声明一个响应式数据
          const vm = new Proxy(
            {},
            {
              set(obj,key,value){
                console.log("set事件触发了");
                document.querySelector("#input").value = value;
                document.querySelector("#output").innerText = value;
              }
            }
          )
    
          document.querySelector("#input").oninput = function(e){
            vm.text = e.target.value;
          }
        script>
      body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这个 demo 实现的功能和使用 Object.defineProperty 的 demo 是完全一样的,也都是基于 setter 的行为完成数据更新的实现,那么为什么 Vue 3 要舍弃 Object.defineProperty ,换成 Proxy 呢?

    主要原因在于 Object.defineProperty 有以下不足:

    1, 无法侦听数组下标的变化, 通过 arr[i] = newValue 修改值,无法实时响应。

    2,无法侦听数据长度的变化,例如通过 arr.length = 10 修改数组长度,无法实时响应。

    3,只能侦听对象的属性,对于整个对象需要遍历,特别是多级对象更是要通过嵌套来深度侦听。

    4,使用 Object.assign() 等方法给对象添加新属性时,也不会触发更新。

    这也是为什么 Vue 2 要提供一个 Vue.set API 的原因,可以在官网 Vue 2 中检测变化的注意事项 一章了解更多说明。

    而这些问题在 Proxy 都可以得到解决,可以在官网 深入 Vue 3 的响应式原理 一章了解更多这部分的内容。

    🚀 用法上变化

    本篇只使用 Composition API 编写组件,这是使用 Vue 3 的最大优势。

    ⭐️ 虽然官方文档在各个 API 的使用上都做了一定的举例,但在实际使用过程中可能会遇到一些问题,常见的情况就是有些数据用着用着就失去了响应,或者是在 TypeScript 里出现类型不匹配的报错等等。

    当然,一般遇到这种情况并不一定是框架的 BUG ,而可能是使用方式不对,本章节将结合笔者最初入门 Vue 3 时遇到的问题和解决问题的心得,复盘这些响应式 API 的使用。

    相对与 vue2 在 data 里面声明候,即可通过 thisl.xxx 调用响应式数据,在 vue3 的生命周期里没有了 vue 实例的 this 指向, 需要导入 ref 、reactive 等响应式API 才能声明并使用响应式数据。

    // 这里导入的 `ref` 是一个响应式 API
    import { defineComponent, ref } from 'vue'
    
    export default defineComponent({
      setup() {
        // 通过响应式 API 创建的变量具备了响应性
        const msg = ref<string>('Hello World!')
      },
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    由于VUE3 新的API 很多,这里只展示常用的API,更多的请查看官方文档 响应性 API 一章查阅。

    🌈响应式API之ref

    ref 是最常用的一个响应式API, 它可以用来定义所有的类型的数据,包括Node 节点和组件。

    没错,在 Vue 2 常用的 this.$refs.xxx 来取代 document.querySelector('.xxx') 获取 Node 节点的方式,也是使用这个 API 来取代。

    🚀类型声明

    在开始使用 API 之前,需要先了解在 TypeScript 中如何声明 Ref 变量的类型。

    🐳API 本身的类型

    先看 API 本身, ref API 是一个函数,通过接受一个泛型入参,返回一个响应式对象,所有的值都通过 .value 属性获取,这是 API 本身的 TS 类型:

    // `ref` API 的 TS 类型
    function ref<T>(value: T): Ref<UnwrapRef<T>>
    
    // `ref` API 的返回值的 TS 类型
    interface Ref<T> {
      value: T
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因此在声明变量时,是使用尖括号 <> 包裹其 TS 类型,紧跟在 ref API 之后:

    // 显式指定 `msg.value` 是 `string` 类型
    const msg = ref<string>('Hello World!')
    
    • 1
    • 2

    再回看该 API 本身的类型,其中使用了 T 泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value 属性的类型,确定当前变量的类型。

    因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:

    // TypeScript 会推导 `msg.value` 是 `string` 类型
    const msg = ref('Hello World')
    
    • 1
    • 2

    对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。

    而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined 类型:

    const msg = ref<string>()
    console.log(msg.value) // undefined
    
    msg.value = 'Hello World!'
    console.log(msg.value) // Hello World!
    
    • 1
    • 2
    • 3
    • 4
    • 5

    因为入参留空时,虽然指定了 string 类型,但实际上此时的值是 undefined ,因此实际上这个时候的 msg.value 是一个 string | undefined 的联合类型。

    对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value 值进行有效性判断。

    而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any 类型,除非真的无法确认类型,否则不建议这么做。

    🐳API 返回值的类型

    细心的开发者还会留意到 ref API 类型里面还标注了一个返回值的 TS 类型:

    interface Ref<T> {
      value: T
    }
    
    • 1
    • 2
    • 3

    它是代表整个 Ref 变量的完整类型:

    • 上文声明 Ref 变量时,提到的 string 类型都是指 msg.value 这个 .value 属性的类型
    • msg 这个响应式变量,其本身是 Ref 类型

    如果在开发过程中需要在函数里返回一个 Ref 变量,那么其 TypeScript 类型就可以这样写(请留意 Calculator 里的 num 变量的类型):

    // 导入 `ref` API
    import { ref } from 'vue'
    // 导入 `ref` API 的返回值类型
    import type { Ref } from 'vue'
    
    // 声明 `useCalculator` 函数的返回值类型
    interface Calculator {
      // 这里包含了一个 Ref 变量
      num: Ref<number>
      add: () => void
    }
    
    // 声明一个 “使用计算器” 的函数
    function useCalculator(): Calculator {
      const num = ref<number>(0)
    
      function add() {
        num.value++
      }
    
      return {
        num,
        add,
      }
    }
    
    // 在执行使用计算器函数时,可以获取到一个 Ref 变量和其他方法
    const { num, add } = useCalculator()
    add()
    console.log(num.value) // 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!

    🚀变量的定义

    在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!但不同类型的值之间还是有少许差异和注意事项,例如上文提及到该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。

    🐳基本类型

    对字符串、布尔值等基本类型的定义方式,比较简单:

    // 字符串
    const msg = ref<string>('Hello World!')
    
    // 数值
    const count = ref<number>(1)
    
    // 布尔值
    const isVip = ref<boolean>(false)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    🐳引用类型

    对于对象、数组等引用类型也适用,比如要定义一个对象:

    // 先声明对象的格式
    interface Member {
      id: number
      name: string
    }
    
    // 在定义对象时指定该类型
    const userInfo = ref<Member>({
      id: 1,
      name: 'Tom',
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    定义一个普通数组:

    // 数值数组
    const uids = ref<number[]>([1, 2, 3])
    
    // 字符串数组
    const names = ref<string[]>(['Tom', 'Petter', 'Andy'])
    
    • 1
    • 2
    • 3
    • 4
    • 5

    定义一个对象数组:

    // 声明对象的格式
    interface Member {
      id: number
      name: string
    }
    
    // 定义一个对象数组
    const memberList = ref<Member[]>([
      {
        id: 1,
        name: 'Tom',
      },
      {
        id: 2,
        name: 'Petter',
      },
    ])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    🚀DOM 元素与子组件

    除了可以定义数据,ref 也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,也就是对应在 Vue 2 时常用的 this.$refs.xxx 获取 DOM 元素信息的作用。

    模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref 属性:

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    案例演示

    在这里插入图片描述

    reactive 让数据保持响应式的操作方式是:通过重置数组的 length 长度来实现数据的重置:

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    案例演示

    在这里插入图片描述

    🚀 特别注意

    ❤️ ❤️ ❤️ 不要对 Reactive 数据进行 ES6 的解构 操作,因为解构后得到的变量会失去响应性。

    比如这些情况,在 2s 后都得不到新的 name 信息:

    ts

    import { defineComponent, reactive } from 'vue'
    
    interface Member {
      id: number
      name: string
    }
    
    export default defineComponent({
      setup() {
        // 定义一个带有响应性的对象
        const userInfo: Member = reactive({
          id: 1,
          name: 'Petter',
        })
    
        // 在 2s 后更新 `userInfo`
        setTimeout(() => {
          userInfo.name = 'Tom'
        }, 2000)
    
        // 这个变量在 2s 后不会同步更新
        const newUserInfo: Member = { ...userInfo }
    
        // 这个变量在 2s 后不会再同步更新
        const { name } = userInfo
    
        // 这样 `return` 出去给模板用,在 2s 后也不会同步更新
        return {
          ...userInfo,
        }
      },
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
  • 相关阅读:
    达梦8全量备份和增量备份备份策略
    Spring Data JPA @Entity之间的关联关系注解如何正确使用?
    PDF 如何高效的转换成 markdown
    Semantic Kernel 入门系列:💬Semantic Function
    希尔排序算法(思路分析) [数据结构][Java]
    Python Playwright 基本使用(步骤详细)
    C# 轻量级 ORM 框架 NPoco 的简单应用
    【Vue】条件渲染
    【.NET源码解读】Configuration组件及自动更新
    载均衡技术全解析:Pulsar 分布式系统的最佳实践
  • 原文地址:https://blog.csdn.net/No_Name_Cao_Ni_Mei/article/details/134545168