vue3 早期学习 基础篇

Vue 3 基础知识-常用 API

今日目标:

1️⃣能够使用 Vue CLIVite 创建 Vue3 项目

2️⃣了解 Vue3Vue RouterVuex 中新增了哪些 API

3️⃣了解 setup() 函数的作用以及相关的细节

4️⃣熟练掌握常用的响应式 API

5️⃣熟练掌握常用的响应式工具

6️⃣熟练计算属性 computed 的使用

7️⃣熟练监听器 watch 的使用

8️⃣熟练掌握 Vue3 中新增的 API

9️⃣了解组合式函数(hooks) 的封装

🔟能够说出 Vue3 的响应式原理

01.初识 Vue 3

Vue 是一款渐进式 JavaScript 框架

Vue 官方文档

1.1 Vue 版本介绍

Vue 主流版本是 Vue 2Vue 3

Vue 2 在 2016 年 10 月 1日发布,Vue 2 在 2022 年 6 月发布了最后一个小版本 (2.7)。目前 Vue 2 已经进入维护模式:它将不再提供新特性,但从 2.7 的发布日期开始的 18 个月内,它将继续针对重大错误修复和安全更新进行发布。这意味着 Vue 2 在 2023 年底将到达它的截止维护日期

Vue 3 在 2020 年 09 月 18 日正式发布,在 2022 年 2 月 7 日 成为新的默认版本,意味着 Vue 3 是 Vue 当前的最新主版本,也意味着 Vue3 将是 Vue 的未来主流

1.2 Vue3 新增、移除哪些内容

新增:

  1. 新增了组合式 API ,即 Composition API

  2. 新增 setup 函数,Composition API 新特性提供了统一的入口

    • setup() 函数中手动暴露大量的状态和方法非常繁琐
    • 当使用单文件组件(SFC)时,我们可以使用 <script setup> 来大幅度地简化代码。
  3. 新增了一些 API 即 Teleport (传送组件)、Suspense

    • Teleport :传送组件,将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
    • Suspense :处理异步组件,等待多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态
  4. 新增了响应式的 API:ref()reactive()

    • 数据驱动视图,数据改变,视图改变
  5. Composition API 新增了相关的钩子函数

  6. 新增其他 …

移除:

  1. 移除了过滤器:filter
  2. 移除 $on$off$once 方法,不能在注册全局事件总线
  3. 移除 $children 属性获取子组件实例
  4. 按键修饰符不再支持 keycode 作为 v-on 的修饰符,不再支持 config.keyCode
  5. 其他 …

Vue 3 迁移指南

1.3 Vue3 有哪些优势

1. 响应式性能提升:

  • 重写 虚拟 DOM 的实现,对 diff 算法 进行了优化

  • 采用 Proxy 替换 Object.defineProperty,重写了响应式原理,代码的执行效果更快

2. 性能提升

  1. 通过构建工具使用 Vue 时,Vue 的许多 API 都是可以 Tree-Shaking

  2. 打包大小减少 41%

    • 减少项目打包后的体积

    • Tree Shaking 摇树优化:指当引入一个模块的时,并不引入该模块的所有代码,只引入我们需要的代码

  3. 初次渲染快 55%, 更新渲染快133%,内存减少54%

    • 组件采用按需引入,使得打包后的体积也更小了
    • 所以项目运行的时候速度更快,更顺畅了

新的组合式 API:

	能够更好的组织逻辑,封装逻辑,复用逻辑

对 TS 的支持非常友好:

`Vue3` 基于`typeScript`进行编写,提供了更好的类型检查,能支持复杂的类型推导

1.4 Vue 3 技术栈

版本脚手架状态管理路由IDE 支持
Vue 2Vue CLIVuex 3Vue Router 3Vetur
Vue 3Vue CLI、Vite (推荐)Vuex 4、Pina (推荐)Vue Router 4Volar

02.使用 Vue CLI 创建 Vue3 项目

2.1 Vue CLI 创建 Vue3 项目

λ vue create vue-base


Vue CLI v5.0.8

# 选择安装预设,选择手动选择特性,支持更多自定义选项
? Please pick a preset: (Manually select features)

# 安装项目中需要使用的库以及功能:Babel, TS, Router, Vuex, CSS Pre-processors, Linter
? Check the features needed for your project: (Babel, TS, Router, Vuex, CSS Pre-processors, Linter)

