Skip to content

Vue3 快速上手

第一章:Vue3 简介

一、介绍

二、Vue3 带来了什么

性能的提升

  • 打包大小减少 41%

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

  • 内存减少 54%

    ......

源码的升级

  • 使用 Proxy 代替 defineProperty 实现响应式

  • 重写虚拟 DOM 的实现和 Tree-Shaking

    ......

拥抱 TypeScript

  • Vue3 可以更好的支持 TypeScript

新的特性

  1. Composition API(组合 API)

    • setup 配置

    • ref 与 reactive

    • watch 与 watchEffect

    • provide 与 inject

      .....

  2. 新的内置组件

    • Fragment
    • Teleport
    • Suspense
  3. 其他改变

    • 配套的工程化工具也进行了更新:Vue-cli --> create-vue、Vuex --> pinia

    • 新的生命周期钩子

    • data 选项应始终被声明为一个函数

    • 移除 keyCode 支持作为 v-on 的修饰符

      ......

三、创建 Vue3.0 工程

1. vue-cli 创建

官方文档:Vue CLI

bash
# 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
# 安装或者升级你的@vue/cli
npm install -g @vue/cli
# 创建
vue create vue_test
# 启动
cd vue_test
npm run serve

2. create-vue 创建

create-vue 创建的项目使用的构建工具是 vite

1)什么是 vite?新一代前端构建工具。

2)优势如下

  • 开发环境中,无需打包操作,可快速的冷启动。
  • 轻量快速的热重载(HMR)。
  • 真正的按需编译,不再等待整个应用编译完成。

3)对比

特性Vue CLIcreate-vue
构建工具WebpackVite
启动速度较慢(10-30秒)极快(1-2秒)
热更新较慢极快
Vue 版本Vue 2/3Vue 3
官方推荐已不推荐,进入维护模式✅ 推荐

4)创建项目

bash
# 创建工程
npm create vue@latest

# 进入工程目录
cd <project-name>
# 安装依赖
npm install
# 运行
npm run dev

第二章:Composition API

官方文档:https://cn.vuejs.org/api

                   https://cn.vuejs.org/guide/extras/composition-api-faq.html

一、是什么

使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data、methods、computed 里查找并修改。

而 Composition API 让我们可以更加优雅的组织我们的代码、函数。让相关功能的代码更加有序的组织在一起。

二、响应式 API 核心

1. 拉开序幕的 setup

1)基本使用

Vue3.0 中一个新的配置项,值为一个函数。setup 是所有 Composition API(组合 API)“ 表演的舞台 ”。组件中所用到的:数据、方法等等,均要配置在 setup 中。

setup 函数的两种返回值

  • 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。(重点关注)
  • 若返回一个渲染函数:则可以自定义渲染内容。(了解)

使用细节

  • 尽量不要与 Vue2.x 配置混用

    • Vue2.x 配置(data、methos、computed ...)中可以访问到 setup 中的属性、方法。

    • 但在 setup 中不能访问到 Vue2.x 配置(data、methos、computed...)。

      因为 setup 中,this 是 undefined。

    • 如果有重名,setup 优先。

  • setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 Promise,模板看不到 return 对象中的属性。

    后期也可以返回一个 Promise 实例,但需要 Suspense 和异步组件的配合。

setup 的两个注意点

  • setup 执行的时机

    • 在 beforeCreate 之前执行一次,且 this 是 undefined。
  • setup 的参数

    • props:值为对象,包含组件外部传递过来,且组件内部声明接收了的属性。
    • context:上下文对象
      • attrs:值为对象,包含组件外部传递过来,但没有在 props 配置中声明的属性,相当于 this.$attrs
      • slots:收到的插槽内容,相当于 this.$slots
      • emit:分发自定义事件的函数,相当于 this.$emit
    js
    export default {
      props: { // 在父组件中给子组件传递了一个 msg props,子组件需要配置 props 来接收。如果不配置,不能通过 setup 中的第一个参数获取到,只能通过 setup 中的第二个参数获取到。且 Vue 会在浏览器控制台发出警告,让你配置这个
        msg: String
      },
      emits: ["myEvent"] // 在父组件中给子组件传递了一个 myEvent 自定义事件,虽然不配置 emits 选项,子组件也可以使用 emit 方法触发此事件,但 Vue 会在浏览器控制台发出警告,让你配置这个
      setup(props, context) {
        // 在setup函数中,你可以访问props和context
        console.log(props.msg)
        console.log(context.attrs)
        console.log(context.slots)
        context.emit('myEvent')
      }
    }
