Vue3入门

211 阅读12分钟

为什么要学vue3

vue3的优势

image.png

vue3 组合式API vs vue2选项式 API

image.png

// vue2 options API
<script>
    export default {
        data() {
            return {
                count: 0
            }
        },
        methods: {
            addCount() {
                this.count++;
            }
        }
    }
</script>

// vue3 Composition API 
<script setup>
import { ref } from 'vue';

const count = ref(0)
const addCount = () => count.value++
</script>

从上述例子中可以看出:

  • 组合式API相比 代码量 变少了
  • 分散式维护转为集中式维护,更易封装复用。

官方新的脚手架工具create-vue

create-vuevue官方新的脚手架工具,底层由原来Vue-CLIwebpack切换到了vite(新的构建工具),为开发提供极速响应。 image.png

使用create-vue创建项目

  • 前提环境条件: 已安装 16.0 或 更高版本的 Node.js
  • 创建一个 Vue 应用
// 创建命令
npm init vue@latest
// 该指令将会 安装并执行 create-vue
  • 项目目录结构
——.vscode
——node_modules
——public
——src
    ——assets
    ——components
    ——App.vue     // SFC单文件组件 script、template、style
                  // 变化一:script 和 template 顺序调整
                  // 变化二:template 不再要求唯一根元素
                  // 变化三:script 添加 setup 标识 支持 组合式API
    ——main.js    // createApp函数创建应用实例
——.eslintrc.cjs
——env.d.ts        // ts的声明文件。 识别.ts\.css\.scss\.js\.jsx等各个文件的声明
——.gitignore
——index.html      // 单文件入口,提供id为app的挂载点
——package-lock.json
——package.json   // 核心依赖项 变成了 vue3.x 和 vite
——README.md
——vite.config.js // 项目的配置文件 基于 vite的配置

组合式API —— setup

  • 时机:在 beforeCreate 钩子之前,自动执行 image.png
  • 写法:定义 数据+函数 然后以对象方式return
<script>
export default {
    name: 'Person',
    setup() {
        // 数据
        const msg = '这是一条数据';
        // 函数
        const logMsg = () => {
            console.log(msg)
        }
        
        return {
            msg,
            logMsg
        }
    }
}
<script>

<template>
    <!-- 使用数据和方法 -->
    {{ msg }}
    <button @click="logMsg">按钮</button>
</template>
  • 语法糖(简单写法)
 // 组件的配置
<script>
export default { 
    name: 'Person'
}
</script>

// setup处理
<script setup>
// 数据
const msg = '这是一条数据';

// 函数
const logMsg = () => { console.log(msg) }
</script>

安装插件 vite-plugin-vue-setup-extend

安装命令npm i vite-plugin-vue-setup-extend -D

然后再 vite.config.ts 中引入 插件

// vite.config.js

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

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

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

即可将上述代码 继续简化为

// setup处理
<script setup name='Person'>
// 数据
const msg = '这是一条数据';

// 函数
const logMsg = () => { console.log(msg) }
</script>

注意: setup 中的this 指向的是undefined,不是组件实例了

组合式API —— reactive

  • 作用: 接受 对象类型数据的参数 传入返回 一个响应式的对象
<script setup>
// 导入
import { reactive } from 'vue'

// 执行函数、 传入参数、 变量接收
const state = reactive(对象类型数据)
</script>

// 访问
模版中:{{ state }}
script中: console.log(state)
  • 坑点:reactive定义数组 —— Vue 3 的 reactive 仅对初始对象进行响应式包装,直接赋值新对象会丢失响应性。
const tableData = reactive([]) // 定义

// 错误修改示例
const getData = async () => {
    const { data } = await getDataApi(); // 模拟接口,获取数据
    tableData = data; // 错误修改示例,这样会导致 tableData 的响应式丢失。
}

// 正确操作

// 方式一: 使用 ref 定义

// 方式二:先Object.assign
Object.assign(tableData, data)

// 方式三:定义时,包裹成对象,通过访问对象属性,重新赋值
const tableData = reactive({ list: [] })
tableData.list = data;

组合式API —— ref

  • 作用:接收 简单类型 或 对象类型的数据的参数 传入返回一个 响应式的对象
<script setup>
// 导入
import { ref } from 'vue'

