vue3新特性学习

338 阅读1分钟

新特性简介

(1)性能提升

用proxy代替defineProperty实现响应式,支持数组修改某项和对象添加属性的响应式,无需再用$set

a. defineProperty API 的局限性最大原因是它只能针对单例属性做监听。

Vue2.x 中的响应式实现正是基于 defineProperty 中的 descriptor,对 data 中的属性做了遍历 + 递归,为每个属性设置了 getter、setter。

这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到 setter 监听的,这是 defineProperty 的局限性。

b. Proxy API 的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作, 这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码。

Proxy 可以理解成,在目标对象之前架设一层 “拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

打包大小减少41%;初次渲染快55%,更新快133%;内存使用减少54%

(2)Composition API

(3)其他特性和新增加的内置组件

(4)更好的 Typescript 支持

为什么要有 Vue3?

(1)随着功能的增长,复杂组件的代码变得越来越难以维护。

caab3d80ccc5e6996a7ff212eceb24e.jpg

a64d8dab20c5499dfb0403442b375ce.jpg 虽然mixin可以解决这个问题,但使用mixin会有其他问题

vue3推荐逻辑相关的变量声明和函数写在一起

(2)Vue2 对 Typescript 的支持不完善。

vue3支持2的大多数特性,可以沿用vue2的写法

选项式 API (Options API)

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

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

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

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

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

组合式 API (Composition API)

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。 这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。 比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

下面是使用了组合式 API 与 <script setup> 改造后和上面的模板完全一样的组件:

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

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

根组件

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

import { createApp } from 'vue' // 从一个单文件组件中导入根组件 
import App from './App.vue' 
const app = createApp(App)

应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,它将捕获所有由子组件上抛而未被处理的错误:

app.config.errorHandler = (err) => {
  /* 处理错误 */
}

应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:

app.component('TodoDeleteButton', TodoDeleteButton)
这使得 TodoDeleteButton 在应用的任何地方都是可用的。

多个应用实例 应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。


const app1 = createApp({
  /* ... */
})
app1.mount('#container-1')

const app2 = createApp({
  /* ... */
})
app2.mount('#container-2')

如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上, 而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。

文本插值

双大括号会将数据解释为纯文本,而不是 HTML。 若想插入 HTML,你需要使用 v-html 指令:

在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容

指令

指令由 v- 作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。

v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:

(1)、在文本插值中 (双大括号)

(2)、在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中

支持表达式

每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return 后面。

因此,下面的例子都是无效的:

template
<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}

<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}

v-if v-show v-for

因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。

template

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-else 和 v-else-if 也可以在 <template> 上使用。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。请查看风格指南获得更多信息。

当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行。请查看列表渲染指南获取更多细节。

意 v-for 是如何对应 forEach 回调的函数签名的。实际上,你也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:

<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。

举例来说:

const numbers = ref([1, 2, 3, 4, 5])

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0)
})
template
<li v-for="n in evenNumbers">{{ n }}</li>

在计算属性不可行的情况下 (例如在多层嵌套的 v-for 循环中),你可以使用以下方法:

const sets = ref([
  [1, 2, 3, 4, 5],
  [6, 7, 8, 9, 10]
])

function even(numbers) {
  return numbers.filter((number) => number % 2 === 0)
}
template
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

在计算属性中使用 reverse() 和 sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:

diff

  • return numbers.reverse()
  • return [...numbers].reverse()

调用函数

我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName" 或 @click="handler"

事件处理器的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
const name = ref('Vue.js')