# 选择需要安装的 Vue 版本
? Choose a version of Vue.js that you want to start the project (with 3.x)

# 是否使用Class风格装饰器 
? Use class-style component syntax? (No)

# 是否使用 babel 做转义
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Yes)

# 配置路由模式,是否使用 history 路由模式
? Use history mode for router? (Requires proper server setup for index fallback in production) (No)

# 选择 CSS 预处理器
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Sass/SCSS (with dart-sass)# 选择代码格式规范
? Pick a linter / formatter config: (Standard)

# 在什么时机下触发代码格式校验
? Pick additional lint features: (Lint on save, Lint and fix on commit)

# Babel、ESLint 等工具会有一些额外的配置文件,需要将这些工具相关的配置文件写到哪里
? Where do you prefer placing config for Babel, ESLint, etc.? (In dedicated config files)

# 是否需要将刚才选择一系列配置保存起来,并可以帮我们记住上面的一系列选择,以便下次直接复用
? Save this as a preset for future projects? (No)

2.2 分析文件和文件夹的作用

  1. .browserslistrc: 浏览器兼容文件

    > 1%			 // 兼容全球超过 1% 的人还在使用的浏览器
    last 2 versions  // 兼容所有浏览器到最后两个版本
    not dead		 // 不在兼容还没有 “死亡” 的浏览器 (24个月没有官方不在维护和更新的浏览器)
    not ie 11		 // 不在兼容 ie 11 以及以下浏览器
    
  2. .editorconfig: 编辑器相关的配置信息,用于在项目中维持一致的编码风格和设置

    [*.{js,jsx,ts,tsx,vue}]  		// 对后缀为js,jsx,ts,tsx,vue 文件有效
    indent_style = space	 		// 使用空格进行缩进
    indent_size = 2			 		// 缩进两个空格
    trim_trailing_whitespace = true	// 表示会去除换行行首的任意空白字符
    insert_final_newline = true		// 表示使文件以一个空白行结尾
    
    
  3. eslintrc.js: Eslint 语法规则配置文件

    module.exports = {
      // 默认情况下,Eslint 将在根目录以下的所有父文件夹中查找配置文件
      // 告诉 Eslint 不需要去父文件夹查找配置文件
      root: true,
      // 指定 Eslint 启动环境(Vue Cli底层是 node 支持)
      env: {
        node: true
      },
      // 当成 Vue 项目里遵循的规则
      // 使 Eslint 继承 @vue/cli 脚手架里的 standard 代码规范
      extends: [
        'plugin:vue/vue3-essential',
        '@vue/standard',
        '@vue/typescript/recommended'
      ],
      // 此项是用来指定 javaScript 语言类型和风格
      parserOptions: {
        ecmaVersion: 2020 // 允许解析较新的ES特性
      },
      // 自定义 Eslint 验证规则
      // "off" -> 0 关闭规则
      // "warn" -> 1 开启警告规则
      // "error" -> 2 开启错误规则
      rules: {
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
      }
    }
    
    
  4. babel.config.js :babel 的配置文件

    module.exports = {
      // presets 字段设定转码规则
      presets: [
        '@vue/cli-plugin-babel/preset'
      ]
    }
    
    
  5. lint-staged.config.js:执行 git add .、git commit -m 命令时,运行已配置的 linter 任务

  6. tsconfig.json:TypeScript 编译器的配置文件,TypeScript 编译器可以根据配置的规则来对代码进行编译

  7. vue.config.js:可选的配置文件,用来对 Vue 项目进行个性化的配置和覆盖默认的配置

  8. shims-vue.d.ts:Vue 类型声明文件,为了让 TypeScript 识别 .vue 格式的文件,因为.vue 文件不是一个常规的文件类型,TypeScript 不知道 vue 文件是干嘛的,加这一段是是告诉 tsTypeScript .vue 文件是什么

03.分析 main.js 中的代码

知识点:

Vue 3 中每个Vue 3 应用都是通过 createApp 函数创建一个新的应用实例

createApp 接收一个组件(根组件)作为参数,每个应用都需要一个根组件,其他组件将作为根组件的子组件

落地代码:

➡️ main.js