2)语法糖

<script setup> 语法糖

在 SFC 中的 script 标签添加 setup 标记之后:

① 不再需要写 export default {}。因为里面的代码会被编译成组件 setup() 函数的内容。

② 不再需要 return。因为顶层的绑定会被暴露给模板。

③ 组件无需注册,可以直接使用。

④ 与普通的 <script> 只在组件被首次引入的时候执行一次不同。<script setup> 中的代码会在每次组件实例被创建的时候执行。

vue
<script setup>
  // message、logMessage 可以在 Vue 模板里直接使用
  const message = 'this is message'
  const logMessage = ()=>{
    console.log(message)
  }
</script>

使用 setup 语法糖之后,必备的其他函数

下面的函数都是宏函数,可以直接使用,使用前无须导入。

  • defineProps() 和 defineEmits()

    vue
    <script setup>
    // const props = defineProps(['name', 'age'])
    const props = defineProps({
      name: {
        type: String,
        default: ''
      },
      age: {
        type: Number,
        default: 0
      }
    })
    
    const emit = defineEmits(['changeAge'])
    
    function changeAge() {
      emit('changeAge', 200)
    }
    </script>
    
    <template>
      <h2>ShowInfo: {{ name }} - {{ age }}</h2>
      <button @click="changeAge">修改age</button>
    </template>
  • defineExpose()

    使用 <script setup> 的组件,顶层属性默认是私有的。通过模板 ref 或者 $parent 能够获取到组件的公开实例,但不会暴露任何在 <script setup> 中声明的绑定。通过 defineExpose 编写暴露方法与属性在 <script setup> 组件中需要暴露出去的 property。

    vue
    <script setup>
    import { ref } from 'vue'
    
    const a = 1
    const b = ref(2)
    
    defineExpose({
      a,
      b
    })
    </script>

2. 定义响应式数据

1)ref 函数

(1) 作用:定义一个响应式的数据。

(2) 语法:const xxx = ref(initValue)

  • 创建一个包含响应式数据的引用对象(reference 对象,简称 ref 对象)

  • JS 中操作数据:xxx.value

  • 模板中读取数据:不需要 .value,直接:<div>{{xxx}}</div>

    因为 vue3 会在解析模板的时候,发现是 RefImpl 的对象,会自动从 value 中读数据。

(3) 备注

  • 接收的数据可以是:基本类型、也可以是对象类型。
  • 基本类型的数据:响应式依然是靠 Object.defineProperty() 的 get 与 set 完成的。
  • 对象类型的数据:内部“ 求助 ”了 Vue3.0 中的一个新函数 —— reactive 函数。

(4) 使用 ref 获取元素对象

可以将这个引用绑定到模板中的元素或组件的 ref 属性上,然后在 setup 函数中访问这个元素或组件。

例如:创建一个引用并绑定到一个输入元素上:

javascript
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const inputRef = ref(null)

    onMounted(() => {
      console.log(inputRef.value) // 打印出绑定的元素
	})

	return {
	  inputRef
	}
  }
}

然后在模板中这样使用这个引用:

html
<input ref="inputRef" />

inputRef 是一个响应式引用,它被绑定到一个输入元素上。当组件被挂载后,onMounted 钩子函数会被调用,可以在这个函数中访问 inputRef.value 来获取这个输入元素。

注意:需要在 setup 函数的返回值中包含 inputRef,这样它才能在模板中被访问。

2)reactive 函数

1)作用:定义一个对象类型的响应式数据(基本类型不要用它,要用 ref 函数)。

2)语法:const 代理对象 = reactive(源对象) 接收一个对象(或数组),返回一个代理对象(Proxy 的实例对象,简称 proxy 对象)

  • reactive 定义的响应式数据是“深层次的”。
  • 内部基于 ES6 的 Proxy 实现,通过操作代理对象来对源对象内部数据进行修改与增强。

reactive 对比 ref

  • 从定义数据角度对比:
    • ref 用来定义:基本类型数据
    • reactive 用来定义:对象(或数组)类型数据
    • 备注:ref 也可以用来定义对象(或数组)类型数据,它内部会自动通过 reactive 转为代理对象
  • 从原理角度对比:
    • ref 通过 Object.defineProperty() 的 get 与 set 来实现响应式(数据劫持)。
    • reactive 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据。
  • 从使用角度对比:
    • ref 定义的数据:操作数据需要 .value,读取数据时模板中直接读取不需要 .value
    • reactive 定义的数据:操作数据与读取数据均不需要 .value
3)toRef & toRefs

作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。

