vue3 快速入門系列 - 基礎 前面我們已經用 vue2 和 react 做過開發了。 從 vue2 升級到 vue3 成本較大,特別是較大的項目。所以許多公司對舊項目繼續使用vue2,新項目則使用 vue3。 有些UI框架,比如ant design vue1.x 使用的 vue2。但現在 ant ...
vue3 快速入門系列 - 基礎
前面我們已經用 vue2 和 react 做過開發了。
從 vue2 升級到 vue3 成本較大,特別是較大的項目。所以許多公司對舊項目繼續使用vue2,新項目則使用 vue3。
有些UI框架,比如ant design vue1.x 使用的 vue2。但現在 ant design vue4.x 都是基於 vue3,示例預設是 TypeScript。比如 table 組件管理。
另外 vue3 官網介紹也使用了 TypeScript,例如:響應式 API:核心
本篇主要介紹:vite 創建vue3項目、組合式api、響應式數據、計算屬性、監聽、ref、ts、生命周期、自定義hooks。
vue3 簡介
Vue.js 3.0,代號海賊王,於2020年9月18日發佈 —— v3.0.0 海賊王
主要有如下改進:
性能
改進:與 Vue 2 相比,Vue 3 在包大小(通過 Tree-Shaking 減少最多 41%)、初始渲染(快 55%)、更新(快 133%)和記憶體使用方面表現出了顯著的性能改進(最多減少 54%)。- 擁抱
TypeScript
:更好的支持 TS。有的公司在 vue2 中就用 TS 了 - 用於應對規模問題的新 API:引入了
Composition API
——一組新的 API,旨在解決大規模應用程式中 Vue 使用的痛點。Composition API 構建在反應性 API 之上,支持類似於 React hooks 的邏輯組合和重用、更靈活的代碼組織模式以及比 2.x 基於對象的 API 更可靠的類型推斷。 - 分層內部模塊:還公開了較低級別的 API,可解鎖許多高級用例
創建 vue3 工程
vue-cli 創建
前面我們用 vue-cli 創建過 vue2 的項目,用其構建 vue3 也類似,差別就是選擇 vue3 版本。最後生成的項目結構如下:
Vue CLI 是官方提供的基於 Webpack 的 Vue 工具鏈,它現在處於維護模式。我們建議使用 Vite 開始新的項目
,除非你依賴特定的 Webpack 的特性。在大多數情況下,Vite 將提供更優秀的開發體驗 —— 官網 - 項目腳手架
vite 創建
另一種方式是使用 vite。有如下優勢:
- 對 TypeScript、JSX、CSS 等支持開箱即用。
- 無論應用程式大小如何,都始終極快的模塊熱替換(HMR)
- 極速的服務啟動。使用原生 ESM(參考 mdn esm) 文件,無需打包
Tip:
- vue腳手架(vue-cli) 和創建 react的腳手架(create-react-app)都是基於 webpack。而 vite 也是一種構建工具,和 webpack 類似,也有一些區別,其作者就是 Vue.js 的創始人尤雨溪
- HMR 它用於開發環境,不適用於生產環境。更多介紹請看這裡。
- jsx 在學習 react 中用到過(請看這裡),vue 中用 template 寫視圖部分,react 用 jsx。在 Vue 3 項目中使用 JSX 時,Vite 會將 JSX 語法編譯為 Vue 3 的渲染函數。
筆者首先使用 npm create vite@latest
創建項目,自己根據需要選擇對應預設(比如要 TypeScript or javascript),創建完成後根據提示進入項目,安裝依賴,本地啟動:
npm install
npm run dev
結果報錯:
> [email protected] 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 版本才能正常運行,當你的包管理器發出警告時,請註意升級你的 Node 版本 —— vite 官網-搭建第一個 Vite 項目
於是使用 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
根據提示切換到 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
創建 vue3 項目 —— vue3 官網 創建一個 Vue 應用(這裡提到 node 需要18+):
PS \test-projects> npm create vue@latest
Need to install the following packages:
[email protected]
Ok to proceed? (y) y
Vue.js - The Progressive JavaScript Framework
√ 請輸入項目名稱: ... hello_vue3
√ 是否使用 TypeScript 語法? ... 否 / 是
√ 是否啟用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 進行單頁面應用開發? ... 否 / 是
√ 是否引入 Pinia 用於狀態管理? ... 否 / 是
√ 是否引入 Vitest 用於單元測試? ... 否 / 是
√ 是否要引入一款端到端(End to End)測試工具? » 不需要
√ 是否引入 ESLint 用於代碼質量檢測? ... 否 / 是
正在構建項目 \test-projects\hello_vue3...
項目構建完成,可執行以下命令:
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 [email protected] to update!
npm notice
根據提示按照依賴,本地啟動項目成功:
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
> [email protected] 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 作用和用途不同,兩者效果也不同,總的來說前者創建 Vite 項目,而 npm create vue@latest
是用來創建 Vue.js 項目。
PS \test-projects> npm create vite@latest
Need to install the following packages:
[email protected]
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 本地啟動非常快
vite 本地啟動非常快。真正按需編譯,不在等待整個應用編譯完成。
用 webpack 本地啟動伺服器,需要經歷如下幾步:entry->route->module->bundle->伺服器啟動
(下圖左);而用 vite 啟動伺服器,伺服器啟動卻從末尾移到開頭(下圖右)
有點像懶載入,你需要訪問哪個路由,就載入哪個,非常快速。
vue3項目目錄結構淺析
前面我們用 vite 創建了 hello_vue3 項目。目錄結構如下:
我們先說其他文件,最後在分析src文件夾
extensions.json
內容如下:
// .vscode/extensions.json
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
推薦你安裝這兩個插件,當你用 vscode 啟動項目,點擊切換到其他文件上,vscode 右下角就會提示你是否安裝這兩個插件。就像這樣:
這兩個是vue官方給 vscode 提供的插件:
- TypeScript Vue Plugin (Volar)
- Vue Language Features
env.d.ts
內容如下:
/// <reference types="vite/client" />
是一個在 Vue.js 項目中使用 Vite 構建工具時引入的指令,它的作用是讓 TypeScript 編譯器能夠識別並利用 Vite 客戶端類型聲明文件提供的類型信息,以提供更好的智能編碼功能和類型檢查支持。
Tip:如果你刪除 node_modules 文件夾,你在vscode 中會發現 vite/client
下有紅色波浪線。
TypeScript 主要用於處理 JavaScript 代碼,並且在處理模塊時,它會關註 .ts、.tsx、.js 和 .jsx 這些與 JavaScript 相關的文件類型。
TypeScript 預設情況下並不會識別或處理像 .txt、.gif 這樣的非 TypeScript 文件類型。這個文件的作用就是讓 ts 認識 txt、jpg、gif等。
比如你在src 下新建 a.txt、b.ts,然後在 b.ts 中編寫:
import a from 'a.txt'
console.log(a)
當你清空 env.d.ts
,你會發現 import a from 'a.txt'
中 a.txt 下有紅色波浪線。再次還原 env.d.ts
則好了。
通過 ctrl + 滑鼠點擊進入 vite/client
,你會發現 vue 給我們聲明好了我們需要使用的其他類型文件。比如 txt:
declare module '*.txt' {
const src: string
export default src
}
index.html
index.html 這就是我們的入口文件
。內容如下:
<!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>
無需重啟服務,頁面就顯示 a
。
其他
- tsconfig 文件,ts 配置相關,不要刪,ts 可能會有問題:
tsconfig.app.json
tsconfig.json
tsconfig.node.json
-
vite.config.ts 項目配置文件。比如代理、安裝插件
-
public/favicon.ico 頁簽圖標
-
package.json、package-lock.json
src
src 就是我們編碼的地方。
我們先將 src 中的文件都刪除,我們自己重新創建。
創建 main.ts 和 App.vue 兩個文件。內容如下:
- main.ts 是index.html載入的入口文件
// src/main.ts
import {createApp} from 'vue'
// 項目的根
import App from './App.vue'
// Vue.js 3.x 中用於創建和掛載應用
// 創建一個新的 Vue 應用,並將根組件指定為 App。.mount('#app') 將應用掛載到指定的 DOM 元素上
createApp(App).mount('#app')
// src/App.vue
<template>
<div>
你好 vue3
</div>
</template>
<!-- 可以指定語言是 ts,ts中也可以寫js -->
<script lang="ts">
</script>
<style scoped>
</style>
瀏覽器訪問,頁面顯示 你好 vue3
。
前面我們說到 vite 啟動後,伺服器就已就緒。然後會根據用戶請求哪裡,就會給你載入哪裡。
vue3 向下相容 vue2 語法
有些項目使用了 vue3,但寫法還是 vue2 —— 不建議這麼做
。
為了證明 vue3 中能寫 vue2,筆者在 vue3 項目中寫一個 vue2 示例。請看代碼:
// 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
// 按鈕,點擊後,date 後的數字就會變化
change date
options Api 和 compositionApi
Vue 2 使用的是選項式 API,而 Vue 3 引入了組合式 API
雖然 Vue 3 推薦使用組合式 API,但它仍然完全支持 Vue 2 的選項式 API,以保持向下相容性。所以在 Vue 3 中,你可以自由選擇使用選項式 API 或組合式 API 來編寫你的組件邏輯。
選項式API有一個缺點:新增一個功能,需要分別在 data、methods、computed、watch等選項中修改代碼,如果代碼上千,修改或抽取封裝這部分功能,有困難。
Tip:我們用 大帥老猿
的圖說明以下這個問題
而組合式 api 可以簡化這個問題,我們可以感受下(代碼如何實現暫時不用管):
Tip: 具體如何拆分,請看本篇最後自定義 hooks
章節。
setup
setup 函數是組合式 API 的入口
,用於組合組件的邏輯和功能。
setup 概述
首先我們用 vue2 語法寫一個示例:展示名字和日期,點擊按鈕能改變日期。代碼如下:
<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>
現在我們把 data 和 methods 兩個配置去除,改成 setup 就完成了 vue3 示例的重構
<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);
}
// 將數據和方法都交出去
return {name, date, changeDate}
}
}
</script>
setup 是一個方法,平時如何定義變數和方法,這裡就怎麼寫,最後將方法和變數都交出去。
這裡其實還有一個問題,點擊 button 日期在界面沒變,但方法卻執行了。這是因為 date 變數不是響應式
的。
Tip:現在我們先說 setup,後面在將響應式的東西。這裡要修複可以使用 ref(這個 ref 和 vue2 中指向元素或組件的ref,不是同一個東西):
<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);
}
// 將數據和方法都交出去
另外 setup 中的 this 是undefined,vue3 開始弱化 this。
最後說一下 setup 執行時機,比 beforeCreat 還早:
name: "App",
beforeCreate() {
console.log(1);
},
setup() {
console.log(2);
先輸出 2 再輸出 1。
setup 返回函數
setup 返回值也可以是一個函數,比如這個:
return () => 'hello vue3'
頁面就會顯示hello vue3
,模板是什麼都不重要了,直接根據這個函數返回值渲染
這種用法不多,常用的還是返回對象。
setup 和配置項的關係
- setup 能否和 data、method 能否同時寫,如果衝突,以誰為準?
- 配置項能否讀取setup 中的東西,setup 能否讀取setup 中的東西?
setup 能和 data、method 同時存在
請看示例:
<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 執行順序
beforeCreate() {
console.log("beforeCreate");
},
setup() {
console.log("setup");
return () => 'hello vue3'
},
setup
beforeCreate
data 讀取 setup 中的屬性
data 能夠讀取 setup 中的屬性。請看示例:
<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 中的數據。就像 vue2 中 data 可以讀取 props 中的數據,因為 props 比 data 先初始化 —— initstate 初始化狀態。
在 setup 中無法使用 data 中的數據。請看示例,直接報錯:
// vscode 報錯
let newAge = age,
// vscode 報錯 - setup 中沒有this
let newAge2 = this.age,
setup 語法糖
每次都得寫 setup(),還需要將方法或屬性交出去,能否只寫屬性和方法,自動交出去?
方式1
setup() {
let name = "pengjiali";
let date = ref(-1);
function changeDate() {
date.value = new Date().getTime();
console.log("date: ", date);
}
// 將數據和方法都交出去
return { name, date, changeDate };
},
有的。將 setup() 專門提取出去。就像這樣:
<script lang="ts">
import { ref } from "vue";
export default {
name: "App",
};
</script>
<script lang="ts" setup>
// 屬性和方法自動交出去
let name = "pengjiali";
let date = ref(-1);
function changeDate() {
date.value = new Date().getTime();
console.log("date: ", date);
}
</script>
方式2
方式一還是需要寫l了兩個 <script>
,其中一個專門用於定義組件名。
<script lang="ts">
import { ref } from "vue";
export default {
name: "App",
};
</script>
不想寫兩個 <script>
,可以利用插件 vite-plugin-vue-setup-extend
。
先安裝:
PS \test-projects\hello_vue3> npm i vite-plugin-vue-setup-extend -D
npm WARN deprecated [email protected]: 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: {
最後應用:
-<script lang="ts" setup>
+<script lang="ts" setup name="App3">
響應式數據
vue2 中放在 data 中的數據都是響應式的,在vue3 中可以通過 ref和reactive 兩種方式來處理響應式。
通過 vue devtools,我們知道數據為尊,因為方法放在後面(除了方法,其他的也會放在這裡),而數據放在前面。
ref創建基本類型響應式數據
想讓哪個數據是響應式的,就將數據用 ref 包裹一下。
註
:這裡的 ref 和 vue2 中 ref 不是一個東西
用法請看示例(和註釋):
<template>
<section>
<p>name: {{ name }}</p>
<!-- 不能寫 date.value,這裡自動會給 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 創建一個基本類型的響應式數據
let date = ref(-1);
// 使用 ref 函數創建的響應式變數是一個包裝過的對象,你需要通過 .value 來訪問和修改其值
// 使用 ref 創建變數時,實際上你得到的是一個包含了值的對象,而不是直接的值。因此,在修改這個變數時,你需要通過 .value 來訪問和修改實際的值,這樣 Vue 才能夠正確地追蹤變化併進行響應。
// 使用 ref 創建的變數必須通過 .value 來訪問和修改其值,這是為了確保 Vue 能夠正確捕捉變化並更新視圖。
function changeDate() {
// date: RefImpl {__v_isShallow: false, dep: Map(1), __v_isRef: true, _rawValue: -1, _value: -1}
console.log('date: ', date);
// 通過 value 修改響應式數據。
date.value = new Date().getTime();
console.log("date: ", date);
}
</script>
變數用ref包裹後,類型變成 RefImpl
。需要通過 .value
來訪問和修改實際的值。
Tip:越過 .value 直接整體替換是不可以的,就像這樣:
let count = ref(0)
function changeCount(){
// 生效
count = 9
// 失效
// count = ref(9)
}
註
:模板中不需要 .value
有點像 proxy 的感覺:
// 創建一個普通的對象作為目標對象
let target = {
name: 'Alice',
age: 30
};
// 創建一個 Proxy 對象,用來代理目標對象
let proxy = new Proxy(target, {
// 攔截屬性讀取的操作
get: function(target, property) {
console.log(`Reading ${property} property`);
return target[property]; // 返回目標對象相應的屬性值
},
// 攔截屬性設置的操作
set: function(target, property, value) {
console.log(`Setting ${property} property to ${value}`);
target[property] = value; // 設置目標對象相應的屬性值
}
});
// 通過 Proxy 訪問和修改屬性
// Reading name property
// Alice
console.log(proxy.name); // 讀取屬性
// Setting age property to 35
// 35
proxy.age = 35; // 設置屬性
Tip:Proxy 是 ES6 引入的一個特性,它允許你創建一個代理對象,可以用來攔截並自定義目標對象的基本操作,比如屬性查找、賦值、刪除等
reactive 定義響應式數據
利用 reactive 將對象
轉成響應式,重寫上述示例:
<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>
經過 reactive 封裝後的對象類型變成 Proxy。專業術語叫響應式對象
reactive 同樣可以處理數組(數組也是對象),請看示例:
<ul>
<li v-for="(item, index) in ages" :key="index">{{ item }}</li>
</ul>
const ages = reactive([18, 19, 20])
對深層次對象也同樣起作用。請看示例:
<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()
}
不能定義基本類型,比如將字元串轉成響應式,vscode 和瀏覽器控制台報錯如下:
// vscode:類型“string”的參數不能賦給類型“object”的參數。
// 控制台:value cannot be made reactive: #abc
const color = reactive('#abc');
ref 定義對象類型數據
直接看示例,我們將 reactive 示例中的 reactive 換成 ref,修改值時加 .value 即可,模板不用動。
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 能處理基本類型和對象,但是遇到對象,實際上是搖人了。請看示例:
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 對象的 value 屬性,發現了 Proxy(Object)
,所以本質上是 reactive 處理了對象
ref vs reactive
巨集觀:
- ref 能定義基本類型和對象的響應式數據
- reactive 只能用於對象
ref 自動生成 .value
寫代碼時還得記著是 ref 類型,需要增加 .value,好麻煩。可以使用 vscode 插件:
vscode 直接安裝 Vue - Official
(vscode 提示 TypeScript Vue Plugin (Volar)
已棄用,使用 Vue - Official 替代)
通過 vscode 設置,勾選 Auto-complete Ref value with .value
,並設置 Applies to all profiles
重啟後,只要輸入 ref 變數,則會自動添加 .value,非常方便。
const person = ref({
name: "pengjiali",
date: -1,
})
const person2 = reactive({
name: "pengjiali",
date: -1,
})
// 輸入 person 則會自動添加 .value
person.value
// 對於非 ref 則不會添加 .value
person2
reactive 的局限性
reactive 重新分配一個對象,會失去響應式(可使用 Object.assign 整體替換)。請看示例:
<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() {
// 失效 - 響應性連接已丟失!
// person = reactive({name: 'peng', age: 25})
// 失效
// person = {name: 'peng', age: 25}
// 正常
Object.assign(person, {name: 'peng', age: 25})
}
</script>
Tip: Object.assign() 靜態方法將一個或者多個源對象中所有可枚舉的自有屬性複製到目標對象,並返回
修改後的目標對象。
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 使用場景
由於這些限制,我們建議使用 ref() 作為聲明響應式狀態的主要 API —— 官網 - reactive 局限性
筆者習慣:
- 需要一個基本類型的響應式數據,只可使用 ref
- 對象使用 reactive
- 如果是表單,使用 ref 會出現很多 .value,不好看
toRefs
將一個響應式對象轉換為一個普通對象,這個普通對象的每個屬性都是指向源對象相應屬性的 ref。每個單獨的 ref 都是使用 toRef() 創建的。
不明白請看下麵代碼。
比如這段代碼:
<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>
我從響應式對象中解構出 age,然後通過方法修改 age 的值,發現頁面沒更新:
+ <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;
+}
+
這是因為解構出的 age 不在是響應式。可以使用 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 用的較少。
比如層級比較深的場景,請看示例:
<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>
計算屬性
作用和vue2相同,先回憶下 vue2 中的計算屬性。寫法如下:
computed: {
now: function () {
}
}
改成 vue3 需要使用 computed 方法。就像這樣:
let now = computed(() => {
return Date.now()
})
請看示例:
<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(){
}
})
// 觸發 set 方法
fullName.value = 'li-si'
watch
vue3 中 watch 作用應該和 vue2 中相同,先回憶下vue2 中 watch 寫法。就像這樣:
new Vue({
data: {
message: 'Hello, Vue!'
},
watch: {
message: function(newValue, oldValue) {
console.log('消息從', oldValue, '變為', newValue);
}
}
});
vue3 中說 watch 只能監視4種數據:
- ref定義的數據
- reactive 定義的數據
- 函數返回一個值(getter函數)
- 一個包含上述內容的數組
Tip: vue2 watch 中有deep、immediate、unwatch,下文 vue3 中 watch 也都有。
ref 基本類型
請看示例:
<template>
<section>
<p>age: {{ age}}</p>
<p><button @click="age += 1">change age</button></p>
<p><button @click="stopWatch">停止監聽 age 變化</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { ref, watch } from "vue";
let age = ref(18)
// watch(age.value, ... ) 錯誤寫法
let stopWatch = watch(age, (newValue, oldValue) => {
console.log('年齡從', oldValue, '變為', newValue);
});
</script>
- watch 監視的ref變數,無需增加
.value
。安裝好vscode 插件,在這種情況下也不會自動給你加 .value。 - watch 返回一個函數,執行後將解除監視。就像 vue2 中的 vm.$watch 方法,返回 unwatch。
ref 對象類型
核心語法:
watch(person, (newValue, oldValue) => {
}, { deep: true});
比如用 ref 定義一個對象,裡面有兩個按鈕,一個只改變“年齡”,一個改變整個 ref 對象。就像這樣:
<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(替換整個對象)</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 相同。通常工作只關心新值
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>
只有改變整個對象時 watch 中的方法才會執行,而改變ref對象中的屬性,watch 方法卻不會執行。
加上一個配置項,這樣改變整個對象,以及改變ref對象中的屬性,watch 中的方法都會執行。
console.log('New person:', newValue);
console.log('Old person:', oldValue);
- }, );
+ }, {deep: true});
其實還有一個屬性 immediate,初始時就會執行 watch 中的方法。就像這樣:
// 完全替換person,newValue 和 oldValue 不同
// 只替換person中屬性,newValue 和 oldValue 相同。通常工作只關心新值
watch(person, (newValue, oldValue) => {
console.log('Person changed');
console.log('New person:', newValue);
console.log('Old person:', oldValue);
}, { deep: true, immediate: true });
reactive
核心語法:
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(替換整個對象)</button></p>
</section>
</template>
<script lang="ts" setup name="App">
import { reactive, ref, watch } from "vue";
let person = reactive({
name: "pengjiali",
age: 18,
});
// 預設開啟深度監聽,而且通過 {deep: false} 也關閉不了
watch(person, (newValue, oldValue) => {
console.log('Person changed');
console.log('New person:', newValue);
console.log('Old person:', oldValue);
}, {deep: false});
function changePerson() {
// 不能整個替換,只能用 Object.assign。不能像 ref.value = {...}
Object.assign(person, {name: 'peng', age: 100})
}
</script>
監視 ref 或 reactive 的對象中的某屬性
前面我們監視的都是整個對象,比如現在要監視對象中的某個屬性。這裡分為基本類型
和對象類型
。
// reactive 和 ref 都可以用如下形式
// 利用 getter。如果需要則增加 deep
watch(() => person.car, () => {
}, {deep: true})
基本類型
就以 reactive 對象為例,直接將監視源改為 person.name
vscode 就會出現紅色波浪線:
<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>
運行後在瀏覽器控制臺中報錯更明顯:
// 無效的監視源:只能是 getter 函數、ref、reactive object、或這些類型的數組
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>
現在 person.name 不屬於上述4種類型。
將 person.name 改成 getter。代碼如下:
Tip:getter 一個函數,返回一個值 —— vue3 watch
watch(() => person.name, (newValue, oldValue) => {
console.log('Person changed');
});
這樣修改 age 時不會觸發 watch,只有 name 改變時才會觸發 watch。
對象類型
這裡給 person 定義了一個 jineng 的對象屬性,並定義兩個按鈕,一個會改變 jineng 的屬性,一個改變整個技能。代碼如下:
<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>
首先我們這麼寫,發現只能監聽 jineng 裡面的屬性改變:
// 點擊`change jineng.a` 執行
// 點擊`替換 jineng` 不執行
watch(person.jineng, () => {
console.log('watch jineng');
})
Tip:通過列印我們知道 person.jineng 類型是Proxy,也就是 reactive 類型,根據前文我們知道 reactive 預設開啟深度監視,而且不能整個替換,之前用的都是 Object.assign,這裡用的是 person.jineng = {a: 'a吃飯', b:'a睡覺'}
。
改成 getter 發現只能監聽替換整個 jineng:
// 點擊`change jineng.a` 不執行
// 點擊`替換 jineng` 執行
watch(() => person.jineng, () => {
console.log('watch jineng');
})
在 getter 基礎上增加 {deep: tree} 則都能監視到:
// 點擊`change jineng.a` 執行
// 點擊`替換 jineng` 執行
// 說官網一直都是用函數
watch(() => person.jineng, () => {
console.log('watch jineng');
}, {deep: true})
Tip:將上述示例從 reactive 改成 ref,watch 監視方式還是不變。請看代碼:
<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>
監視多個
核心語法:
watch([() => xx.name, () => xx.xx.age], (newValue, oldValue) {
// newValue oldValue 是整個數組
})
// 通常這麼寫
watch([() => xx.name, () => xx.xx.age], (value) {
const [name, age] = value;
// ...
})
前面幾種學完了,監視多個就是贈送。請看示例:
<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>
總結
用的較多的有:
- ref 基本類型
- 監視對象中某個屬性,反手就是一個函數,無論是基本類型、ref還是reactive都可以。
watchEffect
核心語法:
// watchEffect 是一個立即執行的副作用操作,因此回調函數會在組件渲染時立即執行一次,併在每個相關響應式數據變化時再次執行。
watchEffect(() => {
// 立即執行
console.log('立即執行');
if(temp.value > 60 || height.value >80){
...
}
})
比如我需要在”溫度“和”高度“大於20的時候發出請求,用 watch 可以這麼實現:
<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) {
// 在條件滿足時執行副作用代碼
console.log("watch: Temperature is greater than 20 or height is greater than 20", temp, height);
// 可以在這裡進行一些邏輯處理
}
})
</script>
可以直接替換成 watchEffect(變數直接用就好,框架會自動幫你監視),效果和上述例子相同,但代碼量少一些。
watchEffect(() => {
if (temp.value > 20 || height.value > 20) {
// 在條件滿足時執行副作用代碼
console.log("Temperature is greater than 20 or height is greater than 20", temp.value, height.value);
}
});
Tip:筆者最初測試 watchEffect 時遇到了問題。筆者認為每次修改 watchEffect 監視的變數應該都會執行,如果條件有滿足的就應該觸發,但沒有。後來用 watch 重寫後發現 watchEffect 又符合期望。可能是本地環境出了點問題。
ref
vue2 中 ref 可以作用於普通dom元素
,也可以作用於vue 組件
。vue3 中也是這樣。
普通dom元素
請看示例:
<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";
// 創建一個容器
const pElem = ref();
function say(){
// <p>hello</p>
console.log(pElem.value)
}
</script>
點擊按鈕,將列印 p 元素。
vue 組件
下麵我們定義個組件 Dog.vue,然後在 App.vue 引入。請看示例:
在 Dog 中定義了兩個變數,通過 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 是一個用於在組合式 API 中將組件的屬性或方法暴露給父組件的函數。它可以讓父組件直接訪問子組件內部的屬性和方法。
App.vue 中直接引入Dog,無需註冊即可使用,然後用 ref 指向 Dog,點擊按鈕則能通過 ref 訪問 Dog 暴露出的變數:
// App.vue:
<template>
<section>
<Dog ref="box"/>
<p><button @click="handle1">click</button></p>
</section>
</template>
<script lang="ts" setup name="App">
// 自動幫你註冊組件
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 用法很像。
簡單引入 TypeScript
Tip:若不熟悉 ts,可以參考:前端 Typescript 入門
新建 ts 文件,定義兩個類型:
// 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,因為這是一個規範,不是值
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 是一個規範,所以引入時需要增加 type,告訴自己這是一個規範。筆者中間報錯:文件 "...../src/types/index.ts" 不在項目 "...../tsconfig.app.json" 的文件列表中。項目必須列出所有文件,或使用 "include" 模式
。其實不用到 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 也沒給錯誤提示。
let person:Persons = reactive([
{ name: 'peng', age: 18, id: 3},
{ name: 'peng2', age: 18},
{ name: 'peng3', age: 18}
]);
// 調用函數 reactive 時增加泛型,會提示: “id”不在類型“Person”中
let person2 = reactive<Persons>([
{ name: 'peng', age: 18, id: 3},
{ name: 'peng2', age: 18},
{ name: 'peng3', age: 18}
]);
</script>
props
父組件通過 props 給子組件傳遞數據,而通過事件給子組件傳遞方法。
首先回顧下 vue2 props
核心語法
// 孩子沒脾氣
defineProps(['persons'])
// 接收+限制類型
defineProps<{persons:Persons}>()
// 接收+限制類型+限制必要性 —— 可以不傳
defineProps<{persons?:Persons}>()
// 接收+限制類型+限制必要性+預設值
import {withDefaults} from 'vue'
withDefaults(defineProps<{persons?:Persons}>(), {
persons: () => []
})
defineProps 基本使用
父組件傳遞兩個屬性:
<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 中,子組件不應該直接修改 props。單向數據流,如果需要修改可以讓父組件修改
// props.age += 10;
}
</script>
有兩點:
- defineProps 接收一個也需要數組
// 即使一個也需要使用數組。
// 沒有與此調用匹配的重載。
// const props2 = defineProps('name')
- defineProps 返回值能接收 props
- 子類中不能修改 props 屬性,否則報錯
傳遞一個對象給子組件,核心代碼如下:
// 父組件
<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'])
接收+限制類型+限制必要性+預設值
vue3官網針對 ts 寫法介紹如下:
const props = defineProps<{
foo: string
bar?: number
}>()
請看示例:
// 子組件
<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'
// 只接收。不傳也不報錯
// defineProps(['persons'])
// 接收 persons + 類型。不傳,父組件報錯:Missing required prop: "persons"
// defineProps<{persons:Persons}>()
// 接收 persons + 類型 + 必要性
// 通過 ? 表明可以不傳,父組件不會報錯
// defineProps<{persons?:Persons}>()
withDefaults(defineProps<{persons?:Persons}>(), {
// 註:預設值需要通過函數返回,類似 vue2 中數據返回也是通過函數
// persons: [{ name: '空2', age: 0},]
persons: () => [{ name: '空', age: 0},]
})
</script>
註
:defineExpose、defineProps 不需要引入。define
XXx 是巨集函數,在vue3 中無需引入(筆者自測通過)。
生命周期
創建、掛載、更新、銷毀
vue2
4個階段,8個鉤子:
-
創建階段:
beforeCreate: 在實例初始化之後,數據觀測 (data observer) 和 event/watcher 事件配置之前被調用。
created: 實例已經創建完成之後被調用。在這一步,實例已經完成了數據觀測、屬性和方法的運算,但是尚未開始掛載到頁面上。 -
掛載階段:
beforeMount: 在掛載開始之前被調用:相關的 render 函數首次被調用。
mounted: el 被新創建的 vm.$el 替換,並掛載到實例上去之後調用該鉤子。 -
更新階段:
beforeUpdate: 數據更新時調用,發生在虛擬 DOM 重新渲染和打補丁之前。可以在該鉤子中進一步地更改狀態,不會觸發附加的重渲染過程。
updated: 由於數據更改導致的虛擬 DOM 重新渲染和打補丁後調用。 -
銷毀階段:
beforeDestroy: 在實例銷毀之前調用。實例仍然完全可用。
destroyed: 在實例銷毀之後調用。該鉤子被調用時,Vue 實例的所有指令都被解綁定,所有事件監聽器被移除,所有子實例也被銷毀。
vue3
和 vue2 類似,4個階段,8個鉤子,但稍微有差異:
- 沒有 beforeCreat、created,由 setup 替代(setup 比 beforeCreat 早)
- beforeMount 改成 onBeforeMount,mounted 改成 onMounted (加
on和駝峰
,其他幾個也這樣) - 銷毀改成
卸載
,和掛載對應 - 生命周期鉤子函數語法改為
function onBeforeMount(callback: () => void): void
Tip:這隻是最基本的生命周期鉤子,比如路由也會有,現學現查即可。
請看示例:
// 子組件
<template>
<section>
{{ count }}
<button @click="count += 1">change count</button>
</section>
</templat