vue3快速上手指南

Heymar-10發表於2024-07-15

1. Vue3簡介

  • 2020年9月18日,Vue.js釋出版3.0版本,代號:One Piece(n

  • 經歷了:4800+次提交40+個RFC600+次PR300+貢獻者

  • 官方發版地址:Release v3.0.0 One Piece · vuejs/core

  • 截止2023年10月,最新的公開版本為:3.3.4

    image-20240715194906003

1.1. 【效能的提升】

  • 打包大小減少41%

  • 初次渲染快55%, 更新渲染快133%

  • 記憶體減少54%

1.2.【 原始碼的升級】

  • 使用Proxy代替defineProperty實現響應式。

  • 重寫虛擬DOM的實現和Tree-Shaking

1.3. 【擁抱TypeScript】

  • Vue3可以更好的支援TypeScript

1.4. 【新的特性】

  1. Composition API(組合API):

    • setup

    • refreactive

    • computedwatch

      ......

  2. 新的內建元件:

    • Fragment

    • Teleport

    • Suspense

      ......

  3. 其他改變:

    • 新的生命週期鉤子

    • data 選項應始終被宣告為一個函式

    • 移除keyCode支援作為 v-on 的修飾符

      ......

2. 建立Vue3工程

2.1. 【基於 vue-cli 建立】

點選檢視官方文件

備註:目前vue-cli已處於維護模式,官方推薦基於 Vite 建立專案。

## 檢視@vue/cli版本,確保@vue/cli版本在4.5.0以上
vue --version

## 安裝或者升級你的@vue/cli 
npm install -g @vue/cli

## 執行建立命令
vue create vue_test

##  隨後選擇3.x
##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)
##  > 3.x
##    2.x

## 啟動
cd vue_test
npm run serve

2.2. 【基於 vite 建立】(推薦)

vite 是新一代前端構建工具,官網地址:https://vitejs.cnvite的優勢如下:

  • 輕量快速的熱過載(HMR),能實現極速的服務啟動。
  • TypeScriptJSXCSS 等支援開箱即用。
  • 真正的按需編譯,不再等待整個應用編譯完成。
  • webpack構建 與 vite構建對比圖如下:
    webpack構建 vite構建
  • 具體操作如下(點選檢視官方文件
## 1.建立命令
npm create vue@latest

## 2.具體配置
## 配置專案名稱
√ Project name: vue3_test
## 是否新增TypeScript支援
√ Add TypeScript?  Yes
## 是否新增JSX支援
√ Add JSX Support?  No
## 是否新增路由環境
√ Add Vue Router for Single Page Application development?  No
## 是否新增pinia環境
√ Add Pinia for state management?  No
## 是否新增單元測試
√ Add Vitest for Unit Testing?  No
## 是否新增端到端測試方案
√ Add an End-to-End Testing Solution? » No
## 是否新增ESLint語法檢查
√ Add ESLint for code quality?  Yes
## 是否新增Prettiert程式碼格式化
√ Add Prettier for code formatting?  No

自己動手編寫一個App元件

<template>
  <div class="app">
    <h1>你好啊!</h1>
  </div>
</template>

<script lang="ts">
  export default {
    name:'App' //元件名
  }
</script>

<style>
  .app {
    background-color: #ddd;
    box-shadow: 0 0 10px;
    border-radius: 10px;
    padding: 20px;
  }
</style>

安裝官方推薦的vscode外掛:

image-20240715195013198

image-20240715195017872

總結:

  • Vite 專案中,index.html 是專案的入口檔案,在專案最外層。
  • 載入index.html後,Vite 解析 <script type="module" src="xxx"> 指向的JavaScript
  • Vue3**中是透過 **createApp 函式建立一個應用例項。

2.3. 【一個簡單的效果】

Vue3向下相容Vue2語法,且Vue3中的模板中可以沒有根標籤

<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年齡:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年齡+1</button>
    <button @click="showTel">點我檢視聯絡方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'App',
    data() {
      return {
        name:'張三',
        age:18,
        tel:'13888888888'
      }
    },
    methods:{
      changeName(){
        this.name = 'zhang-san'
      },
      changeAge(){
        this.age += 1
      },
      showTel(){
        alert(this.tel)
      }
    },
  }
</script>

3. Vue3核心語法

3.1. 【OptionsAPI 與 CompositionAPI】

  • Vue2API設計是Options(配置)風格的。
  • Vue3API設計是Composition(組合)風格的。

Options API 的弊端

Options型別的 API,資料、方法、計算屬性等,是分散在:datamethodscomputed中的,若想新增或者修改一個需求,就需要分別修改:datamethodscomputed,不便於維護和複用。

2.gif1696662200734-1bad8249-d7a2-423e-a3c3-ab4c110628be

Composition API 的優勢

可以用函式的方式,更加優雅的組織程式碼,讓相關功能的程式碼更加有序的組織在一起。

1696662249851-db6403a1-acb5-481a-88e0-e1e34d2ef53a1696662256560-7239b9f9-a770-43c1-9386-6cc12ef1e9c0

說明:以上四張動圖原創作者:大帥老猿

3.2. 【拉開序幕的 setup】

setup 概述

setupVue3中一個新的配置項,值是一個函式,它是 Composition API “表演的舞臺,元件中所用到的:資料、方法、計算屬性、監視......等等,均配置在setup中。

特點如下:

  • setup函式返回的物件中的內容,可直接在模板中使用。
  • setup中訪問thisundefined
  • setup函式會在beforeCreate之前呼叫,它是“領先”所有鉤子執行的。
<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年齡:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年齡+1</button>
    <button @click="showTel">點我檢視聯絡方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'Person',
    setup(){
      // 資料,原來寫在data中(注意:此時的name、age、tel資料都不是響應式資料)
      let name = '張三'
      let age = 18
      let tel = '13888888888'

      // 方法,原來寫在methods中
      function changeName(){
        name = 'zhang-san' //注意:此時這麼修改name頁面是不變化的
        console.log(name)
      }
      function changeAge(){
        age += 1 //注意:此時這麼修改age頁面是不變化的
        console.log(age)
      }
      function showTel(){
        alert(tel)
      }

      // 返回一個物件,物件中的內容,模板中可以直接使用
      return {name,age,tel,changeName,changeAge,showTel}
    }
  }
</script>

setup 的返回值

  • 若返回一個物件:則物件中的:屬性、方法等,在模板中均可以直接使用(重點關注)。
  • 若返回一個函式:則可以自定義渲染內容,程式碼如下:
setup(){
  return ()=> '你好啊!'
}

setup 與 Options API 的關係

  • Vue2 的配置(datamethos......)中可以訪問到 setup中的屬性、方法。
  • 但在setup不能訪問到Vue2的配置(datamethos......)。
  • 如果與Vue2衝突,則setup優先。

setup 語法糖

setup函式有一個語法糖,這個語法糖,可以讓我們把setup獨立出去,程式碼如下:

<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年齡:{{age}}</h2>
    <button @click="changName">修改名字</button>
    <button @click="changAge">年齡+1</button>
    <button @click="showTel">點我檢視聯絡方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'Person',
  }
</script>

<!-- 下面的寫法是setup語法糖 -->
<script setup lang="ts">
  console.log(this) //undefined
  
  // 資料(注意:此時的name、age、tel都不是響應式資料)
  let name = '張三'
  let age = 18
  let tel = '13888888888'

  // 方法
  function changName(){
    name = '李四'//注意:此時這麼修改name頁面是不變化的
  }
  function changAge(){
    console.log(age)
    age += 1 //注意:此時這麼修改age頁面是不變化的
  }
  function showTel(){
    alert(tel)
  }
</script>

這樣寫了之後,之前的script一般就拿來寫個name就可以了,但是隻是寫個name專門用個script標籤麻煩,所以可以藉助下面的方法

直接在steup的script標籤寫上

image-20240618143203354

擴充套件:上述程式碼,還需要編寫一個不寫setupscript標籤,去指定元件名字,比較麻煩,我們可以藉助vite中的外掛簡化

  1. 第一步:npm i vite-plugin-vue-setup-extend -D
  2. 第二步:vite.config.ts
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

export default defineConfig({
  plugins: [ VueSetupExtend() ]
})
  1. 第三步:<script setup lang="ts" name="Person">

3.3. 【ref 建立:基本型別的響應式資料】

  • 作用:定義響應式變數。
  • 語法:let xxx = ref(初始值)
  • 返回值:一個RefImpl的例項物件,簡稱ref物件refref物件的value屬性是響應式的

