Vue3新特性-创建Vue3项目、setup选项、reactive 和 ref函数、computed、watch、生命周期函数、父子通信、模板引用、provide和inject、defineOptions、defineModel、Pinia及新工具

Vue3的新特性

Vue2不会新增功能,只是进行维护

Vue3的优势:

  • 更易维护:

    • 组合式API
    • 更好的TS支持
  • 更快的速度:

    • 重写diff算法
    • 模板编译优化
    • 更高效的组件初始化
  • 更小的体积:

    • 良好的TreeShaking
    • 按需引入
  • 更优的数据响应式:

    • proxy

Vue2-选项式API与Vue3组合式API区别:

  • 当代码量很大时,Vue2选项式API的代码不易维护;而Vue3的组合式API使得同功能的代码集中在一起(集合式管理同功能的代码)提高了代码的可维护性(Vue3的选项式API代码更易复用,进行封装)

  • App.vue:

    • 脚本script和模板template顺序调整(结构和样式放在一起更易维护)
    • 模板template不再要求唯一根元素
    • 脚本script添加setup标识支持组合式API

image

例:

<!--Vue2选项式API-->
<script>
  export default{
    data(){
      return{
        count: 0
      }
    },
    methods:{
      addCount(){
        this.count++
      }
    }
  }
</script>
<!--Vue3组合式API-->
<script setup>
import {ref} from 'vue'
const count = ref(0)
const addCount = ()=> count.value++
</script>

创建Vue3项目

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

需要16.0版本以上的Node.js:node -v 查看node版本

  1. 创建Vue应用:npm init vue@latest

  2. cd Vue3Demo 切换到对应目录中

  3. npm install 安装相关依赖

  4. 启动项目:npm run dev

Vite相比于Webpack创建项目的速度非常快

image

// main.js解读
import './assets/main.css'

// new Vue()创建一个应用实例 => createApp()
// createApp() createStore()
// 将创建实例进行了封装,保证每个实例的独立封闭性
import { createApp } from 'vue'
import App from './App.vue'

// mount设置挂载点 #app(index.html中id为app的盒子)
createApp(App).mount('#app')

setup选项

export default{
  setup(){
    
  },
  beforeCreate(){
    
  }
}

image

注意:

  • setup的生命周期(执行时机)比beforeCreate还要早

  • 由于执行时机过早,setup函数获取不到this(this是undefined)

    <script>
    // eslint-disable-next-line vue/no-export-in-script-setup
    export default {
      setup() {
        console.log('setup', this)
      },
      beforeCreate() {
        console.log('beforeCreate')
      }
    }
    </script>
    
    <template>
      学习Vue3
    </template>
    

    image

  • 数据和函数需要在setup中return才能在模板中应用

    <script>
    export default {
      setup() {
        // 数据
        const message = 'this is a message'
        // 方法
        const logMessage = () => {
          console.log(message)
        }
        return {
          message,
          logMessage
        }
      }
    }
    </script>
    
    <template>
      {{message}}
      <button @click="logMessage">打印</button>
    </template>
    

    image

为了简化上述步骤,可使用<script setup> 语法糖

<script setup>
  // 数据
  const message = 'this is a message'
  // 方法
  const logMessage = () => {
  console.log(message)
}
</script>

<template>
  {{ message }}
  <button @click="logMessage">打印</button>
</template>

<script setup>语法糖原理:

底层帮我写好了setup(){ return{}}


reactive 和 ref函数

共同作用:用函数调用的方式生成响应式数据

reactive接受对象类型的数据参数传入,并返回一个响应式的对象

步骤:

  1. 从vue包中导入reactive函数

  2. 在<script setup>中执行reactive函数并传入类型为对象的初始值,并使用变量接收返回值

<script setup>
import { reactive } from 'vue'  
const state = reactive({复杂类型})
</script>

ref接收简单类型或对象类型的数据传入并返回一个响应式的对象

本质:在原有传入数据的基础上,外层包了一层对象,包成了复杂类型,在底层借助reactive实现响应式

因此,在脚本中访问ref中的值需要通过.value进行访问;在模板中则可以直接访问

例:

<script setup>
import { ref } from 'vue'  
const state = ref(0)
const setCount = () => {
  state.value++
}
</script>

<template>
  {{ state }}
  <button @click="setCount">+1</button>
</template>

computed

计算属性的基本思想与Vue2中完全一致,只是写法不一样

步骤:

  1. 导入computed函数

  2. 执行函数在回调参数中return基于响应式数据做计算的值,用变量接收