function greet(event) {
  alert(`Hello ${name.value}!`)
  // `event` 是 DOM 原生事件
  if (event) {
    alert(event.target.tagName)
  }
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button> //  方法事件处理器
<button @click="count++">Add 1</button> // 内联事件处理器

方法与内联事件判断

模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。

举例来说,foo、foo.bar 和 foo['bar'] 会被视为方法事件处理器,而 foo() 和 count++ 会被视为内联事件处理器。

在内联处理器中调用方法

除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:

function say(message) {
  alert(message)
}
template
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

在内联事件处理器中访问事件参数 有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>

function warn(message, event) {
  // 这里可以访问原生事件
  if (event) {
    event.preventDefault()
  }
  alert(message)
}

修饰符

.lazy 默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:


<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

.number 如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

<input v-model.number="age" />

如果该值无法被 parseFloat() 处理,那么将返回原始值。

number 修饰符会在输入框有 type="number" 时自动启用。

.trim 如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:

<input v-model.trim="msg" />

.prevent

修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault():

<form @submit.prevent="onSubmit">...</form>

当调用 onMounted 时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:

setTimeout(() => {
  onMounted(() => {
    // 异步注册时当前组件实例已丢失
    // 这将不会正常工作
  })
}, 100)

注意这并不意味着对 onMounted 的调用必须放在 setup() 或 <script setup> 内的词法上下文中。

onMounted() 也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup() 就可以。

setup

setup 开始

新的 setup 选项在组件创建之前执行

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时, 我们可以使用 <script setup> 来大幅度地简化代码。

ref

ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

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

一个包含对象类型值的 ref 可以响应式地替换整个对象:

const objectRef = ref({ count: 0 })

// 这是响应式的替换
objectRef.value = { count: 1 }

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

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

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

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

reactive 注意丧失响应性

定义一个响应式对象

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

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本。

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

js let state = reactive({ count: 0 })

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

toRefs

使用reactive定义的响应式对象解构后每个属性依然是响应式

<script lang="ts">
import { ref, computed, reactive, toRefs} from 'vue'
interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}
export default {
  name: 'App',
  setup() {
    const data: DataProps  = reactive({
      count: 0,
      increase: () => { data.count++},
      double: computed(() => data.count * 2),
    })
   
    const refData = toRefs(data)
    return {
      ...refData,
    }
  }
};
</script>

生命周期钩子函数

// 在setup中使用
选项式 API           | Hook inside `setup` |
| ----------------- | ------------------- |
| `beforeCreate`    | Not needed*         |
| `created`         | Not needed*         |
| `beforeMount`     | `onBeforeMount`     |
| `mounted`         | `onMounted`         |
| `beforeUpdate`    | `onBeforeUpdate`    |
| `updated`         | `onUpdated`         |
| `beforeUnmount`   | `onBeforeUnmount`   |
| `unmounted`       | `onUnmounted`       |
| `errorCaptured`   | `onErrorCaptured`   |
| `renderTracked`   | `onRenderTracked`   |
| `renderTriggered` | `onRenderTriggered` |
| `activated`       | `onActivated`       |
| `deactivated`     | `onDeactivated`

计算属性

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

计算属性缓存 vs 方法 你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:

<p>{{ calculateBooksMessage() }}</p>
js
// 组件中
function calculateBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。 一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果, 而不用重复执行 getter 函数。

这也解释了为什么下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:

const now = computed(() => Date.now())

相比之下,方法调用总是会在重渲染发生时再次执行函数。

为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list。 没有缓存的话,我们会重复执行非常多次 list 的计算函数,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。

最佳实践

计算函数不应有副作用

计算属性的计算函数应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在计算函数中做异步请求或者更改 DOM!

一个计算属性的声明中描述的是如何根据其他值派生一个值。因此计算函数的职责应该仅为计算和返回该值。

在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。

避免直接修改计算属性值

从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,

因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

watch

setup中用watch函数

 watch(result, () => {
      if (result.value) {
        console.log('value', result.value[0].url)
      }
    })

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

watchEffect()

watch() 是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。

举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。我们可以这样写

const url = ref('https://...')
const data = ref(null)

async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}

// 立即获取
fetchData()
// ...再侦听 url 变化
watch(url, fetchData)

我们可以用 watchEffect 函数 来简化上面的代码。watchEffect() 会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源。上面的例子可以重写为:

js

watchEffect(async () => {
  const response = await fetch(url.value)
  data.value = await response.json()
})

这个例子中,回调会立即执行。在执行期间,它会自动追踪 url.value 作为依赖(和计算属性的行为类似)。每当 url.value 变化时,回调会再次执行。

当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

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

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

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

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

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

import { watchPostEffect } from 'vue'

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

