1 生命周期的变化

Vue2.xVue3
beforeCreate使用 setup()
created使用 setup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
errorCapturedonErrorCaptured

使用setup代替了之前的beforeCreatecreated,其他生命周期名字有些变化,功能都是没有变化的

2 响应式原理不同

  • Vue2 响应式原理是利用 Object.defineProperty()对数据进行劫持结合发布订阅模式来实现
  • Vue3 响应式原理是利用proxy 对数据代理,通过reactive() 函数给每一个对象都包一层 proxy,通过 proxy 监听属性的变化,从而实现对数据的监控

2.1 Object.defineProperty()语法

Object.defineProperty( Obj, 'name', {
    enumerable: true, //可枚举
    configurable: true, //可配置
    // writable:true, //跟可配置不能同时存在
    // value:'name',  //可写死直
    get: function () {
        return def
    },
    set: function ( val ) {
        def = val
    }
} )

缺点:

  1. Object.defineProperty不支持数组的拦截
  2. 对象新增、删除属性没有响应式。需要使用Vue.set(this.user, 'age', 30)来添加
  3. 只能重定义属性的读取(get)设置(set)行为

2.2 Proxy的语法

Proxy,可以重定义更多的行为,比如 indelete函数调用等更多行为。

Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty

//两个参数,对象,13个配置项
const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    },
    set: function(target, prop, val) { // 拦截写入操作
	    if (typeof val == 'number') {
	      target[prop] = val;
	      return true;
		} else {
	      return false;
	}
  }
    ...13个配置项
};
const p = new Proxy({}, handler);

3 Diff算法的提升

3.1 以往的渲染策略

vue2将template模板编译成渲染函数来返回虚拟DOM树。Vue框架通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性,来确定实际DOM的哪些部分需要更新。

3.2 Vue3的突破

编译器分析模板并生成带有优化提示的代码,而运行时尽可能获取提示并采用快速路径。这里有三个主要的优化:

  • 在DOM树级别。将一个模板分成由没有动态改变节点结构的模板指令(例如v-if和v-for)分隔的嵌套“块”,则每个块中的节点结构将再次完全静态。当我们更新块中的节点时,我们不再需要递归遍历DOM树 - 该块内的动态绑定可以在一个平面数组中跟踪。
  • 编译器积极地检测模板中的静态节点、子树甚至数据对象,并在生成的代码中将它们提升到渲染函数之外。这样可以避免在每次渲染时重新创建这些对象,从而大大提高内存使用率并减少垃圾回收的频率。
  • 在元素级别。编译器还根据需要执行的更新类型,为每个具有动态绑定的元素生成一个优化标志。例如,具有动态类绑定和许多静态属性的元素将收到一个标志,提示只需要进行类检查。运行时将获取这些提示并采用专用的快速路径。

Vue2与Vue3的Diff算法差异:

  • Vue2和Vue3的Diff算法在处理完首尾节点后,对剩余节点的处理方式有所不同。
  • Vue2通过建立一个旧节点的映射表来寻找可复用的节点,而Vue3则是通过建立一个索引映射关系数组来计算最长递增子序列,以优化节点的复用和移动。

4 TypeScript的支持

Vue3 借鉴了react hook实现了更自由的编程方式,提出了Composition APIComposition API不需要通过指定一长串选项来定义组件,而是允许用户像编写函数一样自由地表达、组合和重用有状态的组件逻辑,同时提供出色的TypeScript支持

5 优化打包体积

在Vue 3中,通过将大多数全局API和内部帮助程序移动到Javascriptmodule.exports属性上实现这一点。这允许现代模式下的module bundler能够静态地分析模块依赖关系,并删除与未使用的module.exports属性相关的代码。模板编译器还生成了对树抖动友好的代码,只有在模板中实际使用某个特性时,该代码才导入该特性的帮助程序。

6 新的响应性 API

6.1 reactive()

我们可以使用 reactive() 函数创建一个响应式对象或数组

reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的

import { reactive } from 'vue'

