Vue3.0 新特性(一)

140 阅读3分钟

1. Teleport

Teleport (俗称传送门组件) 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML。

  • Props:
    • to - string
      必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用). 指定将在其中移动 <teleport> 内容的目标元素
    • disabled - boolean。(可选属性)
      用于禁用 <teleport> 的功能,这意味着其插槽内容将不会移动到任何位置,而是在你在周围父组件中指定了 <teleport> 的位置渲染。
<teleport to="#modals">
    <div>A</div>
</teleport>
<teleport to="#modals">
    <div>B</div>
</teleport>

<!-- result-->
<div id="modals">
    <div>A</div>
    <div>B</div>
</div>

2. 片段

组件可以包含多个根节点
这要求开发者显式定义 attribute 应该分布在哪里

<!-- Layout.vue -->
<template>
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
</template>

3. 触发组件选项

emits

  • 类型: Array<string> | Object

  • 详细:

    emits 可以是数组或对象,对象允许配置事件验证。

    对象语法,每个 property 的值可以为 null 或验证函数。 验证函数将接收传递给 $emit 调用的其他参数。如果 this.$emit('foo',1) 被调用,foo 的相应验证函数将接收参数 1。验证函数应返回布尔值,以表示事件参数是否有效。

  • 用法:

export default {
    emits: ['submit'],
    created() {
      this.$emit('submit')
    },
}

验证抛出的事件

export default {
    emits: {
        // 没有验证
        click: null,

        // 验证 submit 事件
        submit: ({ email, password }) => {
            if (email && password) {
                return true
            } else {
                console.warn('Invalid submit event payload!')
                return false
            }
        }
    },
}

4. 单文件组件状态驱动的 CSS 变量

<template>
    <div class="text">hello</div>
</template>

<script>
export default {
    data() {
        return {
        color: 'red'
        }
    }
}
</script>

<style>
.text {
    color: v-bind(color);
}
</style>

5. 单文件组件 <style scoped> 现在可以包含全局规则或只针对插槽内容的规则

<style scoped>
/* 深度选择器 */
:deep(.foo) {}

/* 插槽选择器 */
:slotted(.foo) {}

/* 全局选择器 */
:global(.foo) {}
</style>

6. 组合式 API

setup

一个组件选项,在组件被创建之前,props 被解析之后执行。它是组合式 API 的入口。

参数:

- {Data} props
- {SetupContext} context
    - attrs     Attribute (非响应式对象,等同于 $attrs)
    - slots     插槽 (非响应式对象,等同于 $slots)
    - emit      触发事件 (方法,等同于 $emit)
    - expose    暴露公共 property (函数)

访问组件的 property:

执行 `setup` 时,你只能访问以下 property:

-   `props`
-   `attrs`
-   `slots`
-   `emit`

换句话说,你**将无法访问**以下组件选项:

-   `data`
-   `computed`
-   `methods`
-   `refs` (模板 ref)

注:props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作。

toRefs 可以在不丢失响应性的情况下对返回的对象进行解构/展开

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)

  console.log(title.value)
}

reactive、ref

reactive

  • 返回对象的响应式副本

用法:

const obj = reactive({ count: 0 })

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

reactive 将解包所有深层的 refs,同时维持 ref 的响应性。

const count = ref(1)
const obj = reactive({ count })

// ref 会被解包
console.log(obj.count === count.value) // true

// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

ref

  • 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
const count = ref(0)
console.log(count.value) // 0

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

对比

相同: 创建一个响应式对象

不同:

  • reactive 接受入参必须是对象或者数组,而 ref 可以是对象、数组,也可以是一个单值(基本数据类型)
  • 读取/赋值不一样,ref 必须从.value 属性中读取值

模板引用

<template> 
  <div ref="root">This is a root element</div>
