在学习前,我们要知道:

# 一、API 风格

  Vue 的组件可以按两种不同的风格书写:选项式 API组合式 API

比较

官方: https://cn.vuejs.org/guide/extras/composition-api-faq.html

  官方也比较人性化,文档是一些示例代码你可以自行选择那种 API 风格:

在线训练场 👀

# 1、选项式 API

特点

选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例

  在 非单文件 中:

和 vue2 不同,vue3 中的 data 只能使用函数式

<div id="app">{{ count }}</div>

<script type="text/JavaScript">
  Vue.createApp({
    data() {
      return {
        count: 0
      }
    }
  }).mount('#app')
</script>

  在 单文件组件(.vue) 中:

关于 this 指向问题

  不能使用箭头函数来定义 method 函数

箭头函数绑定的是父级作用域的上下文(最终指向 windows)

  正常情况下,methods 中的函数其 this,指向的是 publicThis,其他可以查看 vue3 源码中的applyOptions (opens new window)

const publicThis = instance.proxy
<template>{{ count }}</template>

<script>
  export default {
    // reactive data
    data() {
      return {
        count: 0
      }
    },

    // methods
    methods: {
      increment() {}
    },

    // lifecycle hooks
    mounted() {}
  }
</script>
TS ✍ 与 选项式 API

  在 vue3 早期的时候,为了更好的在 vue3 组件中使用 TS,往往会使用defineComponent

  • 单个 vue 文件
<script lang="ts">
import { defineComponent } from 'vue'

// 提示更好ts类型提示(volar插件会智能提示🤔)
export default defineComponent({
  data() {
    return {}
  },
  methods: {}
  // ……
})
</script>
  • 一次性声明
// .env.d.ts

declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent

  export default component
}

  当然,现在 vue3 官方提供的 Vue.vscode-typescript-vue-plugin (opens new window) 插件是可以直接识别并提供 ts 辅助提示的,不用下面的 ts 声明也可以。

# 2、组合式 API ✨

特点

<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

为什么要使用 🤔 组合式 API?

参考:

该选哪一个 (opens new window)什么是组合式 API (opens new window)

优点:

  组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,这种形式更加自由,组织和逻辑复用 (opens new window)的模式变得更加强大(对比 vue2 中的 minxins 混入 👀 的 this 指向问题)

基于组合式 API 提供的逻辑复用能力,还孵化一些社区项目,比如:VueUse (opens new window)

  在 非单文件 中:

并且,在 HTML 中无法直接使用 ES6 模块的 import 语法

<div id="app">{{ count }}</div>

<script>
  // import { createApp } from 'vue'
  // 注意:🎈
  const { createApp, ref } = Vue

  createApp({
    setup() {
      const count = ref(0)

      return { count }
    }
  }).mount('#app')
</script>

  在 单文件组件(.vue) 中:

  • 方式 1

setup() 钩子是组合式 API 的入口

<template>{{ count }}</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      // reactive data
      const count = ref(0)

      // methods
      function increment() {}

      // lifecycle hooks
      onMounted(() => {})

      return {
        count,
        increment
      }
    }
  }
</script>
  • 方式 2 👍

组合式 API 通常会与 <script setup> (opens new window) 搭配使用

优点:

  -<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖

  • 无需 return 声明的变量、函数以及 import 引入的内容
  • 引入组件将自动注册
  • <script setup>中必须使用 defineProps 和 defineEmits API 来替代 props 和 emits
  • 需主动向父组件暴露子组件属性 :defineExpose
<template>{{ count }}</template>

<script setup>
  import { ref, onMounted } from 'vue'

  // reactive data
  const count = ref(0)

  // methods
  function increment() {}

  // lifecycle hooks
  onMounted(() => {})
</script>

# 3、混合开发

  用于组合式有一个入口 setup,我们不难想到:

在 setup 中不能访问到Vue2.x 配置(data、methos、computed...)

Vue2.x 配置(data、methos、computed...)中可以访问到 setup 中的属性、方法

思考

  这里的 setup 和闭包有什么 🤔 相似之处?

出现重名,以 setup 为准

使用示例
<template>{{ count }}</template>

<script>
  import { ref } from 'vue'

  export default {
    // 1、组合式
    setup() {
      const count = ref(1)

      // 返回值会暴露给模板和其他的选项式 API 钩子
      return {
        count
      }
    },

    // 2、选项式
    data() {
      return {
        count: 2
      }
    },

    mounted() {
      // 出现重名,以setup🤔为准
      console.log(this.count) // 1
    }
  }