export default {
  // `setup` 是一个专门用于组合式 API 的特殊钩子函数
  setup() {
    const state = reactive({ count: 0 })

    function increment() {
      state.count++
    }

    // 暴露 state increment 函数 到模板
    return {
      state,
      increment
    }
  }
}

要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。

6.2 <script setup>

可以使用 <script setup> 来大幅度地简化代码。

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

<script setup> 中的顶层的导入和变量声明可在同一组件的模板中直接使用。你可以理解为模板中的表达式和 <script setup> 中的代码处在同一个作用域中。

6.3 nextTick()

DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只更新一次。

若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:

import { nextTick } from 'vue'

function increment() {
  state.count++
  nextTick(() => {
    // 访问更新后的 DOM
  })
}

6.4 reactive() 的局限性

  1. 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
  2. 因为 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)

6.5 ref()

Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

和响应式对象的属性类似,ref.value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value

ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)

// 仍然是响应式的
const { foo, bar } = obj

ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value。下面是计数器例子,用 ref() 代替:

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

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- 无需 .value -->
  </button>
</template>

6.6 计算属性用法不同

Vue2 中 computed 最见的使用场景一般有: mapGetters/mapState 获取状态管理的属性、 获取 url 上的属性、条件判断、类型转换之类的,支持函数和对象两种写法

Vue3 中 computed 不再是一个对象,而是一个函数,用法其实基本差不多,函数第一个参数是侦听器源,用于返回计算的新值,也支持对象写法,第二个参数可用于调试

<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value

Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books,所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。

计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。相比之下,方法调用总是会在重渲染发生时再次执行函数。

6.7 侦听器的用法不同

watch 就是用来监听一个已有属性,发生变化的时候做某些操作,Vue2 中常用有如下三种写法

watch: {
    userId: 'getData',
    userName (newName, oldName) {
        this.getData()
    },
    userInfo: {
        handler (newVal, newVal) { this.getData() },
        immediate: true,
        deep: true
    }
}

Vue3 的 watch 是一个函数,能接收三个参数,参数一是监听的属性,参数二是接收新值和老值的回调函数,参数三是配置项

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

const name = ref('沐华')
const data = reactive({
    age: 18,
    money: 100000000000000000000,
    children: []
})

// 监听 ref 属性
watch(name, (newName, oldName) => { ... })

// 监听其他属性、路由或者状态管理的都这样
watch(
    () => data.age, 
    (newAge, oldAge) => { ... }
)

// 监听多个属性,数组放多个值,返回的新值和老值也是数组形式
watch([data.age, data.money], ([newAge, newMoney], [oldAge, oldMoney]) => { ... })

// 第三个参数是一个对象,为可配置项,有5个可配置属性
watch(data.children, (newList, oldList) => { ... }, {
    // 这两个和 Vue2 一样,没啥说的
    immediate: true,
    deep: true,
    // 回调函数的执行时机,默认在组件更新之前调用。更新后调用改成post
    flush: 'pre', // 默认值是 pre,可改成 post 或 sync
    // 下面两个是调试用的
    onTrack (e) { debugger }
    onTrigger (e) { debugger }
})
</script>

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

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

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

注意,你不能直接侦听响应式对象的属性值,例如:

const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

这里需要用一个返回该属性的 getter 函数:

// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

6.7.1 深层侦听器

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

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

6.7.2 即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

6.7.3 watchEffect()

watchEffect() 允许我们自动跟踪回调的响应式依赖。

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

6.7.4 回调的触发时机

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post’ 选项:

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

6.8 toRef 和 toRefs

这两共同点就是用来创建响应式的引用的,主要用来取出响应式对象里的属性,或者解构响应式对象,解构出来的属性值依然是响应式属性,如果不用这两直接解构的话是会丢失响应式效果的

<script setup>
import { reactive, toRef, toRefs } from 'vue'

const data = reactive({
    name: '沐华',
    age: 18
})

// 这样虽然能拿到 name / age,但是会变成普通变量,没有响应式效果了
const { name, age } = data

// 取出来一个响应式属性
const name = toRef(data, 'name')