</template>

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

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

      onMounted(() => {
        // DOM 元素将在初始渲染后分配给 ref
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

computed 与 watch

computed

  • 接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2
  • 或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

watch

  • watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。

参数:

  • {string | Function} source
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate
    • {string} flush

返回:{Function} unwatch


flush:

  • 作用:flush 选项可以更好地控制回调的时间。
  • 可选值: 'pre'、'post' 或 'sync'
  • 默认值:'pre'

'pre': 在渲染前调用指定的回调。
‘post’: 在渲染后调用指定的回调。如果回调需要通过 $refs 访问更新的 DOM 或子组件,那么则使用该值。
'sync': 一旦值发生了变化,回调将被同步调用。(少用)

对于 'pre' 和 'post',回调使用队列进行缓冲。回调只被添加到队列中一次,即使观察值变化了多次。值的中间变化将被跳过,不会传递给回调。

1、侦听单一源

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

2、侦听多个源

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

与 watchEffect 相比,watch 允许我们:

  • 惰性地执行副作用(即只有当被侦听的源发生变化时才执行回调);
  • 更具体地说明应触发侦听器重新运行的状态;
  • 访问被侦听状态的先前值和当前值。

watchEffect

  • 立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

watchPostEffect

  • watchEffect 的别名,带有 flush: 'post' 选项

watchSyncEffect

  • watchEffect 的别名,带有 flush: 'sync' 选项。

生命周期钩子

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

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

getCurrentInstance

getCurrentInstance 支持访问内部组件实例。

import { getCurrentInstance } from 'vue'

const MyComponent = {
  setup() {
    const internalInstance = getCurrentInstance()

    internalInstance.appContext.config.globalProperties // 访问 globalProperties
  }
}

7. 单文件组件组合式 API 语法糖

单文件组件<script setup>

是什么:是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖

优势:

  • 简洁:更少的样板内容,更简洁的代码。
  • 性能:更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • IDE:更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
  • TS:能够使用纯 Typescript 声明 props 和抛出事件。

用法:

  • 任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用.
  • 使用自定义指令
  • defineProps 声明 props
  • defineEmits 声明 emits
  • defineExpose 声明 要暴露出去的属性
  • useSlotsuseAttrs
  • 与普通的 <script> 一起使用
  • 使用自定义指令

注1:defineProps 和 defineEmits 都是只在 <script setup> 中才能使用的编译器宏。他们不需要导入且会随着 <script setup> 处理过程一同被编译掉。

注2: 使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。
为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏

definePropsdefineEmits

<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
</script>

defineExpose

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

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

useSlotsuseAttrs

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

使用自定义指令

限制:必须以 vNameOfDirective 的形式来命名本地自定义指令

<script setup>
const vMyDirective = {
  beforeMount: (el) => {
    // 在元素上做些操作
  }
}
</script>
<template>
  <h1 v-my-directive>This is a Heading</h1>
</template>
<script setup>
  // 导入的指令同样能够工作,并且能够通过重命名来使其符合命名规范
  import { myDirective as vMyDirective } from './MyDirective.js'
</script>

与普通的 <script> 一起使用

  • 无法在 <script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项。
  • 声明命名导出。
  • 运行副作用或者创建只需要执行一次的对象。

注意:该场景下不支持使用 render 函数。请使用一个普通的 <script> 结合 setup 选项来代替。

<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()

// 声明额外的选项
export default {
  inheritAttrs: false,
  customOptions: {}
}
</script>

<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>

8. vuex

useStore

访问 State 和 Getter

import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()

    return {
      // 在 computed 函数中访问 state
      count: computed(() => store.state.count),

      // 在 computed 函数中访问 getter
      double: computed(() => store.getters.double)
    }
  }
}

访问 Mutation 和 Action

import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()

    return {
      // 使用 mutation
      increment: () => store.commit('increment'),

      // 使用 action
      asyncIncrement: () => store.dispatch('asyncIncrement')
    }
  }
}

9. vue-router

路由和当前路由:useRouter, useRoute

import { useRouter, useRoute } from 'vue-router'

export default {
setup() {
    const router = useRouter()
    const route = useRoute()

    function pushWithQuery(query) {
    router.push({
        name: 'search',
        query: {
        ...route.query,
        },
    })
    }
},
}

导航守卫:onBeforeRouteLeave, onBeforeRouteUpdate

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

export default {
setup() {
    // 与 beforeRouteLeave 相同,无法访问 `this`
    onBeforeRouteLeave((to, from) => {
    const answer = window.confirm(
        'Do you really want to leave? you have unsaved changes!'
    )
    // 取消导航并停留在同一页面上
    if (!answer) return false
    })

    const userData = ref()

    // 与 beforeRouteUpdate 相同,无法访问 `this`
    onBeforeRouteUpdate(async (to, from) => {
    //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
    if (to.params.id !== from.params.id) {
        userData.value = await fetchUser(to.params.id)
    }
    })
},
}

10. 其他变更

组件上 v-model 用法已更改,以替换 v-bind.sync

非兼容: 用于自定义组件时,v-model prop 和事件默认名称已更改:

  • prop:value -> modelValue;
  • 事件:input -> update:modelValue;
    非兼容: v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替;
    新增: 现在可以在同一个组件上使用多个 v-model 绑定;
    新增: 现在可以自定义 v-model 修饰符。

vue 2.0: v-model 默认会利用名为 value 的 prop 和名为 input 的事件.

<my-component v-model="bookTitle"></my-component>

<!-- 是以下的简写: -->
<my-component :value="bookTitle" @input="bookTitle = $event"></my-component>
Vue.component('my-component', {
    model: {
        prop: 'title',
        event: 'change'
    },
    props: {
        title: String
    },
    template: `
        <input
            type="text"
            :value="title"
            @input="$emit('change', $event.target.value)">
        `
})
<my-component :title.sync="bookTitle"></my-component>

<!-- 是以下的简写: -->
<my-component :title="bookTitle" @update:title="bookTitle = $event"></my-component>

vue 3.0: 默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。

<my-component v-model:title="bookTitle"></my-component>

<!-- 是以下的简写: -->
<my-component
    :title="bookTitle"
    @update:title="bookTitle = $event"
></my-component
app.component('my-component', {
props: {
    title: String
},
emits: ['update:title'],
template: `
    <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)">
`
})

其他:处理 v-model 修饰符

<template v-for> 和非 v-for 节点上的 key 用法已更改

  • <template>可设置 key
  • v-if/v-else/v-else-if 不建议设置 key,会自动生成唯一的 key

v-on:event.native 修饰符已移除

移除过滤器(filter)

destroyed 生命周期选项被重命名为 unmounted

beforeDestroy 生命周期选项被重命名为 beforeUnmount

v-if 总是优选 v-for 生效