// 在 Vue3 中每个 Vue 3 应用都是通过 createApp 函数创建一个新的应用实例
import { createApp } from 'vue'
// 导入根组件
import App from './App.vue'
// 导入路由文件
import router from './router'
import store from './store'

// createApp(App).use(store).use(router).mount('#app')

// createApp 传入的对象实际上是一个组件,每个应用都需要一个跟组件,其他组件将作为根组件的子组件
const app = createApp(App)

app.use(router)
app.use(store)

// 应用实例必须在调用了 .mount() 方法后才会渲染出来。
// 该方法接收一个 "容器" 参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串
// 应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分
app.mount('#app')

04.分析 Vue Router

知识点:

Vue RouterVue.js 的官方路由,它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举

  1. createRouter:创建一个可以被 Vue 应用程序使用的路由实例
  2. createWebHashHistory:将路由设置为 Hash 路由模式
  3. createWebHistory:将路由设置为 history 路由模式
  4. RouteRecordRaw:用户通过 routes option 或者 router.addRoutes() 添加路由时,可以得到路由记录
    • 那么路由记录里面得有那些参数呢,又该怎么配置呢 ❓🤔
    • 可以点击查看 RouteRecordRaw 类型 😀❗

📌 Tip:

在 Vue3 项目中,我们会经常使用 router 实例

请记住,this.$router 与直接使用通过 createRouter 创建的 router 实例完全相同

我们使用 this.$router 的原因是,我们不想在每个需要操作路由的组件中都导入路由

落地代码:

➡️ router/index.ts

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'

// 给 routes 添加泛型,要求 routes 中定义的路由规则属性,每一项需要符合 RouteRecordRaw 类型
const routes: Array<RouteRecordRaw> = [
  // ......
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

05.分析 Vuex 代码

知识点:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

每一个 Vuex 应用的核心就是 store (仓库)store 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)

createStore:创建一个 store 实例

落地代码:

// 创建一个 store 实例
import { createStore } from 'vuex'

export default createStore({
  state: { },
  getters: { },
  mutations: { },
  actions: { },
  modules: { }
})

06.使用 Vite 创建 Vue 3 项目

6.1 什么是 Vite

Vite 是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  1. 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)
  2. 一套构建指令,使用 Rollup 对代码打包,并且可预配置,可输出用于生产环境的高度优化过的静态资源

6.2 使用 Vite 创建项目

Vite 基本使用

Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本

# 使用 Npm
npm create vite@latest

# 使用 Yarn
yarn create vite

在输入命令后,可以根据 Vite 提供的交互式安装助手进行操作即可

使用 Vite 搭建 Vue 项目

除了上面这种方式创建 Vue 项目外,还可以通过 Vite 附加的命令行选项直接指定项目名称和你想要使用的模板

📌 Tip:

通过 Vite 附加的命令行选项进行创建项目,需要确认自己的 npm 版本

npm --versionnpm -v , 根据版本确认安装方式

# npm 6.x
npm create vite@latest 项目名称 --template 模板名称
# 例如
npm create vite@latest vue-base --template vue-ts


# npm 7+
npm create vite@latest 项目名称  -- --template vue-ts
# 例如
npm create vite@latest vue-base -- --template vue-ts

6.3 Vite 为什么启动速度比 webpack 快

Webpack 在进行应用开发中,会 先获取项目中所有的文件、资源进行构建打包,然后才能够将应用进行启动。这就导致当我们的项目越来越大时,构建的时间就会越长 ,构建的时间越长,也就导致项目启动速度也就会越慢。也会产生另一个问题,如果项目中某一个组件存在错误,整个项目都会在启动、构建的时候报错

vite 不会在启动项目的时候去获取项目中所有的文件、资源进行构建,而是按需加载Vite 在进行项目构建的时候,会将应用中的模块区分为 依赖项目代码 两部分,对于项目必须的内容Vite 会在一开始就进行构建,对于 项目代码 部分,根据 路由来拆分 代码模块,也就是访问路由的时候才会加载路由对应的资源

07.组合式 API-setup() 函数介绍

知识点

  1. Vue3 中可以完全写 Vue2 中的配置项,但是我们推荐使用 Vue3 写法
  2. setup 函数是 组合式 API 的入口
  3. Vue3 + Ts 项目中,可以将 setup 函数用 defineComponent 函数进行包裹,用来获取类型推断
  4. setup() 函数定义的变量、方法等,需要使用 return 暴露给模板和组件实例使用

