vue3 快速入門系列 - 基礎(chǔ)
前面我們已經(jīng)用 vue2 和 react 做過開發(fā)了。
從 vue2 升級(jí)到 vue3 成本較大,特別是較大的項(xiàng)目。所以許多公司對(duì)舊項(xiàng)目繼續(xù)使用vue2,新項(xiàng)目則使用 vue3。
有些UI框架,比如ant design vue1.x 使用的 vue2。但現(xiàn)在 ant design vue4.x 都是基于 vue3,示例默認(rèn)是 TypeScript。比如 table 組件管理。
另外 vue3 官網(wǎng)介紹也使用了 TypeScript,例如:響應(yīng)式 API:核心
本篇主要介紹:vite 創(chuàng)建vue3項(xiàng)目、組合式api、響應(yīng)式數(shù)據(jù)、計(jì)算屬性、監(jiān)聽、ref、ts、生命周期、自定義hooks。
vue3 簡(jiǎn)介
Vue.js 3.0,代號(hào)海賊王,于2020年9月18日發(fā)布 —— v3.0.0 海賊王
主要有如下改進(jìn):
-
性能
改進(jìn):與 Vue 2 相比,Vue 3 在包大?。ㄍㄟ^ Tree-Shaking 減少最多 41%)、初始渲染(快 55%)、更新(快 133%)和內(nèi)存使用方面表現(xiàn)出了顯著的性能改進(jìn)(最多減少 54%)。 - 擁抱
TypeScript
:更好的支持 TS。有的公司在 vue2 中就用 TS 了 - 用于應(yīng)對(duì)規(guī)模問題的新 API:引入了
Composition API
——一組新的 API,旨在解決大規(guī)模應(yīng)用程序中 Vue 使用的痛點(diǎn)。Composition API 構(gòu)建在反應(yīng)性 API 之上,支持類似于 React hooks 的邏輯組合和重用、更靈活的代碼組織模式以及比 2.x 基于對(duì)象的 API 更可靠的類型推斷。 - 分層內(nèi)部模塊:還公開了較低級(jí)別的 API,可解鎖許多高級(jí)用例
創(chuàng)建 vue3 工程
vue-cli 創(chuàng)建
前面我們用 vue-cli 創(chuàng)建過 vue2 的項(xiàng)目,用其構(gòu)建 vue3 也類似,差別就是選擇 vue3 版本。最后生成的項(xiàng)目結(jié)構(gòu)如下:
Vue CLI 是官方提供的基于 Webpack 的 Vue 工具鏈,它現(xiàn)在處于維護(hù)模式。我們建議使用 Vite 開始新的項(xiàng)目
,除非你依賴特定的 Webpack 的特性。在大多數(shù)情況下,Vite 將提供更優(yōu)秀的開發(fā)體驗(yàn) —— 官網(wǎng) - 項(xiàng)目腳手架
vite 創(chuàng)建
另一種方式是使用 vite。有如下優(yōu)勢(shì):
- 對(duì) TypeScript、JSX、CSS 等支持開箱即用。
- 無論應(yīng)用程序大小如何,都始終極快的模塊熱替換(HMR)
- 極速的服務(wù)啟動(dòng)。使用原生 ESM(參考 mdn esm) 文件,無需打包
Tip:
- vue腳手架(vue-cli) 和創(chuàng)建 react的腳手架(create-react-app)都是基于 webpack。而 vite 也是一種構(gòu)建工具,和 webpack 類似,也有一些區(qū)別,其作者就是 Vue.js 的創(chuàng)始人尤雨溪
- HMR 它用于開發(fā)環(huán)境,不適用于生產(chǎn)環(huán)境。更多介紹請(qǐng)看這里。
- jsx 在學(xué)習(xí) react 中用到過(請(qǐng)看這里),vue 中用 template 寫視圖部分,react 用 jsx。在 Vue 3 項(xiàng)目中使用 JSX 時(shí),Vite 會(huì)將 JSX 語(yǔ)法編譯為 Vue 3 的渲染函數(shù)。
筆者首先使用 npm create vite@latest
創(chuàng)建項(xiàng)目,自己根據(jù)需要選擇對(duì)應(yīng)預(yù)設(shè)(比如要 TypeScript or javascript),創(chuàng)建完成后根據(jù)提示進(jìn)入項(xiàng)目,安裝依賴,本地啟動(dòng):
npm install
npm run dev
結(jié)果報(bào)錯(cuò):
> vite-vue3@0.0.0 dev \test-projects\vite-vue3
> vite
(node:40312) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token '??='
at Loader.moduleStrategy (internal/modules/esm/translators.js:145:18)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:40312) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:40312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
說是 node 版本可能低了。
Tip: Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依賴更高的 Node 版本才能正常運(yùn)行,當(dāng)你的包管理器發(fā)出警告時(shí),請(qǐng)注意升級(jí)你的 Node 版本 —— vite 官網(wǎng)-搭建第一個(gè) Vite 項(xiàng)目
于是使用 nvm 安裝 18.16.0。步驟如下:
// 目前版本 14.19
PS \test-projects\vite-vue3> node -v
v14.19.0
// nvm 已安裝
PS \test-projects\vite-vue3> nvm -v
1.1.10
// nvm 安裝 18.16.0
PS \test-projects\vite-vue3> nvm install 18.16.0
Downloading node.js version 18.16.0 (64-bit)...
Extracting node and npm...
Complete
npm v9.5.1 installed successfully.
Installation complete. If you want to use this version, type
nvm use 18.16.0
根據(jù)提示切換到 18.16.0
PS \test-projects> nvm use 18.16.0
Now using node v18.16.0 (64-bit)
PS \test-projects> node -v
v18.16.0
npm create vue
使用 npm create vue@latest
創(chuàng)建 vue3 項(xiàng)目 —— vue3 官網(wǎng) 創(chuàng)建一個(gè) Vue 應(yīng)用(這里提到 node 需要18+):
PS \test-projects> npm create vue@latest
Need to install the following packages:
create-vue@3.9.2
Ok to proceed? (y) y
Vue.js - The Progressive JavaScript Framework
√ 請(qǐng)輸入項(xiàng)目名稱: ... hello_vue3
√ 是否使用 TypeScript 語(yǔ)法? ... 否 / 是
√ 是否啟用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 進(jìn)行單頁(yè)面應(yīng)用開發(fā)? ... 否 / 是
√ 是否引入 Pinia 用于狀態(tài)管理? ... 否 / 是
√ 是否引入 Vitest 用于單元測(cè)試? ... 否 / 是
√ 是否要引入一款端到端(End to End)測(cè)試工具? ? 不需要
√ 是否引入 ESLint 用于代碼質(zhì)量檢測(cè)? ... 否 / 是
正在構(gòu)建項(xiàng)目 \test-projects\hello_vue3...
項(xiàng)目構(gòu)建完成,可執(zhí)行以下命令:
cd hello_vue3
npm install
npm run dev
npm notice
npm notice New major version of npm available! 9.5.1 -> 10.4.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.4.0
npm notice Run npm install -g npm@10.4.0 to update!
npm notice
根據(jù)提示按照依賴,本地啟動(dòng)項(xiàng)目成功:
PS \test-projects> cd .\hello_vue3\
PS \test-projects\hello_vue3> npm install
added 63 packages, and audited 64 packages in 20s
7 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
PS \test-projects\hello_vue3> npm run dev
> hello_vue3@0.0.0 dev
> vite
VITE v5.1.3 ready in 3045 ms
? Local: http://localhost:5173/
? Network: use --host to expose
? press h + enter to show help
npm create vite/vue
npm create vite@latest 和 npm create vue@latest 作用和用途不同,兩者效果也不同,總的來說前者創(chuàng)建 Vite 項(xiàng)目,而 npm create vue@latest
是用來創(chuàng)建 Vue.js 項(xiàng)目。
PS \test-projects> npm create vite@latest
Need to install the following packages:
create-vite@5.2.0
Ok to proceed? (y) y
√ Project name: ... hello-vue3
√ Select a framework: ? Vue
√ Select a variant: ? TypeScript
Scaffolding project in \test-projects\hello-vue3...
Done. Now run:
cd hello-vue3
npm install
npm run dev
vite 本地啟動(dòng)非???/h5>
vite 本地啟動(dòng)非???。真正按需編譯,不在等待整個(gè)應(yīng)用編譯完成。
用 webpack 本地啟動(dòng)服務(wù)器,需要經(jīng)歷如下幾步:entry->route->module->bundle->服務(wù)器啟動(dòng)
(下圖左);而用 vite 啟動(dòng)服務(wù)器,服務(wù)器啟動(dòng)卻從末尾移到開頭(下圖右)
有點(diǎn)像懶加載,你需要訪問哪個(gè)路由,就加載哪個(gè),非??焖?。
vue3項(xiàng)目目錄結(jié)構(gòu)淺析
前面我們用 vite 創(chuàng)建了 hello_vue3 項(xiàng)目。目錄結(jié)構(gòu)如下:
我們先說其他文件,最后在分析src文件夾
extensions.json
內(nèi)容如下:
// .vscode/extensions.json
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
推薦你安裝這兩個(gè)插件,當(dāng)你用 vscode 啟動(dòng)項(xiàng)目,點(diǎn)擊切換到其他文件上,vscode 右下角就會(huì)提示你是否安裝這兩個(gè)插件。就像這樣:
這兩個(gè)是vue官方給 vscode 提供的插件:
- TypeScript Vue Plugin (Volar)
- Vue Language Features
env.d.ts
內(nèi)容如下:
/// <reference types="vite/client" />
是一個(gè)在 Vue.js 項(xiàng)目中使用 Vite 構(gòu)建工具時(shí)引入的指令,它的作用是讓 TypeScript 編譯器能夠識(shí)別并利用 Vite 客戶端類型聲明文件提供的類型信息,以提供更好的智能編碼功能和類型檢查支持。
Tip:如果你刪除 node_modules 文件夾,你在vscode 中會(huì)發(fā)現(xiàn) vite/client
下有紅色波浪線。
TypeScript 主要用于處理 JavaScript 代碼,并且在處理模塊時(shí),它會(huì)關(guān)注 .ts、.tsx、.js 和 .jsx 這些與 JavaScript 相關(guān)的文件類型。
TypeScript 默認(rèn)情況下并不會(huì)識(shí)別或處理像 .txt、.gif 這樣的非 TypeScript 文件類型。這個(gè)文件的作用就是讓 ts 認(rèn)識(shí) txt、jpg、gif等。
比如你在src 下新建 a.txt、b.ts,然后在 b.ts 中編寫:
import a from 'a.txt'
console.log(a)
當(dāng)你清空 env.d.ts
,你會(huì)發(fā)現(xiàn) import a from 'a.txt'
中 a.txt 下有紅色波浪線。再次還原 env.d.ts
則好了。
通過 ctrl + 鼠標(biāo)點(diǎn)擊進(jìn)入 vite/client
,你會(huì)發(fā)現(xiàn) vue 給我們聲明好了我們需要使用的其他類型文件。比如 txt:
declare module '*.txt' {
const src: string
export default src
}
index.html
index.html 這就是我們的入口文件
。內(nèi)容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
你可以嘗試改成
<body>
a
</body>
無需重啟服務(wù),頁(yè)面就顯示 a
。
其他
- tsconfig 文件,ts 配置相關(guān),不要?jiǎng)h,ts 可能會(huì)有問題:
tsconfig.app.json
tsconfig.json
tsconfig.node.json
-
vite.config.ts 項(xiàng)目配置文件。比如代理、安裝插件
-
public/favicon.ico 頁(yè)簽圖標(biāo)
-
package.json、package-lock.json
src
src 就是我們編碼的地方。
我們先將 src 中的文件都刪除,我們自己重新創(chuàng)建。
創(chuàng)建 main.ts 和 App.vue 兩個(gè)文件。內(nèi)容如下:
- main.ts 是index.html加載的入口文件
// src/main.ts
import {createApp} from 'vue'
// 項(xiàng)目的根
import App from './App.vue'
// Vue.js 3.x 中用于創(chuàng)建和掛載應(yīng)用
// 創(chuàng)建一個(gè)新的 Vue 應(yīng)用,并將根組件指定為 App。.mount('#app') 將應(yīng)用掛載到指定的 DOM 元素上
createApp(App).mount('#app')
// src/App.vue
<template>
<div>
你好 vue3
</div>
</template>
<!-- 可以指定語(yǔ)言是 ts,ts中也可以寫js -->
<script lang="ts">
</script>
<style scoped>
</style>
瀏覽器訪問,頁(yè)面顯示 你好 vue3
。
前面我們說到 vite 啟動(dòng)后,服務(wù)器就已就緒。然后會(huì)根據(jù)用戶請(qǐng)求哪里,就會(huì)給你加載哪里。
vue3 向下兼容 vue2 語(yǔ)法
有些項(xiàng)目使用了 vue3,但寫法還是 vue2 —— 不建議這么做
。
為了證明 vue3 中能寫 vue2,筆者在 vue3 項(xiàng)目中寫一個(gè) vue2 示例。請(qǐng)看代碼:
// src/App.vue
<template>
<section>
<p>name: {{ name }}</p>
<p>date: {{ date }}</p>
<p><button @click="changeDate">change date</button></p>
</section>
</template>
<script lang="ts">
export default {
name: 'App',
data() {
return {
name: 'pengjiali',
date: -1,
}
},
methods: {
changeDate() {
this.date = new Date().getTime();
}
}
}
</script>
瀏覽器顯示:
name: pengjiali
date: -1
// 按鈕,點(diǎn)擊后,date 后的數(shù)字就會(huì)變化
change date
options Api 和 compositionApi
Vue 2 使用的是選項(xiàng)式 API,而 Vue 3 引入了組合式 API
雖然 Vue 3 推薦使用組合式 API,但它仍然完全支持 Vue 2 的選項(xiàng)式 API,以保持向下兼容性。所以在 Vue 3 中,你可以自由選擇使用選項(xiàng)式 API 或組合式 API 來編寫你的組件邏輯。
選項(xiàng)式API有一個(gè)缺點(diǎn):新增一個(gè)功能,需要分別在 data、methods、computed、watch等選項(xiàng)中修改代碼,如果代碼上千,修改或抽取封裝這部分功能,有困難。
Tip:我們用 大帥老猿
的圖說明以下這個(gè)問題
而組合式 api 可以簡(jiǎn)化這個(gè)問題,我們可以感受下(代碼如何實(shí)現(xiàn)暫時(shí)不用管):
Tip: 具體如何拆分,請(qǐng)看本篇最后自定義 hooks
章節(jié)。
setup
setup 函數(shù)是組合式 API 的入口
,用于組合組件的邏輯和功能。
setup 概述
首先我們用 vue2 語(yǔ)法寫一個(gè)示例:展示名字和日期,點(diǎn)擊按鈕能改變?nèi)掌?。代碼如下:
<template>
<section>
<p>name: {{ name }}</p>
<p>date: {{ date }}</p>
<p><button @click="changeDate">change date</button></p>
</section>
</template>
<script lang="ts">
export default {
name: 'App',
data() {
return {
name: 'pengjiali',
date: -1,
}
},
methods: {
changeDate() {
this.date = new Date().getTime();
}
}
}
</script>
現(xiàn)在我們把 data 和 methods 兩個(gè)配置去除,改成 setup 就完成了 vue3 示例的重構(gòu)
<template>
不變...
</template>
<script lang="ts">
export default {
name: 'App',
setup() {
let name = 'pengjiali2'
let date = -1
function changeDate(){
date = new Date().getTime();
console.log('date: ', date);
}
// 將數(shù)據(jù)和方法都交出去
return {name, date, changeDate}
}
}
</script>
setup 是一個(gè)方法,平時(shí)如何定義變量和方法,這里就怎么寫,最后將方法和變量都交出去。
這里其實(shí)還有一個(gè)問題,點(diǎn)擊 button 日期在界面沒變,但方法卻執(zhí)行了。這是因?yàn)?date 變量不是響應(yīng)式
的。
Tip:現(xiàn)在我們先說 setup,后面在將響應(yīng)式的東西。這里要修復(fù)可以使用 ref(這個(gè) ref 和 vue2 中指向元素或組件的ref,不是同一個(gè)東西):
<script lang="ts">
+import {ref} from 'vue'
export default {
name: 'App',
setup() {
let name = 'pengjiali2'
- let date = -1
+ let date = ref(-1)
function changeDate(){
- date = new Date().getTime();
+ date.value = new Date().getTime();
console.log('date: ', date);
}
// 將數(shù)據(jù)和方法都交出去
另外 setup 中的 this 是undefined,vue3 開始弱化 this。
最后說一下 setup 執(zhí)行時(shí)機(jī),比 beforeCreat 還早:
name: "App",
beforeCreate() {
console.log(1);
},
setup() {
console.log(2);
先輸出 2 再輸出 1。
setup 返回函數(shù)
setup 返回值也可以是一個(gè)函數(shù),比如這個(gè):
return () => 'hello vue3'
頁(yè)面就會(huì)顯示hello vue3
,模板是什么都不重要了,直接根據(jù)這個(gè)函數(shù)返回值渲染
這種用法不多,常用的還是返回對(duì)象。
setup 和配置項(xiàng)的關(guān)系
- setup 能否和 data、method 能否同時(shí)寫,如果沖突,以誰(shuí)為準(zhǔn)?
- 配置項(xiàng)能否讀取setup 中的東西,setup 能否讀取setup 中的東西?
setup 能和 data、method 同時(shí)存在
請(qǐng)看示例:
<p>name: {{ name }}</p>
<p>date: {{ date }}</p>
+ <p>age: {{ age }}</p>
+ <p><button @click="sayAge">獲取年齡</button></p>
</section>
</template>
export default {
beforeCreate() {
console.log("1: ", 1);
},
+ data() {
+ return {
+ age: 18
+ }
+ },
+ methods: {
+ sayAge() {
+ console.log('我的年齡', this.age)
+ }
+ },
setup() {
console.log("2: ", 2);
let name = "pengjiali2";
屬性 age和方法 sayAge 都能正常使用。
setup 和 beforeCreate 執(zhí)行順序
beforeCreate() {
console.log("beforeCreate");
},
setup() {
console.log("setup");
return () => 'hello vue3'
},
setup
beforeCreate
data 讀取 setup 中的屬性
data 能夠讀取 setup 中的屬性。請(qǐng)看示例:
<p><button @click="sayAge">獲取年齡</button></p>
+ <p>dataName: {{ dataName }}</p>
</section>
</template>
export default {
},
data() {
return {
age: 18,
+ dataName: this.name
}
},
methods: {
setup 是最早的生命周期(將vue2 中beforeCreat、created合并),這里證明 data 中可以取得 setup 中的數(shù)據(jù)。就像 vue2 中 data 可以讀取 props 中的數(shù)據(jù),因?yàn)?props 比 data 先初始化 —— initstate 初始化狀態(tài)。
在 setup 中無法使用 data 中的數(shù)據(jù)。請(qǐng)看示例,直接報(bào)錯(cuò):
// vscode 報(bào)錯(cuò)
let newAge = age,
// vscode 報(bào)錯(cuò) - setup 中沒有this
let newAge2 = this.age,
setup 語(yǔ)法糖
每次都得寫 setup(),還需要將方法或?qū)傩越怀鋈ィ芊裰粚憣傩院头椒?,自?dòng)交出去?
方式1
setup() {
let name = "pengjiali";
let date = ref(-1);
function changeDate() {
date.value = new Date().getTime();
console.log("date: ", date);
}
// 將數(shù)據(jù)和方法都交出去
return { name, date, changeDate };
},
有的。將 setup() 專門提取出去。就像這樣:
<script lang="ts">
import { ref } from "vue";
export default {
name: "App",
};
</script>
<script lang="ts" setup>
// 屬性和方法自動(dòng)交出去
let name = "pengjiali";
let date = ref(-1);
function changeDate() {
date.value = new Date().getTime();
console.log("date: ", date);
}
</script>
方式2
方式一還是需要寫l了兩個(gè) <script>
,其中一個(gè)專門用于定義組件名。
<script lang="ts">
import { ref } from "vue";
export default {
name: "App",
};
</script>
不想寫兩個(gè) <script>
,可以利用插件 vite-plugin-vue-setup-extend
。
先安裝:
PS \test-projects\hello_vue3> npm i vite-plugin-vue-setup-extend -D
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead
added 3 packages, and audited 67 packages in 6s
7 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
修改 vite.config.ts 配置文件:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
+import setupExtend from 'vite-plugin-vue-setup-extend'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
+ setupExtend(),
],
resolve: {
alias: {
最后應(yīng)用:
-<script lang="ts" setup>
+<script lang="ts" setup name="App3">
響應(yīng)式數(shù)據(jù)
vue2 中放在 data 中的數(shù)據(jù)都是響應(yīng)式的,在vue3 中可以通過 ref和reactive 兩種方式來處理響應(yīng)式。
通過 vue devtools,我們知道數(shù)據(jù)為尊,因?yàn)榉椒ǚ旁诤竺妫ǔ朔椒?,其他的也?huì)放在這里),而數(shù)據(jù)放在前面。
ref創(chuàng)建基本類型響應(yīng)式數(shù)據(jù)
想讓哪個(gè)數(shù)據(jù)是響應(yīng)式的,就將數(shù)據(jù)用 ref 包裹一下。
注
:這里的 ref 和 vue2 中 ref 不是一個(gè)東西
用法請(qǐng)看示例(和注釋):
<template>
<section>
<p>name: {{ name }}</p>
<!-- 不能寫 date.value,這里自動(dòng)會(huì)給 value -->
<p>date: {{ date }}</p>
<p><button @click="changeDate">change date</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref } from "vue";
let name = "pengjiali";
// 通過 ref 創(chuàng)建一個(gè)基本類型的響應(yīng)式數(shù)據(jù)
let date = ref(-1);
// 使用 ref 函數(shù)創(chuàng)建的響應(yīng)式變量是一個(gè)包裝過的對(duì)象,你需要通過 .value 來訪問和修改其值
// 使用 ref 創(chuàng)建變量時(shí),實(shí)際上你得到的是一個(gè)包含了值的對(duì)象,而不是直接的值。因此,在修改這個(gè)變量時(shí),你需要通過 .value 來訪問和修改實(shí)際的值,這樣 Vue 才能夠正確地追蹤變化并進(jìn)行響應(yīng)。
// 使用 ref 創(chuàng)建的變量必須通過 .value 來訪問和修改其值,這是為了確保 Vue 能夠正確捕捉變化并更新視圖。
function changeDate() {
// date: RefImpl {__v_isShallow: false, dep: Map(1), __v_isRef: true, _rawValue: -1, _value: -1}
console.log('date: ', date);
// 通過 value 修改響應(yīng)式數(shù)據(jù)。
date.value = new Date().getTime();
console.log("date: ", date);
}
</script>
變量用ref包裹后,類型變成 RefImpl
。需要通過 .value
來訪問和修改實(shí)際的值。
Tip:越過 .value 直接整體替換是不可以的,就像這樣:
let count = ref(0)
function changeCount(){
// 生效
count = 9
// 失效
// count = ref(9)
}
注
:模板中不需要 .value
有點(diǎn)像 proxy 的感覺:
// 創(chuàng)建一個(gè)普通的對(duì)象作為目標(biāo)對(duì)象
let target = {
name: 'Alice',
age: 30
};
// 創(chuàng)建一個(gè) Proxy 對(duì)象,用來代理目標(biāo)對(duì)象
let proxy = new Proxy(target, {
// 攔截屬性讀取的操作
get: function(target, property) {
console.log(`Reading ${property} property`);
return target[property]; // 返回目標(biāo)對(duì)象相應(yīng)的屬性值
},
// 攔截屬性設(shè)置的操作
set: function(target, property, value) {
console.log(`Setting ${property} property to ${value}`);
target[property] = value; // 設(shè)置目標(biāo)對(duì)象相應(yīng)的屬性值
}
});
// 通過 Proxy 訪問和修改屬性
// Reading name property
// Alice
console.log(proxy.name); // 讀取屬性
// Setting age property to 35
// 35
proxy.age = 35; // 設(shè)置屬性
Tip:Proxy 是 ES6 引入的一個(gè)特性,它允許你創(chuàng)建一個(gè)代理對(duì)象,可以用來攔截并自定義目標(biāo)對(duì)象的基本操作,比如屬性查找、賦值、刪除等
reactive 定義響應(yīng)式數(shù)據(jù)
利用 reactive 將對(duì)象
轉(zhuǎn)成響應(yīng)式,重寫上述示例:
<template>
<section>
<p>name: {{ person.name }}</p>
<p>date: {{ person.date }}</p>
<p><button @click="changeDate">change date</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref, reactive } from "vue";
const person = reactive({
name: "pengjiali",
date: -1,
})
function changeDate() {
// Proxy(Object)?{name: 'pengjiali', date: -1}
console.log('person: ', person);
person.date = new Date().getTime();
}
</script>
經(jīng)過 reactive 封裝后的對(duì)象類型變成 Proxy。專業(yè)術(shù)語(yǔ)叫響應(yīng)式對(duì)象
reactive 同樣可以處理數(shù)組(數(shù)組也是對(duì)象),請(qǐng)看示例:
<ul>
<li v-for="(item, index) in ages" :key="index">{{ item }}</li>
</ul>
const ages = reactive([18, 19, 20])
對(duì)深層次對(duì)象也同樣起作用。請(qǐng)看示例:
<p>d: {{ obj.a.b.d }} <button @click="changeD">change d</button></p>
let obj = reactive({
a: {
b: {
d: 10
}
}
})
function changeD(){
obj.a.b.d = new Date().getTime()
}
不能定義基本類型,比如將字符串轉(zhuǎn)成響應(yīng)式,vscode 和瀏覽器控制臺(tái)報(bào)錯(cuò)如下:
// vscode:類型“string”的參數(shù)不能賦給類型“object”的參數(shù)。
// 控制臺(tái):value cannot be made reactive: #abc
const color = reactive('#abc');
ref 定義對(duì)象類型數(shù)據(jù)
直接看示例,我們將 reactive 示例中的 reactive 換成 ref,修改值時(shí)加 .value 即可,模板不用動(dòng)。
import { ref, reactive } from "vue";
-const person = reactive({
+const person = ref({
name: "pengjiali",
date: -1,
})
function changeDate() {
- person.date = new Date().getTime();
+ person.value.date = new Date().getTime();
}
</script>
能顯示,能修改,一切正常。
雖然 ref 能處理基本類型和對(duì)象,但是遇到對(duì)象,實(shí)際上是搖人了。請(qǐng)看示例:
const person = ref({
name: "pengjiali",
date: -1,
})
const count = ref(1)
// count: RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 1, _value: 1}
console.log('count: ', count);
// person: RefImpl?{__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: {…}, _value: Proxy(Object)}
console.log('person: ', person);
查看 person 對(duì)象的 value 屬性,發(fā)現(xiàn)了 Proxy(Object)
,所以本質(zhì)上是 reactive 處理了對(duì)象
ref vs reactive
宏觀:
- ref 能定義基本類型和對(duì)象的響應(yīng)式數(shù)據(jù)
- reactive 只能用于對(duì)象
ref 自動(dòng)生成 .value
寫代碼時(shí)還得記著是 ref 類型,需要增加 .value,好麻煩。可以使用 vscode 插件:
vscode 直接安裝 Vue - Official
(vscode 提示 TypeScript Vue Plugin (Volar)
已棄用,使用 Vue - Official 替代)
通過 vscode 設(shè)置,勾選 Auto-complete Ref value with .value
,并設(shè)置 Applies to all profiles
重啟后,只要輸入 ref 變量,則會(huì)自動(dòng)添加 .value,非常方便。
const person = ref({
name: "pengjiali",
date: -1,
})
const person2 = reactive({
name: "pengjiali",
date: -1,
})
// 輸入 person 則會(huì)自動(dòng)添加 .value
person.value
// 對(duì)于非 ref 則不會(huì)添加 .value
person2
reactive 的局限性
reactive 重新分配一個(gè)對(duì)象,會(huì)失去響應(yīng)式(可使用 Object.assign 整體替換)。請(qǐng)看示例:
<template>
<section>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
<p><button @click="changePerson">change person</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref, reactive } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
})
function changePerson() {
// 失效 - 響應(yīng)性連接已丟失!
// person = reactive({name: 'peng', age: 25})
// 失效
// person = {name: 'peng', age: 25}
// 正常
Object.assign(person, {name: 'peng', age: 25})
}
</script>
Tip: Object.assign() 靜態(tài)方法將一個(gè)或者多個(gè)源對(duì)象中所有可枚舉的自有屬性復(fù)制到目標(biāo)對(duì)象,并返回
修改后的目標(biāo)對(duì)象。
let target = {a: 1, b: 2};
let source1 = {b: 4, c: 5};
let source2 = {c: 6, d: 7};
Object.assign(target, source1, source2);
console.log(target); // 輸出: {a: 1, b: 4, c: 6, d: 7}
如果是 ref,直接替換即可。就像這樣
let person = ref({
name: "pengjiali",
age: 18,
})
function changePerson() {
// 直接替換
person.value = {name: 'peng', age: 25}
}
ref 和 reactive 使用場(chǎng)景
由于這些限制,我們建議使用 ref() 作為聲明響應(yīng)式狀態(tài)的主要 API —— 官網(wǎng) - reactive 局限性
筆者習(xí)慣:
- 需要一個(gè)基本類型的響應(yīng)式數(shù)據(jù),只可使用 ref
- 對(duì)象使用 reactive
- 如果是表單,使用 ref 會(huì)出現(xiàn)很多 .value,不好看
toRefs
將一個(gè)響應(yīng)式對(duì)象轉(zhuǎn)換為一個(gè)普通對(duì)象,這個(gè)普通對(duì)象的每個(gè)屬性都是指向源對(duì)象相應(yīng)屬性的 ref。每個(gè)單獨(dú)的 ref 都是使用 toRef() 創(chuàng)建的。
不明白請(qǐng)看下面代碼。
比如這段代碼:
<template>
<section>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
<p><button @click="changePerson">change person</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref, reactive } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
})
function changePerson() {
Object.assign(person, {name: 'peng', age: 25})
}
</script>
我從響應(yīng)式對(duì)象中解構(gòu)出 age,然后通過方法修改 age 的值,發(fā)現(xiàn)頁(yè)面沒更新:
+ <p><button @click="changeAge">change age</button></p>
<p><button @click="changePerson">change person</button></p>
</section>
</template>
let person = reactive({
age: 18,
})
+let {age} = person
+
+function changeAge(){
+ age += 1;
+}
+
這是因?yàn)榻鈽?gòu)出的 age 不在是響應(yīng)式??梢允褂?toRefs,就像這樣:
-import { ref, reactive } from "vue";
+import { ref, reactive, toRefs } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
})
-let {age} = person
+let {age} = toRefs(person)
+// age: ObjectRefImpl {_object: Proxy(Object), _key: 'age', _defaultValue: undefined, __v_isRef: true}
+console.log('age: ', age);
function changeAge(){
- age += 1;
+ age.value += 1;
}
toRef
說 toRef 用的較少。
比如層級(jí)比較深的場(chǎng)景,請(qǐng)看示例:
<template>
<h4>姓名:{{ name }}</h4>
<h4>薪資:{{ salary }}</h4>
<button @click="name += '!'">修改姓名</button>
<button @click="salary++">漲薪</button>
</template>
<script lang="ts" setup name="App">
import { ref, reactive, toRefs, toRef } from "vue";
let person = reactive({
name: "張三",
age: 18,
job: {
ja: {
salary: 20,
},
},
});
let name = toRef(person, "name");
let salary = toRef(person.job.ja, "salary");
</script>
計(jì)算屬性
作用和vue2相同,先回憶下 vue2 中的計(jì)算屬性。寫法如下:
computed: {
now: function () {
}
}
改成 vue3 需要使用 computed 方法。就像這樣:
let now = computed(() => {
return Date.now()
})
請(qǐng)看示例:
<section>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
-
+ <p>name_age: {{ name_age }}</p>
<p><button @click="changePerson">change person</button></p>
</section>
</template>
<script lang="ts" setup name="App">
-import { ref, reactive } from "vue";
+import { ref, reactive, computed } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
});
+const name_age = computed(() => `${person.name}-${person.age}`)
function changePerson() {
Object.assign(person, { name: "peng", age: 25 });
}
Tip:和 vue2 中類似,set很少用。不多介紹,用法大致如下:
let fullname = computed({
get(){
},
set(){
}
})
// 觸發(fā) set 方法
fullName.value = 'li-si'
watch
vue3 中 watch 作用應(yīng)該和 vue2 中相同,先回憶下vue2 中 watch 寫法。就像這樣:
new Vue({
data: {
message: 'Hello, Vue!'
},
watch: {
message: function(newValue, oldValue) {
console.log('消息從', oldValue, '變?yōu)?, newValue);
}
}
});
vue3 中說 watch 只能監(jiān)視4種數(shù)據(jù):
- ref定義的數(shù)據(jù)
- reactive 定義的數(shù)據(jù)
- 函數(shù)返回一個(gè)值(getter函數(shù))
- 一個(gè)包含上述內(nèi)容的數(shù)組
Tip: vue2 watch 中有deep、immediate、unwatch,下文 vue3 中 watch 也都有。
ref 基本類型
請(qǐng)看示例:
<template>
<section>
<p>age: {{ age}}</p>
<p><button @click="age += 1">change age</button></p>
<p><button @click="stopWatch">停止監(jiān)聽 age 變化</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref, watch } from "vue";
let age = ref(18)
// watch(age.value, ... ) 錯(cuò)誤寫法
let stopWatch = watch(age, (newValue, oldValue) => {
console.log('年齡從', oldValue, '變?yōu)?, newValue);
});
</script>
- watch 監(jiān)視的ref變量,無需增加
.value
。安裝好vscode 插件,在這種情況下也不會(huì)自動(dòng)給你加 .value。 - watch 返回一個(gè)函數(shù),執(zhí)行后將解除監(jiān)視。就像 vue2 中的 vm.$watch 方法,返回 unwatch。
ref 對(duì)象類型
核心語(yǔ)法:
watch(person, (newValue, oldValue) => {
}, { deep: true});
比如用 ref 定義一個(gè)對(duì)象,里面有兩個(gè)按鈕,一個(gè)只改變“年齡”,一個(gè)改變整個(gè) ref 對(duì)象。就像這樣:
<template>
<section>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
<p><button @click="person.age += 1">change age</button></p>
<p><button @click="changePerson">change person(替換整個(gè)對(duì)象)</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref, watch } from "vue";
let person = ref({
name: "pengjiali",
age: 18,
});
// 完全替換person,newValue 和 oldValue 不同
// 只替換person中屬性,newValue 和 oldValue 相同。通常工作只關(guān)心新值
watch(person, (newValue, oldValue) => {
console.log('Person changed');
console.log('New person:', newValue);
console.log('Old person:', oldValue);
}, );
function changePerson() {
person.value = {name: 'peng', age: 100}
}
</script>
只有改變整個(gè)對(duì)象時(shí) watch 中的方法才會(huì)執(zhí)行,而改變r(jià)ef對(duì)象中的屬性,watch 方法卻不會(huì)執(zhí)行。
加上一個(gè)配置項(xiàng),這樣改變整個(gè)對(duì)象,以及改變r(jià)ef對(duì)象中的屬性,watch 中的方法都會(huì)執(zhí)行。
console.log('New person:', newValue);
console.log('Old person:', oldValue);
- }, );
+ }, {deep: true});
其實(shí)還有一個(gè)屬性 immediate,初始時(shí)就會(huì)執(zhí)行 watch 中的方法。就像這樣:
// 完全替換person,newValue 和 oldValue 不同
// 只替換person中屬性,newValue 和 oldValue 相同。通常工作只關(guān)心新值
watch(person, (newValue, oldValue) => {
console.log('Person changed');
console.log('New person:', newValue);
console.log('Old person:', oldValue);
}, { deep: true, immediate: true });
reactive
核心語(yǔ)法:
watch(person, (newValue, oldValue) => {
});
完整示例:
<template>
<section>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
<p><button @click="person.age += 1">change age</button></p>
<p><button @click="changePerson">change person(替換整個(gè)對(duì)象)</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
});
// 默認(rèn)開啟深度監(jiān)聽,而且通過 {deep: false} 也關(guān)閉不了
watch(person, (newValue, oldValue) => {
console.log('Person changed');
console.log('New person:', newValue);
console.log('Old person:', oldValue);
}, {deep: false});
function changePerson() {
// 不能整個(gè)替換,只能用 Object.assign。不能像 ref.value = {...}
Object.assign(person, {name: 'peng', age: 100})
}
</script>
監(jiān)視 ref 或 reactive 的對(duì)象中的某屬性
前面我們監(jiān)視的都是整個(gè)對(duì)象,比如現(xiàn)在要監(jiān)視對(duì)象中的某個(gè)屬性。這里分為基本類型
和對(duì)象類型
。
// reactive 和 ref 都可以用如下形式
// 利用 getter。如果需要?jiǎng)t增加 deep
watch(() => person.car, () => {
}, {deep: true})
基本類型
就以 reactive 對(duì)象為例,直接將監(jiān)視源改為 person.name
vscode 就會(huì)出現(xiàn)紅色波浪線:
<template>
<section>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
<p><button @click="person.age += 1">change age</button></p>
<p><button @click="person.name += '~'">change name</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
});
-watch(person, (newValue, oldValue) => {
+watch(person.name, (newValue, oldValue) => {
console.log('Person changed');
});
</script>
運(yùn)行后在瀏覽器控制臺(tái)中報(bào)錯(cuò)更明顯:
// 無效的監(jiān)視源:只能是 getter 函數(shù)、ref、reactive object、或這些類型的數(shù)組
App.vue:17 [Vue warn]: Invalid watch source: pengjiali A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.
at <App>
現(xiàn)在 person.name 不屬于上述4種類型。
將 person.name 改成 getter。代碼如下:
Tip:getter 一個(gè)函數(shù),返回一個(gè)值 —— vue3 watch
watch(() => person.name, (newValue, oldValue) => {
console.log('Person changed');
});
這樣修改 age 時(shí)不會(huì)觸發(fā) watch,只有 name 改變時(shí)才會(huì)觸發(fā) watch。
對(duì)象類型
這里給 person 定義了一個(gè) jineng 的對(duì)象屬性,并定義兩個(gè)按鈕,一個(gè)會(huì)改變 jineng 的屬性,一個(gè)改變整個(gè)技能。代碼如下:
<template>
<section>
<p>jineng.a: {{ person.jineng.a }}</p>
<p><button @click="person.jineng.a += '~'">change jineng.a</button></p>
<p><button @click="changeJineng">替換 jineng</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
jineng: {
a: '吃飯',
b: '睡覺',
}
});
console.log('person: ', person);
// person.jineng: Proxy(Object)?{a: '吃飯', b: '睡覺'}
console.log('person.jineng: ', person.jineng);
function changeJineng(){
person.jineng = {a: 'a吃飯', b:'a睡覺'}
}
</script>
首先我們這么寫,發(fā)現(xiàn)只能監(jiān)聽 jineng 里面的屬性改變:
// 點(diǎn)擊`change jineng.a` 執(zhí)行
// 點(diǎn)擊`替換 jineng` 不執(zhí)行
watch(person.jineng, () => {
console.log('watch jineng');
})
Tip:通過打印我們知道 person.jineng 類型是Proxy,也就是 reactive 類型,根據(jù)前文我們知道 reactive 默認(rèn)開啟深度監(jiān)視,而且不能整個(gè)替換,之前用的都是 Object.assign,這里用的是 person.jineng = {a: 'a吃飯', b:'a睡覺'}
。
改成 getter 發(fā)現(xiàn)只能監(jiān)聽替換整個(gè) jineng:
// 點(diǎn)擊`change jineng.a` 不執(zhí)行
// 點(diǎn)擊`替換 jineng` 執(zhí)行
watch(() => person.jineng, () => {
console.log('watch jineng');
})
在 getter 基礎(chǔ)上增加 {deep: tree} 則都能監(jiān)視到:
// 點(diǎn)擊`change jineng.a` 執(zhí)行
// 點(diǎn)擊`替換 jineng` 執(zhí)行
// 說官網(wǎng)一直都是用函數(shù)
watch(() => person.jineng, () => {
console.log('watch jineng');
}, {deep: true})
Tip:將上述示例從 reactive 改成 ref,watch 監(jiān)視方式還是不變。請(qǐng)看代碼:
<template>
<section>
<p>jineng.a: {{ person.jineng.a }}</p>
<p><button @click="person.jineng.a += '~'">change jineng.a</button></p>
<p><button @click="changeJineng">替換 jineng</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = ref({
name: "pengjiali",
age: 18,
jineng: {
a: '吃飯',
b: '睡覺',
}
});
// person.jineng: Proxy(Object)?{a: '吃飯', b: '睡覺'}
console.log('person.jineng: ', person.value.jineng);
function changeJineng(){
person.value.jineng = {a: 'a吃飯', b:'a睡覺'}
}
watch(() => person.value.jineng, () => {
console.log('watch jineng');
}, {deep: true})
</script>
監(jiān)視多個(gè)
核心語(yǔ)法:
watch([() => xx.name, () => xx.xx.age], (newValue, oldValue) {
// newValue oldValue 是整個(gè)數(shù)組
})
// 通常這么寫
watch([() => xx.name, () => xx.xx.age], (value) {
const [name, age] = value;
// ...
})
前面幾種學(xué)完了,監(jiān)視多個(gè)就是贈(zèng)送。請(qǐng)看示例:
<template>
<section>
<p>age: {{ person.age }}</p>
<p>jineng.a: {{ person.jineng.a }}</p>
<p><button @click="person.age += 1">change age</button></p>
<p><button @click="person.jineng.a += '~'">change jineng.a</button></p>
<p><button @click="changeJineng">替換 jineng</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
jineng: {
a: '吃飯',
b: '睡覺',
}
});
function changeJineng(){
person.jineng = {a: 'a吃飯', b:'a睡覺'}
}
watch([() => person.name, () => person.jineng.a], (newVal, oldVal) => {
console.log('newVal: ', newVal, 'oldVal: ', oldVal);
})
</script>
總結(jié)
用的較多的有:
- ref 基本類型
- 監(jiān)視對(duì)象中某個(gè)屬性,反手就是一個(gè)函數(shù),無論是基本類型、ref還是reactive都可以。
watchEffect
核心語(yǔ)法:
// watchEffect 是一個(gè)立即執(zhí)行的副作用操作,因此回調(diào)函數(shù)會(huì)在組件渲染時(shí)立即執(zhí)行一次,并在每個(gè)相關(guān)響應(yīng)式數(shù)據(jù)變化時(shí)再次執(zhí)行。
watchEffect(() => {
// 立即執(zhí)行
console.log('立即執(zhí)行');
if(temp.value > 60 || height.value >80){
...
}
})
比如我需要在”溫度“和”高度“大于20的時(shí)候發(fā)出請(qǐng)求,用 watch 可以這么實(shí)現(xiàn):
<template>
<section>
<p>Temperature: {{ temp }}</p>
<p>Height: {{ height }}</p>
<button @click="increaseTemp">Increase Temperature by 10</button> <br>
<button @click="increaseHeight">Increase Height by 10</button>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch, watchEffect } from "vue";
const temp = ref(0);
const height = ref(0);
const increaseTemp = () => {
temp.value += 10;
};
const increaseHeight = () => {
height.value += 10;
};
watch([temp, height], (val) => {
const [temp, height] = val
// console.log('val: ', val);
if (temp > 20 || height > 20) {
// 在條件滿足時(shí)執(zhí)行副作用代碼
console.log("watch: Temperature is greater than 20 or height is greater than 20", temp, height);
// 可以在這里進(jìn)行一些邏輯處理
}
})
</script>
可以直接替換成 watchEffect(變量直接用就好,框架會(huì)自動(dòng)幫你監(jiān)視),效果和上述例子相同,但代碼量少一些。
watchEffect(() => {
if (temp.value > 20 || height.value > 20) {
// 在條件滿足時(shí)執(zhí)行副作用代碼
console.log("Temperature is greater than 20 or height is greater than 20", temp.value, height.value);
}
});
Tip:筆者最初測(cè)試 watchEffect 時(shí)遇到了問題。筆者認(rèn)為每次修改 watchEffect 監(jiān)視的變量應(yīng)該都會(huì)執(zhí)行,如果條件有滿足的就應(yīng)該觸發(fā),但沒有。后來用 watch 重寫后發(fā)現(xiàn) watchEffect 又符合期望??赡苁潜镜丨h(huán)境出了點(diǎn)問題。
ref
vue2 中 ref 可以作用于普通dom元素
,也可以作用于vue 組件
。vue3 中也是這樣。
普通dom元素
請(qǐng)看示例:
<template>
<section>
<p ref="pElem">hello</p>
<button @click="say">click</button>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref } from "vue";
// 創(chuàng)建一個(gè)容器
const pElem = ref();
function say(){
// <p>hello</p>
console.log(pElem.value)
}
</script>
點(diǎn)擊按鈕,將打印 p 元素。
vue 組件
下面我們定義個(gè)組件 Dog.vue,然后在 App.vue 引入。請(qǐng)看示例:
在 Dog 中定義了兩個(gè)變量,通過 defineExpose 將 a 交出去:
// Dog.vue
<template>
<section>
dog
</section>
</template>
<script lang="ts" setup name="Dog">
import { ref } from 'vue';
const a = ref(1)
const b = ref(2)
// 無需引入
defineExpose({a})
</script>
Tip: defineExpose 是一個(gè)用于在組合式 API 中將組件的屬性或方法暴露給父組件的函數(shù)。它可以讓父組件直接訪問子組件內(nèi)部的屬性和方法。
App.vue 中直接引入Dog,無需注冊(cè)即可使用,然后用 ref 指向 Dog,點(diǎn)擊按鈕則能通過 ref 訪問 Dog 暴露出的變量:
// App.vue:
<template>
<section>
<Dog ref="box"/>
<p><button @click="handle1">click</button></p>
</section>
</template>
<script lang="ts" setup name="App">
// 自動(dòng)幫你注冊(cè)組件
import { ref } from "vue";
// 筆者 vscode 在Dog這里有紅色波浪線,提示:vue3 Module has no default export.Vetur(1192),筆者將 Vetur 插件卸載重啟就好了。
// vetur 可以高亮 .vue 文件,禁用后,筆者安裝 Vue - Official 插件也能高亮 .vue
import Dog from "./Dog.vue";
const box = ref()
function handle1(){
// Proxy(Object)?{a: RefImpl, __v_skip: true}
console.log('box.value: ', box.value);
// 1
console.log(box.value.a);
}
</script>
Tip: 這里的 ref 用法和 react 中的 create Ref 用法很像。
簡(jiǎn)單引入 TypeScript
Tip:若不熟悉 ts,可以參考:前端 Typescript 入門
新建 ts 文件,定義兩個(gè)類型:
// src/types/index.ts
export interface Person{
name: string,
age: number,
}
export type Persons = Person[]
App.vue 引用 ts 類型:
<script lang="ts" setup name="App">
// 注:需要增加 type,因?yàn)檫@是一個(gè)規(guī)范,不是值
import { type Person, type Persons } from '@/types';
let p2: Person = { name: 'peng', age: 18 }
// let persons: Person[] = [
// let persons: Array<Person> = [
let persons: Persons = [
{ name: 'peng', age: 18, id: 3 },
{ name: 'peng2', age: 18 },
{ name: 'peng3', age: 18 }
]
</script>
Tip:由于 Person 是一個(gè)規(guī)范,所以引入時(shí)需要增加 type,告訴自己這是一個(gè)規(guī)范。筆者中間報(bào)錯(cuò):文件 "...../src/types/index.ts" 不在項(xiàng)目 "...../tsconfig.app.json" 的文件列表中。項(xiàng)目必須列出所有文件,或使用 "include" 模式
。其實(shí)不用到 tsconfig.app.js 中修改 include 屬性值。
注
:將類型用于 reactive,可使用如下泛型形式:
<script lang="ts" setup name="App">
import { ref, reactive, computed } from "vue";
import {type Person, type Persons} from '@/types';
// 筆者這樣寫不起作用。多了 id 也沒給錯(cuò)誤提示。
let person:Persons = reactive([
{ name: 'peng', age: 18, id: 3},
{ name: 'peng2', age: 18},
{ name: 'peng3', age: 18}
]);
// 調(diào)用函數(shù) reactive 時(shí)增加泛型,會(huì)提示: “id”不在類型“Person”中
let person2 = reactive<Persons>([
{ name: 'peng', age: 18, id: 3},
{ name: 'peng2', age: 18},
{ name: 'peng3', age: 18}
]);
</script>
props
父組件通過 props 給子組件傳遞數(shù)據(jù),而通過事件給子組件傳遞方法。
首先回顧下 vue2 props
核心語(yǔ)法
// 孩子沒脾氣
defineProps(['persons'])
// 接收+限制類型
defineProps<{persons:Persons}>()
// 接收+限制類型+限制必要性 —— 可以不傳
defineProps<{persons?:Persons}>()
// 接收+限制類型+限制必要性+默認(rèn)值
import {withDefaults} from 'vue'
withDefaults(defineProps<{persons?:Persons}>(), {
persons: () => []
})
defineProps 基本使用
父組件傳遞兩個(gè)屬性:
<template>
<section>
<Dog name="peng" :age="18"/>
</section>
</template>
<script lang="ts" setup name="App">
import Dog from './Dog.vue'
</script>
子組件通過 defineProps 接收(較vue2 中更靈活),可以直接在模板中使用:
<template>
<section>
<p>props.name:{{ props.name}}</p>
<!-- props 在模板中直接用即可 -->
<p>name:{{ name}}</p>
<p>{{ props.age}}</p>
</section>
</template>
<script lang="ts" setup name="Dog">
import { ref } from 'vue';
import { defineProps } from 'vue';
const props = defineProps(['name', 'age'])
// props: Proxy(Object)?{name: 'peng', age: 18}
console.log('props: ', props);
if(props.age < 20){
// 在 Vue 2.x 中,子組件不應(yīng)該直接修改 props。單向數(shù)據(jù)流,如果需要修改可以讓父組件修改
// props.age += 10;
}
</script>
有兩點(diǎn):
- defineProps 接收一個(gè)也需要數(shù)組
// 即使一個(gè)也需要使用數(shù)組。
// 沒有與此調(diào)用匹配的重載。
// const props2 = defineProps('name')
- defineProps 返回值能接收 props
- 子類中不能修改 props 屬性,否則報(bào)錯(cuò)
傳遞一個(gè)對(duì)象給子組件,核心代碼如下:
// 父組件
<Dog name="peng" :persons="persons"/>
let persons = reactive<Persons>([
{ name: 'peng', age: 18},
{ name: 'peng2', age: 19},
{ name: 'peng3', age: 20}
]);
// 子組件
<ul>
<li v-for="(p, index) in persons" :key="index">{{ p.name }} - {{ p.age }}</li>
</ul>
import { defineProps } from 'vue';
defineProps(['persons'])
接收+限制類型+限制必要性+默認(rèn)值
vue3官網(wǎng)針對(duì) ts 寫法介紹如下:
const props = defineProps<{
foo: string
bar?: number
}>()
請(qǐng)看示例:
// 子組件
<template>
<section>
<ul>
<li v-for="(p, index) in persons" :key="index">{{ p.name }} - {{ p.age }}</li>
</ul>
</section>
</template>
<script lang="ts" setup name="Dog">
import { withDefaults } from 'vue';
import {type Persons} from '@/types'
// 只接收。不傳也不報(bào)錯(cuò)
// defineProps(['persons'])
// 接收 persons + 類型。不傳,父組件報(bào)錯(cuò):Missing required prop: "persons"
// defineProps<{persons:Persons}>()
// 接收 persons + 類型 + 必要性
// 通過 ? 表明可以不傳,父組件不會(huì)報(bào)錯(cuò)
// defineProps<{persons?:Persons}>()
withDefaults(defineProps<{persons?:Persons}>(), {
// 注:默認(rèn)值需要通過函數(shù)返回,類似 vue2 中數(shù)據(jù)返回也是通過函數(shù)
// persons: [{ name: '空2', age: 0},]
persons: () => [{ name: '空', age: 0},]
})
</script>
注
:defineExpose、defineProps 不需要引入。define
XXx 是宏函數(shù),在vue3 中無需引入(筆者自測(cè)通過)。
生命周期
創(chuàng)建、掛載、更新、銷毀
vue2
4個(gè)階段,8個(gè)鉤子:
-
創(chuàng)建階段:
beforeCreate: 在實(shí)例初始化之后,數(shù)據(jù)觀測(cè) (data observer) 和 event/watcher 事件配置之前被調(diào)用。
created: 實(shí)例已經(jīng)創(chuàng)建完成之后被調(diào)用。在這一步,實(shí)例已經(jīng)完成了數(shù)據(jù)觀測(cè)、屬性和方法的運(yùn)算,但是尚未開始掛載到頁(yè)面上。 -
掛載階段:
beforeMount: 在掛載開始之前被調(diào)用:相關(guān)的 render 函數(shù)首次被調(diào)用。
mounted: el 被新創(chuàng)建的 vm.$el 替換,并掛載到實(shí)例上去之后調(diào)用該鉤子。 -
更新階段:
beforeUpdate: 數(shù)據(jù)更新時(shí)調(diào)用,發(fā)生在虛擬 DOM 重新渲染和打補(bǔ)丁之前??梢栽谠撱^子中進(jìn)一步地更改狀態(tài),不會(huì)觸發(fā)附加的重渲染過程。
updated: 由于數(shù)據(jù)更改導(dǎo)致的虛擬 DOM 重新渲染和打補(bǔ)丁后調(diào)用。 -
銷毀階段:
beforeDestroy: 在實(shí)例銷毀之前調(diào)用。實(shí)例仍然完全可用。
destroyed: 在實(shí)例銷毀之后調(diào)用。該鉤子被調(diào)用時(shí),Vue 實(shí)例的所有指令都被解綁定,所有事件監(jiān)聽器被移除,所有子實(shí)例也被銷毀。
vue3
和 vue2 類似,4個(gè)階段,8個(gè)鉤子,但稍微有差異:
- 沒有 beforeCreat、created,由 setup 替代(setup 比 beforeCreat 早)
- beforeMount 改成 onBeforeMount,mounted 改成 onMounted (加
on和駝峰
,其他幾個(gè)也這樣) - 銷毀改成
卸載
,和掛載對(duì)應(yīng) - 生命周期鉤子函數(shù)語(yǔ)法改為
function onBeforeMount(callback: () => void): void
Tip:這只是最基本的生命周期鉤子,比如路由也會(huì)有,現(xiàn)學(xué)現(xiàn)查即可。
請(qǐng)看示例:
// 子組件
<template>
<section>
{{ count }}
<button @click="count += 1">change count</button>
</section>
</template>
<script lang="ts" setup name="Dog">
import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'
import {ref} from 'vue'
let count = ref(0);
// 沒有 beforeCreat、created,由 setup 替代
console.log('創(chuàng)建')
onBeforeUpdate(() => {
console.log('更新前');
})
onUpdated(() => {
console.log('更新后');
})
onBeforeUnmount(() => {
console.log('卸載前');
})
onUnmounted(() => {
console.log('卸載后');
})
// 故意將其放在末尾
onBeforeMount(() => {
console.log('掛載前');
})
onMounted(() => {
console.log('掛載后');
})
</script>
// 父組件
<template>
<section>
<Dog name="peng" v-if="isShow" />
<button @click="isShow = !isShow">toggle show</button>
</section>
</template>
<script lang="ts" setup name="App">
import Dog from './Dog.vue'
import { ref } from "vue";
let isShow = ref(true)
</script>
點(diǎn)擊“change count”更新子組件,點(diǎn)擊“toggle show”會(huì)銷毀或創(chuàng)建子組件??刂婆_(tái)輸出:
創(chuàng)建
掛載前
掛載后
更新前
更新后
卸載前
卸載后
vue3 父組件子組件生命周期
在上述示例中,我們給父組件也增加對(duì)應(yīng)的生命周期,然后看一下生命周期鉤子函數(shù)的順序。
// 父組件
<template>
<section>
<Dog name="peng" v-if="isShow" />
<button @click="isShow = !isShow">toggle show</button>
</section>
</template>
<script lang="ts" setup name="App">
import Dog from './Dog.vue'
import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'
import { ref } from "vue";
let isShow = ref(true)
let count = ref(0);
console.log('父創(chuàng)建')
onBeforeUpdate(() => {
console.log('父更新前');
})
onUpdated(() => {
console.log('父更新后');
})
onBeforeUnmount(() => {
console.log('父卸載前');
})
onUnmounted(() => {
console.log('父卸載后');
})
// 故意將其放在末尾
onBeforeMount(() => {
console.log('父掛載前');
})
onMounted(() => {
console.log('父掛載后');
})
</script>
頁(yè)面初始時(shí)輸出如下(和 vue2 中父元素、子元素的順序一致,甚至和 react 中幾乎相同):
父創(chuàng)建
父掛載前
子創(chuàng)建
子掛載前
子掛載后
父掛載后
點(diǎn)擊“change count”,輸出:子更新前 子更新后
點(diǎn)擊“toggle show”,輸出(和筆者猜測(cè)相同):
父更新前
子卸載前
子卸載后
父更新后
常用生命鉤子
在實(shí)際開發(fā)中,最常用
的 Vue2 生命周期鉤子函數(shù)通常是:
- created: 在實(shí)例創(chuàng)建完成后立即調(diào)用,通常用于進(jìn)行初始化操作、
數(shù)據(jù)請(qǐng)求
等。 - mounted: 在實(shí)例掛載到 DOM 后調(diào)用,通常用于執(zhí)行
DOM 操作
、訪問第三方庫(kù)等與 DOM 相關(guān)的操作。 - updated: 當(dāng)數(shù)據(jù)更新導(dǎo)致虛擬 DOM 重新渲染和打補(bǔ)丁后調(diào)用,通常用于在更新后執(zhí)行一些需要基于 DOM 的操作。
- beforeDestroy: 在實(shí)例銷毀之前調(diào)用,通常用于進(jìn)行一些清理工作、
清除定時(shí)器
、解綁事件監(jiān)聽器
等。
vue3 中常用的和vue2 類似:onMounted
、onUpdated
、onBeforeUnmount
。
自定義hooks
在 Vue 3 中,自定義 Hooks 是指開發(fā)者自己定義的可復(fù)用的 Hooks 函數(shù),用于組件邏輯的封裝和復(fù)用。
前面我們說到 vue3 的組合式Api,比如下面這個(gè) vue 包含兩部分的功能,我們利用 hooks 將其拆分:
<template>
<main>
<section>
功能A:
<p>name: {{ name }}</p>
<p><button @click="changeName">change name</button></p>
</section>
<section>
功能B:
<p>age: {{ age }} 放大2倍: {{ bigAge }}</p>
<p><button @click="changeAge">change age</button></p>
</section>
</main>
</template>
<script lang="ts" setup name="App">
import {onMounted, computed} from 'vue'
import { ref } from "vue";
let name = ref('peng');
let age = ref(18);
function changeName(){
name.value += '~'
}
function changeAge(){
age.value += 1
}
let bigAge = computed(() => {
return age.value * 2
})
onMounted(() => {
age.value += 2
console.log('B mounted: 將age增加2')
})
</script>
使用 hooks 將變成如下樣子:
- App.vue
<template>
<main>
// 不變
</main>
</template>
<script lang="ts" setup name="App">
import FunctionA from '@/hooks/FunctionA'
import FunctionB from '@/hooks/FunctionB'
// 引入功能A
const {name, changeName} = FunctionA()
// // 引入功能B
const {age, changeAge, bigAge,} = FunctionB()
</script>
- FunctionA.ts - 將app.vue 中的
<script
代碼復(fù)制,放入function
中,需要導(dǎo)出
(export)和暴露
(return)
// 所有功能A的數(shù)據(jù)、方法、鉤子、計(jì)算屬性等等
import { ref } from "vue";
// 必須導(dǎo)出
// 用函數(shù)包裹
export default function () {
let name = ref('peng');
function changeName() {
name.value += '~'
}
// 暴露出去
return {name, changeName}
}
- FunctionB.ts
import { onMounted, computed } from 'vue'
import { ref } from "vue";
export default function () {
let age = ref(18);
function changeAge() {
age.value += 1
}
let bigAge = computed(() => {
return age.value * 2
})
onMounted(() => {
age.value += 2
console.log('B mounted: 將age增加2')
})
return {age, changeAge, bigAge,}
}
這樣運(yùn)行后效果和改動(dòng)之前完全相同。
Tip:自定義 Hooks 函數(shù)本身不能直接包含模板代碼,因?yàn)?Hooks 函數(shù)主要用于組件邏輯的封裝和復(fù)用,并不包含視圖邏輯。
vs vue2中的mixin
相似:組件邏輯的封裝和復(fù)用
不同:
- 作用域不同:混入的邏輯會(huì)影響到所有使用該 Mixin 的組件,自定義 Hooks 由開發(fā)者有選擇地在組件中引入和使用
- 組織邏輯方式不同:Mixin 通常將邏輯分散在多個(gè)生命周期鉤子函數(shù)中,自定義 Hooks 將邏輯封裝在一個(gè)函數(shù)中,更加集中
其他
Vue Devtools
vue-devtools是一款基于chrome游覽器的插件,可以用于調(diào)試vue應(yīng)用
有兩種方法安裝,一種是有條件,直接通過 chrome 應(yīng)用商店搜索 vue 安裝
沒有條件的,可以訪問 極簡(jiǎn)插件,搜索,下載,解壓后,打開 chrome 開發(fā)者模式,拖拽到擴(kuò)展插件
三個(gè)花括號(hào)
在使用 vscode 編寫 vue 模板時(shí),有時(shí)會(huì)遇到輸入一個(gè)花括號(hào) {
,結(jié)果出現(xiàn)三個(gè)花括號(hào) { }}
,還得手動(dòng)刪除一個(gè),非常煩人。
解決辦法:打開設(shè)置 -> 搜索Closing Brackets -> 找到 Auto Closing Brackets -> 將 aways 改成beforeWhitespace
。請(qǐng)看下圖文章來源:http://www.zghlxwxcb.cn/news/detail-844266.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-844266.html
可以多個(gè)根標(biāo)簽
<Person/>
<Person/>
<Person/>
到了這里,關(guān)于vue3 快速入門系列 —— 基礎(chǔ)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!