语法:const name = toRef(响应式对象,'属性')

应用:要将响应式对象中的某个属性单独提供给外部使用时。

toRefs:与 toRef 功能一致,但可以批量创建多个 ref 对象,这些对象的值会跟踪响应式对象的所有属性。它接收一个响应式对象作为参数,并返回一个新的对象,这个对象的每个属性都是一个 ref,这些 ref 的值会跟踪响应式对象的相应属性。例如,const personRefs = toRefs(user) 会返回一个新的对象,这个对象中的每个属性都是一个 ref,这些 ref 的值会跟踪 person 对象的相应属性。

javascript
export default {
  setup() {
    const user = reactive({
      name: 'job',
      age: 18
    })

	return {
	  ...toRefs(user)
	}
  }
}

/*
return {
  name: user.name,    // 错误,非响应式。等价于 name: 'job'
  age: ref(user.name) // 错误,非响应式。等价于 age: ref(8)
}
*/
4)注意

口诀

解构对象要 toRefs
提取 value 失响应
unref toRaw 变普通
替换 reactive 需谨慎

函数传参传 ref 对象
扩展运算符也会丢
Props 解构要小心
异步赋值用 ref 包
javascript
// ----------------------------------------------- 1. 解构对象要 toRefs, 勿要直接解构
const state = reactive({
  count: 0,
  name: 'Alice'
});

// ❌ 解构后失去响应式
const { count, name } = state;

count++;  // ❌ 不会触发更新
name = 'Bob';  // ❌ 不会触发更新

// ----------------------------------------------- 2. 提取 ref 的 .value
const count = ref(0);

// ❌ 提取值后失去响应式
let num = count.value;

num++;  // ❌ 不会影响 count
console.log(count.value);  // 0(未改变)

// ----------------------------------------------- 3. 直接赋值替换响应式对象
let state = reactive({
  count: 0
});

// ❌ 直接赋值,失去响应式
state = { count: 100 };  // 新对象不是响应式
// ✅ 方式1:修改属性
state.count = 100;
// ✅ 方式2:使用 Object.assign
Object.assign(state, { count: 100 });
// ✅ 方式3:ref 可以整体替换
const state = ref({
  count: 0
});
state.value = { count: 100 };  // 响应式

// ----------------------------------------------- 4. 使用扩展运算符
const state = reactive({
  count: 0,
  name: 'Alice'
});

// ❌ 扩展后失去响应式
const newState = { ...state };

newState.count++;  // ❌ 不会影响 state

// ----------------------------------------------- 5. 直接解构 props
const props = defineProps(['count', 'name']);

// ✅ 保持响应式
const { count, name } = toRefs(props);

watch(count, () => {
  // ✅ 响应式
});

// ----------------------------------------------- 6. 异步后重新赋值 reactive
let data = reactive({ list: [] });

async function fetchData() {
  const result = await fetch('/api/data');
  
  // ❌ 重新赋值,失去响应式
  data = reactive({ list: result });
}

// 方式1:修改属性
const data = reactive({ list: [] });

async function fetchData() {
  const result = await fetch('/api/data');
  data.list = result;  // ✅ 响应式
}

// 方式2:使用 ref
const data = ref({ list: [] });

async function fetchData() {
  const result = await fetch('/api/data');
  data.value = { list: result };  // ✅ 响应式
}

最佳实践

  • ref 可以整体替换。data.value = { count: 100 };
  • reactive 只修改属性,不替换整体。
  • 解构时用 toRefs。

3. 计算属性与监视

1)computed 函数

与 Vue2.x 中 computed 配置功能一致。

javascript
import { computed } from 'vue'

setup(){
  // ...
  // 计算属性——简写
  let fullName = computed(()=>{
    return person.firstName + '-' + person.lastName
  })
  // 计算属性——完整
  let fullName = computed({
    get(){
      return person.firstName + '-' + person.lastName
    },
    set(value){
      const nameArr = value.split('-')
      person.firstName = nameArr[0]
      person.lastName = nameArr[1]
    }
  })

  return {
    fullName
  }
}
2)watch 函数

与 Vue2.x 中 watch 配置功能一致。

两个小“坑”:

  • 监视 reactive 定义的响应式数据时:oldValue 无法正确获取、强制开启了深度监视(deep 配置失效)。
  • 监视 reactive 定义的响应式数据中某个属性时:deep 配置有效。
javascript
// 情况一:监视ref定义的响应式数据
watch(sum, (newValue, oldValue) => {
  console.log('sum变化了', newValue, oldValue)
}, {immediate:true})