// 执行函数、 传入参数、 变量接收
const state = ref(简单类型 或 复杂类型数据)

// 访问
模版中:{{ state }}
script中: console.log(state.value)
</script>
reactive vs ref对比

宏观角度看:

1、ref用来定义:基本类型数据、对象类型数据

2、reactive用来定义: 对象类型数据

区别:

1、reactive 不能处理 简单类型 的数据。reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。

2、ref 参数类型支持更好,但是必须通过 .value 访问修改。(可以使用插件Vue - Official自动添加.value

  • vscode中扩展中,添加插件后,打开:设置——扩展——Vue——勾选 Auto Insert: Dot Value

3、ref 函数的内部实现依赖于reactive 函数

使用原则:

1、若需要一个基本类型的响应式数据,必须使用ref

2、若需要一个响应式对象,层级不深,refreactive都可以。

3、若需要一个响应式对象,且层级较深,推荐使用reactive

toRefstoRef: 用于处理响应式对象属性的工具函数,它们的主要作用是将响应式对象的属性转换为独立的ref对象,同时保持与源对象的响应式连接。

toRefs

功能:

  • 将 整个响应式对象的所有属性 转换为多个独立的 ref,并保持与源对象的同步。
  • 适用于解构 reactive 对象时保持响应性。

特点:

  1. 批量转换
  • 返回一个对象,所有属性都被转换成 ref
  1. 适用于解构 reactive 对象

使用场景:

  • 在 <script setup> 或组合式函数中解构 reactive 对象时,避免丢失响应性。

toRef

功能:

  • 将 响应式对象的某个属性 转换为一个单独的 ref,并保持与源对象的同步。
  • 如果源对象属性变化,toRef 生成的 ref 也会更新,反之亦然。

特点:

  1. 适用于单个属性

使用场景:

  • 在组合式函数(Composables)中返回某个响应式对象的属性,并希望保持响应性。

核心区别

特性toReftoRefs
作用对象单个属性整个响应式对象的所有属性
返回值单个 ref包含所有 ref 的对象
适用场景需要单独处理某个属性解构 reactive 对象时保持响应性
修改同步性修改 ref 会同步更新源对象修改任意 ref 会同步更新源对象

例1:

<script setup>
import { reactive } from 'vue'
// 此时 person 是响应式数据
let person = reactive({ name: '张三', age: 18 })

// 此时,解构的name, age是基本类型,只是读取了person对象中的 name 和age字段的值。不再是响应式数据。
let { name, age } = person;
</script>

那么如何将 name, age变成响应式的呢?

<script setup>
import { reactive, toRefs, toRef } from 'vue'
// 此时 person 是响应式数据
let person = reactive({ name: '张三', age: 18 })

// 此时,name, age 也是响应式的数据了。
let { name, age } = toRefs(person);

// 此时 n, a 也是响应式数据了。
let n = toRef(person, 'name')
let a = toRef(person, 'age')
</script>

标签的ref属性

  • 用在普通DOM标签上,获取的是DOM节点
  • 用在组件标签上,获取的是组件实例对象。访问组件实例对象中的属性方法,需要在组件中通过defineExpose暴露给父组件。
// 1、用在普通 DOM 标签上
<script setup lang="ts" name="HelloWorld">
import { ref } from 'vue';

let sum = ref(0)

const divRef = ref(); // 创建一个devRef, 用于存储 ref 标记的内容

const showLog = () => {
  console.log(divRef.value)
}
</script>

<template>
  <div ref="divRef">{{ sum }}</div>
  <button @click="showLog">输入h2</button>

</template>

// 2、用在组件标签上:示例请查看下方 defineExpose 讲解

组合式API —— computed

vue2 相比,仅修改了写法

<script setup>
// 1、导入 computed 函数
import { computed, ref } form 'vue'

const state = ref({})

// 执行函数、变量接收、在回调参数中 return 计算值
const computedState = computed(() => {
    return 基于响应式数据做计算之后的值
})
</script>

组合式API —— watch

  • 作用:侦听 一个 或 多个数据的变化,数据变化时执行回调函数。

    • 侦听 单个数据变化
    <script setup>
    // 导入 watch
    import { ref, watch } from 'vue'
    
    const count = ref(0)
    
    // 调用watch 侦听 单个数据变化
    watch(count, (newValue, oldValue) => {
        console.log(`count 发生变化,变化前的值${oldValue},变化后的值${newValue}`)
    })
    
    // 解除watch 监听
    const stopWatch = watch(count, (newValue, oldValue) => {
        if (count > 10) {
            stopWatch()
        }
    })
    </script>
    
    • 侦听 多个数据变化:不管哪个数据变化,都需要执行回调
    <script setup>
    // 导入 watch
    import { ref, reactive, watch } from 'vue'
    
    const count = ref(0)
    const name = ref('张三')
    const person = reactive({ name: '张三', age: 18 })
    
    // 调用watch 侦听 单个数据变化 ref
    watch([count, name], (newValue, oldValue) => {
        console.log(`count 发生变化,变化前的值${oldValue},变化后的值${newValue}`)
    }, {
        immediate: true, // 在侦听器创建时,立即触发回调
        deep: true // 开启深度监听。 监听器默认是 浅层监听对象 的
    })
    
    // 调用watch 侦听 reactive 对象的数据变化
    watch(person, (newValue, oldValue) => {
        console.log(person)
        // reactive 监听默认开启深度监听, 即 隐式创建了深度监听,
        // 并且该深度监视无法通过 {deep: false} 关闭
    })
    
    </script>
    
    • 侦听 对象的某个属性, 即一个函数,返回一个值
    <script setup>
    // 导入 watch
    import { ref,reactive, watch } from 'vue'
    
    const info = ref({ name: '张三', age: 18 })
    const person = reactive({
        name: '张三',
        age: 18,
        car: {
            c1: '奥迪',
            c2: '宝马'
        }
    })
    
    // 调用watch 侦听 单个数据变化
    watch(
    () => info.value.age,
    (newValue, oldValue) => {
        console.log(`count 发生变化,变化前的值${oldValue},变化后的值${newValue}`)
    })
    
    // 调用watch 监听 对象。此时监听的是 地址值,若想监视对象内部属性的变化,需要手动添加深度监听
    watch(info, (newVal, oldVal) => {
        console.log('监听的是对象的 地址值')
    })
    
    const changeC1 = () => {
        person.car.c1 = '大众'
    }
    
    const changeCar = () => {
        person.car = { c1: '雅迪', c2: '爱玛' }
    }
    
    // 监听响应式对象中的某个属性,且该属性是对象类型的:
    // 1、可以直接写 person.car。此时监听时,只有car内部变化(changeC1方法),才会触发监听。
    // 当触发changeCar 时,无法监听到car的变化
    //
    // 2、也能写函数 () => person.car。 此时可以监听到changeCar触发的car变化。即:对象的地址值,
    //若想监听对象内部的变化,此时需要手动开启深度监听 {deep: true}。
    //(官方更推荐该方式)
    watch(() => person.car, (newVal, oldVal) => {
        console.log('person.car 变化了', '官方推荐的方式')
    }, {deep: true})
    </script>
    

watchEffect:立即运行一个函数,同时响应式的追踪其依赖,并在依赖更改时重新执行该函数。

watch的区别:

watch 监听: 需要明确指出监听的数据

watchEffect 监听: 不用明确指出监听的数据,函数中用到哪些属性,就会监听哪些属性

// 使用 watch 监听: 需要明确指出监听的数据
<script setup>
 import { ref, watch } from 'vue';
 
 const a = ref(0);
 const b = ref(0);
 
 const changeA = () => { a += 1; }
 const changeB = () => { b += 1; }
 
 watch([a, b], (newVal) => {
     const [_a, _b] = newVal;
     
     if (_a > 10 || _b > 10 ) {
         console.log('监听达到条件后,触发执行')
     }
 })
</script>

// 使用 watchEffect 监听: 不用明确指出监听的数据,函数中用到哪些属性,就会监听哪些属性
<script setup>
 import { ref, watchEffect } from 'vue';
 
 const a = ref(0);
 const b = ref(0);
 
 const changeA = () => { a += 1; }
 const changeB = () => { b += 1; }
 
 watchEffect(() => {
     if (a.value > 10 || b.value > 10 ) {
         console.log('监听达到条件后,触发执行')
     }
 })
</script>

vue3生命周期API

选项式API组合式API
beforeCreate/createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
  • 生命周期函数基本使用
import { onMounted } from 'vue'

onMounted(() => {
    // 自定义逻辑
})
  • 执行多次

生命周期函数 是可以 执行多次的,多次执行时 传入的回调 会在 时机成熟时依次执行

import { onMounted } from 'vue'

onMounted(() => {
    console.log('mount1')
})

onMounted(() => {
    console.log('mount2')
})

vue3 组件通信

1、组合式API下的父传子:通过defineProps传递

  • 父组件中给子组件绑定属性
<script setup>
// 引入子组件
import SonVue from './son.vue';

import Person from './components/Person.vue';
import { type Persons } from '@/types';
let person = reactive<Persons>([
  { id: '1', name: '张三', age: 18 },
  { id: '2', name: '李四', age: 18 },
])
</script>

<template>
    <Person :list="person" />
    <SonVue msg='这是父组件传给子组件的一个值' />
</template>
  • 子组件内部通过props选项接收
// 通过 defineProps “编译器宏”接收子组件传递的数据
<script setup>
import { defineProps } from 'vue';// 由于 defineProps 是宏函数,在vue3中可以省略引入,直接使用

// 方式一:
const props = defineProps({
    list: String
})

// 方式二:
import { type Persons }  from '@/types'
const props = defineProps<{ list: Persons }>();
</script>

<template>
    <div v-for="item in list">{{ item.name }}</div>
    {{ msg }}
<template>

可以通过 withDefaultsdefineProps设置默认值

// list 是否必传,并设置默认值
const props = withDefaults(defineProps<{ list?: Persons }>(), {
  list: () => [
    {id :'1', name: '王五', age: 18}
  ]
})

2、组合式API下的子传父: 通过defineEmits 事件传递

  • 父组件中给 子组件标签通过@绑定事件
<script setup>
// 引入子组件
import SonVue from './son.vue'

const getMsg = (msg) => {
    console.log(msg)
}
</script>

<template>
    <SonVue @getMsgEvent="getMsg" />
</template>
  • 子组件内部通过emit方法触发事件
<script setup>
// 通过 defineEmits “编译器宏”生成emit方法
const emit = defineEmits(['getMsgEvent'])

const sendMsg = () => {
    emit('getMsgEvent', '子组件回传给父组件的内容')
}
</script>

<template>
    <button @click="sendMsg">发送数据</button>
<template>

3、组合式API-provideinject

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

    1、顶层组件通过provide函数提供数据

    2、底层组件通过inject函数获取数据

    // 组件嵌套关系
    // parentPage => childPage => grandSonPage
    1、顶层组件 `parentPage`
    <script setup>
        import { provide } from 'vue'
        import childPage from './childPage.vue'
    
        // 1、顶层组件提供数据
        provide('data-key', 'this is  from parentPage data')
    </script>
    
    <template>
        <div class='page'>
            顶层组件
            <childPage />
        </div>
    </template>
    
    2、底层组件 `grandSonPage`
    <script setup>
        import { inject } from 'vue'
        
        // 2、接收数据
        const parentData = inject('data-key')
    </script>
    
    <template>
        <div class='grandSonPage'>
            底层组件
            <div>来自顶层组件中的数据:{{ parentData }}</div>
            <div>来自顶层组件中的响应式数据:</div>
        </div>
    </template>
    

组合式API - 模版引用

1、概念

通过 ref标识获取真实的dom对象 或者 组件实例对象。因此,获取模版引用必须在组件挂载完毕

2、使用

<script setup>
import { ref } from 'vue'
import TestCom from './test-com.vue';

// 1、调用 ref 函数得到 ref 对象
const h1Ref = ref(null);
const comRef = ref(null);

// 组件挂载完毕之后才能获取
onMounted(() => {
    console.log(h1Ref.value)
    console.log(comRef.value)
})
</script>

<template>
    <!-- 2、通过 ref 标识绑定 ref对象 -->
    <h1 ref="h1Ref">我是dom标签h1</h1>
    <TestCom ref="comRef" />
</template>

3、defineExpose

使用<script setup>的组件是 默认关闭 的——即通过 模版引用 或者 $parent链获取到的组件的实例,不会 暴露 任何在 <script setup>中 声明的绑定。

可以通过 defineExpose编译器宏来显示指定在<script setup>组件中要暴露出去的属性方法

// 子组件 —— HelloWorld.vue
<script setup lang="ts" name="HelloWorld">
import { ref, defineExpose } from 'vue'; // 由于 defineExpose 是宏函数,在vue3中可以省略引入,直接使用

let sum = ref(0)
let count = ref(1)
let msg = ref('这是一条消息')

defineExpose({ sum, msg })

// 
</script>

// 父组件 —— App.vue
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'

import { ref } from 'vue';

const hello_world = ref();

const show = () => { //这里可以拿到 sum, msg 但是拿不到 count
  console.log(hello_world.value)
}
</script>

<template>
  <HelloWorld ref="hello_world" />
  <button @click="show">点击获取ref标记的内容</button>
</template>

当父组件通过 模版引用 的方式 获取到当前组件的实例,获取到的实例会像这样 { sum, msg }(ref会和在普通实例中一样被自动解包)

vue3新特性—— defineOptions

顾名思义,主要是用来定义Options API的选项。可以用defineOptions定义任意的选项, propsemitsexposeslots除外(因为这些可以使用defineXXX来做到)。

<script setup>
defineOptions({
    name: 'Foo',
    inheritAttrs: false,
    // ... 更多自定义属性
})
</script>

vue3新特性——defineModel

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

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

与 之前 先定义 props,再定义 emits 的方式相比,简化了很多代码。

<script setup>
    const modeValue = defineModel()

    modelValue.value++;
</script>

vue3自定义指令

image.png

// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';

import App from './App.vue';
import router. from './router';

// 引入初始化样式文件
import '@/styles/common.scss';

// useIntersectionObserver 方法判断图片是否进入视口。通过第三方插件`vueuse`
import { useIntersectionObserver } from '@vueuse/core';

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

// 自定义全局指令
app.directive('img-lazy', {
    mounted(el, binding) {
        // el: 指令绑定的那个元素 例如:img
        // binding:binding.value 指令等于号后面绑定的表达式的值。 例如:图片url
        useIntersectionObserver(
            el,
            ([{ isIntersecting }])=> {
                if (isIntersecting) {
                    // 进入视口区域
                    el.src = binding.value;
                }
            }
        )
    }
})


// 指令使用,页面文件home.vue
<template>
    <div v-for="item in list" :key="item.id">
        <img v-img-lazy='item.picture' alt='' />
    </div>
</template>

vue3-自定义插件

// directive/index.js

import { useIntersectionObserver } from '@vueuse/core';

// 自定义懒加载插件
export const lazyPlugin = {
    install (app) {
        app.directive('img-lazy', {
            mounted(el, binding) {
                // el: 指令绑定的那个元素 例如:img
                // binding:binding.value 指令等于号后面绑定的表达式的值。 例如:图片url
                const { stop } = useIntersectionObserver(
                    el,
                    ([{ isIntersecting }])=> {
                        if (isIntersecting) {
                            // 进入视口区域
                            el.src = binding.value;
                            stop(); // 第一次完成赋值后,停止监听
                        }
                    }
                )
            }
        })
    }
}



// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';

import App from './App.vue';
import router. from './router';

// 引入初始化样式文件
import '@/styles/common.scss';

// 引入懒加载指令插件并注册
import { lazyPlugin } from '@/directive';

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(lazyPlugin)

app.mount('#app')

vue3-组件全局插件化注册

// components/index.js

// 将 components 中 通用型的所有组件都进行全局化注册
// 通过插件的方式,避免每次使用时都导入一次组件

import ImageView from './ImageView/index.vue';
import Sku from './sku/index.vue';

export const componentPlugin = {
    install(app) {
        // app.component('组件名', 组件配置对象)
        app.component('ImageView', ImageView);
        app.component('Sku', Sku);
    }
}


// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';

import App from './App.vue';
import router. from './router';

// 引入初始化样式文件
import '@/styles/common.scss';

// 引入懒加载指令插件并注册
import { lazyPlugin } from '@/directive';

// 引入全局组件插件
import { componentPlugin } from '@/components';

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(lazyPlugin)
app.use(componentPlugin)

app.mount('#app')