<script setup>
import { computed } from 'vue'  
const somputedState = computed(() => {
  return 基于响应式数据做计算后的值
})
</script>

例:

<script setup>
import { ref, computed } from 'vue'
const list = ref([1, 2, 3, 4, 5, 6, 7, 8])
const computedList = computed(() =>{
  return list.value.filter(item => item > 2)
})
</script>

<template>
  {{ computedList }}
</template>

上述所创建的计算属性是只读属性,不可被修改;可以通过配置set和get

const count = ref(1)
const newOne = computed({
  get:() = > count.value +1,
  set: (val) = > {
    count.value =val -1
  }
})
newOne.value = 1 //当newOne有值的时候set方法会被触发
console.log(count.value) //0

watch

作用:监听一个或多个数据变化,数据变化时执行回调函数

例:监听单个值的变化(注意: watch(ref对象) watch中监听的是ref对象

语法:

watch(ref对象,(newValue,oldValue) => {...})
<template>
    <div class="main">
        <div>{{ count }}</div>
        <button @click="changeNumber">changeNumber</button>
        <div>{{ name }}</div>
        <button @click="changeName">changeName</button>
    </div>
</template>

<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const name = ref('ddd')

const changeNumber = () => {
    count.value++
}
const changeName = () => {
    name.value = 'ccc'
}

watch(count, (newVal, oldVal) => {
    console.log(`newVal: ${newVal}, ${oldVal}`)
})
</script>


例:监听多个值的变化

语法:

watch([ref对象1,ref对象2],(newArr,oldArr) =>{...})
<template>
    <div class="main">
        <div>{{ count }}</div>
        <button @click="changeNumber">changeNumber</button>
        <div>{{ name }}</div>
        <button @click="changeName">changeName</button>
    </div>
</template>

<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const name = ref('ddd')

const changeNumber = () => {
    count.value++
}
const changeName = () => {
    name.value = 'ccc'
}

watch([count, name], (newVal, oldVal) => {
    console.log(`newVal: ${newVal}, oldVal:${oldVal}`)
})
</script>

image

额外参数:

  • immediate(创建监听器时,立刻触发回调 ,即响应式数据变化后继续执行回调)

    watch(count, (newVal, oldVal) => {
        console.log(`newVal: ${newVal}, oldVal:${oldVal}`)
    }, {
        immediate: true
    })
    

    一进入页面就触发


  • deep(深度监听 监听的是对象中的所有属性)

    默认watch进行的浅层监听(可以监听简单类型的变化,监听不到复杂类型内部数据的变化)

    <template>
        <div class="main">
            <div>{{ userInfo }}</div>
            <button @click="changeUserInfo">changeUserInfo</button>
        </div>
    </template>
    
    <script setup>
    import { ref, watch } from 'vue'
    const userInfo = ref({
        name: 'aaa',
        age: 18
    })
    
    const changeUserInfo = () => {
        userInfo.value.name = 'yuanyu'
        userInfo.value.age = 24
    }
    
    watch(userInfo, (newValue, oldValue) => {
        console.log(newValue, oldValue) // 不会输出内容
    })
    </script>
    

    因为监听的是ref对象的value属性,如果是复杂数据类型,其value属性是该对象的地址,地址没有变化所以不会输出

    <template>
        <div class="main">
            <div>{{ userInfo }}</div>
            <button @click="changeUserInfo">changeUserInfo</button>
        </div>
    </template>
    
    <script setup>
    import { ref, watch } from 'vue'
    const userInfo = ref({
        name: 'aaa',
        age: 18
    })
    
    const changeUserInfo = () => {
        userInfo.value = { // 修改了对象地址才能监听到
            name: 'yuanyu',
            age: 24
        }
    }
    
    watch(userInfo, (newValue) => {
        console.log(newValue) // 此处不需要输出旧的值,因为指向的是同一个地址,对象值发生变化,旧值是也是新的值
    })
    </script>
    

    输出:Proxy(Object) {name: 'yuanyu', age: 24} Proxy(Object) {name: 'aaa', age: 18}

    只监听对象的某个属性第一个参数写成函数写法,返回要监听的具体属性

    <template>
        <div class="main">
            <div>{{ userInfo }}</div>
            <button @click="changeUserInfo">changeUserInfo</button>
        </div>
    </template>
    
    <script setup>
    import { ref, watch } from 'vue'
    const userInfo = ref({
        name: 'aaa',
        age: 18
    })
    
    const changeUserInfo = () => {
        userInfo.value.age++
    }
    
    watch(()=>userInfo.value.age, (newVal, oldVal) => {
        console.log(newVal, oldVal);
        
    })
    </script>
    

生命周期函数

选项式API 组合式API
beforeCreate/created setup
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
<script setup>
import { onMounted } from 'vue'

const getList = () => {
    console.log('getList')
}
getList() // 一进入页面的请求

// 需要在生命周期中执行的代码
onMounted(() => {
    console.log('mounted生命周期函数-逻辑1');

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

})
</script>

注意:

  • Vue3中如果需要销毁不是destroy而是unmoutned

  • 数据请求函数直接写在<script setup></script> 语法糖中,直接调用

  • 其他生命周期函数写成函数调用的方式,可以调用多次不会冲突,而是顺序执行互不影响


父子通信

父传子

基本思想:

  • 父组件给子组件绑定属性

  • 子组件内部通过props选项接收

Vue3中父组件直接导入使用子组件(局部组件)即可

<template>
    <div class="main">
        <DemoOne></DemoOne> 
    </div>
</template>

<script setup>
import DemoOne from '@/components/DemoOne.vue'
</script>

父组件以添加属性的方式给子组件传值,子组件需要借助编译器宏函数接收子组件传递的数据

父组件:

<template>
    <div class="main">
        <h3>父组件-{{ money }}</h3>
        <DemoOne food="cake" :money="money"></DemoOne>
    </div>
</template>

<script setup>
import DemoOne from '@/components/DemoOne.vue'
import { ref } from 'vue'
const money = ref(10000)
</script>

子组件:

<template>
    <div class="main">
        我是子组件
         {{ food }}  <!--模板中可以直接使用父组件传递过来的数据 -->
         {{ money  }}
    </div>
</template>

<script setup>
const props = defineProps({
    food: {
        type: String,
    },
    money: {
        type: Number,
    } 
})
console.log(props.food);
console.log(props.money);

</script>

<style scoped>
.main{
    border: 1px solid black;
}
</style>

image


defineProps原理

defineProps是编译阶段的一个标识,实际编译器解析时,遇到后会进行编译转换,将其转换为props:{}形式

image


子传父

基本思想:

  • 父组件中给子组件标签绑定事件

  • 子组件内部通过emit方法触发自定义事件 (编译器宏defineEmits获取自定义事件

父组件

<template>
    <div class="main">
        <h3>父组件-{{ money }} <button @click="getMoney">挣钱</button></h3>
        <DemoOne food="cake" :money="money" @useMoney="changeFn"></DemoOne>
    </div>
</template>

<script setup>
import DemoOne from '@/components/DemoOne.vue'
import { ref } from 'vue'
const money = ref(10000)
const getMoney = () => {
    money.value += 100
}
const changeFn = (newMoney) => {
    money.value = newMoney
}
</script>

子组件

<template>
    <div class="main">
        我是子组件
         {{ food }}  <!--模板中可以直接使用父组件传递过来的数据 -->
         {{ money  }}
         <button @click="useMoney">花钱</button>
    </div>
</template>

<script setup>
const props = defineProps({
    food: {
        type: String,
    },
    money: {
        type: Number,
    } 
})
console.log(props.food);
console.log(props.money);

const emit = defineEmits(['useMoney']) // 定义一个事件,用于子组件向父组件传递数据
const useMoney = () => {
    emit('useMoney',5) // 触发事件,向父组件传递数据
}

</script>

<style scoped>
.main{
    border: 1px solid black;
}
</style>

模板引用

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

步骤:

  1. 调用ref函数生成一个ref对象

  2. 通过ref标识绑定ref对象到标签

  3. 通过ref对象,value即可访问到绑定的元素(必须渲染后才能拿到)

<script setup>
  import TestCom from '@/components/test-com.vue'
  import {onMounted, ref} from 'vue'
  
  const inp= ref(null)
  
  onMounted(()=>{
    console.log(inp.value)
    inp.value.focus()
  })
</script>

<template>
  <div>
      <input ref="inp" type="text">
      <button>点击让输入框聚焦</button>
  </div>
  <TestCom></TestCom>
</template>

默认情况下,<script setup>语法糖下组件内部的属性和方法是不开放给父组件访问的

可以通过defineExpose编译宏指定哪些属性和方法允许访问(显示暴露组件内部的属性和方法

如:

<script setup>
  import {ref} from 'vue'
  const message= ref('This is a message')
  defineExpose({
    testMessage
  })
</script>

provide和inject

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

注意:传递的可以是数据方法

顶层组件传递的方法可以允许底层组件修改顶层组件中的数据

步骤:

  1. 顶层组件通过provide函数提供数据:provide('key',顶层组件中的数据)

  2. 底层组件通过inject函数获取数据:const message = inject('key')

例:

父组件

<template>
    <div class="main">
        <h1>我是父亲-{{ count }}</h1>
        <DemoOne></DemoOne>
    </div>
</template>

<script setup>
import DemoOne from '@/components/DemoOne.vue'
import { provide, ref } from 'vue'

const count = ref(100)

provide('color', 'pink')
provide('count', count)
provide('changeCount', () => {
    count.value++
})
</script>

子组件

<template>
    <div class="main">
        <h2>我是儿子-{{ color }}</h2>
        {{ count }}
        <button @click="changeCount">修改数据</button>
        <DemoTwo></DemoTwo>
    </div>
</template>

<script setup>
import DemoTwo from '@/components/DemoTwo.vue';
import { inject } from 'vue'

const color = inject('color')
const count = inject('count')
const changeCount = inject('changeCount')
</script>

<style scoped>
.main{
    border: 1px solid black;
}
</style>

孙子组件

<template>
    <div class="main">
        <h3>我是孙子</h3>
    </div>
</template>

<script setup>

</script>

<style scoped>
.main {
    border: 1px solid saddlebrown;
}
</style>

image


defineOptions-Vue3.3新特性

由于setup语法糖,使得无法添加与setup平级的属性

为了解决这一问题引入了definePropsdefineEmits这两个宏,但只解决了propsemits这两个属性


defineOptions宏,主要用来定义OptionsAPI选项,可以用defineOptions定义任意的选项

props、emits、expose、slots除外(因为这些可以使用defineXXX来做到)

例:

<script setup>
defineOptions({
    name: 'DemoTwo' 
})
</script>

等价于
<script>
export default {
    name: 'DemoTwo' 
}
</script>

defineModel-Vue3.3新特性

Vue2中的v-model= :value + @input 需要先定义props,再定义emits

Vue3中的v-model= :modelValue + @update


defineModel可以实现父子组件的数据传递:

  • defineModel 是Vue 3提供的一个函数,用于在组件中定义 v-model 绑定。它返回一个响应式的 modelValue,这个值与父组件传递的 v-model 绑定

例:

父组件

<template>
    <div class="main">
        <DemoOne v-model="txt"></DemoOne>
        {{ txt }}
    </div>
</template>

<script setup>
import { ref } from 'vue'
import DemoOne from '@/components/DemoOne.vue'

const txt = ref('123456')
</script>

子组件

<template>
    <div class="main">
        <input type="text" :value="modelValue" @input="e=>modelValue=e.target.value">
    </div>
</template>

<script setup>
import { defineModel } from 'vue'
const modelValue = defineModel()
</script>

<style scoped>

</style>

image


Pinia

Pinia是最新的状态管理工具,是Vuex的替代品

相比于Vuex(state、mutations、actions、getters、modules),Pinia(state、actions、getters)的优点

  • 提供更简单的API(去掉了mutation,直接通过actions修改仓库数据)

  • 提供符合组合式风格的API,和Vue3新语法统一

  • 去掉了modules的概念,每一个store都是一个独立的模块

  • 配合TS更加友好,提供可靠的类型推断


使用步骤:

  1. 安装pinia:npm i pinia

  2. 挂载到main.js中:

    // 将创建实例进行了封装,保证每个实例的独立封闭性
    import { createApp } from 'vue'
    import App from './App.vue'
    import { createPinia } from 'pinia'
    
    const pinia = createPinia()
    // mount设置挂载点 #app(index.html中id为app的盒子)
    createApp(App).app.use(pinia).mount('#app') //支持链式
    

Pinia的基本使用

定义Store:

import { defineStore } from 'pinia'

// 第一个参数是你的应用中 Store 的唯一 ID。(仓库名)
export const useAlertsStore = defineStore('alerts', {
  // 其他配置...
})
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

Setup Store 中:

  • ref() 就是 state 属性

  • computed() 就是 getters

  • function() 就是 actions


例:counter.js

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 定义store
// defineStore(仓库唯一的标识,() => {...})
export const useCountStore = defineStore('counter', () => {
    // 声明数据 state
    const counter = ref(0)
    // 声明操作数据的方法 actions
    const addCounter = () => {
        counter.value++
    }
    const subCounter = () => {
        counter.value--
    }
    // 声明基于数据派生状态的计算属性 getters
    const double = computed(() => counter.value * 2)

    const msg = ref('hello pinia')

    return {
        counter,
        msg,
        addCounter,
        subCounter,
        double
    }
})
<template>
    <div class="main">
        <h1>App.vue根组件 - 0</h1>
        {{ countStore.counter }}
        {{ countStore.msg }}
        {{ countStore.double }}
        <DemoOne></DemoOne>
        <DemoTwo></DemoTwo>
    </div>
</template>

<script setup>
import DemoOne from '@/components/DemoOne.vue'
import DemoTwo from '@/components/DemoTwo.vue'
import { useCountStore } from '@/store/counter'

const countStore = useCountStore()

</script>
<template>
    <div class="main">
        <h2>我是One -{{ countStore.counter }}- <button @click="countStore.subCounter">-</button></h2>
    </div>
</template>

<script setup>
import { useCountStore } from '@/store/counter'
const countStore = useCountStore()

</script>

<style scoped>

</style>

image


解构问题

不能对得到的仓库对象进行解构,否则会丢失数据的响应式

可以通过:

import {storeToRefs} from 'pinia'
const {count, msg} = storeToRefs(counterStore)

将解构出来的数据转化为响应式数据


store是一个用reactive包装的对象,如果进行解构只是将对象中的值赋值给了新的变量

此时可以通过storeToRefs()函数将变量转化为响应式属性

但是可以直接从store中解构action,因为方法不需要响应式


action异步实现

接口地址:http://geek.itheima.net/v1_0/channels

import { defineStore } from "pinia";
import { ref } from "vue";
import axios from "axios";

export const useChannelStore = defineStore('channel', () => {
    const channelList = ref([]);
    const getChannelList = async () => {
        const res = await axios.get('http://geek.itheima.net/v1_0/channels')
        channelList.value = res.data.data.channels;
        console.log(res);
        
    }
    return {
        channelList,
        getChannelList
    }
});
<template>
    <div class="main">
        <h1>App.vue根组件 - 0</h1>
        {{ countStore.counter }}
        {{ countStore.msg }}
        {{ countStore.double }}
        <hr>
        <ul>
            <li v-for="item in channelStore.channelList" :key="channelStore.channelList.id">{{ item.name }}</li>
        </ul>
        <button @click="channelStore.getChannelList">获取数据</button>
        <DemoOne></DemoOne>
        <DemoTwo></DemoTwo>
    </div>
</template>

<script setup>
import DemoOne from '@/components/DemoOne.vue'
import DemoTwo from '@/components/DemoTwo.vue'
import { useCountStore } from '@/store/counter'
import { useChannelStore } from '@/store/channel'

const countStore = useCountStore()
const channelStore = useChannelStore()

</script>

image


持久化插件

官方网址:Pinia Plugin Persistedstate


使用步骤:

  1. 安装插件:npm i pinia-plugin-persistedstate

  2. 将插件添加到pinia实例

    import { createPinia } from 'pinia'
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    
    const pinia = createPinia()
    pinia.use(piniaPluginPersistedstate)
    
  3. 声明store时将persist 选项设置为true(第三个参数位置)

    自动实现将数据存储在本地存储(默认)

    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'
    
    // 定义store
    // defineStore(仓库唯一的标识,() => {...})
    export const useCountStore = defineStore('counter', () => {
        // 声明数据 state
        const counter = ref(0)
        // 声明操作数据的方法 actions
        const addCounter = () => {
            counter.value++
        }
        // 声明基于数据派生状态的计算属性 getters
        const double = computed(() => counter.value * 2)
    
        return {
            counter,
            addCounter,
            double
        }
    }, {
        persist: true
    })
    

    可以通过配置persist中的属性实现键名、本地存储的改变、指定持久化的变量

    persist:{
    	 key:'newCounter',
         storage: sessionStorage,
         pick: ['save.me', 'saveMeToo'] //只对'save.me', 'saveMeToo'持久化
    }
    

新工具

pnpm包管理器

npm => yarn => pnpm

优势:比同类工具快2倍左右,节省磁盘空间


安装:npm i -g pnpm

创建项目:pnpm create vue

项目初始化:pnpm install

启动项目:pnpm dev


区别:

npm yarn pnpm
npm install yarn pnpm install
npm install axios yarn add axios pnpm add axios
npm install axios -D yarn add axios -D pnpm add axios -D
npm uninstall axios yarn remove axios pnpm remove axios
npm run dev yarn dev pnpm dev

Eslint配置&prettier

  • prettier:美化代码的格式化工具

  • Eslint:校验代码,注重规范

二者通常一起使用


husky

代码进git仓库前的校验

posted @ 2025-03-29 12:19  原语  阅读(404)  评论(0)    收藏  举报