// 情况二:监视多个ref定义的响应式数据
watch([sum, msg], (newValue, oldValue) => {
  console.log('sum或msg变化了', newValue, oldValue)
})

/* 情况三: 监视reactive定义的响应式数据
          若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
          若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 
*/
watch(person, (newValue, oldValue) => {
  console.log('person变化了', newValue, oldValue)
}, {immediate:true,deep:false}) // 此处的deep配置不再奏效

// 情况四:监视reactive定义的响应式数据中的某个属性
watch(()=>person.age, (newValue, oldValue) => {
  console.log('person的job变化了', newValue, oldValue)
}, {immediate:true,deep:true})

// 情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.age, ()=>person.name], (newValue, oldValue) => {
  console.log('person的job变化了', newValue, oldValue)
}, {immediate:true,deep:true})

// 特殊情况
// 无法正确获得oldValue!!
watch(()=>person.job, (newValue, oldValue) => {
  console.log('person的job变化了', newValue, oldValue)
}, {deep:true}) // 此处由于监视的是reactive定义的对象中的某个属性,所以deep配置有效

注意:watch 监视源只能是 getter/effect 函数、ref、reactive 对象或数组这些类型。

            如果 watch 监视的是 ref (原始类型) ,不用写 value;而如果是 ref (对象) 需要 value 或者开启 deep 配置。

3)watchEffect 函数

作用:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

watch 的套路:既要指明监视的属性,也要指明监视的回调。

watchEffect 的套路:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。

watchEffect 有点像 computed:

  • 但 computed 注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
  • 而 watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值。
javascript
// watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
const stop = watchEffect(()=>{
  const x1 = sum.value
  const x2 = person.age
  console.log(x1, x2)
  console.log('watchEffect配置的回调执行了!')
})

// 当不再需要此侦听器时
stop()

4. 生命周期

官方文档:https://cn.vuejs.org/api/options-lifecycle.html

                   https://cn.vuejs.org/guide/essentials/lifecycle.html

vue2.x 的生命周期lifecycle_2
vue3.0 的生命周期lifecycle_3

Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有有两个被更名:

  • beforeDestroy 改名为 beforeUnmount
  • destroyed 改名为 unmounted

Vue3.0 也提供了 Composition API 形式的生命周期钩子,与 Vue2.x 中钩子对应关系如下:

  • beforeCreate ==> setup()
  • created ==> setup()
  • beforeMount ==> onBeforeMount
  • mounted ==> onMounted
  • beforeUpdate ==> onBeforeUpdate
  • updated ==> onUpdated
  • beforeUnmount ==> onBeforeUnmount
  • unmounted ==> onUnmounted

Composition API 的生命周期钩子定义多次的时候,会按照顺序依次执行。而原来的选项式 API 可以使用混入来实现多次执行。

5. 自定义 hook / 组合式函数

官方文档:https://cn.vuejs.org/guide/reusability/composables.html

什么是 hook?本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装。类似于 vue2.x 中的 mixin。

优势:复用代码,让 setup 中的逻辑更清楚易懂。

1)例子一

如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:

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

<script 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>

但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:

javascript
// 文件位置:./src/hook/useMouse.js
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 }
}

下面是它在组件中使用的方式:

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

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

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

一个组合式函数中可以调用一个或多个其他的组合式函数。

举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:

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

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

有了它,之前的 useMouse() 组合式函数可以被简化为:

javascript
// useMouse.js
import { ref } from 'vue'
import { useEventListener } from 'hook/useEvent'

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 }
}
2)例子二
javascript
import { ref, watch } from "vue";

export default function useTitle(titleValue) {
  // 定义ref的引入参数
  const title = ref(titleValue);

  // 监听title的改变
  watch(title, (newValue) => {
    document.title = newValue;
  }, {
    immediate: true
  });

  // 返回ref值
  return title;
}

三、其它 Composition API

1. shallowRef 与 shallowReactive

shallowReactive:只处理对象第一层属性的响应式(浅响应式)。

shallowRef:只处理基本数据类型的响应式、对象最外层(根)的响应式。

javascript
const person = shallowReactive({
  name: "张三",  //   <-- 响应式 person.name="Alice"
  age: 18,      //   <-- 响应式 person.age=22
  job: {        //   <-- 响应式 person.job={}
    a: 100      //   <-- 非响应式 person.job.a=66
  }
})

const score = shallowRef(0)  //   <-- 响应式 score=100