落地代码:

➡️ views/HomeView.vue

<script lang="ts">
// lang: 当前 script 使用哪种语言,lang="ts" 当前模块按照 TS 语言进行识别和解析
// 在 Vue3 中可以完全写 Vue2 中的配置项(选项式 API)
export default {
  name: 'HomeView'
  // data() {
  //   return {
  //     name: 'Tom'
  //   }
  // },
  // methods: {}
}
</script>

➡️ views/HomeView.vue

<script lang="ts">
import { defineComponent } from 'vue'

// defineComponent 它的存在主要是为 TypeScript 来进行服务的
// 在 TypeScript下,给予了组件正确的参数类型推断
export default defineComponent({
  setup() {
    // setup 函数从生命周期上来看,是在 beforCreate 之前执行的
    // 因此在 setup 中进行不能够使用 this
    console.log(this) // undefined

    const name = 'Tom'

    return {
      name
    }
  }
})
</script>

08.组合式 API-setup 函数语法糖写法

知识点:

如果都将代码逻辑放到 setup 函数中,代码会非常不简洁,需要定义后,在 return 出去。为了提高开发体验、运行性能以及更好的和 Ts 进行结合,<script setup> 应运而生!

<script setup> 是在单文件组件 (SFC) 中使用 组合式 API 的编译时语法糖。当同时使用 SFC组合式 API 时该语法是默认推荐

📌 Tip

什么是语法糖:

语法糖(Syntactic Sugar)也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性

另外,一般我们在开发中需要给组件定义别名 name ,但是 setup 语法中不能写 name,解决办法是:

  1. 同一个 SFC 文件写两个 script,但其中一个不能使用 export default
  2. 因此一个script使用 setup 语法糖,一个使用 export default 进行声明别名
    • PS:Vue3 会根据文件名推断组件的 name ,但是很多时候还是自定义 name 更加方便一点
    • 特别使用 keep-aliveincludeexclude功能时,必须显示声明 name 才能正常执行逻辑

还有一种方法使用 Vite 插件: vite-plugin-vue-setup-extend (后面讲)

落地代码

➡️ views/HomeView.vue

<script lang="ts" setup>
const name = 'Tom'
</script>

➡️ views/HomeView.vue :给组件声明 name 名称

<script lang="ts" setup>
const name = 'Tom'
</script>

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

09.组合式 API-选项式 与 组合式 API 区别

Vue 的组件可以按两种不同的风格书写:选项式 API (Options API)组合式 API (Composition API)

选项式 API

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethodsmounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例

➡️ App.vue

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

<script>
export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 `this` 上
  data() {
    return {
      count: 0
    }
  },

  // methods 是一些用来更改状态与触发更新的函数
  // 它们可以在模板中作为事件监听器绑定
  methods: {
    increment() {
      this.count++
    }
  },

  // 生命周期钩子会在组件生命周期的各个不同阶段被调用
  // 例如这个函数就会在组件挂载完成后被调用
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

组合式 API

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 setup 函数搭配使用。在 <script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。从生命周期的角度来看, setup 函数在组件挂载之前进行执行,类似于 vue2.xbeforeCreate执行,在这个阶段,Vue 还没有进行数据劫持和代理,因此不能在 setup 中使用 this

➡️ App.vue

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

选项式 API 和 组合式 API 的区别

API 选项优点缺点Vue 的中庸之道
选项式 API对新手友好,方便快速上手代码组织性差,
相似的逻辑代码不便于复用
组合式 API方便逻辑功能开发
利于代码的组织和维护
需要有良好的代码组织能力
和拆分逻辑能力
Vue3 也支持 Vue2 的 选项式写法

10.响应式 API-reactive() 创建响应式对象

知识点:

reactive() 函数用于定义一个对象类型(数组或对象)的响应式数据

reactive() 函数传入一个对象类型的数据,返回一个对象的响应式代理(Proxy 实例对象,简称 Proxy 对象)

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

const state = reactive({
  name: 'Tom',
  age: 10
})
</script>

reactive() 函数定义的响应式数据,可以在 js 以及 template 中直接使用

reactive() 函数定义的响应式数据是 “深层次的”,可以对对象和数组直接进行更新和新增