</script>

# 二、ref 和 reactive

# 1、ref()

  接受一个内部值,返回一个 响应式的、可更改的 ref 对象(引用对象),此对象只有一个指向其内部值的属性 .value

提示

在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

# ① 原始数据类型

代码演示(常规类型)
<div id="app">
  内容1(普通):{{ msg1 }} <br />
  内容2(响应式):{{ msg2 }} <br />
  <button @click="changeValue">修改内容</button>
</div>

<script>
  const { createApp, ref, onMounted } = Vue

  createApp({
    setup() {
      let msg = 'lencamo'
      let msg1 = msg // 普通
      let msg2 = ref(msg) // 响应式

      onMounted(() => {
        console.log(msg1) // "lencamo"
        console.log(msg2) // RefImpl
      })

      function changeValue() {
        msg1 = 'change success'

        // 注意🤔
        // msg2 = "change success";
        msg2.value = 'change success'
      }

      return {
        msg1,
        msg2, // 自动浅层解包 msg2.value
        changeValue
      }
    }
  }).mount('#app')
</script>

# ② 对象数据类型

代码演示(数组、对象)
<div id="app">
  {{ user }} <br />
  <button @click="changeValue">修改内容</button>
</div>

<script>
  const { createApp, ref, onMounted } = Vue

  createApp({
    setup() {
      // 1、使用ref
      let user = ref({
        name: 'lencamo',
        age: 20
      })

      onMounted(() => {
        console.log(user) // RefImpl
        console.log(user.value) // Proxy(Object) -->(这里并不是RefImpl🤔)
      })

      function changeValue() {
        // 注意🤔(why?)
        // user.value.name.value = "ren"
        user.value.name = 'ren'
      }

      return {
        user,
        person,
        changeValue
      }
    }
  }).mount('#app')
</script>

  way?的原因:具体见下面的 reactive()

# ③ 思考 ✍

  我们通过打印一下,vue3 中的响应式和 vue2 中的响应式实现有什么区别呢?

结论:

ref( )基本类型作响应式处理时,依然使用的是 Object.defineProperty()getset 完成的。

区别:

vue2 中vm实例对象 对 vm._data 使用数据代理,可以简化或者说更方便的操作 data 中的数据

vue3 中RefImpl引用对象的 value 对其原型对象上的 value 进行数据代理

  vue3 中为什么要弄一个 value 属性,向 vue2 一样直接点不好吗?

答案

  Vue 3 的响应式系统是基于 ES6 的 Proxy 对象实现的,而 Vue 2 的响应式系统是使用 Object.defineProperty 实现的。

所以,Vue 3 引入 value 属性是为了适应新的响应式系统

  具体可以查看Proxy 代理 (opens new window)【响应式系统】

# 2、reactive()

  上接前面 ref()使用对象数据类型存在的疑惑:

答案
<div id="app">
  {{ user1 }} <br />
  {{ user2 }} <br />
  <button @click="changeValue">修改内容</button>
</div>

<script>
  const { createApp, ref, reactive, onMounted } = Vue

  createApp({
    setup() {
      // 1、使用ref
      let user1 = ref({
        name: 'lencamo',
        age: 20
      })

      // 2、使用reactive
      let user2 = reactive({
        name: 'lencamo',
        age: 20
      })

      onMounted(() => {
        // console.log(user1);

        console.log(user1.value) // Proxy(Object)
        console.log(user2) // Proxy(Object)
      })

      function changeValue() {
        // 区别 🌈
        user1.value.name = 'ref'
        user2.name = 'reactive'
      }

      return {
        user1,
        user2,
        changeValue
      }
    }
  }).mount('#app')
</script>

  reactive() 返回的是一个原始对象的 Proxy。相较于 ref(),它虽然不用加上.value,但也有一定的局限性:

# Proxy的实例对象:Proxy(Object)
# 原始对象:Object
let Proxy(Object) = reactive(Object)

注意