停止侦听器

在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。 因此,在大多数情况下,你无需关心怎么停止一个侦听器。

一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:

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

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:

// 需要异步请求得到的数据
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})

访问模板、组件、dom引用 ref

为了通过组合式 API 获得该模板引用,我们需要声明一个同名的 ref:

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

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!

如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况:

watchEffect(() => {
  if (input.value) {
    input.value.focus()
  } else {
    // 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
  }
})

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

组件

传递 props

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps 宏:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。 defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:

我们可以通过 defineEmits 宏来声明需要抛出的事件:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

和 defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 emit方法的emit函数。它可以被用于在组件的<scriptsetup>中抛出事件,因为此处无法直接访问emit 方法的 emit 函数。 它可以被用于在组件的 `<script setup>` 中抛出事件,因为此处无法直接访问 emit:

<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

动态组件 有些场景会需要在两个组件间来回切换,比如 Tab 界面:

在演练场中查看示例

上面的例子是通过 Vue 的 <component> 元素和特殊的 is attribute 实现的:

<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

在上面的例子中,被传给 :is 的值可以是以下几种:

被注册的组件名 导入的组件对象 你也可以使用 is attribute 来创建一般的 HTML 元素。

当使用<component :is="...">来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持“存活”的状态。

元素位置限制 某些 HTML 元素对于放在其中的元素类型有限制,例如<ul>,<ol>,<table><select>,相应的,某些元素仅在放置于特定元素中时才会显示,例如 <li>,<tr><option>

这将导致在使用带有此类限制元素的组件时出现问题。例如:

<table>
  <blog-post-row></blog-post-row>
</table>

自定义的组件<blog-post-row>将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。我们可以使用特殊的 is attribute 作为一种解决方案:

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

当使用在原生 HTML 元素上时,is 的值必须加上前缀 vue: 才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。

自定义 Hooks

就是自定义函数,函数内容的写法类似于setup函数,可以用vue的api如ref、onMounted等,父组件中用的时候调用这个函数

(1)useMousePosition / useURLLoader

定义hooks

// useURLLoader.ts
import { ref } from 'vue'
import axios from 'axios'

function useURLLoader<T>(url: string) {
  const result = ref<T | null>(null)
  const loading = ref(true)
  const loaded = ref(false)
  const error = ref(null)

  axios.get(url).then((rawData) => {
    loading.value = false
    loaded.value = true
    result.value = rawData.data
  }).catch(e => {
    error.value = e
    loading.value = false
  })

  return {
    result,
    loading,
    error,
    loaded
  }
}

export default useURLLoader

使用hooks

import useURLLoader from './hooks/useURLLoader'
interface CatResult {
  id: string;
  url: string;
  width: number;
  height: number;
}
const { result, loading, loaded } = useURLLoader<CatResult[]>('https://api.thecatapi.com/v1/images/search?limit=1')

(2)将相关的 feature 组合在一起

(3)非常易于重用 Vue3用hook代替mixin

自定义函数的优点

(1)以函数的形式调用,清楚的了解参数和返回的类型

(2)避免命名冲突

(3)代码逻辑脱离组件存在

(4)泛型在函数中的使用

和 Typescript 结合

(1)defineComponent

import { defineComponent } from 'vue'
const component = 
 defineComponent({
  name: 'HelloWorld',
  props: {
    msg: {
      required: true,
      type: String
    }
  }, 
  setup(props, context) {
    console.log(props)
    console.log(context)
  }
})
export default component;

Teleport

瞬移组件的位置

<template>
<teleport to="#modal">
  <div id="center" v-if="isOpen">
    <h2><slot>this is a modal</slot></h2>
    <button @click="buttonClick">Close</button>
  </div>
</teleport>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  props: {
    isOpen: Boolean,
  },
  emits: {
    'close-modal': null
  },
  setup(props, context) {
    const buttonClick = () => {
      context.emit('close-modal')
    }
    return {
      buttonClick
    }
  }
})
</script>

使用