reactive() 函数内部基于 Proxy 的实现,通过数据代理的方式对源对象内部数据进行操作

落地代码:

➡️ App.vue

<template>
  <div>{{ state.name }}</div>
  <div>{{ state.age }}</div>
  <div>{{ state.hobby[3] }}</div>
  <div>{{ state.obj.address }}</div>
</template>

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

interface IPerson {
  name: string
  age: number
  hobby: string[]
  obj: {
    address?: string
  }
}

// 使用 reactive 创建对象类型的响应式数据
// reactive 方法接收的是一个对象类型的数据
// 返回的是一个 Proxy 实例对象(以后简称 Proxy 对象)
const state = reactive<IPerson>({
  name: 'Tom',
  age: 10,
  hobby: ['吃饭', '睡觉', '峡谷遨游'],
  obj: {}
})

// reactive() 函数创建的响应式对象,可以直接使用
console.log(state.name)
console.log(state.age)

// reactive() 创建的对象是深层次的,可以直接对对象和数组中的每一项进行新增,而不用担心没有响应式
state.hobby[3] = '吃鸡'
state.obj.address = '西安'
</script>

11.响应式 API-ref() 创建响应式数据

知识点:

Vue2 中, ref 是 标签或者组件的属性,主要作用是:获取 DOM 或者 获取组件实例

Vue3 中,ref() 是一个函数,将传入参数的值包装为一个带 .value 属性的 ref 对象

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

const state = ref(xxx)
</script>

ref 对象 是可更改的,也就是说你可以为 .value 赋予新的值

ref 对象 也是响应式的,所有对 .value 的操作都将被追踪

ref 对象js 中想获取或者更新数据,使用需要使用 ref对象.value 的方式进行获取或者更新

ref 对象 在模板中使用的时候,不需要带上 .value

落地代码:

➡️ App.vue

<template>
  <div>{{ name }}</div>
  <div>{{ age }}</div>

  <button @click="updateAge">更改 age</button>
</template>

<script lang="ts" setup>
// 使用 const 声明的变量不具有响应式,驱动不了视图更新
// const name = 'Tom'
// let age = 10

import { ref } from 'vue'

// 如果想将基本数据转换为响应式的数据,需要使用 ref 函数将数据进行包裹
// ref 函数就会返回一个 ref 对象,为 ref 对象的中 value 进行赋值,.value 属性具有响应式
// ref 对象,在 js 中想获取或者跟新数据,使用需要使用 [ref对象.value] 的方式进行操作
// ref 对象,在模板中使用的时候,不需要带上 .value
const name = ref('Tom')
const age = ref(10)

function updateAge() {
  age.value = 10
}
</script>

12.响应式 API-ref() 创建响应式对象

知识点:

ref()方法允许我们创建基础数据类型的响应式数据,还允许我们创建可以使用任何值类型的响应式 ref

当接收的值是基础数据类型时,ref.value 属性也是响应式的

当接收的值为对象类型的时候,那么这个对象将通过 reactive 转为具有深层次响应式的对象

落地代码:

➡️ App.vue

<template>
  <div>{{ state.name }}</div>
  <div>{{ state.age }}</div>
  <div>{{ state.hobby[3] }}</div>
  <div>{{ state.obj.address }}</div>
</template>

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

interface IPerson {
  name: string
  age: number
  hobby: string[]
  obj: {
    address?: string
  }
}

const state = ref<IPerson>({
  name: 'Tom',
  age: 10,
  hobby: ['吃饭', '睡觉', '峡谷遨游'],
  obj: {}
})

// 当接收的值为对象类型的时候,那么这个对象将通过 reactive 转为具有深层次响应式的对象
// 然后将这个深层次响应式的对象赋值给 ref 的 value 属性
console.log(state)

console.log(state.value.name)
console.log(state.value.age)

state.value.hobby[3] = '吃鸡'
state.value.obj.address = '西安'
</script>

13.响应式 API-ref() 方法 和 ref 属性

知识点:

Vue 中提倡数据驱动视图更新,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref 属性

ref 是一个特殊的属性,它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用

落地代码:

➡️ App.vue

<template>
  <div>
    <input ref="inputRef" type="text" />
  </div>
</template>

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