ref()是 vue 为我们提供的允许我们创建可以使用任何值类型的响应式 ref(当然,涉及对象类型是其内部会直接调用 reactive()


reactive()仅对 🚩 对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。

代码演示
<div id="app">
  {{ name }} <br />
  {{ person }} <br />
  <button @click="changeValue">修改内容</button>
</div>

<script>
  const { createApp, ref, reactive, onMounted } = Vue

  createApp({
    setup() {
      // 1、缺点
      // 警告:value cannnot be made reactive: lencamo
      let name = reactive('lencamo')

      // 2、优点
      let person = reactive({
        // 数组
        hobby: ['喝酒', '吉他', '游泳'],
        // 对象(多级)
        job: {
          type: '前端工程师',
          local: {
            hot: true
          }
        }
      })

      function changeValue() {
        // 修改:👀简洁明了
        person.hobby[0] = '篮球'
        person.job.local.hot = false
      }

      return {
        name,
        person,
        changeValue
      }
    }
  }).mount('#app')
</script>

# 3、对比总结 ✨

① 从定义数据角度对比:

  • ref() 用来定义:基本类型数据。
  • reactive() 用来定义:对象类型数据。

备注:ref 也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象。

② 从原理角度对比:

  • ref() 通过Object.defineProperty()getset来实现响应式(数据劫持)。
  • reactive() 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据。

③ 从使用角度对比:

  • ref 定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
  • reactive 定义的数据:操作数据与读取数据:均不需要.value

# 三、常用函数

# 1、ref 属性

  ref 用于注册元素或子组件的引用。

在 setup 中是不可以使用 this

<template>
  <h2 ref="titleRef">我是标题</h2>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const titleRef = ref()

    // 对titleRef的操作
    onMounted(() => {
      console.log(titleRef.value)
    })

    return {
      titleRef
    }
  },

  mounted() {
    console.log(this.$refs.titleRef)
  }
}
</script>

# 2、computed()

computed(cb())

  接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。

说明

  computed()也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象

computed:可写的 ref 对象
<template>
  姓:<input type="text" v-model="person.fristName" /> <br />
  名:<input type="text" v-model="person.lastName" /> <br />
  姓名(选项式):<input type="text" v-model="fullName1" /> <br />
  姓名(组合式):<input type="text" v-model="fullName2" /> <br />
</template>

<script>
  import { reactive, computed } from 'vue'

  export default {
    name: 'Demo',
    setup() {
      let person = reactive({
        fristName: '张',
        lastName: '三丰'
      })

      // 组合式
      // const fullName2 = computed(() => {
      //   return person.fristName + '-' + person.lastName
      // })
      const fullName2 = computed({
        get() {
          return person.fristName + '-' + person.lastName
        },
        set(newValue) {
          let nameArr = newValue.split('-')
          person.fristName = nameArr[0]
          person.lastName = nameArr[1]
        }
      })

      return { person, fullName2 }
    },

    // 选项式
    computed: {
      // fullName1: function () {
      //   return this.person.fristName + '-' + this.person.lastName
      // }
      fullName1: {
        get: function () {
          return this.person.fristName + '-' + this.person.lastName
        },
        set: function (newValue) {
          let nameArr = newValue.split('-')
          this.person.fristName = nameArr[0]
          this.person.lastName = nameArr[1]
        }
      }
    }
  }
</script>
<script>
  export default {
    name: 'Demo',
    setup() {
      const msg = ref('')

      // 组合式
      const reverseMsg2 = computed(() => {
        return msg.value.split('').reverse().join('')
      })

      return { msg, reverseMsg2 }
    },

    // 选项式
    computed: {
      reverseMsg1: function () {
        // 注意使用this
        return this.msg.split('').reverse().join('')
      }
    }
  }
</script>

# 3、watch()

watch(arr,cb(),options)

  在 vue3 中,侦听器可以监听一个或多个响应式的数据,并且可以通过一些配置选项来自定义侦听器的行为。

computed() 和 watch() 使用对比
<template>
  <input type="text" v-model="msg" />
  <p>选项式:{{ reverseMsg1 }}</p>
  <p>组合式:{{ reverseMsg2 }}</p>
</template>

<script>
  import { ref, computed, watch } from 'vue'

  export default {
    name: 'Demo',
    setup() {
      let msg = ref('')

      // 二、组合式
      // 1、计算属性
      // const reverseMsg2 = computed(() => {
      //   return msg.value.split('').reverse().join('')
      // })
      // 2、监听器
      let reverseMsg2 = ref('')
      watch(msg, (newValue) => {
        reverseMsg2.value = newValue.split('').reverse().join('')
      })

      return { msg, reverseMsg2 }
    },

    // 一、选项式
    // 1、计算属性
    // computed: {
    //   reverseMsg1: function () {
    //     // 注意使用this
    //     return this.msg.split('').reverse().join('')
    //   }
    // }
    // 2、监听器
    data() {
      return {
        // 使用watch监听做数据计算的问题😒
        reverseMsg1: ''
      }
    },
    watch: {
      msg(newValue, oldValue) {
        this.reverseMsg1 = newValue.split('').reverse().join('')
      }
    }
  }