// 这种modal在父组件中一直存在,通过父组件参数传到子组件中,在子组件中控制显隐,
// 如果出现子组件中数据显隐后一直存在,可以在父组件中用v-if控制子组件显隐
<template>
  <div id="app">
    <button @click="openModal">Open Modal</button><br/>
    <modal :isOpen="modalIsOpen" @close-modal="onModalClose"> My Modal !!!!</modal>
  </div>
</template>

<script lang="ts">
import { ref} from 'vue'
import Modal from './components/Modal.vue'
export default {
  name: 'App',
  components: {
    Modal,
  },
  setup() {
    const modalIsOpen = ref(false)
    const openModal = () => {
      modalIsOpen.value = true
    }
    const onModalClose = () => {
      modalIsOpen.value = false
    }
    return {
      modalIsOpen,
      openModal,
      onModalClose,
    }
  }
};
</script>

Suspense

异步加载组件的新福音

(1)setup 返回一个 Promise

(2)内置的 Suspense 标签,有两个 slot template

(3)可以添加多个异步组件

// AsyncShow.ts
<template>
  <h1>{{result}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  setup() {
    return new Promise((resolve) => {
      setTimeout(() => {
        return resolve({
          result: 42
        })
      }, 3000)
    })
  }
})
</script>

// dogShow.ts
<template>
  <img :src="result && result.message">
</template>
<script lang="ts">
import axios from 'axios'
import { defineComponent } from 'vue'
export default defineComponent({
  async setup() {
    const rawData = await axios.get('https://dog.ceo/api/breeds/image/random')
    return {
      result: rawData.data
    }
  }
})
</script>

使用

<template>
  <div id="app">
    <Suspense>
      <template #default>
        <div>
          <async-show />
          <dog-show />
        </div>
      </template>
      <template #fallback>
        <h1>Loading !...</h1>
      </template>
    </Suspense>
  </div>
</template>

<script lang="ts">
import { ref, computed, reactive, toRefs, watch, onErrorCaptured } from 'vue'
import AsyncShow from './components/AsyncShow.vue'
import DogShow from './components/DogShow.vue'
export default {
  name: 'App',
  components: {
    AsyncShow,
    DogShow,
  },
  setup() {
  }
};
</script>

全局 API 修改

(1)从 vue2 的全局 Vue 对象 变成了 Vue3 中 的多实例

createApp 解决vue2中直接修改Vue类带来的风险,改为修改vue的实例app

x0D2GcTKxA.png

ODBXSg8FHw.png

应用实例和组件实例

Vue2

  • 每个 Vue 应用都是 new Vue 函数创建的一个新的实例 组件实例
  • 创建的时候将 data 作为 property 添加到响应式系统中
  • cn.vuejs.org/v2/guide/in…

Vue3 - Application Instance 应用实例

  • createApp 创建一个 Application Instance 应用实例
  • 应用实例用来注册应用中的全局内容
  • 大多数方法支持链式调用,返回应用实例
  • v3.vuejs.org/guide/insta…
  • createApp 传递的那个组件,称之为 root component
const app = createApp(App)
app.use().use()

Vue3 - Component Instance 组件实例

刚才我们说 应用实例上的大部分方法都返回应用实例本身, 当然这里面是有例外的,比如 mount 方法,

  • mount 方法用来:

    • 将 应用实例 挂载到 DOM 节点上
    • 返回的不是应用实例,而是组件实例(和 Vue2 那个一样)
const vm = app.mount('#app') // app应用实例,vm组件实例
console.log(vm)
  • 关于 组件实例

    • 组件中的所有属性 (methods,props,computed,setup …) 都会在实例上平铺展示
    • 还有一系列内置或者全局的属性,比如内置的 attrs,attrs, refs, 全局的 message,message, confirm
  • 各种属性解释 请看文档 v3.vuejs.org/api/instanc…

  • 通过 ref 拿到的子组件实例属于组件实例

这就是 Vue3 中组件的两大类型

script setup 使用

参照

element-plus使用问题

(1) 在dialog中使用v-loading有问题,由于dialog的z-index高于v-loading所以遮罩层展示不出来,现在的解决方案是dialog中的主题内容放到div中loading,dialog的按钮loading