// 这样解构出来的所有属性都是有响应式的
const { name, age } = toRefs(data)

// 不管是 toRef 还是 toRefs,这样修改是会把 data 里的 name 改掉的
// 就是会改变源对象属性,这才是响应式该有的样子
name.value = '沐沐华华'
</script>

8 依赖注入

一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

8.1 Provide (提供)

要为组件后代提供数据,需要使用到 provide() 函数:

<script setup>
import { provide } from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

8.2 应用层 Provide

除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。

8.3 Inject (注入)

要注入上层组件提供的数据,需使用 inject() 函数:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

8.4 和响应式数据配合使用

有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>

<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly() 来包装提供的值。

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

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

9 defineAsyncComponent实现异步组件

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

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promiseresolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败。

与普通组件一样,异步组件可以使用 app.component() 全局注册:

app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

也可以直接在父组件中直接定义它们:

<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>

9.1 加载与错误状态

异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。

如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。

9.2 与Suspense搭配使用

10 内置组件

10.1 Suspense

在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 ,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。

有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

<Suspense> 可以等待的异步依赖有两种:

  1. 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件。
  2. 异步组件。

10.1.1 加载中状态

<Suspense> 组件有两个插槽:#default#fallback。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。

<Suspense>
  <!-- 具有深层异步依赖的组件 -->
  <Dashboard />

  <!-- 在 #fallback 插槽中显示 “正在加载中” -->
  <template #fallback>
    Loading...
  </template>
</Suspense>

在初始渲染时,<Suspense> 将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,<Suspense> 会进入完成状态,并将展示出默认插槽的内容。

如果在初次渲染时没有遇到异步依赖,<Suspense> 会直接进入完成状态。

进入完成状态后,只有当默认插槽的根节点被替换时,<Suspense> 才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成 <Suspense> 回退到挂起状态。

发生回退时,后备内容不会立即展示出来。相反,<Suspense> 在等待新内容和异步依赖完成时,会展示之前 #default 插槽的内容。这个行为可以通过一个 timeout prop 进行配置:在等待渲染新内容耗时超过 timeout 之后,<Suspense> 将会切换为展示后备内容。若 timeout 值为 0 将导致在替换默认内容时立即显示后备内容。

10.1.2 事件

<Suspense> 组件会触发三个事件:pendingresolvefallbackpending 事件是在进入挂起状态时触发。resolve 事件是在 default 插槽完成获取新内容时触发。fallback 事件则是在 fallback 插槽的内容显示时触发。

例如,可以使用这些事件在加载新组件时在之前的 DOM 最上层显示一个加载指示器。

10.1.3 错误处理

<Suspense> 组件自身目前还不提供错误处理,不过你可以使用 errorCaptured 选项或者 onErrorCaptured() 钩子,在使用到 <Suspense> 的父组件中捕获和处理异步错误。

onErrorCaptured(): 注册一个钩子,在捕获了后代组件传递的错误时调用。

10.1.4 和其他组件结合

我们常常会将 <Suspense><Transition><KeepAlive> 等组件结合。要保证这些组件都能正常工作,嵌套的顺序非常重要。

另外,这些组件都通常与 Vue Router 中的 <RouterView> 组件结合使用。

下面的示例展示了如何嵌套这些组件,使它们都能按照预期的方式运行。若想组合得更简单,你也可以删除一些你不需要的组件:

<RouterView v-slot="{ Component }">
  <template v-if="Component">
    <Transition mode="out-in">
      <KeepAlive>
        <Suspense>
          <!-- 主要内容 -->
          <component :is="Component"></component>

          <!-- 加载中状态 -->
          <template #fallback>
            正在加载...
          </template>
        </Suspense>
      </KeepAlive>
    </Transition>
  </template>
</RouterView>

Vue Router 使用动态导入对懒加载组件进行了内置支持。这些与异步组件不同,目前他们不会触发 <Suspense>。但是,它们仍然可以有异步组件作为后代,这些组件可以照常触发 <Suspense>

10.2 KeepAlive

<KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例

默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。