</script>

  下面,我们以监听普通数据为例:

<template>
  <button @click="sum += 1">改变sum</button>
  <button @click="msg += '-'">改变msg</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    setup() {
      let sum = ref(0)
      let msg = ref('lencamo')

      // 监听一个(不用加value)
      watch(sum, (newValue, oldValue) => {
        console.log(newValue)
      })

      // 监听多个(不用加value)
      watch([sum, msg], (newValue, oldValue) => {
        console.log(newValue) // 一个数组
      })

      return { sum, msg }
    }
  }
</script>
监听对象数据 ✨
<template>
  <button @click="job.type = '外卖小哥'">修改job</button>
  <button @click="job.rank = 2">深度监听检测</button>
</template>

<script>
export default {
  setup() {
    let job = reactive({
      type: '前端工程师',
      local: {
        rank: 2
      }
    })

    // 1、默认隐式地创建一个深层侦听器
    watch(job, (newValue, oldValue) => {
      // 注意:`newValue` 此处和 `oldValue` 是相等的
      // 因为它们是同一个对象!
    })

    // 2、显示的强制创建一个深层侦听器
    watch(
      () => job.local,
      (newValue, oldValue) => {
        // 注意:`newValue` 此处和 `oldValue` 是相等的
        // *除非* job.local 被整个替换了
      },
      { deep: true }
    )

    // 3、默认返回Proxy对象,手动获取普通对象🤔
    watch(
      () => ({ ...job }),
      (newValue, oldValue) => {
        // 注意:`newValue` 此处和 `oldValue` 是相等的
        // 因为它们是同一个对象!
      },
      { immediate: true }
    )

    return { job }
  }
}
</script>
是否加 value 问题

产生是否加 value 的问题根源是:我们对对象做响应式时采用了 ref,而不是 reactive

<template>
  <button @click="job.type += '-'">改变type</button>
  <button @click="job.local.rank += 1">改变rank</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    setup() {
      let job = ref({
        type: '前端工程师',
        local: {
          rank: 2
        }
      })

      // 要加上value 🤔
      watch(job.value, (newValue, oldValue) => {
        console.log('one--', newValue === oldValue) // true
      })

      // 等效于
      // watch(
      //   job,
      //   (newValue, oldValue) => {
      //     console.log('one--', newValue === oldValue) // true
      //   },
      //   { deep: true }
      // )

      return { job }
    }
  }
</script>

# 4、watchEffect()

watchEffect(cb)

  vue3 中的监听器除了,还引入了一种新的侦听器类型,叫做“侦听器副作用函数”(watchEffect),可以用于监听一个或多个响应式数据的变化,并在响应式数据变化时自动执行副作用函数

watch 与 watchEffect 的区别
  • watch 的套路是:既要指明监视的属性,也要指明监视的回调。
  • watchEffect 的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性

  watchEffect 会自动执行(相当于加了一个 { immediate: true })

<script>
export default {
  setup() {
    // 立即执行当前函数
    watchEffect(() => {
      console.log('===========')
    })
  }
}
</script>

  思考一下,我们为什么要使用 watchEffect 函数呢?是解决什么问题吗?

答案

  当我们的监听源在回调中也使用了,为了简化代码我们可以使用 watchEffect()

<script>
export default {
  setup() {
    const todoId = ref(1)
    const data = ref(null)

    // watch 两次使用了 todoId,一次是作为源,另一次是在回调中
    watch(
      todoId,
      async () => {
        const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
        data.value = await response.json()
      },
      { immediate: true }
    )

    // watchEffect() 会自动跟踪回调的响应式依赖(todoId、data)
    watchEffect(async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
      data.value = await response.json()
    })

    return { todoId, data }
  }
}
</script>
<script>
export default {
  setup() {
    const name = 'lencamo'
    const count = ref(1)

    // 立即执行一个函数(相当于加了一个 { immediate: true })
    watchEffect(() => {
      console.log('===========')
    })

    // 1、响应式数据发生变化,自动执行
    watchEffect(() => {
      console.log(name.value)
    })

    // 2、关闭watchEffect监听器
    const unwatch = watchEffect(() => {
      console.log(count.value)

      if (count.value > 6) {
        unwatch() // 手动关闭
      }
    })
  }
}
</script>