const person = shallowRef({  //   <-- 响应式 person={}
  name: "张三",  //   <-- 非响应式 person.name="Alice"
  age: 18,      //   <-- 非响应式 person.age=22
  job: {        //   <-- 非响应式 person.job={}
    a: 100      //   <-- 非响应式 person.job.a=66
  }
})

什么时候使用?

  • 如果有一个对象数据,结构比较深,但变化时只是外层属性变化 ===> shallowReactive。
  • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换 ===> shallowRef。

2. readonly 与 shallowReadonly

应用场景:不希望数据被修改时。

  • readonly:让一个响应式数据变为只读的(深只读)。

    接受的类型:普通对象、reactive 返回的对象、ref 的对象。

  • shallowReadonly:让一个响应式数据变为只读的(浅只读)。

3. toRaw 与 markRaw

  • toRaw

    • 作用:只能将一个由 reactive 生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
  • markRaw

    • 作用:标记一个对象,使其永远不会再成为响应式对象。

    • 应用场景

      ① 有些值不应被设置为响应式的,例如复杂的第三方类库等。

      ② 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

4. customRef

作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。

实现防抖效果:

vue
<template>
  <input type="text" v-model="keyword">
  <h3>{{keyword}}</h3>
</template>

<script>
  import {ref,customRef} from 'vue'

  export default {
    name:'Demo',
    setup(){
      // let keyword = ref('hello') // 使用Vue准备好的内置ref
      // 自定义一个myRef
      function myRef(value,delay){
        let timer
        // 通过customRef去实现自定义
        return customRef((track,trigger)=>{
          return {
			get(){
			  track() // 告诉Vue这个value值是需要被“追踪”的
			  return value
			},
			set(newValue){
			  clearTimeout(timer)
			  timer = setTimeout(()=>{
			    value = newValue
			    trigger() // 告诉Vue去更新界面
			  },delay)
			}
          }
        })
      }
	  let keyword = myRef('hello',500) // 使用程序员自定义的ref
	  return {
		keyword
	  }
    }
  }
</script>

5. 响应式数据的判断

响应式 API:工具函数

  • isRef:检查一个值是否为一个 ref 对象。

  • isReactive:检查一个对象是否是由 reactive()shallowReactive() 创建的代理。

    如果是 readonly,但是由 reactive 创建的另一个代理,它也会返回 true。

    javascript
    import { reactive, readonly, isReactive } from 'vue';
    
    const original = reactive({ a: 1 });
    const readOnlyVersion = readonly(original);
    
    console.log(isReactive(original));  // true,因为 original 是由 reactive 创建的
    console.log(isReactive(readOnlyVersion)); // true,尽管 readOnlyVersion 是只读的,但它基于由 reactive 创建
  • isReadonly:用于判断对象是否由 readonly()shallowReadonly() 创建。

  • isProxy:检查一个对象是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理。

6. unref

unref 方法用于获取一个 ref 引用中的 value 值。如果传递给 unref 的参数是一个 ref 对象,那么它会返回该 ref 对象内部的值。如果传递的参数不是 ref 对象,那么它会直接返回参数本身。

javascript
import { ref, unref, isRef } from 'vue';

// 创建一个 ref 对象
const count = ref(10);

// 使用 unref 获取 ref 对象的值
const value = unref(count);

console.log(value); // 输出:10

// 如果传递的不是 ref 对象,unref 直接返回参数本身
const notRef = 20;
const value2 = unref(notRef);

console.log(value2); // 输出:20

7. triggerRef

triggerRef 方法用于手动触发与 shallowRef 相关的副作用。

在 Vue 中,shallowRef 是一种浅层的响应式引用,它只对引用本身的变化做出反应,而不会对其内部对象的变化做出反应。有时,我们可能需要手动触发与 shallowRef 相关的副作用,这时就可以使用 triggerRef 方法。

javascript
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 触发该副作用,第一次应该会打印 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这次变更不应触发副作用,因为这个 ref 是浅层的(修改不是响应式的)
shallow.value.greet = 'Hello, universe'

const changeInfo = () => {
  shallow.value.greet = "Hello, yxts"
  // 手动触发
  triggerRef(shallow) // 打印 "Hello, yxts"
};

8. 依赖注入

官方文档:https://cn.vuejs.org/guide/components/provide-inject.html

                   https://cn.vuejs.org/api/composition-api-dependency-injection.html

  • 作用:实现祖与后代组件间通信。

  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据。

  • 具体写法:

    祖组件中

    javascript
    setup(){
      // ......
      let car = reactive({name:'奔驰',price:'40万'})
      provide('car',car)
      // ......
    }

    后代组件中

    javascript
    setup(props,context){
      // ......
      const car = inject('car', {name:'yxts'}) // 第二个参数为兜底的值
      return {car}
    }

