Vue3

发布时间 2023-11-29 22:37:30作者: 自学Java笔记本

官网

https://cn.vuejs.org/guide/quick-start.html

再vue3中采用的是组合式API风格(组合式 API (Composition API))
通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup>

下面是使用了组合式 API 与 <script setup> 改造后和上面的模板完全一样的组件:

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

<!-- 加上 setup 允许再script中直接编写组合式API -->
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

构建工具

create-vue 是Vue 官方新的脚手架工具,底层切换到了 vite,为开发提供极速响应

前提条件:

  • 熟悉命令行
  • 已安装 16.0 或更高版本的 Node.js

创建一个vue应用:npm init vue@latest 这一指令将会安装并执行 create-vue
image

通过npm run dev启动项目后,浏览器显示以下图片表示创建成功,并运行了一个vue3的项目
image

vue3的项目目录和关键文件

image

创建Vue实例

vue2和vue3创建vue实例发生了很大的变化,对于vue2来说我们再min.js中是通过 new Vue({})去创建的,而再vue3中是通过createApp 去创建,虽然通过 .mount 去渲染组件,根据官方文档描述,应用实例必须在调用了 .mount() 方法后才会渲染出来
.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。

main.js 文件如下:

import './assets/main.css'
// 如果要创建路由就通过 createRouter() 创建仓库就通过 createStore()
// 将创建实例进行了封装,保证每个实例的独立封闭性
import { createApp } from 'vue'
import App from './App.vue'
// mount 设置挂载点 #app 为app的盒子
createApp(App).mount('#app')

组合式API-setup选项

setup选项的写法和执行时机:

<script>
  export default {
    setup(){
      
    },
    beforeCreate(){
      
    }
  }
</script>

执行时机

在beforeCreate钩子之前执行

setup中写代码的特点:

  • 在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用

例如:

<script>
  export default {
    setup(){
      const message = 'this is message'
      const logMessage = ()=>{
        console.log(message)
      }
      // 必须return才可以
      return {
        message,
        logMessage
      }
    }
  }
</script>

不过这样总是觉得会很麻烦,所以在vue3中提供了语法糖,我们只需要在<script>标签中添加setup属性即可
例如:

<script setup>
   // 定义一个变量
  const message = 'this is message'
  // 定义一个方法
  const logMessage = ()=>{
    console.log(message)
  }
</script>

在vue3中 setup中是获取不到this对象的,根据官网的说法是在原来的vue2中,原来的选项式api暴露了this大致意思是这样。

总结:

  • setup选项的执行时机?
    在beforeCreate钩子之前 自动执行
  • setup写代码的特点是什么?
    定义数据+函数 然后以对象方式return
  • <script setup></script>解决了什么问题?
    经过语法糖的封装更简单的使用组合式API
  • setup中的this还指向组件实例吗?
    不指向了,根据第一项在beforCreate之前就执行了,说明不存在组件实例了,指向undefined

组合式API - reactive 和 ref 函数

对于reactive() 和 ref() 来说都是vue3中新出的概念,都是用来表示响应式状态的。

reactive()

reactive: 主要用于创建一个包装对象的响应式对象。它接受一个普通对象,并返回一个代理对象,该代理对象会拦截对原始对象的访问。这使得在修改代理对象时,Vue 可以监听到变化。

<script setup>
import { reactive } from 'vue'
//reactive接收一个对象类型的数据,返回一个响应式对象,num就是一个响应式对象
// reactive() 返回的是一个原始对象的 Proxy 只有代理对象是响应式的,更改原始对象不会触发更新
const num = reactive({count:0})
const btnAdd = () =>{
  num.count += 1
}
</script>

更多详细细节看官网:https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive

reactive() 的局限性

  • 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。

  • 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
  • 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
const state = reactive({ count: 0 })

// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

ref()

ref: 主要用于创建一个包装基本类型复杂类型的响应式对象。它接受一个参数,并返回一个具有 .value 属性的对象,该属性包含传入的参数。ref 通常用于包装原始值,比如数字、字符串等。

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构

为什么在脚本中要使用.value ,在模板中 不使用?

模板编译器在处理模板时,会自动解析为组件实例
在js类或对象里面 就需要显式this指定上下文
所以类比ref 他返回的是包裹的值,在模板层会解析,在js层需要加.value属性指向内部的原始值
import { ref } from 'vue';
// 接收简单类型 或 复杂类型,返回一个响应式的对象
// 本质:实在原有传入数据的基础上,外层包了一层对象,包成了复杂类型
// 底层: 包层复杂类型之后,在借助 reactive 实现的响应式
// 注意点:
//   1.脚本中访问数据,需要通过 .value
//   2.在模板中,.value不需要加,模板解析器自动帮我们解析了
const count = ref(0);
// 接收复杂类型 ,返回一个响应式对象
const message = ref({count:0,num:10})
console.log(count.value); // 访问值  0
console.log(message.value.count); // 访问值  0