# 四、其他

# 1、readonly()

问题描述

  在 vue2 我们已经提到了,如果使用 props 向子组件传递对象类型的数据时,子组件是可以修改该对象数据并直接影响到父组件,显然这是不符合 vue 的单向数据流的。

  readonly()可以接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理

<template>
  <son :info="roInfo"></son>
</template>

<script>
export default {
  setup() {
    const info = reactive({
      name: 'lencamo',
      age: 22,
      height: 210
    })

    const roInfo = readonly(info)

    return {
      roInfo
    }
  }
}
</script>

# 2、toRefs()

  在有些情况下,我们仅仅需要从已定义的 reactive 中取出某个数据,但伴随的可能是响应式的丢失。

单独取出某个数据,往往是处于书写简化的考虑

响应式丢失

  reactive 被解构后会变成普通的值,失去响应式。

使用 toRefs()可以将解构后的属性都转换为 ref

<template>
  <p>常规方式:{{ job.local.rank }}</p>
  <p>简写方式:{{ local.rank }}</p>
</template>

<script>
export default {
  setup() {
    let job = reactive({
      type: '前端工程师',
      local: {
        rank: 2
      }
    })

    // let job2 = toRefs(job)
    let { local } = toRefs(job) // 解构1

    // return { job, ...job2 } // 解构2
    return {
      job,
      local
    }
  }
}
</script>

  当然,我们可以不用使用解构把所有的对象属性都变为 ref,如果我们仅仅只需要一个对象属性的话,我们可以使用 toRef()

<script>
export default {
  setup() {
    let local = toRef(job, 'local') // 只要job对象下的local属性

    return {
      local
    }
  }
}
</script>

# 3、浅层对象

  • shallowRef()、shallowReactive()、shallowReadonly()
shallowRef() ✨

  和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

简单是说,就是不会求助 reactive,只处理基本数据类型的响应式(不处理对象的响应式)

----> 应用:高德地图 map 对象引入

  如果直接使用 ref( ),Vue3 所使用的 Proxy 拦截操作会改变 JS API 原生对象。

  所以应该改用 shallowRef( ),即:让 JS API 相关对象采用非响应式的普通对象来存储

import { shallowRef } from '@vue/reactivity'

const map = shallowRef(null)

// 初始化地图
const initMap = () => {
  AMapLoader.load({
    // ……
  }).then((AMap) => {
    map.value = new AMap.Map('container', {
      // ……
    })
  })
}
----> 相关:triggerRef 手动触发响应式

  强制触发依赖于一个浅层 ref 的副作用

const info = shallowRef({
  greet: 'Hello, world'
})

// 1、自动触发
watchEffect(() => {
  console.log(shallow.value.greet) // 可以访问,但不是响应式的
})

// 2、手动触发
const changeInfo = () => {
  info.value.greet = 'My name is Lencamo!'

  triggerRef(info)
}

shallowReactive()

  和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

简单是说,只处理对象最外层属性的响应式(浅响应式)

----> 相关:toRaw() 返回 Proxy 的原始对象
const app = Vue.createApp({
  methods: {
    changeMessage() {
      this.info = { name: 'lencamo', age: 22 }
    }
  },
  watch: {
    info(newValue, oldValue) {
      // console.log({...newValue}) // 错误
      console.log(Vue.toRaw(newValue))
    }
  }
})

# 4、工具函数

  • isRef()、isReactive()、isReadonly()、isProxy()

判断响应式数据类型

  • unref()

获取普通数据和 ref 数据的值

// const age = 18
// const age = ref(18)

const val = Ref(age) // 相当于:val = isRef(val) ? val.value : val
// const val = age
// const val = age.value
  • toRaw()

返回 Proxy(reactive、readonly 相关对象) 的原始对象

const app = Vue.createApp({
  methods: {
    changeMessage() {
      this.info = { name: 'lencamo', age: 22 }
    }
  },
  watch: {
    info(newValue, oldValue) {
      // console.log({...newValue}) // 错误
      console.log({ ...Vue.toRaw(newValue) })
    }
  }
})
更新于 : 8/7/2024, 2:16:31 PM