四、自定义指令

1. 定义语法

<script setup> 中,任何以 v 开头的驼峰式命名的变量都可以当作自定义指令使用。

vue
<script setup>
// 在模板中启用 v-highlight
const vHighlight = {
  mounted: (el) => {
    el.classList.add('highlight')
  }
}
</script>

<template>
  <p v-highlight>This sentence is important!</p>
</template>

将一个自定义指令全局注册到应用层级也是一种常见的做法:

javascript
const app = createApp({})

// 使 v-highlight 在所有组件中都可用
app.directive('highlight', {
  /* ... */
})

简写

jsx
<div v-color="color"></div>

app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

2. 指令钩子 / 生命周期

javascript
const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding) {},
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding) {}
}
  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。

3. 实战

1)项目中真实使用 - 01

(1) 新建 @/directive 文件夹。用来存放自定义指令。

(2) 在 @/directive 下建 focus.js 文件。

javascript
export default function directiveFocus(app) {
  // 注册一个名为 "focus" 的自定义指令
  app.directive("focus", {
    // 生命周期钩子:指令绑定的元素挂载到页面时触发
    mounted(el) {
      // 如果元素存在,则调用 `focus()` 方法使其获得焦点
      el?.focus();
    }
  });
}

(3) 在 @/directive 下建 index.js 文件,用于汇总自定义指令文件。

javascript
import directiveFocus from "./focus";

export default function useDirectives(app) {
  directiveFocus(app);
}

(4) 在 main.js 中引入自定义指令。

javascript
import useDirectives from "./directive";
useDirectives(app)
2)项目中真实使用 - 02
[1] 前置知识

以前我们 createAPP(app).use(router).use(store).mount('#app'),其实 use 函数可以接收一个对象或函数。

如果是对象:要求有一个 install 属性,值为函数。默认会给这个函数传递一个 app 对象。
如果是函数:会直接执行这个函数。默认会给这个函数传递一个 app 对象。
[2] 进阶写法

此版为【项目中真实使用 - 01】的进阶写法。

javascript
// index.js
import directiveFocus from "./focus";

export default function directives(app) {
  directiveFocus(app);
}
javascript
import directives from "./directive";

createAPP(app)
  .use(directives)
  .use(router)
  .use(store)
  .mount('#app')

第三章:新的组件

一、Fragment

在 Vue2 中:组件必须有一个根标签。

在 Vue3 中:组件可以没有根标签,内部会将多个标签包含在一个 Fragment 虚拟元素中。

好处:减少标签层级,减小内存占用。

二、Teleport

Teleport 是一种能够将我们的组件中某片段 html 结构移动到指定位置的技术。

vue
<teleport to="移动位置">
  <div v-if="isShow" class="mask">
    <div class="dialog">
	  <h3>我是一个弹窗</h3>
	  <button @click="isShow=false">关闭弹窗</button>
    </div>
  </div>
</teleport>

移动位置(to 属性)可以填写 html、body、CSS 选择器字符串。

三、Suspense

1. 作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验。

2. 使用步骤

1)异步引入组件

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能:

defineAsyncComponent 方法接收一个返回 Promise 的加载函数。正好 import() 函数返回的是 Promise 对象。

javascript
import {defineAsyncComponent} from 'vue' // 静态引入
// import Child from './components/Child.vue'; // 之前的导入方式,使用 es6 模块化语法,是同步的
const Child = defineAsyncComponent(()=>import('./components/Child.vue')) // 动态引入 / 异步引入

2)使用 Suspense 包裹组件,并配置好 default 与 fallback

vue
<template>
  <div class="app">
    <h3>我是App组件</h3>
	<Suspense>
	  <template v-slot:default>
	    <Child/>
	  </template>
      <template v-slot:fallback>
        <h3>加载中.....</h3>
      </template>
    </Suspense>
  </div>
</template>

当 Child 组件正在加载时,用户会看到“加载中.....”。当 Child 组件加载完成并准备好被渲染时,<Suspense> 会自动切换到 default 插槽,显示 Child 组件。

四、动态组件

动态组件是使用 component 组件,通过一个特殊的属性 is 来实现。

vue
<template>
  <component :is="currentComponent"></component>
</template>

<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

export default {
  components: {
    ComponentA,
    ComponentB
  },
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  }
}
</script>

这个 currentComponent 值需要什么内容?

  • 全局注册:可以是通过 component 查找注册的组件。
  • 局部注册:在一个组件对象里的 components 对象中注册的组件。