<template>
  <div>
    {{ message.num }} --- {{ num.count }}
    <button @click="btnAdd">点我+1</button>
  </div>
</template>

组合式API - computed

计算属性基本思想和Vue2保持一致,组合式API下的计算属性只是修改了API写法
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

语法如下:

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

// reactive 接收一个对象,返回一个响应式对象
const num = reactive({count:0})
// 计算属性
const computedNum = computed(()=>{
  return num.count + 1
})
</script>

<template>
  <div>
    <span>计算属性-{{ computedNum }}</span>
  </div>
</template>

我们都知道,在vue2中 计算属性 是不能直接去修改值得,如果我们需要修改值,需要通过对象的形式去定义,并提供对应的getset方法,在vue3中同样的,下列有两个示例,分别是只读的的计算属性 和 可修改的计算属性的创建:

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

创建一个可写的计算属性 ref:

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

示例:

const arrList = ref([1,2,3,4,5,6,7,8,9,10])

const filterList = computed(()=>{
  return arrList.value.filter(item => item>2)
})

console.log(filterList.value) // [3,4,5,6,7,8,9,10]

组合式API - watch

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组
语法:

<script setup>
import { ref ,watch } from 'vue'
// 定义一个响应式对象count
const count = ref(0)

// 调用watch 侦听变化
watch(count,(newValue,oldValue)=>{
  console.log(`count发生了变化,旧值为${oldValue} ,新值为:${newValue}`)
})
</script >

<template>
    <button @click="count++">点我加1</button>
</template>

效果展示:
image

侦听 getter函数 和 数组

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

// 多个数据源监听
// 第一个参数是要监听的ref对象,第二个参数是变化后的新旧值
const stop = watch(
  [x, () => y.value],
  ([newX, newY], [oldX, oldY]) => {
    console.log(`x is ${newX} (was ${oldX}) and y is ${newY} (was ${oldY})`);
  }
);

对于 多个来源组成的数组的解释如下:
x 是一个 ref 对象,所以 newX 会直接获取其值。
() => y.value 是一个 getter 函数,所以 newY 会通过调用这个函数来获取其值。
这样,当 x 或 y 的值发生变化时,回调函数就会执行,打印新的 x 和 y 的值。
这种形式的 watch 可以很方便地同时监听多个数据源的变化。

深层侦听器

直接给 watch() 传入一个响应式对象,此响应对象必须是reactive会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

const obj = ref({ count: 0 })
// deep 深度监视,默认watch 进行的是浅层前世
// const obj = ref(简单类型) 可以直接监视
// cosnt obj = ref(复杂类型) 监视不到复杂类型内部数据的变化
watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
}, { deep: true })
obj.count++

例如我现在去修改这个obj的count值:

 <button @click="obj.count++"></button>

在监听obj的监听函数上,不能监听到,底层监听的是,当这个obj整个对象发生变化时即地址变化才会监听,而不是其中的一个属性,所以我们要想产生效果监听,必须在监听时加上 {deep:true}

谨慎使用
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

immediate 立即执行

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

精确侦听对象的某个属性

需求,在不开启deep的前提下,侦听age的变化,只有age变化时才执行回调

例如:

const info = ref({
    name:'cp',
    age:18
})

// 要监听其中的一个值变化
watch(
    ()=>info.age,
    (newValue)=>{console.log('age发生了变化'+newValue)}
)

<template>
    <button @click="info.age++">点我info属性age+1</button>
</template>

image

组合式API - 生命周期函数

官网:https://cn.vuejs.org/api/composition-api-lifecycle.html

Vue3的生命周期API (选项式 VS 组合式)

选项式API 组合式API 说明
beforeCreate/created setup 在组件实例被创建之前调用
beforeMount onBeforeMount 在挂载之前调用
mounted onMounted 在挂载之后调用
beforeUpdate onBeforeUpdate 在更新之前调用
updated onUpdated 在更新之后调用
beforeUnmount onBeforeUnmount 在卸载之前调用
unmounted onUnmounted 在卸载之后调用
<script setup>
// beforeCreate 和 created 的相关代码,一律放在setup中执行
// 如果要使用生命周期的钩子,需要引入
import { onMounted } from 'vue'
const getList = () =>{
  setTimeout(()=>{
    console.log('模拟发起请求获取数据渲染')
  },2000)
}
// 调用请求数据的方法
getList()

// 如果有些代码需要在mounted生命周期中执行
onMounted(() => {
  console.log('mounted生命周期函数逻辑1')
})