// 声明一个 ref 来存放该元素的引用
// 创建出来的 inputRef 需要和模板中 ref 的属性值保持一致
// 📌 tip: 模板引用需要通过一个显式指定的泛型参数和一个初始值 null 来创建
const inputRef = ref<HTMLInputElement | null>(null)

// 组件挂载好后,添加光标效果
onMounted(() => {
  // 为了严格的类型安全,有必要在访问 el.value 时使用可选链或类型守卫。这是因为直到组件被挂载前,
  // 这个 ref 的值都是初始的 null,并且在由于 v-if 的行为将引用的元素卸载时也可以被设置为 null。
  inputRef.value?.focus()
})
</script>

14.响应式 API-ref() 和 reactive() 选择

知识点:

  1. reactive 可以转换对象成为响应式数据对象,但是不支持简单数据类型
  2. ref 可以转换简单数据类型为响应式数据对象,也支持复杂数据类型,但是操作的时候需要 .value
  3. 它们各有特点,现在也没有最佳实践,没有明显的界限,所有大家可以自由选择。

推荐用法:

自从引入组合式 API 的概念以来,一个主要的未解决的问题就是 ref 和响应式对象到底用哪个

响应式对象存在解构丢失响应性的问题

而 ref 需要到处使用 .value 则感觉很繁琐,并且在没有类型系统的帮助时很容易漏掉 .value

如果能确定数据是对象且字段名称也确定,可使用 reactive 转成响应式数据

其他一概使用 ref

15.响应式工具-toRef 和 toRefs

知识点:

Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失

let state = reactive({ count: 0 })

// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })

同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性

const state = reactive({ count: 0 })

// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++

// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)

如果我们需要将响应式对象的属性赋值或解构至本地变量时,依然让其具有响应式的特性,需要借助两个方法:

  1. toRef():基于响应式对象上的一个属性,创建一个对应的 ref

    • 这样创建的 ref 与其源属性保持同步
    • 改变源属性的值将更新 ref 的值,反之亦然
  2. toRefs():将一个响应式对象转换为一个普通对象

    • 这个普通对象的每个属性都是指向源对象相应属性的 ref
    • 每个单独的 ref 都是使用 toRef() 创建的

落地代码:

➡️ router/index.ts:演示失去响应式

<template>
  <div>
    <h2>{{ state.name }}</h2>
    <h2>{{ newName }}</h2>

    <button @click="changeName">更改 name</button>

    <hr />

    <h2>{{ age }}</h2>
    <button @click="changeAge">更改 age</button>
  </div>
</template>

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

const state = reactive({
  name: 'Tom',
  age: 10
})

// newName 是一个局部变量,同 state.newName
// 失去响应性连接
let newName = state.name
// 对 newName 进行改变后,不会影响到 state 中的 name,视图也不会发生改变
// newName = 'Jerry'

const changeName = () => {
  newName = 'Spyke'
  // 数据改变,但是不具有响应式的效果
  console.log(newName)
}

// 当我们将响应式对象的解构至本地变量时,属性会失去响应性
let { age } = state
age++

const changeAge = () => {
  age += 1

  console.log(age)
}
</script>

➡️ router/index.ts:演示 toRef 和 toRefs 的使用

<template>
  <div>
    <h2>{{ state.name }}</h2>
    <h2>{{ newName }}</h2>

    <button @click="changeName">更改 name</button>

    <hr />

    <h2>{{ age }}</h2>
    <button @click="changeAge">更改 age</button>
  </div>
</template>

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

const state = reactive({
  name: 'Tom',
  age: 10
})

// 通过 toRef 将响应式对象上的一个属性转换为 ref 响应式数据
const newName = toRef(state, 'name')

const changeName = () => {
  // 目前 newName 已经是一个 ref 响应式数据,因此需要加 .value 才能进行更改
  // 同时将数据改变后,视图也会随之发生改变
  newName.value = 'Spyke'
}

// 通过 toRefs() 方法将一个响应式对象转换为一个普通对象,
// 这个普通对象的每个属性都是指向源对象相应属性的 ref
// 每个单独的 ref 都是使用 toRef() 创建的。
// const newState = toRefs(state)

const { age } = toRefs(state)

const changeAge = () => {
  age.value++
}
</script>

16.响应式 API-computed() 计算属性

知识点:

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑

computed() 方法接受一个 getter 函数,返回一个只读的响应式 ref 对象,该 ref 对象通过 .value 暴露 getter 函数的返回值

computed() 方法也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象

落地代码:

➡️ App.vue

<template>
  <div>
    <p>姓氏:<input type="text" v-model="state.firstName" /></p>
    <p>名字:<input type="text" v-model="state.lastName" /></p>
    <p>全名:{{ fullName }}</p>

    <hr />

    <input type="text" v-model="fullName" />
  </div>
</template>

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

const state = reactive({
  firstName: 'zhang',
  lastName: 'san'
})

// computed 接收一个 getters 函数,返回一个只读的响应式 ref 对象
// const fullName = computed(() => {
//   return state.firstName + ' ' + state.lastName
// })

// 返回的 ref 对象通过 .value 暴露 getters 函数的返回值
// console.log(fullName.value)


// computed() 方法也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref对象
const fullName = computed({
  get() {
    return state.firstName + '-' + state.lastName
  },
  set(newVal) {
    const newArr = newVal.split('-')
    state.firstName = newArr[0]
    state.lastName = newArr[1]
  }
})

console.log(fullName.value)
</script>

17.响应式 API-watch() 监听器

知识点:

watch 用于声明在数据更改时调用的侦听回调,即监听数据的变化,在数据变化后执行相应的逻辑。

watch 的第一个参数可以是不同形式的 “数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

📌 Tip:

直接给 watch() 传入一个 reactive() 定义的响应式对象,会隐式地创建一个深层侦听器,该回调函数在所有嵌套的变更时都会被触发

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能

落地代码:

➡️ App.vue

<template>
  <h2>{{ count }} - {{ age }} - {{ obj.name }} - {{ obj.otherInfo.hobby }}</h2>

  <button @click="changeCount">更新 count</button>
  <button @click="changeAge">更新 age</button>
  <button @click="changeObj">更新 obj</button>
</template>

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

const count = ref(0)
const age = ref(0)
const obj = reactive({
  name: 'Tom',
  gender: '男',
  otherInfo: {
    hobby: '王者',
    address: '北京'
  }
})

const myObj = ref({
  otherInfo: {
    hobby: '王者',
    address: '北京'
  }
})

const changeCount = () => {
  count.value += 1
}

const changeAge = () => {
  age.value += 1
}

const changeObj = () => {
  // obj.name = 'Jerry' + Math.random()
  // obj.gender = obj.gender === '男' ? '女' : '男'
  // obj.otherInfo.hobby = '吃鸡' + Math.random()
  myObj.value.otherInfo.address = '上海' + Math.random()
}

// 1. 监听 ref 创建的响应式数据
// watch(count, function (newVal, oldVal) {
//   console.log(newVal, oldVal)
// })

// 2. 监听 ref 创建的多个响应式数据的变化
// watch([count, age], function (newVal, oldVal) {
//   console.log(newVal, oldVal)
// })

// 3. 监听使用 reactive 创建的响应式数据中单独一个属性的变化
// watch(
//   () => obj.name,
//   (newVal, oldVal) => {
//     console.log(newVal, oldVal)
//   }
// )

// 4. 监听使用 reactive 创建的响应式数据中一组属性的变化
// watch([() => obj.name, () => obj.gender], (newVal, oldVal) => {
//   console.log(newVal, oldVal)
// })

// 5. 监听 reactive 创建的响应式数据的变化,默认开启了 deep
// watch(obj, (newVal, oldVal) => {
//   console.log(newVal, oldVal)
// })

// 6. 监听 ref 创建的响应式数据的变化,需要添加 deep
watch(
  myObj,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
  },
  {
    deep: true
  }
)
</script>

<style lang="scss" scoped></style>

18.生命周期钩子函数

知识点:

每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。

选项式 APIHook inside setup执行时机
beforeCreateNot needed*beforeCreate在数据代理完成之前调用
setup()执行时机在beforeCreatedcreated两个周期函数之前
createdNot needed*created数据代理、数据劫持完成以后
setup()执行时机在beforeCreatedcreated两个周期函数之前
beforeMountonBeforeMount在组件被挂载之前被调用
mountedonMounted在组件挂载完成后执行
beforeUpdateonBeforeUpdate在组件即将因为响应式状态变更而更新其 DOM 树之前调用
updatedonUpdated在组件因为响应式状态变更而更新其 DOM 树之后调用
beforeUnmountonBeforeUnmount在组件实例被卸载之前调用
unmountedonUnmounted在组件实例被卸载之后调用