加了ref後就是ref的一個例項物件,然後值是在裡面的value屬性裡面

image-20240618145341550

image-20240618145348312

  • 注意點:

    • JS中運算元據需要:xxx.value,但模板中不需要.value,直接使用即可。(本來是要name.value,模板裡面自己給你新增上了value就不用新增
    • 對於let name = ref('張三')來說,name不是響應式的,name.value是響應式的。但是要修改他的話,在修改(函式)裡面是需要寫明value的才能響應式

    image-20240618145554920

<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年齡:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年齡+1</button>
    <button @click="showTel">點我檢視聯絡方式</button>
  </div>
</template>

<script setup lang="ts" name="Person">
  import {ref} from 'vue'
  // name和age是一個RefImpl的例項物件,簡稱ref物件,它們的value屬性是響應式的。
  let name = ref('張三')
  let age = ref(18)
  // tel就是一個普通的字串,不是響應式的
  let tel = '13888888888'

  function changeName(){
    // JS中操作ref物件時候需要.value
    name.value = '李四'
    console.log(name.value)

    // 注意:name不是響應式的,name.value是響應式的,所以如下程式碼並不會引起頁面的更新。
    // name = ref('zhang-san')
  }
  function changeAge(){
    // JS中操作ref物件時候需要.value
    age.value += 1 
    console.log(age.value)
  }
  function showTel(){
    alert(tel)
  }
</script>

3.4. 【reactive 建立:物件型別的響應式資料】

  • 作用:定義一個響應式物件(基本型別不要用它,要用ref,否則報錯)
  • 語法:let 響應式物件= reactive(源物件)
  • 返回值:一個Proxy的例項物件,簡稱:響應式物件。

這個是將一個物件程式設計一個proxy建立的物件接受響應式(陣列同理也是這個方法)

image-20240618150214652

image-20240618150219506

  • 注意點:reactive定義的響應式資料是“深層次”的。
<template>
  <div class="person">
    <h2>汽車資訊:一臺{{ car.brand }}汽車,價值{{ car.price }}萬</h2>
    <h2>遊戲列表:</h2>
    <ul>
      <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
    <h2>測試:{{obj.a.b.c.d}}</h2>
    <button @click="changeCarPrice">修改汽車價格</button>
    <button @click="changeFirstGame">修改第一遊戲</button>
    <button @click="test">測試</button>
  </div>
</template>

<script lang="ts" setup name="Person">
import { reactive } from 'vue'

// 資料
let car = reactive({ brand: '賓士', price: 100 })
let games = reactive([ 
  { id: 'ahsgdyfa01', name: '英雄聯盟' },
  { id: 'ahsgdyfa02', name: '王者榮耀' },
  { id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

function changeCarPrice() {
  car.price += 10
}
function changeFirstGame() {
  games[0].name = '流星蝴蝶劍'
}
function test(){
  obj.a.b.c.d = 999
}
</script>

3.5. 【ref 建立:物件型別的響應式資料】

  • 其實ref接收的資料可以是:基本型別物件型別ref可以對物件響應式資料,但是注意改的時候要加value,特別是陣列形式加在哪裡

其原理還是把變數做了一個響應式變成了value形式,但是裡面的物件還是做了一個proxy

image-20240618151009851

  • ref接收的是物件型別,內部其實也是呼叫了reactive函式。
<template>
  <div class="person">
    <h2>汽車資訊:一臺{{ car.brand }}汽車,價值{{ car.price }}萬</h2>
    <h2>遊戲列表:</h2>
    <ul>
      <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
    <h2>測試:{{obj.a.b.c.d}}</h2>
    <button @click="changeCarPrice">修改汽車價格</button>
    <button @click="changeFirstGame">修改第一遊戲</button>
    <button @click="test">測試</button>
  </div>
</template>

<script lang="ts" setup name="Person">
import { ref } from 'vue'

// 資料
let car = ref({ brand: '賓士', price: 100 })
let games = ref([
  { id: 'ahsgdyfa01', name: '英雄聯盟' },
  { id: 'ahsgdyfa02', name: '王者榮耀' },
  { id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

console.log(car)

function changeCarPrice() {
  car.value.price += 10
}
function changeFirstGame() {
  games.value[0].name = '流星蝴蝶劍'
}
function test(){
  obj.value.a.b.c.d = 999
}
</script>

3.6. 【ref 對比 reactive】

宏觀角度看:

  1. ref用來定義:基本型別資料物件型別資料

  2. reactive用來定義:物件型別資料

  • 區別:
  1. ref建立的變數必須使用.value(可以使用volar外掛自動新增.value)。

    image-20240715195302384

    image-20240618151309065

  2. reactive重新分配一個新物件,會失去響應式(可以使用Object.assign去整體替換)。

image-20240618151802755

這樣寫也不行

image-20240618151918799

可以這樣

image-20240618152106715

如果是用的ref響應物件,就可以直接改

image-20240618152223647

  • 使用原則:
  1. 若需要一個基本型別的響應式資料,必須使用ref
  2. 若需要一個響應式物件,層級不深,refreactive都可以。
  3. 若需要一個響應式物件,且層級較深,推薦使用reactive

3.7. 【toRefs 與 toRef】

如果像這樣解構賦值,那麼出來的name age都不是響應式資料

image-20240618153249185

想讓響應式資料轉換出來也是響應式就押用到這個

  • 作用:將一個響應式物件中的每一個屬性,轉換為ref物件。

注意:torefs之後會建立一個新的reactive響應式物件,但是值都是指向同一個值你修改一個兩個都會變

image-20240618153728117

image-20240618153745849

image-20240618153736075

  • 備註:toRefstoRef功能一致,但toRefs可以批次轉換。
  • 語法如下:
<template>
  <div class="person">
    <h2>姓名:{{person.name}}</h2>
    <h2>年齡:{{person.age}}</h2>
    <h2>性別:{{person.gender}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年齡</button>
    <button @click="changeGender">修改性別</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,reactive,toRefs,toRef} from 'vue'

  // 資料
  let person = reactive({name:'張三', age:18, gender:'男'})
	
  // 透過toRefs將person物件中的n個屬性批次取出,且依然保持響應式的能力
  let {name,gender} =  toRefs(person)
	
  // 透過toRef將person物件中的gender屬性取出,且依然保持響應式的能力
  let age = toRef(person,'age')

  // 方法
  function changeName(){
    name.value += '~'
  }
  function changeAge(){
    age.value += 1
  }
  function changeGender(){
    gender.value = '女'
  }
</script>

3.8. 【computed】

作用:根據已有資料計算出新資料(和Vue2中的computed作用一致)。

computed

計算屬性本身是監視它裡面的值的變化從而引起自身的變化,所以要改他的時候,也不是直接改他本身,而是改他裡面的值

image-20240618161651520

image-20240618161733797

<template>
  <div class="person">
    姓:<input type="text" v-model="firstName"> <br>
    名:<input type="text" v-model="lastName"> <br>
    全名:<span>{{fullName}}</span> <br>
    <button @click="changeFullName">全名改為:li-si</button>
  </div>
</template>

<script setup lang="ts" name="App">
  import {ref,computed} from 'vue'

  let firstName = ref('zhang')
  let lastName = ref('san')

  // 計算屬性——只讀取,不修改
  /* let fullName = computed(()=>{
    return firstName.value + '-' + lastName.value
  }) */


  // 計算屬性——既讀取又修改
  let fullName = computed({
    // 讀取
    get(){
      return firstName.value + '-' + lastName.value
    },
    // 修改
    set(val){
      console.log('有人修改了fullName',val)
      firstName.value = val.split('-')[0]
      lastName.value = val.split('-')[1]
    }
  })

  function changeFullName(){
    fullName.value = 'li-si'
  } 
</script>

3.9.【watch】

  • 作用:監視資料的變化(和Vue2中的watch作用一致)
  • 特點:Vue3中的watch只能監視以下四種資料
  1. ref定義的資料。
  2. reactive定義的資料。
  3. 函式返回一個值(getter函式)。
  4. 一個包含上述內容的陣列。

我們在Vue3中使用watch的時候,通常會遇到以下幾種情況:

* 情況一

監視ref定義的【基本型別】資料:直接寫資料名即可,監視的是其value值的改變。

stopwatch監視會返回一個值是一個箭頭函式,呼叫這個函式可以取消掉監視

<template>
  <div class="person">
    <h1>情況一:監視【ref】定義的【基本型別】資料</h1>
    <h2>當前求和為:{{sum}}</h2>
    <button @click="changeSum">點我sum+1</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,watch} from 'vue'
  // 資料
  let sum = ref(0)
  // 方法
  function changeSum(){
    sum.value += 1
  }
  // 監視,情況一:監視【ref】定義的【基本型別】資料
  const stopWatch = watch(sum,(newValue,oldValue)=>{
    console.log('sum變化了',newValue,oldValue)
    if(newValue >= 10){
      stopWatch()
    }
  })
</script>

* 情況二

監視ref定義的【物件型別】資料:直接寫資料名,監視的是物件的【地址值】,若想監視物件內部的資料,要手動開啟深度監視。

注意:

  • 若修改的是ref定義的物件中的屬性,newValueoldValue 都是新值,因為它們是同一個物件。

image-20240618163350407

  • 若修改整個ref定義的物件,newValue 是新值, oldValue 是舊值,因為不是同一個物件了。

不開啟深度,整個物件改變了,才會觸發監視

image-20240618162919153

<template>
  <div class="person">
    <h1>情況二:監視【ref】定義的【物件型別】資料</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年齡:{{ person.age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年齡</button>
    <button @click="changePerson">修改整個人</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,watch} from 'vue'
  // 資料
  let person = ref({
    name:'張三',
    age:18
  })
  // 方法
  function changeName(){
    person.value.name += '~'
  }
  function changeAge(){
    person.value.age += 1
  }
  function changePerson(){
    person.value = {name:'李四',age:90}
  }
  /* 
    監視,情況一:監視【ref】定義的【物件型別】資料,監視的是物件的地址值,若想監視物件內部屬性的變化,需要手動開啟深度監視
    watch的第一個引數是:被監視的資料
    watch的第二個引數是:監視的回撥
    watch的第三個引數是:配置物件(deep、immediate等等.....) 
  */
  watch(person,(newValue,oldValue)=>{
    console.log('person變化了',newValue,oldValue)
  },{deep:true})
  
</script>

* 情況三

監視reactive定義的【物件型別】資料,且預設開啟了深度監視並且修改屬性新老value還是一樣的

<template>
  <div class="person">
    <h1>情況三:監視【reactive】定義的【物件型別】資料</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年齡:{{ person.age }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年齡</button>
    <button @click="changePerson">修改整個人</button>
    <hr>
    <h2>測試:{{obj.a.b.c}}</h2>
    <button @click="test">修改obj.a.b.c</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {reactive,watch} from 'vue'
  // 資料
  let person = reactive({
    name:'張三',
    age:18
  })
  let obj = reactive({
    a:{
      b:{
        c:666
      }
    }
  })
  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changePerson(){
    Object.assign(person,{name:'李四',age:80})
  }
  function test(){
    obj.a.b.c = 888
  }

  // 監視,情況三:監視【reactive】定義的【物件型別】資料,且預設是開啟深度監視的
  watch(person,(newValue,oldValue)=>{
    console.log('person變化了',newValue,oldValue)
  })
  watch(obj,(newValue,oldValue)=>{
    console.log('Obj變化了',newValue,oldValue)
  })
</script>

* 情況四

監視refreactive定義的【物件型別】資料中的某個屬性,注意點如下:

  1. 若該屬性值不是【物件型別】,需要寫成函式形式。

    image-20240618170450244

  2. 若該屬性值是依然是【物件型別】,可直接寫,也可寫成函式,建議寫成函式。

但是此時如果是這整個物件改變不會監視到

image-20240618170735829

這樣就是這個物件怎麼變都監視得到,寫成函式是為了整個物件變,deep是物件裡面變化

image-20240618170904972

結論:監視的要是物件裡的屬性,那麼最好寫函式式,注意點:若是物件監視的是地址值,需要關注物件內部,需要手動開啟深度監視。

<template>
  <div class="person">
    <h1>情況四:監視【ref】或【reactive】定義的【物件型別】資料中的某個屬性</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年齡:{{ person.age }}</h2>
    <h2>汽車:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年齡</button>
    <button @click="changeC1">修改第一臺車</button>
    <button @click="changeC2">修改第二臺車</button>
    <button @click="changeCar">修改整個車</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {reactive,watch} from 'vue'

  // 資料
  let person = reactive({
    name:'張三',
    age:18,
    car:{
      c1:'賓士',
      c2:'寶馬'
    }
  })
  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changeC1(){
    person.car.c1 = '奧迪'
  }
  function changeC2(){
    person.car.c2 = '大眾'
  }
  function changeCar(){
    person.car = {c1:'雅迪',c2:'愛瑪'}
  }

  // 監視,情況四:監視響應式物件中的某個屬性,且該屬性是基本型別的,要寫成函式式
  /* watch(()=> person.name,(newValue,oldValue)=>{
    console.log('person.name變化了',newValue,oldValue)
  }) */

  // 監視,情況四:監視響應式物件中的某個屬性,且該屬性是物件型別的,可以直接寫,也能寫函式,更推薦寫函式
  watch(()=>person.car,(newValue,oldValue)=>{
    console.log('person.car變化了',newValue,oldValue)
  },{deep:true})
</script>

* 情況五

監視上述的多個資料

寫成陣列形式就可以,value是這個陣列

image-20240618173108012

<template>
  <div class="person">
    <h1>情況五:監視上述的多個資料</h1>
    <h2>姓名:{{ person.name }}</h2>
    <h2>年齡:{{ person.age }}</h2>
    <h2>汽車:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年齡</button>
    <button @click="changeC1">修改第一臺車</button>
    <button @click="changeC2">修改第二臺車</button>
    <button @click="changeCar">修改整個車</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {reactive,watch} from 'vue'

  // 資料
  let person = reactive({
    name:'張三',
    age:18,
    car:{
      c1:'賓士',
      c2:'寶馬'
    }
  })
  // 方法
  function changeName(){
    person.name += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changeC1(){
    person.car.c1 = '奧迪'
  }
  function changeC2(){
    person.car.c2 = '大眾'
  }
  function changeCar(){
    person.car = {c1:'雅迪',c2:'愛瑪'}
  }

  // 監視,情況五:監視上述的多個資料
  watch([()=>person.name,person.car],(newValue,oldValue)=>{
    console.log('person.car變化了',newValue,oldValue)
  },{deep:true})

</script>

3.10. 【watchEffect】

  • 官網:立即執行一個函式,同時響應式地追蹤其依賴,並在依賴更改時重新執行該函式。

  • watch對比watchEffect

    1. 都能監聽響應式資料的變化,不同的是監聽資料變化的方式不同

    2. watch:要明確指出監視的資料

    3. watchEffect:不用明確指出監視的資料(函式中用到哪些屬性,那就監視哪些屬性)。

比如這個例子,都可以完成但是watcheffect上來就執行一次,而且會根據裡面所寫的自動監測要監視誰

image-20240618174038113

  • 示例程式碼:

    <template>
      <div class="person">
        <h1>需求:水溫達到50℃,或水位達到20cm,則聯絡伺服器</h1>
        <h2 id="demo">水溫:{{temp}}</h2>
        <h2>水位:{{height}}</h2>
        <button @click="changePrice">水溫+1</button>
        <button @click="changeSum">水位+10</button>
      </div>
    </template>
    
    <script lang="ts" setup name="Person">
      import {ref,watch,watchEffect} from 'vue'
      // 資料
      let temp = ref(0)
      let height = ref(0)
    
      // 方法
      function changePrice(){
        temp.value += 10
      }
      function changeSum(){
        height.value += 1
      }
    
      // 用watch實現,需要明確的指出要監視:temp、height
      watch([temp,height],(value)=>{
        // 從value中獲取最新的temp值、height值
        const [newTemp,newHeight] = value
        // 室溫達到50℃,或水位達到20cm,立刻聯絡伺服器
        if(newTemp >= 50 || newHeight >= 20){
          console.log('聯絡伺服器')
        }
      })
    
      // 用watchEffect實現,不用
      const stopWtach = watchEffect(()=>{
        // 室溫達到50℃,或水位達到20cm,立刻聯絡伺服器
        if(temp.value >= 50 || height.value >= 20){
          console.log(document.getElementById('demo')?.innerText)
          console.log('聯絡伺服器')
        }
        // 水溫達到100,或水位達到50,取消監視
        if(temp.value === 100 || height.value === 50){
          console.log('清理了')
          stopWtach()
        }
      })
    </script>
    

3.11. 【標籤的 ref 屬性】

作用:用於註冊模板引用。

  • 用在普通DOM標籤上,獲取的是DOM節點。

  • 用在元件標籤上,獲取的是元件例項物件。

用在普通DOM標籤上:

<template>
  <div class="person">
    <h1 ref="title1">尚矽谷</h1>
    <h2 ref="title2">前端</h2>
    <h3 ref="title3">Vue</h3>
    <input type="text" ref="inpt"> <br><br>
    <button @click="showLog">點我列印內容</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref} from 'vue'
	
  let title1 = ref()
  let title2 = ref()
  let title3 = ref()

  function showLog(){
    // 透過id獲取元素
    const t1 = document.getElementById('title1')
    // 列印內容
    console.log((t1 as HTMLElement).innerText)
    console.log((<HTMLElement>t1).innerText)
    console.log(t1?.innerText)
    
		/************************************/
		
    // 透過ref獲取元素
    console.log(title1.value)
    console.log(title2.value)
    console.log(title3.value)
  }
</script>

用在元件標籤上:

但是要獲取元件上的資料需要子元件做出defineexpose匯出操作

<!-- 父元件App.vue -->
<template>
  <Person ref="ren"/>
  <button @click="test">測試</button>
</template>

<script lang="ts" setup name="App">
  import Person from './components/Person.vue'
  import {ref} from 'vue'

  let ren = ref()

  function test(){
    console.log(ren.value.name)
    console.log(ren.value.age)
  }
</script>


<!-- 子元件Person.vue中要使用defineExpose暴露內容 -->
<script lang="ts" setup name="Person">
  import {ref,defineExpose} from 'vue'
	// 資料
  let name = ref('張三')
  let age = ref(18)
  /****************************/
  /****************************/
  // 使用defineExpose將元件中的資料交給外部
  defineExpose({name,age})
</script>

3.11.1 回顧TS中的介面、泛型、自定義型別

注意vue3引用元件直接匯入即可

image-20240619095613391

如果在子元件定義了一組資料,但是不小心名字打錯了

image-20240619102135028

後期就容易呼叫不到

我們就可以去定義一個介面來約束型別,一般是在src下面建立一個types資料夾,然後匯出

image-20240619102324144

要用的話就在元件匯入,並且前面要加type表明匯入的是一個型別,然後在要用的變數後規則如下

image-20240619102558991

image-20240619102610562

如果是一個陣列資料想這樣用就不行

image-20240619105310248

除非是用泛型先規定它是一個陣列,然後陣列裡面每一項需要符合這個規則

image-20240619105401516

當然也可以去自定義型別,直接在ts裡面宣告

image-20240619105543151

image-20240619105616518

3.12. 【props】

// 定義一個介面,限制每個Person物件的格式
export interface PersonInter {
id:string,
name:string,
age:number
}

// 定義一個自定義型別Persons
export type Persons = Array<PersonInter>

App.vue中程式碼:

注意用reactive響應式資料,此時介面應該用泛型來約束,而且資料是不能多不能少的

image-20240619110844779

如果某個屬性想可有可無

image-20240619110911690

<template>
	<Person :list="persons"/>
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {reactive} from 'vue'
import {type Persons} from './types'

let persons = reactive<Persons>([
{id:'e98219e12',name:'張三',age:18},
{id:'e98219e13',name:'李四',age:19},
 {id:'e98219e14',name:'王五',age:20}
])
</script>

Person.vue中程式碼:

只接受會出問題,萬一傳過來是個數字也會遍歷

image-20240619111609486

注意接收+限制型別的寫法,prop括號裡面不寫了,提出來規定一個泛型,裡面為一個物件,誰要約束什麼

image-20240619111855781

image-20240619111859051

限制必要性,一個?父元件就可傳可不傳了

image-20240619112122252

如果不寫自己還有預設值,匯入,再用

image-20240619112322154

用的時候注意,把原來寫的全部包起來,第二個引數寫預設值,並且鍵值對,屬性值不能為一個陣列形式,可以寫為一個函式返回

image-20240619112326242

<template>
<div class="person">
<ul>
<li v-for="item in list" :key="item.id">
  {{item.name}}--{{item.age}}
</li>
</ul>
</div>
</template>

<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
import {type PersonInter} from '@/types'

// 第一種寫法:僅接收
// const props = defineProps(['list'])

// 第二種寫法:接收+限制型別
// defineProps<{list:Persons}>()

// 第三種寫法:接收+限制型別+指定預設值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小豬佩奇',age:18}]
})
console.log(props)
</script>

3.13. 【生命週期】

  • 規律:

    生命週期整體分為四個階段,分別是:建立、掛載、更新、銷燬,每個階段都有兩個鉤子,一前一後。

  • Vue2的生命週期

    建立階段:beforeCreatecreated

    掛載階段:beforeMountmounted

    更新階段:beforeUpdateupdated

    銷燬階段:beforeDestroydestroyed

v3的建立就是setup本身

  • Vue3的生命週期

    建立階段:setup

    掛載階段:onBeforeMountonMounted

    更新階段:onBeforeUpdateonUpdated

    解除安裝階段:onBeforeUnmountonUnmounted

  • 常用的鉤子:onMounted(掛載完畢)、onUpdated(更新完畢)、onBeforeUnmount(解除安裝之前)

  • 示例程式碼:

    <template>
      <div class="person">
        <h2>當前求和為:{{ sum }}</h2>
        <button @click="changeSum">點我sum+1</button>
      </div>
    </template>
    
    <!-- vue3寫法 -->
    <script lang="ts" setup name="Person">
      import { 
        ref, 
        onBeforeMount, 
        onMounted, 
        onBeforeUpdate, 
        onUpdated, 
        onBeforeUnmount, 
        onUnmounted 
      } from 'vue'
    
      // 資料
      let sum = ref(0)
      // 方法
      function changeSum() {
        sum.value += 1
      }
      console.log('setup')
      // 生命週期鉤子
      onBeforeMount(()=>{
        console.log('掛載之前')
      })
      onMounted(()=>{
        console.log('掛載完畢')
      })
      onBeforeUpdate(()=>{
        console.log('更新之前')
      })
      onUpdated(()=>{
        console.log('更新完畢')
      })
      onBeforeUnmount(()=>{
        console.log('解除安裝之前')
      })
      onUnmounted(()=>{
        console.log('解除安裝完畢')
      })
    </script>
    

3.14. 【自定義hook】

如果一個頁面中全是資料和方法,看起很臃腫複雜,跟vue2又很像

注意:如果要往空陣列push等作出操作需要規定陣列的泛型

image-20240619123154863

而hooks就類似於mixin,把所有程式碼模組化,src建立資料夾hooks,建立模組的ts檔案,把該屬於這個部分的程式碼都放進來

image-20240619123524361

注意:這裡面就跟剛才一樣什麼都能寫包括生命鉤子,但是會對外暴露一個函式,然後把所有資料方法返回

image-20240619123529934

image-20240619123535545

元件要用就直接匯入,並且那邊是函式,這邊直接呼叫,透過解構賦值,獲得return出來的資料和方法

image-20240619123848962

示例程式碼:

  • useSum.ts中內容如下:

    import {ref,onMounted} from 'vue'
    
    export default function(){
      let sum = ref(0)
    
      const increment = ()=>{
        sum.value += 1
      }
      const decrement = ()=>{
        sum.value -= 1
      }
      onMounted(()=>{
        increment()
      })
    
      //向外部暴露資料
      return {sum,increment,decrement}
    }		
    
  • useDog.ts中內容如下:

    import {reactive,onMounted} from 'vue'
    import axios,{AxiosError} from 'axios'
    
    export default function(){
      let dogList = reactive<string[]>([])
    
      // 方法
      async function getDog(){
        try {
          // 發請求
          let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
          // 維護資料
          dogList.push(data.message)
        } catch (error) {
          // 處理錯誤
          const err = <AxiosError>error
          console.log(err.message)
        }
      }
    
      // 掛載鉤子
      onMounted(()=>{
        getDog()
      })
    	
      //向外部暴露資料
      return {dogList,getDog}
    }
    
  • 元件中具體使用:

    <template>
      <h2>當前求和為:{{sum}}</h2>
      <button @click="increment">點我+1</button>
      <button @click="decrement">點我-1</button>
      <hr>
      <img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)"> 
      <span v-show="dogList.isLoading">載入中......</span><br>
      <button @click="getDog">再來一隻狗</button>
    </template>
    
    <script lang="ts">
      import {defineComponent} from 'vue'
    
      export default defineComponent({
        name:'App',
      })
    </script>
    
    <script setup lang="ts">
      import useSum from './hooks/useSum'
      import useDog from './hooks/useDog'
    	
      let {sum,increment,decrement} = useSum()
      let {dogList,getDog} = useDog()
    </script>
    

4. 路由

4.1. 【對路由的理解】

image-20240715195545218

4.2. 【基本切換效果】

  • Vue3中要使用vue-router的最新版本,目前是4版本。

  • 基本規則

image-20240620105128654

第一步,建立導航區,展示區

image-20240620105239582

  • 第二步建立store資料夾,匯入兩個api,第一個必須宣告路由工作模式,第二個如何來建立路由createrouter

image-20240620104921122

入口檔案匯入註冊

image-20240620105101469

  • 路由配置檔案程式碼如下:

    import {createRouter,createWebHistory} from 'vue-router'
    import Home from '@/pages/Home.vue'
    import News from '@/pages/News.vue'
    import About from '@/pages/About.vue'
    
    const router = createRouter({
    	history:createWebHistory(),
    	routes:[
    		{
    			path:'/home',
    			component:Home
    		},
    		{
    			path:'/about',
    			component:About
    		}
    	]
    })
    export default router
    
  • main.ts程式碼如下:

    import router from './router/index'
    app.use(router)
    
    app.mount('#app')
    
  • App.vue程式碼如下 第三步

    <template>
      <div class="app">
        <h2 class="title">Vue路由測試</h2>
        <!-- 導航區 -->
        <div class="navigate">
          <RouterLink to="/home" active-class="active">首頁</RouterLink>
          <RouterLink to="/news" active-class="active">新聞</RouterLink>
          <RouterLink to="/about" active-class="active">關於</RouterLink>
        </div>
        <!-- 展示區 -->
        <div class="main-content">
          <RouterView></RouterView>
        </div>
      </div>
    </template>
    
    <script lang="ts" setup name="App">
      import {RouterLink,RouterView} from 'vue-router'  
    </script>
    

4.3. 【兩個注意點】

  1. 路由元件通常存放在pagesviews資料夾,一般元件通常存放在components資料夾。

  2. 透過點選導航,視覺效果上“消失” 了的路由元件,預設是被解除安裝掉的,需要的時候再去掛載

4.4.【路由器工作模式】

  1. history模式

    優點:URL更加美觀,不帶有#,更接近傳統的網站URL

    缺點:後期專案上線,需要服務端配合處理路徑問題,否則重新整理會有404錯誤。

    const router = createRouter({
    	history:createWebHistory(), //history模式
    	/******/
    })
    
  2. hash模式

    優點:相容性更好,因為不需要伺服器端處理路徑。

    缺點:URL帶有#不太美觀,且在SEO最佳化方面相對較差。

    const router = createRouter({
    	history:createWebHashHistory(), //hash模式
    	/******/
    })
    

4.5. 【to的兩種寫法】

<!-- 第一種:to的字串寫法 -->
<router-link active-class="active" to="/home">主頁</router-link>

<!-- 第二種:to的物件寫法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>

4.6. 【命名路由】

作用:可以簡化路由跳轉及傳參(後面就講)。

給路由規則命名:

routes:[
  {
    name:'zhuye',
    path:'/home',
    component:Home
  },
  {
    name:'xinwen',
    path:'/news',
    component:News,
  },
  {
    name:'guanyu',
    path:'/about',
    component:About
  }
]

跳轉路由:

<!--簡化前:需要寫完整的路徑(to的字串寫法) -->
<router-link to="/news/detail">跳轉</router-link>

<!--簡化後:直接透過名字跳轉(to的物件寫法配合name屬性) -->
<router-link :to="{name:'guanyu'}">跳轉</router-link>

4.7. 【巢狀路由】

  1. 編寫News的子路由:Detail.vue

  2. 配置路由規則,使用children配置項:

    const router = createRouter({
      history:createWebHistory(),
    	routes:[
    		{
    			name:'zhuye',
    			path:'/home',
    			component:Home
    		},
    		{
    			name:'xinwen',
    			path:'/news',
    			component:News,
    			children:[
    				{
    					name:'xiang',
    					path:'detail',
    					component:Detail
    				}
    			]
    		},
    		{
    			name:'guanyu',
    			path:'/about',
    			component:About
    		}
    	]
    })
    export default router
    
  3. 跳轉路由(記得要加完整路徑):

    <router-link to="/news/detail">xxxx</router-link>
    <!-- 或 -->
    <router-link :to="{path:'/news/detail'}">xxxx</router-link>
    
  4. 記得去Home元件中預留一個<router-view>

    <template>
      <div class="news">
        <nav class="news-list">
          <RouterLink v-for="news in newsList" :key="news.id" :to="{path:'/news/detail'}">
            {{news.name}}
          </RouterLink>
        </nav>
        <div class="news-detail">
          <RouterView/>
        </div>
      </div>
    </template>
    

4.8. 【路由傳參】

query引數

  1. 傳遞引數

    <!-- 跳轉並攜帶query引數(to的字串寫法) -->
    <router-link to="/news/detail?a=1&b=2&content=歡迎你">
    	跳轉
    </router-link>
    				
    <!-- 跳轉並攜帶query引數(to的物件寫法) -->
    <RouterLink 
      :to="{
        //name:'xiang', //用name也可以跳轉
        path:'/news/detail',
        query:{
          id:news.id,
          title:news.title,
          content:news.content
        }
      }"
    >
      {{news.title}}
    </RouterLink>
    
  2. 接收引數:

    import {useRoute} from 'vue-router'
    const route = useRoute()
    // 列印query引數
    console.log(route.query)
    

接收引數解構賦值版

image-20240620111747395

但是這樣有個問題,之前說過在響應式物件上面直接解構,會讓他失去響應式,解決方法是一個torefs

image-20240620111859940

params引數

要配置

image-20240620112146867

  1. 傳遞引數

    <!-- 跳轉並攜帶params引數(to的字串寫法) -->
    <RouterLink :to="`/news/detail/001/新聞001/內容001`">{{news.title}}</RouterLink>
    				
    <!-- 跳轉並攜帶params引數(to的物件寫法) -->
    <RouterLink 
      :to="{
        name:'xiang', //用name跳轉
        params:{
          id:news.id,
          title:news.title,
          content:news.title
        }
      }"
    >
      {{news.title}}
    </RouterLink>
    
  2. 接收引數:

    import {useRoute} from 'vue-router'
    const route = useRoute()
    // 列印params引數
    console.log(route.params)
    

備註1:傳遞params引數時,若使用to的物件寫法,必須使用name配置項,不能用path

備註2:傳遞params引數時,需要提前在規則中佔位。

4.9. 【路由的props配置】

就算剛才用這個方法,但是還是複雜在標籤上

image-20240620112543493

作用:讓路由元件更方便的收到引數(可以將路由引數作為props傳給元件)

第一種,只需要在路由配置裡面開啟

image-20240620112738226

image-20240620112800533

第二種

image-20240620112955678

{
	name:'xiang',
	path:'detail/:id/:title/:content',
	component:Detail,

  // props的物件寫法,作用:把物件中的每一組key-value作為props傳給Detail元件
  // props:{a:1,b:2,c:3}, 

  // props的布林值寫法,作用:把收到了每一組params引數,作為props傳給Detail元件
  // props:true
  
  // props的函式寫法,作用:把返回的物件中每一組key-value作為props傳給Detail元件
  props(route){
    return route.query
  }
}

4.10. 【 replace屬性】

  1. 作用:控制路由跳轉時操作瀏覽器歷史記錄的模式。

  2. 瀏覽器的歷史記錄有兩種寫入方式:分別為pushreplace

    • push是追加歷史記錄(預設值)。
    • replace是替換當前記錄。
  3. 開啟replace模式:

    <RouterLink replace .......>News</RouterLink>
    

4.11. 【程式設計式導航】

路由元件的兩個重要的屬性:$route$router變成了兩個hooks

import {useRoute,useRouter} from 'vue-router'

const route = useRoute()
const router = useRouter()

console.log(route.query)
console.log(route.parmas)
console.log(router.push)
console.log(router.replace)

4.12. 【重定向】

  1. 作用:將特定的路徑,重新定向到已有路由。

  2. 具體編碼:

    {
        path:'/',
        redirect:'/about'
    }
    

5. pinia

5.1【準備一個效果】

pinia_example

點選獲取,就往陣列增加一條土味情話

注意先從promise返回解構data,如果還想繼續從data裡面解構可以在後面來個:加物件,因為content是data物件裡面的,甚至直接給content:給他來個別名

image-20240620143354515

5.2【搭建 pinia 環境】

第一步:npm install pinia

第二步:操作src/main.ts

import { createApp } from 'vue'
import App from './App.vue'

/* 引入createPinia,用於建立pinia */
import { createPinia } from 'pinia'

/* 建立pinia */
const pinia = createPinia()
const app = createApp(App)

/* 使用外掛 */{}
app.use(pinia)
app.mount('#app')

此時開發者工具中已經有了pinia選項

image-20240715195634763

5.3【儲存+讀取資料】

  1. 它有三個概念:stategetteraction,相當於元件中的: datacomputedmethods

  2. 具體編碼:src/store/count.ts

    取名最好要跟hooks一樣, 函式里面的名字最好跟檔名保持一致

    // 引入defineStore用於建立store
    import {defineStore} from 'pinia'
    
    // 定義並暴露一個store
    export const useCountStore = defineStore('count',{
      // 動作
      actions:{},
      // 狀態
      state(){
        return {
          sum:6
        }
      },
      // 計算
      getters:{}
    })
    
  3. 具體編碼:src/store/talk.ts

    // 引入defineStore用於建立store
    import {defineStore} from 'pinia'
    
    // 定義並暴露一個store
    export const useTalkStore = defineStore('talk',{
      // 動作
      actions:{},
      // 狀態
      state(){
        return {
          talkList:[
            {id:'yuysada01',content:'你今天有點怪,哪裡怪?怪好看的!'},
         		{id:'yuysada02',content:'草莓、藍莓、蔓越莓,你想我了沒?'},
            {id:'yuysada03',content:'心裡給你留了一塊地,我的死心塌地'}
          ]
        }
      },
      // 計算
      getters:{}
    })
    
  4. 元件中使用state中的資料

    注意:單獨用ref那麼是.value,但是如果裡面是ref定義,外面又用了一個reactive,那他就會給你拆包,也就是直接拿不用再value

    image-20240620154636499

    得到對應store之後它是一個reactive物件,然後裡面state定義的資料是放在ref裡面的,那就拆包了

    image-20240620154820609

    image-20240620154829471

    <template>
      <h2>當前求和為:{{ sumStore.sum }}</h2>
    </template>
    
    <script setup lang="ts" name="Count">
      // 引入對應的useXxxxxStore	
      import {useSumStore} from '@/store/sum'
      
      // 呼叫useXxxxxStore得到對應的store
      const sumStore = useSumStore()
    </script>
    
    <template>
    	<ul>
        <li v-for="talk in talkStore.talkList" :key="talk.id">
          {{ talk.content }}
        </li>
      </ul>
    </template>
    
    <script setup lang="ts" name="Count">
      import axios from 'axios'
      import {useTalkStore} from '@/store/talk'
    
      const talkStore = useTalkStore()
    </script>
    

5.4.【修改資料】(三種方式)

  1. 第一種修改方式,直接修改

    countStore.sum = 666
    
  2. 第二種修改方式:批次修改

    要改的資料較多的時候

    countStore.$patch({
      sum:999,
      school:'atguigu'
    })
    
  3. 第三種修改方式:藉助action修改(action中可以編寫一些業務邏輯)

    import { defineStore } from 'pinia'
    
    export const useCountStore = defineStore('count', {
      /*************/
      actions: {
        //加
        increment(value:number) {
          if (this.sum < 10) {
            //操作countStore中的sum
            this.sum += value
          }
        },
        //減
        decrement(value:number){
          if(this.sum > 1){
            this.sum -= value
          }
        }
      },
      /*************/
    })
    
  4. 元件中呼叫action即可

    // 使用countStore
    const countStore = useCountStore()
    
    // 呼叫對應action
    countStore.incrementOdd(n.value)
    

5.5.【storeToRefs】

有個問題,現在模板上全部要加什麼.sum都有個字首,所以我想就是直接解構賦值拿到我要的資料,但是從響應式解構賦值又是老問題了

image-20240620163349775

這樣做可以得到響應式資料,但有點浪費,因為他不僅是將資料變成ref,把整個store包括裡面的方法也變了

image-20240620163503050

所以就可以用pinia上有個專門的方法來轉他就是隻變資料

  • 藉助storeToRefsstore中的資料轉為ref物件,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只會將資料做轉換,而VuetoRefs會轉換store中資料。
<template>
	<div class="count">
		<h2>當前求和為:{{sum}}</h2>
	</div>
</template>

<script setup lang="ts" name="Count">
  import { useCountStore } from '@/store/count'
  /* 引入storeToRefs */
  import { storeToRefs } from 'pinia'

	/* 得到countStore */
  const countStore = useCountStore()
  /* 使用storeToRefs轉換countStore,隨後解構 */
  const {sum} = storeToRefs(countStore)
</script>

5.6.【getters】

  1. 概念:當state中的資料,需要經過處理後再使用時,可以使用getters配置。

  2. 追加getters配置。

    // 引入defineStore用於建立store
    import {defineStore} from 'pinia'
    
    // 定義並暴露一個store
    export const useCountStore = defineStore('count',{
      // 動作
      actions:{
        /************/
      },
      // 狀態
      state(){
        return {
          sum:1,
          school:'atguigu'
        }
      },
      // 計算
      getters:{
        bigSum:(state):number => state.sum *10,
        upperSchool():string{
          return this. school.toUpperCase()
        }
      }
    })
    
  3. 元件中讀取資料:

    const {increment,decrement} = countStore
    let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)
    

5.7.【$subscribe】

透過 store 的 $subscribe() 方法偵聽 state 及其變化

talkStore.$subscribe((mutate,state)=>{
  console.log('LoveTalk',mutate,state)
  localStorage.setItem('talk',JSON.stringify(talkList.value))
})

5.8. 【store組合式寫法】

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
import {reactive} from 'vue'

export const useTalkStore = defineStore('talk',()=>{
  // talkList就是state
  const talkList = reactive(
    JSON.parse(localStorage.getItem('talkList') as string) || []
  )

  // getATalk函式相當於action
  async function getATalk(){
    // 發請求,下面這行的寫法是:連續解構賦值+重新命名
    let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
    // 把請求回來的字串,包裝成一個物件
    let obj = {id:nanoid(),title}
    // 放到陣列中
    talkList.unshift(obj)
  }
  return {talkList,getATalk}
})

6. 元件通訊

Vue3元件通訊和Vue2的區別:

  • 移出事件匯流排,使用mitt代替。
  • vuex換成了pinia
  • .sync最佳化到了v-model裡面了。
  • $listeners所有的東西,合併到$attrs中了。
  • $children被砍掉了。

常見搭配形式:

image-20240715195702252

6.1. 【props】

概述:props是使用頻率最高的一種通訊方式,常用與 :父 ↔ 子

  • 父傳子:屬性值是非函式
  • 子傳父:屬性值是函式

父元件:

<template>
  <div class="father">
    <h3>父元件,</h3>
		<h4>我的車:{{ car }}</h4>
		<h4>兒子給的玩具:{{ toy }}</h4>
		<Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	// 資料
	const car = ref('賓士')
	const toy = ref()
	// 方法
	function getToy(value:string){
		toy.value = value
	}
</script>

子元件

字傳父一種是可以直接寫在標籤裡面類似下面

<template>
  <div class="child">
    <h3>子元件</h3>
		<h4>我的玩具:{{ toy }}</h4>
		<h4>父給我的車:{{ car }}</h4>
		<button @click="getToy(toy)">玩具給父親</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	const toy = ref('奧特曼')
	
	defineProps(['car','getToy'])
</script>

還有一種就是一個變數接受

image-20240621110322478

6.2. 【自定義事件】

  1. 概述:自定義事件常用於:子 => 父。

event型別可以為event

image-20240621110655876

  1. 注意區分好:原生事件、自定義事件。
  • 原生事件:
    • 事件名是特定的(clickmosueenter等等)
    • 事件物件$event: 是包含事件相關資訊的物件(pageXpageYtargetkeyCode
  • 自定義事件:
    • 事件名是任意名稱
    • 事件物件$event: 是呼叫emit時所提供的資料,可以是任意型別!!!
  1. 示例:

    <!--在父元件中,給子元件繫結自定義事件:-->
    <Child @send-toy="toy = $event"/>
    
    <!--注意區分原生事件與自定義事件中的$event-->
    <button @click="toy = $event">測試</button>
    
    //子元件中,觸發事件:
    this.$emit('send-toy', 具體資料)
    

也可以正常走邏輯,這樣來傳

image-20240621111552597

6.3. 【mitt】

概述:與訊息訂閱與釋出(pubsub)功能類似,可以實現任意元件間通訊。

安裝mitt

npm i mitt

新建檔案:src\utils\emitter.ts

都是寫在這個ts檔案裡面,一共四個api,on是繫結,off解綁,emit觸發,all全部事件可以.clear全部清空

image-20240621112715056

// 引入mitt 
import mitt from "mitt";

// 建立emitter
const emitter = mitt()

/*
  // 繫結事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被觸發',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被觸發',value)
  })

  setInterval(() => {
    // 觸發事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 建立並暴露mitt
export default emitter

元件內使用

接收資料的元件中:繫結事件、同時在銷燬前解綁事件

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 繫結事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被觸發',value)
})

onUnmounted(()=>{
  // 解綁事件
  emitter.off('send-toy')
})

【第三步】:提供資料的元件,在合適的時候觸發事件

import emitter from "@/utils/emitter";

function sendToy(){
  // 觸發事件
  emitter.emit('send-toy',toy.value)
}

注意這個重要的內建關係,匯流排依賴著這個內建關係

6.4.【v-model】

  1. 概述:實現 父↔子 之間相互通訊。

  2. 前序知識 —— v-model的本質

    直接寫$event.target.value會報錯,e.target是一個物件,這個獨享就可能為空,所以這裡用了個斷言告訴他是一個html輸入的元素

    image-20240621114005497

    <!-- 使用v-model指令 -->
    <input type="text" v-model="userName">
    
    <!-- v-model的本質是下面這行程式碼 -->
    <input 
      type="text" 
      :value="userName" 
      @input="userName =(<HTMLInputElement>$event.target).value"
    >
    
  3. 元件標籤上的v-model的本質::moldeValueupdate:modelValue事件。

    下面是本質,上面是程式設計師簡寫方式,如果v-model不寫別名,預設名字傳過去就是modelValue

    <!-- 元件標籤上使用v-model指令 -->
    <AtguiguInput v-model="userName"/>
    
    <!-- 元件標籤上v-model的本質 -->
    <AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>
    

    AtguiguInput元件中:

    <template>
      <div class="box">
        <!--將接收的value值賦給input元素的value屬性,目的是:為了呈現資料 -->
    		<!--給input元素繫結原生input事件,觸發input事件時,進而觸發update:model-value事件-->
        <input 
           type="text" 
           :value="modelValue" 
           @input="emit('update:model-value',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['modelValue'])
      // 宣告事件
      const emit = defineEmits(['update:model-value'])
    </script>
    
  4. 也可以更換value,例如改成abc

    也可以改名字,注意:使用父元件這邊可以直接使用簡寫方式,但是接受子元件這邊,還是要跟著原理來,一個props繫結,一個觸發自定義事件

    <!-- 也可以更換value,例如改成abc-->
    <AtguiguInput v-model:abc="userName"/>
    
    <!-- 上面程式碼的本質如下 -->
    <AtguiguInput :abc="userName" @update:abc="userName = $event"/>
    

    AtguiguInput元件中:

    <template>
      <div class="box">
        <input 
           type="text" 
           :value="abc" 
           @input="emit('update:abc',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['abc'])
      // 宣告事件
      const emit = defineEmits(['update:abc'])
    </script>
    
  5. 如果value可以更換,那麼就可以在元件標籤上多次使用v-model

    <AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
    

6.5.【$attrs 】

  1. 概述:(祖→孫)。

  2. 具體說明:$attrs是一個物件,包含所有父元件傳入的標籤屬性。

    注意:$attrs會自動排除props中宣告的屬性(可以認為宣告過的 props 被子元件自己“消費”了)

    image-20240621143802662

    image-20240621143756187

父元件:

<template>
  <div class="father">
    <h3>父元件</h3>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	let a = ref(1)
	let b = ref(2)
	let c = ref(3)
	let d = ref(4)

	function updateA(value){
		a.value = value
	}
</script>

子元件:

<template>
	<div class="child">
		<h3>子元件</h3>
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" name="Child">
	import GrandChild from './GrandChild.vue'
</script>

孫元件:

v-bind後面寫物件,相當於一個一個繫結

image-20240621143709174

<template>
	<div class="grand-child">
		<h3>孫元件</h3>
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(666)">點我更新A</button>
	</div>
</template>

<script setup lang="ts" name="GrandChild">
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

父傳一個自定義事件,同理也可以實現孫給祖傳

image-20240621144130285

image-20240621144056636

6.6. 【$refs、$parent】

  1. 概述:

    • $refs用於 :父→子。

    父元件這邊先給子元件繫結ref,然後給這個ref的名字呼叫ref函式,透過事件就可以拿到子元件傳過來的

    image-20240621144647300

    子元件

    一個宏函式把資料曝光出來

    image-20240621144522090

    `$refs就是一個api獲取所有子元件

    這裡面放的就是一個個打了ref的子元件物件

    image-20240621145256713

    image-20240621145749646

    • $parent用於:子→父。

    image-20240621145823955

    父元件

    image-20240621145845515

    image-20240621145858251

  2. 原理如下:

    屬性 說明
    $refs 值為物件,包含所有被ref屬性標識的DOM元素或元件例項。
    $parent 值為物件,當前元件的父元件例項物件。

6.7. 【provide、inject】

  1. 概述:實現祖孫元件直接通訊(attrs需要中間人)

  2. 具體使用:

    • 在祖先元件中透過provide配置向後代元件提供資料
    • 在後代元件中透過inject配置來宣告接收資料
  3. 具體編碼:

    【第一步】父元件中,使用provide提供資料

    <template>
      <div class="father">
        <h3>父元件</h3>
        <h4>資產:{{ money }}</h4>
        <h4>汽車:{{ car }}</h4>
        <button @click="money += 1">資產+1</button>
        <button @click="car.price += 1">汽車價格+1</button>
        <Child/>
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
      import Child from './Child.vue'
      import { ref,reactive,provide } from "vue";
      // 資料
      let money = ref(100)
      let car = reactive({
        brand:'賓士',
        price:100
      })
      // 用於更新money的方法
      function updateMoney(value:number){
        money.value += value
      }
      // 提供資料
      provide('moneyContext',{money,updateMoney})
      provide('car',car)
    </script>
    

    注意:子元件中不用編寫任何東西,是不受到任何打擾的

    提供資料時候,ref資料不用.value,前面名字,後面資料

    【第二步】孫元件中使用inject配置項接受資料。

    <template>
      <div class="grand-child">
        <h3>我是孫元件</h3>
        <h4>資產:{{ money }}</h4>
        <h4>汽車:{{ car }}</h4>
        <button @click="updateMoney(6)">點我</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
      import { inject } from 'vue';
      // 注入資料
     let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
      let car = inject('car')
    </script>
    

注入資料,就用一個變數來接受就可以了,第二個引數是不寫的話也可以由預設值

孫給爺傳

爺元件

image-20240621152124080

孫元件

image-20240621152106974

6.8. 【pinia】

參考之前pinia部分的講解

6.9. 【slot】

1. 預設插槽

img

父元件中:
        <Category title="今日熱門遊戲">
          <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
          </ul>
        </Category>
子元件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <!-- 預設插槽 -->
            <slot></slot>
          </div>
        </template>

image-20240621154705813

2. 具名插槽

父元件中:
        <Category title="今日熱門遊戲">
          <template v-slot:s1>
            <ul>
              <li v-for="g in games" :key="g.id">{{ g.name }}</li>
            </ul>
          </template>
          <template #s2>
            <a href="">更多</a>
          </template>
        </Category>
子元件中:
        <template>
          <div class="item">
            <h3>{{ title }}</h3>
            <slot name="s1"></slot>
            <slot name="s2"></slot>
          </div>
        </template>

3. 作用域插槽

  1. 理解:資料在元件的自身,但根據資料生成的結構需要元件的使用者來決定。(新聞資料在News元件中,但使用資料所遍歷出來的結構由App元件決定)

  2. 具體編碼:

    父元件中:
          <Game v-slot="params">
          <!-- <Game v-slot:default="params"> -->
          <!-- <Game #default="params"> -->
            <ul>
              <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
            </ul>
          </Game>
    
    子元件中:
          <template>
            <div class="category">
              <h2>今日遊戲榜單</h2>
              <slot :games="games" a="哈哈"></slot>
            </div>
          </template>
    
          <script setup lang="ts" name="Category">
            import {reactive} from 'vue'
            let games = reactive([
              {id:'asgdytsa01',name:'英雄聯盟'},
              {id:'asgdytsa02',name:'王者榮耀'},
              {id:'asgdytsa03',name:'紅色警戒'},
              {id:'asgdytsa04',name:'斗羅大陸'}
            ])
          </script>
    

作用於插槽也可以有名字

image-20240621155522623

7. 其它 API

7.1.【shallowRef 與 shallowReactive 】

shallowRef

  1. 作用:建立一個響應式資料,但只對頂層屬性進行響應式處理。

  2. 用法:

    之前用ref繫結的資料隨便改

    image-20240621155851071

    如果都改為shallowref

    image-20240621155938968

    之前單資料和整個物件還可以改,但是單獨修改物件裡面某個屬性就不行了

    image-20240621155932392

    此淺層就是隻把第一層程式設計響應式,.value之後就不管了

    image-20240621160044842

    用處就是如果有個物件很大,我不關心它裡面某個值變沒變化,就關心物件有沒有整體被改就可以用這個

  3. 特點:只跟蹤引用值的變化,不關心值內部的屬性變化。

shallowReactive

  1. 作用:建立一個淺層響應式物件,只會使物件的最頂層屬性變成響應式的,物件內部的巢狀屬性則不會變成響應式的

  2. 用法:

    image-20240621160506086

    此時修改都可以生效

    image-20240621160517693

    如果用淺層次,那就是第二層改不了了

    image-20240621160609214

    作用同上

  3. 特點:物件的頂層屬性是響應式的,但巢狀物件的屬性不是。

總結

透過使用 shallowRef()shallowReactive() 來繞開深度響應。淺層式 API 建立的狀態只在其頂層是響應式的,對所有深層的物件不會做任何處理,避免了對每一個內部屬性做響應式所帶來的效能成本,這使得屬性的訪問變得更快,可提升效能。

7.2.【readonly 與 shallowReadonly】

readonly

  1. 用法:

    只能讀不能改

    image-20240621160914805

    也可以針對物件,裡面的屬性都不能改

    image-20240621161149692

  2. 特點:

    • 物件的所有巢狀屬性都將變為只讀。
    • 任何嘗試修改這個物件的操作都會被阻止(在開發模式下,還會在控制檯中發出警告)。
  3. 應用場景:

    • 建立不可變的狀態快照。
    • 保護全域性狀態或配置不被修改。

shallowReadonly

  1. 作用:與 readonly 類似,但只作用於物件的頂層屬性。

  2. 用法:

    只對淺層次生效,也就是第二層可以改裡面的屬性可以改

    image-20240621161243567

    上面不可改,下面可改

    image-20240621161315703

  3. 特點:

    • 只將物件的頂層屬性設定為只讀,物件內部的巢狀屬性仍然是可變的。

    • 適用於只需保護物件頂層屬性的場景。

7.3.【toRaw 與 markRaw】

toRaw

  1. 作用:用於獲取一個響應式物件的原始物件, toRaw 返回的物件不再是響應式的,不會觸發檢視更新。

    官網描述:這是一個可以用於臨時讀取而不引起代理訪問/跟蹤開銷,或是寫入而不觸發更改的特殊方法。不建議儲存對原始物件的持久引用,請謹慎使用。

    何時使用? —— 在需要將響應式物件傳遞給非 Vue 的庫或外部系統時,使用 toRaw 可以確保它們收到的是普通物件

    使用場景,並不推薦,把一個響應物件變為不響應之後給到另一個物件,而是加入一個函式需要這個物件,要動到他,但是也不想引起改變

    image-20240621161906958

  2. 具體編碼:

將一個響應式資料程式設計不再響應

image-20240621161550976

image-20240621161523850

markRaw

  1. 作用:標記一個物件,使其永遠不會變成響應式的。

    例如使用mockjs時,為了防止誤把mockjs變為響應式物件,可以使用 markRaw 去標記mockjs

  2. 編碼:

    /* markRaw */
    let citys = markRaw([
      {id:'asdda01',name:'北京'},
      {id:'asdda02',name:'上海'},
      {id:'asdda03',name:'天津'},
      {id:'asdda04',name:'重慶'}
    ])
    // 根據原始物件citys去建立響應式物件citys2 —— 建立失敗,因為citys被markRaw標記了
    let citys2 = reactive(citys)
    

7.4.【customRef】

作用:建立一個自定義的ref,並對其依賴項跟蹤和更新觸發進行邏輯控制。

如果使用ref,那就是資料已更新,頁面馬上更新

如果我想實現資料更新,頁面等一下在更新這個做不到

image-20240621163444408

customref基本使用

image-20240621163536818

需要一個初試值,但是模板用的還是ref的

image-20240621163754658

但如果是這樣寫的,頁面根本不會響應

真正寫法,需要在其回撥兩個引數,在讀取時呼叫前面那個函式,修改後呼叫後面那個函式

image-20240621164036745

這兩個函式很重要,面試容易問道

image-20240621164217171

這樣就可以實現剛才的效果了

image-20240621164350996

自定義ref感覺就類似於defineproperty那個函式,就是對資料更新變化的時候可以去寫一些自己的邏輯在裡面

但是此時快速輸入有bug,解決方式如下

實現防抖效果(useSumRef.ts):

import {customRef } from "vue";

export default function(initValue:string,delay:number){
  let msg = customRef((track,trigger)=>{
    let timer:number
    return {
      get(){
        track() // 告訴Vue資料msg很重要,要對msg持續關注,一旦變化就更新
        return initValue
      },
      set(value){
        clearTimeout(timer)
        timer = setTimeout(() => {
          initValue = value
          trigger() //通知Vue資料msg變化了
        }, delay);
      }
    }
  }) 
  return {msg}
}

真實公司開發需求可能是要在這裡來一個hooks

image-20240621164744605

image-20240621164756147

image-20240621164816173

8. Vue3新元件

8.1. 【Teleport】

  • 什麼是Teleport?—— Teleport 是一種能夠將我們的元件html結構移動到指定位置的技術。

to裡面可以為id class 標籤等選擇器

<teleport to='body' >
    <div class="modal" v-show="isShow">
      <h2>我是一個彈窗</h2>
      <p>我是彈窗中的一些內容</p>
      <button @click="isShow = false">關閉彈窗</button>
    </div>
</teleport>

image-20240621165514939

8.2. 【Suspense】

實驗性功能

  • 等待非同步元件時渲染一些額外內容,讓應用有更好的使用者體驗

比如現在子元件在發請求,如果半天出不來,那麼先展示fallback插槽裡面的內容,等非同步回來之後在展示上面的內容

image-20240621170302483

image-20240621170340636

  • 使用步驟:
    • 非同步引入元件
    • 使用Suspense包裹元件,並配置好defaultfallback
import { defineAsyncComponent,Suspense } from "vue";
const Child = defineAsyncComponent(()=>import('./Child.vue'))
<template>
    <div class="app">
        <h3>我是App元件</h3>
        <Suspense>
          <template v-slot:default>
            <Child/>
          </template>
          <template v-slot:fallback>
            <h3>載入中.......</h3>
          </template>
        </Suspense>
    </div>
</template>

8.3.【全域性API轉移到應用物件】

  • app.component註冊全域性元件

image-20240621170451778

  • app.config全域性配置物件

image-20240621170535136

所有元件都訪問得到

image-20240621170539628

image-20240621170601985

但是會報錯 vue官閘道器於這個屬性global有解釋

image-20240621170705266

![image-20240621170712175](F:\前端\24前端\階段七 vue3\尚矽谷前端學科全套教程\Vue3快速上手.assets\image-20240621170712175.png)

  • app.directive註冊全域性指令

image-20240621170810945

image-20240621170839758

  • app.mount
  • app.unmount
  • app.use

8.4.【其他】

  • 過渡類名 v-enter 修改為 v-enter-from、過渡類名 v-leave 修改為 v-leave-from

  • keyCode 作為 v-on 修飾符的支援。

image-20240621171233083

  • v-model 指令在元件上的使用已經被重新設計,替換掉了 v-bind.sync。

  • v-ifv-for 在同一個元素身上使用時的優先順序發生了變化。(可以用在同一元件上)

  • 移除了$on$off$once 例項方法。

  • 移除了過濾器 filter

  • 移除了$children 例項 propert

    ......

    vue官網

    image-20240621171341260

    基於vue2 vue3的改變 面試暢聊

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章