如果是动态组件,我们可以给它传值和监听事件吗?只需要将属性和监听事件放到 component 上来使用。

vue
<template>
  <component 
    :is="currentComponent"
    :message="message"
    @custom-event="handleCustomEvent"
  ></component>
</template>

<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

export default {
  components: {
    ComponentA,
    ComponentB
  },
  data() {
    return {
      currentComponent: 'ComponentA',
      message: 'Hello from parent'
    }
  },
  methods: {
    handleCustomEvent(data) {
      console.log('Custom event received:', data)
    }
  }
}
</script>

在子组件中,可以这样使用:

vue
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="emitEvent">Emit Event</button>
  </div>
</template>

<script>
export default {
  props: ['message'],
  methods: {
    emitEvent() {
      this.$emit('custom-event', 'Data from ComponentA')
    }
  }
}
</script>

第四章:vue-router

使用 vue-router 的版本为 4.x。

一、快速入门

1)创建路由需要映射的组件。

2)通过 createRouter 创建路由对象,并且传入 routes 和 history 模式;

  • routes:路由映射。组件和路径映射关系的 routes 数组。
  • history:创建基于 hash 或者 history 的模式。
javascript
// 导入vue-router的相关方法
import { createRouter, createWebHashHistory } from 'vue-router'

// 导入创建的组件
import Home from '../pages/Home.vue'
import About from '../pages/About.vue'

// 配置路由的映射
const routes = [
  { path: '/home', component: Home },
  { path: '/about', component: About }
]

// 创建router对象
const router = createRouter({
  routes,
  history: createWebHashHistory()
})

export default router

3)使用 app 注册路由对象(use 方法);

javascript
// 在主文件中引入router
import router from './router'

// 使用router并挂载到Vue实例
createApp(App).use(router).mount('#app')

4)路由使用:通过 <router-link><router-view>

vue
<template>
  <div class="nav">
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
  </div>
  <router-view></router-view>
</template>

二、零碎知识

1. 切换路由模式

Hash 模式(默认)。

javascript
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(), // createWebHistory(),
  routes: [
    // 你的路由配置
  ]
})

2. SFC 拿到 route / router

JavaScript 中:

javascript
export default {
  created() {
    console.log(this.$route.params.id); // vue2
  },
  setup() {  // vue3
    const router = useRouter();
    const route = useRoute();
    console.log(route.params.id);
  }
}

Vue 模板中:

vue
<template>
  <div>
    <h2>用户界面(vue2):{{ $route.params.id }}</h2>
    <h2>用户界面(vue3):{{ route.params.id }}</h2>
  </div>
</template>

3. NotFound

json
{
  path: '/:pathMatch(.*)',
  component: () => import('../pages/NotFound.vue')
}

当使用 Vue Router 的通配符路由(如 path: '*',path: '/:pathMatch(.*)')时,pathMatch 会捕获用户访问的未匹配路径。

vue
<h2>Not Found: {{ $route.params.pathMatch }}</h2> <!-- Not Found: user/haha/123 -->

还可以在 path: '/:pathMatch(.*)' 后加 *

json
{
  path: '/:pathMatch(.*)*',
  component: () => import('../pages/NotFound.vue')
}

两者区别

vue
<h2>Not Found: {{ $route.params.pathMatch }}</h2> <!-- Not Found: ["user", "haha", "123"] -->

三、动态添加路由

Vue Router 文档:动态路由

1. 添加一级路由

javascript
router.addRoute({ path: '/about', component: About })

2. 添加嵌套路由

要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:

javascript
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })

这等效于:

javascript
router.addRoute({
  name: 'admin',
  path: '/admin',
  component: Admin,
  children: [{ path: 'settings', component: AdminSettings }],
})

3. 删除路由

  • 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:

    javascript
    router.addRoute({ path: '/about', name: 'about', component: About })
    // 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
    router.addRoute({ path: '/other', name: 'about', component: Other })
  • 通过调用 router.addRoute() 返回的回调:

    javascript
    const removeRoute = router.addRoute(routeRecord)
    removeRoute() // 删除路由如果存在的话

    当路由没有名称时,这很有用。

  • 通过使用 router.removeRoute() 按名称删除路由:

    javascript
    router.addRoute({ path: '/about', name: 'about', component: About })
    // 删除路由
    router.removeRoute('about')

    需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 Symbol 作为名字。

当路由被删除时,所有的别名和子路由也会被同时删除。

4. 查看现有路由

Vue Router 提供了两个功能来查看现有的路由:

四、路由守卫

1. beforeEach

它有返回值

  • false:取消当前导航。
  • 不返回或者 undefined:进行默认导航。
  • 返回一个路由地址:
    • 可以是一个 string 类型的路径。
    • 可以是一个对象,对象中包含 path、query、params 等信息。

可选的第三个参数:next(不推荐使用)

-在 Vue2 中我们是通过 next 函数来决定如何进行跳转的;

但是在 Vue3 中我们是通过返回值来控制的,不再推荐使用 next 函数,这是因为开发中很容易调用多次 next。

2. 完整的导航解析流程

官方文档:完整的导航解析流程

用户点击链接 / 编程式导航

1. 导航被触发

2. 离开旧页面 → beforeRouteLeave (失活组件, 即将离开的组件)              询问用户是否要离开此页面

3. 全局前置守卫 → beforeEach                                          权限验证

4. 重用组件更新 → beforeRouteUpdate (如果有) (路径变了, 但组件实例被复用)  `/user/1` → `/user/2`: User 组件被重用, 只是参数变了, 如果是从 `/home` 跳到 `/user/1`, 是不会触发它的 (而是触发 beforeRouteEnter) 。

5. 路由配置守卫 → beforeEnter (路由独享的守卫)                          单独给某个组件内做权限验证

6. 解析异步组件 (如果有)                                               如果组件是异步加载的, 此时会加载组件

7. 进入新页面 → beforeRouteEnter (激活组件, 即将进入的新组件)

8. 全局解析守卫 → beforeResolve (和 router.beforeEach 类似)            解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。在这里做最后的检查

9. 导航确认 ✅                                                       导航确认,准备更新视图

10. 全局后置钩子 → afterEach (导航确认后调用, 不接受next参数, 无法改变导航) 修改页面标题、发送分析数据

11. DOM 更新                                                         Vue 开始渲染新组件,旧组件被销毁

12. beforeRouteEnter 的 next 回调执行                                 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

第五章:其他

一、全局 API 的转移

  • Vue2.x 有许多全局 API 和配置。

    例如:注册全局组件、注册全局指令等。

    javascript
    // 注册全局组件
    Vue.component('MyButton', {
      data: () => ({
        count: 0
      }),
      template: '<button @click="count++">Clicked {{ count }} times.</button>'
    })
    
    // 注册全局指令
    Vue.directive('focus', {
      inserted: el => el.focus()
    }
  • Vue3.0 中对这些 API 做出了调整

    将全局的 API,即:Vue.xxx 调整到应用实例上。

    2.x 全局 API(Vue3.x 实例 API (app)
    Vue.config.xxxxapp.config.xxxx
    Vue.config.productionTip移除
    Vue.componentapp.component
    Vue.directiveapp.directive
    Vue.mixinapp.mixin
    Vue.useapp.use
    Vue.prototypeapp.config.globalProperties

    应用实例(app)什么鬼?

    javascript
    import { createAPP } from 'vue'
    import APP from './App.vue'
    
    const app = createApp(App)
    
    app.mount('#app')

二、其他改变

  • data 选项应始终被声明为一个函数。

  • 过渡类名的更改

    • Vue2.x 写法

      css
      .v-enter,
      .v-leave-to {
        opacity: 0;
      }
      .v-leave,
      .v-enter-to {
        opacity: 1;
      }
    • Vue3.x 写法

      css
      .v-enter-from,
      .v-leave-to {
        opacity: 0;
      }
      
      .v-leave-from,
      .v-enter-to {
        opacity: 1;
      }
  • 移除 keyCode 作为 v-on 的修饰符,同时也不再支持 config.keyCodes

  • 移除 v-on:xxx.native 修饰符。使用自定义事件必须用 emits 选项来规定。

    • 父组件中绑定事件

      vue
      <my-component
        v-on:close="handleComponentEvent"
        v-on:click="handleNativeClickEvent"
      />
    • 子组件中声明自定义事件

      vue
      <script>
      export default {
        // 第一种写法【常用】
        emits: ['close']
      
        // 第二种写法: 自定义事件的参数和验证 (了解)
        // 自定义事件接受的数据必须满足函数提供的校验,如果返回 fales 就会在控制台警告
        emits: {
          addOne: null, // 无要求
          subOne: null, // 无要求
          addTen: function(payload) { // addTen 事件传递的参数必须等于 10
            if (payload === 10) {
              return true
            }
            return false;
          }
        },
      }
      </script>
  • 移除过滤器(filter)

    过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

    ……

preview
图片加载中
预览

Released under the MIT License.