// 写成函数的调用方式,可以调用多次,并不会冲突,而是按照顺序依次执行
onMounted(() => {
  console.log('mounted生命周期函数逻辑2')
})

</script>

组合式API - 父传子通信

基本思想

  1. 父组件中给子组件绑定属性
  2. 子组件内部通过props选项接收数据

父传子:
image

<script setup>
import { onMounted , ref } from 'vue'
import SonCom from './components/son-com.vue'
const num = ref(0)
const addBtn = () => {
  num.value++
}
// 构建一个响应式对象,传递给子组件
const user = ref({name:'zgf',age:22})

</script>

<template>
  <h1>父组件</h1>
  <p>{{num}}</p>
  <button @click="addBtn">点击+1</button>
  <!-- 传递一条基本消息 和  对象消息 -->
  <SonCom :message="`向子组件传递一条消息${num}`" :data="user"></SonCom>
</template>

子组件接收消息:

<template>
  <div class="son">
    我是子组件:我接收到父组件传递过来的数据是:
    {{ message }}
    {{ data }}
  </div>
</template>

<script setup>
// 注意:由于写了 setup,所有无法直接配置props选项
// 所以:此处需要借助于”编译器宏“函数接收子组件传递过来的数据
defineProps({
  message:String,
  data:Object
})

</script>

组合式API 子传父

基本思想:

  1. 父组件中给子组件标签通过@绑定事件
  2. 子组件内部通过 emit 方法触发事件

image

组合式API - 模板引用

通过 ref标识,获取真实的dom对象或者组件实例对象

案例引用:
例如,在模板渲染时,我们需要对输入框自动聚焦,此时我们就需要获取到dom对象实例,然后操作dom对象使其聚焦
以下是代码演示:

  • 调用ref函数生成一个ref对象
  • 通过ref标识绑定ref对象到标签
<template>
  <div>
    
    输入框:<input ref="input"/>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(()=>{
  input.value.focus()
})
</script>

v-for 中的模板引用

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
应该注意的是,ref 数组并不保证与源数组相同的顺序。
一般场景:拿到引用,是组件就可以调用该组件的方法,是元素就可以修改样式,class,绑定事件等等

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

const list = ref([
  /* ... */
])

const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">
      {{ item }}
    </li>
  </ul>
</template>

组件上的模板引用 ref

官方文档:https://cn.vuejs.org/guide/essentials/template-refs.html#ref-on-component
使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

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

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。

具体使用:
image

总结:
获取模板引用的时机是什么?

  • 组件挂载完毕

defineExpose编译宏的作用是什么?

  • 显示暴露子组件对应的属性或方法

组合式API - provide 和 inject

作用和场景:顶层组件向任意的底层组件传递数据和方法,实现跨层级组件通信

跨层传递普通数据 和 对象数据

1.顶层组件通过 provide 函数提供数据
2.底层组件通过 inject 函数获取数据

image

image

跨层级传递函数,实现底层数据对顶层数据的修改

image

Vue3.3新特性-defineOptions

背景说明:

<script setup> 之前,如果要定义 props, emits 可以轻而易举地添加一个与 setup 平级的属性。

但是用了<script setup>后,就没法这么干了 setup 属性已经没有了,自然无法添加与其平级的属性。


为了解决这一问题,引入了 defineProps 与 defineEmits 这两个宏。但这只解决了 props 与 emits 这两个属性。

如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的<script>标签。

这样就会存在两个<script>标签。让人无法接受。


所以在 Vue 3.3 中新引入了 defineOptions 宏。顾名思义,主要是用来定义 Options API 的选项。可以用 defineOptions 定义任意的选项, props, emits, expose, slots 除外(因为这些可以使用 defineXXX 来做到)

<script setup>
// 对于es来说,单个组件页面取名字需要两个单词,此时我们可以
// 通过 defaultOptions 去配置名称
import { defaultOptions } from 'vue'
defaultOptions({
  // 除了配置 props,emits,expose,slots之外的所有选项
  name: 'LoginIndex',
  // 更多自定义属性
})
</script>

Vue3.3新特性 - defineModel

在Vue3中,自定义组件上使用v-model 相当于传递一个modelValue属性,同时触发update:modelValue事件

<Child v-model='isVisible'>
//相当于
<Child :modelValue='isVisible' @update:modelValue='isVisible=$event'>

我们需要先定义 props,在定义emits。其中有许多重复代码,如果需要修改此值,还需要手动调用emit函数
这样太麻烦了,于是出了 defineModel

代码引入:
image

从上图中的代码看起来实在是太麻烦了:下面通过 defineModel() 去实现
image
生效需要配置 vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue({
      script: {
        defineModel: true
      }
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})