可以用 <KeepAlive> 内置组件将这些动态组件包装起来:

<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

10.2.1 包含/排除

<KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 includeexclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。

在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。

10.2.2 最大缓存实例数

我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

10.2.3 缓存实例的生命周期

当一个组件实例从 DOM 上移除但因为被 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。

一个持续存在的组件可以通过 onActivated()onDeactivated() 注册相应的两个状态的生命周期钩子:

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

11 Composition API

在 Vue2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue3 中保留了 mixins 支持,但对于组件间的逻辑复用,Composition API 是现在更推荐的方式。

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

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

11.1 鼠标跟踪器示例

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

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

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

可以把这个逻辑以一个组合式函数的形式提取到外部文件中:

// mouse.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 }
}

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

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

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

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

11.2 异步状态示例

在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:

// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  function doFetch() {
    // 在请求之前重设状态...
    data.value = null
    error.value = null
    // unref() 解包可能为 ref 的值
    fetch(unref(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  if (isRef(url)) {
    // 若输入的 URL 是一个 ref,那么启动一个响应式的请求
    watchEffect(doFetch)
  } else {
    // 否则只请求一次
    // 避免监听器的额外开销
    doFetch()
  }

  return { data, error }
}

这个版本的 useFetch() 现在同时可以接收静态的 URL 字符串和 URL 字符串的 ref。当通过 isRef() 检测到 URL 是一个动态 ref 时,它会使用 watchEffect() 启动一个响应式的 effect。该 effect 会立刻执行一次,并在此过程中将 URL 的 ref 作为依赖进行跟踪。当 URL 的 ref 发生改变时,数据就会被重置,并重新请求。

12 获取组件示例方式不同

Vue2 中每个组件里使用 this 都指向当前组件实例,this 上还包含了全局挂载的东西、路由、状态管理等啥啥都有

Vue3 中没有 this,如果想要类似的用法,有两种,一是获取当前组件实例,二是获取全局实例

<script setup>
import { getCurrentInstance } from 'vue'

// proxy 就是当前组件实例,可以理解为组件级别的 this,没有全局的、路由、状态管理之类的
const { proxy, appContext } = getCurrentInstance()

// 这个 global 就是全局实例
const global = appContext.config.globalProperties
</script>

13 全局注册(属性/方法)不同

Vue2 中我们要往全局上挂载东西通常就是如下,然后在所有组件里都可以通过 this.xxx 获取到了

Vue.prototype.xxx = xxx

而 Vue3 中不能这么写了,换成了一个能被所有组件访问到的全局对象,就是上面说的全局实例的那个对象,比如在 main.js 中做全局注册

// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 添加全局属性
app.config.globalProperties.name = '沐华'

在其他组件中调用

<script setup>
import { getCurrentInstance } from 'vue'
const { appContext } = getCurrentInstance()

const global = appContext.config.globalProperties
console.log(global.name) // 沐华
</script>

14 获取 DOM 方式不同

Vue3 中获取 DOM 如下

<template>
    <el-form ref="formRef"></el-form>
    <child-component />
</template>
<script setup lang="ts">
import ChildComponent from './child.vue'
import { getCurrentInstance } from 'vue'
import { ElForm } from 'element-plus'

// 方法一,这个变量名和 DOM 上的 ref 属性必须同名,会自动形成绑定
const formRef = ref(null)
console.log(formRef.value) // 这就获取到 DOM 了

// 方法二
const { proxy } = getCurrentInstance()
proxy.$refs.formRef.validate((valid) => { ... })

// 方法三,比如在 ts 里,可以直接获取到组件类型
// 可以这样获取子组件
const formRef = ref<InstanceType<typeof ChildComponent>>()
// 也可以这样 获取 element ui 的组件类型
const formRef = ref<InstanceType<typeof ElForm>>()
formRef.value?.validate((valid) => { ... })
</script>

15 CSS 样式穿透

Vue2 中在 scoped 中修改子组件或者组件库中的组件样式,改不了的情况下,就可以用样式穿透,不管是 Less 还是 SASS 都是用 /deep/ .class {} 来做样式穿透,而 Vue3 中就不支持 /deep/ 的写法了,换成 :deep(.class)

<style lang="scss" scoped>
// 这样写不生效的话
.el-form {
    .el-form-item { ... }
}
// Vue2 中这样写
/deep/ .el-form {
    .el-form-item { ... }
}
// Vue3 中这样写
:deep(.el-form) {
    .el-form-item { ... }
}
</style>

// 别再这样加一个没有 scoped 的 style 标签了,全都加到全局上去了
// <style lang="scss">
//  .el-form {
//     .el-form-item { ... }
//  }
// </style>

16 CSS 绑定 JS 变量

<template>
    <div class="name">沐华</div>
</template>
<script setup>
import { ref } from "vue"
const str = ref('#f00') // 红色
</script>
<style scoped lang="scss">
.name {
    background-color: v-bind(str); // JS 中的色值变量 #f00 就赋值到这来了
}
</style>

17 defineProps声明Props

在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用 v-bind 而非 :prop-name

const post = {
  id: 1,
  title: 'My Journey with Vue'
}


<BlogPost v-bind="post" />
而这实际上等价于:
<BlogPost :id="post.id" :title="post.title" />

18 defineComponent 定义组件

在 Vue 3 中,defineComponent 是一个新的方法,用于定义组件。这个方法提供了一种更现代、更灵活的方式来定义组件,并且它是 Vue Composition API 的一部分。

使用 defineComponent 有几个关键好处:

  1. Composition API 支持defineComponent 直接与 Vue Composition API 集成,使你可以更方便地使用 ref, reactive, computed 等函数来管理组件的状态和属性。
  2. 类型安全:通过 TypeScript,你可以为 defineComponent 的参数提供类型定义,从而获得更好的代码提示和类型检查。
  3. 属性类型和默认值:你可以为组件的属性提供默认值和类型,从而更清楚地描述组件的预期输入。
  4. 属性/属性组合的装饰器:使用装饰器(例如 @Prop)来描述属性,这有助于提高代码的可读性和可维护性。
  5. 更清晰的模板和渲染函数:通过使用 setup 函数,你可以将模板和逻辑分开,使代码结构更加清晰。
  6. 更灵活的选项:与传统的 Vue.extend 相比,defineComponent 提供了更多的选项和灵活性,例如 emitssetup

一个简单的 defineComponent 的例子:

import { defineComponent, ref } from 'vue'  
  
export default defineComponent({  
  setup() {  
    const count = ref(0)  
  
    function increment() {  
      count.value++  
    }  
  
    return {  
      count,  
      increment  
    }  
  }  
})

19 defineEmits 定义事件

defineEmits 是 Vue 3 中 Composition API 的一部分,用于定义组件可以触发的自定义事件。在 Vue 3 中,子组件通过触发这些自定义事件来与父组件进行通信,传递数据或通知状态变化。

import { defineComponent, defineEmits } from 'vue';  
  
export default defineComponent({  
  setup() {  
    const emit = defineEmits(['add', 'decrement']);  
  
    // ...组件的其他逻辑  
  
    function handleClick() {  
      emit('add', 1); // 触发名为 'add' 的事件,并传递参数 1  
    }  
  
    return {  
      handleClick  
    };  
  },  
  // 组件其他部分的代码...  
});

setup写法

<script setup>
const emit = defineEmits(['update'])

</script>

父组件中传递方法

<template>  
  <div>  
    <ChildComponent @add="handleAdd" @decrement="handleDecrement" />  
  </div>  
</template>  
  
<script>  
import ChildComponent from './ChildComponent.vue';  
  
export default {  
  components: {  
    ChildComponent  
  },  
  methods: {  
    handleAdd(payload) {  
      console.log('Add event triggered with payload:', payload);  
      // 处理 'add' 事件的逻辑  
    },  
    handleDecrement() {  
      console.log('Decrement event triggered');  
      // 处理 'decrement' 事件的逻辑  
    }  
  }  
};  
</script>
Logo

前往低代码交流专区

更多推荐