vue3 快速入门系列 —— 基础

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:

  1. vue脚手架(vue-cli) 和创建 react的脚手架(create-react-app)都是基于 webpack。而 vite 也是一种构建工具,和 webpack 类似,也有一些区别,其作者就是 Vue.js 的创始人尤雨溪
  2. HMR 它用于开发环境,不适用于生产环境。更多介绍请看这里
  3. 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 不需要引入。defineXXx 是宏函数,在vue3 中无需引入(笔者自测通过)。

生命周期

创建、挂载、更新、销毁

vue2

4个阶段,8个钩子:

  1. 创建阶段:
    beforeCreate: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
    created: 实例已经创建完成之后被调用。在这一步,实例已经完成了数据观测、属性和方法的运算,但是尚未开始挂载到页面上。

  2. 挂载阶段:
    beforeMount: 在挂载开始之前被调用:相关的 render 函数首次被调用。
    mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。

  3. 更新阶段:
    beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
    updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。

  4. 销毁阶段:
    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>
</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('创建')


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>

点击“change count”更新子组件,点击“toggle show”会销毁或创建子组件。控制台输出:

创建
挂载前
挂载后
更新前
更新后
卸载前
卸载后

vue3 父组件子组件生命周期

在上述示例中,我们给父组件也增加对应的生命周期,然后看一下生命周期钩子函数的顺序。

// 父组件
<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('父创建')

onBeforeUpdate(() => {
  console.log('父更新前');
})
onUpdated(() => {
  console.log('父更新后');
})
onBeforeUnmount(() => {
  console.log('父卸载前');
})
onUnmounted(() => {
  console.log('父卸载后');
})

// 故意将其放在末尾
onBeforeMount(() => {
  console.log('父挂载前');
})
onMounted(() => {
  console.log('父挂载后');
})

</script>

页面初始时输出如下(和 vue2 中父元素、子元素的顺序一致,甚至和 react 中几乎相同):

父创建
父挂载前

子创建
子挂载前
子挂载后

父挂载后

点击“change count”,输出:子更新前 子更新后

点击“toggle show”,输出(和笔者猜测相同):

父更新前

子卸载前
子卸载后

父更新后

常用生命钩子

在实际开发中,最常用的 Vue2 生命周期钩子函数通常是:

  • created: 在实例创建完成后立即调用,通常用于进行初始化操作、数据请求等。
  • mounted: 在实例挂载到 DOM 后调用,通常用于执行 DOM 操作、访问第三方库等与 DOM 相关的操作。
  • updated: 当数据更新导致虚拟 DOM 重新渲染和打补丁后调用,通常用于在更新后执行一些需要基于 DOM 的操作。
  • beforeDestroy: 在实例销毁之前调用,通常用于进行一些清理工作、清除定时器解绑事件监听器等。

vue3 中常用的和vue2 类似:onMountedonUpdatedonBeforeUnmount

自定义hooks

在 Vue 3 中,自定义 Hooks 是指开发者自己定义的可复用的 Hooks 函数,用于组件逻辑的封装和复用。

前面我们说到 vue3 的组合式Api,比如下面这个 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代码复制,放入 function 中,需要导出(export)和暴露(return)
// 所有功能A的数据、方法、钩子、计算属性等等
import { ref } from "vue";

// 必须导出
// 用函数包裹
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,}
}

这样运行后效果和改动之前完全相同。

Tip:自定义 Hooks 函数本身不能直接包含模板代码,因为 Hooks 函数主要用于组件逻辑的封装和复用,并不包含视图逻辑。

vs vue2中的mixin

相似:组件逻辑的封装和复用

不同:

  • 作用域不同:混入的逻辑会影响到所有使用该 Mixin 的组件,自定义 Hooks 由开发者有选择地在组件中引入和使用
  • 组织逻辑方式不同:Mixin 通常将逻辑分散在多个生命周期钩子函数中,自定义 Hooks 将逻辑封装在一个函数中,更加集中

其他

Vue Devtools

vue-devtools是一款基于chrome游览器的插件,可以用于调试vue应用

有两种方法安装,一种是有条件,直接通过 chrome 应用商店搜索 vue 安装

没有条件的,可以访问 极简插件,搜索,下载,解压后,打开 chrome 开发者模式,拖拽到扩展插件

三个花括号

在使用 vscode 编写 vue 模板时,有时会遇到输入一个花括号 {,结果出现三个花括号 { }},还得手动删除一个,非常烦人。

解决办法:打开设置 -> 搜索Closing Brackets -> 找到 Auto Closing Brackets -> 将 aways 改成beforeWhitespace。请看下图

可以多个根标签

<Person/>
<Person/>
<Person/>

热门相关:隐婚99天:首长,请矜持   别吃那个鬼   绝色符师:龙皇的狂傲妃   我的女友不可能是怪物   万妖帝主