const loadingInstance = ElLoading.service({
    target: 'elDialogRef',
    fullscreen: false // fullscreen是false的时候loading层z-index是2000,比dialog要低,所以看不到,fullscreen为true的时候又会全屏
  });

(2)、elementUI select 回显 从路由传过来的参数query都是字符串,如果select中的value是数字,要parseInt一下

(3)、table的回显 要用要回显的id去匹配table列表,取列表中的数据item进行toggle,不能直接用回显的数据toggle

(4)、样式 <style scope> 中的样式 编译后会有后缀 v-5242d2d7,所以唯一,不会污染全局

整个组件都有

<ul class="infinite-list" data-v-5242d2d7 style="overflow: auto;">
  <li class="infinite-list-item active" data-v-5242d2d7>
    <div data-v-5242d2d7 style="font-size: 12px;">模型层级</div>
  </li>
</ul>
.infinite-list[data-v-5242d2d7] { 
    height: 61vh; 
    padding: 0; 
    margin: 0; 
    list-style: none; 
 } 
 .infinite-list .infinite-list-item[data-v-5242d2d7] { 
     display: flex; 
     align-items: center; 
     margin: 10px; 
     padding: 0 10px; 
     border-radius: 4px; 
     background: #f4f4f5; 
     cursor: pointer; 
 }    

这样的样式在最初渲染就定了,像后端给的数据里有<span class="highlight">wwww</span> v-html渲染 即使组件里highlight样式,也会渲染不出样式,因为后来生成的html里没有后缀v-5242d2d7

这样就要写全局样式,全局样式不需要后缀

// 修改elementUI样式 要在#app下 全局有效
#app .is-link:focus {
  color: #409eff;
}
// 全局有效
.highlight {
  color: #f1403c;
}

(4)表单校验

// 可以写多个rules 动态表单校验要写好对应的循环的prop
<el-form ref="tableFormRef" :model="state.tableForm" :rules="state.tableFormRules" label-width="100px">
    <el-table :data="state.tableForm.human.columns" border style="margin-bottom: 20px;">
                <el-table-column prop="columnName" label="字段名称" width="250">
                  <template #default="scope">
                    <el-form-item label="" label-width="0" :prop="`human.columns.${scope.$index}.columnName`" :rules="{required: true, validator: checkColumnName, trigger: 'change'}">
                      <el-input v-model="scope.row.columnName" :disabled="state.update && !!scope.row.columnId" placeholder="请填写字段名称"/>
                    </el-form-item>
                  </template>
                </el-table-column>

(5)、刷新当前页面 通过在父页面的<router-view></router-view>上添加v-if的控制来销毁和重新创建页面的方式刷新页面,并且用到provideinject实现多层级组件通信方式,父页面通过provide提供reload方法,子页面通过inject获取reload方法,调用方法做刷新

<script>
//框架页
let Layout = {
    template: `
        <div class="container">
            <div class="aside">左侧菜单</div>    
            <!-- 通过v-if实现销毁和重新创建组件 -->
            <div class="main"><router-view v-if="isRouterAlive"></router-view></div>
        </div>
    `,
    created() {
        console.log('框架页加载')
    },
    // 通过provide提供reload方法给后代组件
    provide(){
        return {
            reload: this.reload
        }
    },
    data(){
        return {
            isRouterAlive: true
        }
    },
    methods: {
        async reload(){
            this.isRouterAlive = false
            //通过this.$nextTick()产生一个微任务,在一次dom事件循环后,重新创建组件
            await this.$nextTick()
            this.isRouterAlive = true
        }
    }
}
//首页
let Home = {
    template: `
        <div>
            首页
            <button @click="onClick">刷新</button>
        </div>
    `,
    created() {
        console.log('首页加载')
    },
    //通过inject获取祖先元素的reload方法
    inject: ['reload'],
    methods: {
        onClick(){
            this.reload()
        }
    },
}
//路由配置
let router = new VueRouter({
    routes: [
        {path: '/', component: Layout, children:[
            {path: '', component: Home}
        ]}
    ]
}) 
Vue.use(VueRouter)
//根组件
new Vue({
    router,
    el: '#app'
})
</script>