19.组合式函数与自定义 hooks

知识点:

组合式函数 (Composables): 在 Vue 中是指一个个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数

组合式函数 的作用:复用公共任务的逻辑

组合式函数命名约定:用驼峰命名法命名,并以 “use” 作为开头

组合式 API 名字的由来:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为 组合式 API

📌 Tip:

无状态逻辑函数

  函数在接收一些输入后立刻返回所期望的输出,常见的复用无状态逻辑的库有:`lodash`、`date-fns`
  
  说白了:函数负责接收数据,返回数据

有状态逻辑函数

  负责管理会随时间而变化的状态
  
  说白了:函数内部定义数据,内部处理出数据处理的逻辑,返回数据

19.1 鼠标跟踪器示例

不使用组合式函数实现鼠标跟踪器示例

➡️ App.vue

<template>
  <div>
    Mouse position is at: {{ x }}, {{ y }}  
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

19.2 使用组合式函数实现鼠标跟踪器

➡️ src/hook/useMouse.js 使用组合式 API 抽取成函数

import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

➡️ App.vue 在组件中使用组合式函数

<script setup>
import { useMouse } from './hook/useMouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

19.3 优化组合式函数实现的鼠标跟踪器

➡️ src/hook/event.js 将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中

// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  // 如果你想的话,
  // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

➡️ src/hook/useMouse.js 使用组合式 API 抽取成函数

import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })

  return { x, y }
}

20.组合式函数与异步状态

知识点:

useMouse() 组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。

在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败

接口1:https://dog.ceo/api/breeds/image/random

接口2:https://api.thecatapi.com/v1/images/search

20.1 使用组合式函数封装异步状态

➡️ src/hook/fetch.js 使用组合式 API 抽取成函数

import axios from 'axios'
import { ref } from 'vue'

export default function useFetch(url: string, method: string) {
  const res = ref()
  const loading = ref(true)
  const errorMsg = ref()

  axios({
    method,
    url
  })
    .then((data) => {
      console.log(data)
      loading.value = false
      res.value = data.data
    })
    .catch((err) => {
      loading.value = false
      errorMsg.value = err.message || '未知错误'
    })

  return { res, loading, errorMsg }
}

➡️ src/App.vue

<template>
  <div>
    <p v-if="loading">数据正在加载中......</p>
    <p v-else-if="res.message">
      <img :src="res.message" alt="" />
    </p>
    <p>
      {{ errorMsg }}
    </p>
  </div>
</template>

<script lang="ts" setup>
import useFetch from '@/hooks/fetch'

const { res, loading, errorMsg } = useFetch('https://dog.ceo/api/breeds/image/random', 'get')
</script>

<style lang="scss" scoped>
img {
  width: 100px;
  height: 100px;
}
</style>

20.2 给异步状态函数添加泛型

➡️ src/hook/fetch.js 使用组合式 API 抽取成函数

import axios from 'axios'
import { ref } from 'vue'

export default function useFetch<T>(url: string, method: string) {
  const res = ref<T>()
  const loading = ref(true)
  const errorMsg = ref()

  axios({
    method,
    url
  })
    .then((data) => {
      console.log(data)
      loading.value = false
      res.value = data.data
    })
    .catch((err) => {
      loading.value = false
      errorMsg.value = err.message || '未知错误'
    })

  return { res, loading, errorMsg }
}

➡️ src/App.vue

<template>
  <div>
    <p v-if="loading">数据正在加载中......</p>
    <p v-else-if="res?.message">
      <img :src="res.message" alt="" />
    </p>
    <p>
      {{ errorMsg }}
    </p>
  </div>
</template>

<script lang="ts" setup>
import useFetch from '@/hooks/fetch'

interface IDog {
  message: string
  status: string
}

const { res, loading, errorMsg } = useFetch<IDog>('https://dog.ceo/api/breeds/image/random', 'get')
</script>

<style lang="scss" scoped>
img {
  width: 100px;
  height: 100px;
}
</style>

xxx. Vue3 响应式原理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值