跳到主要内容

Vue-新变化

整体的提升和优化

源码体积的优化

  • 重写了虚拟dom
  • 移除了不常用的API
  • 更好的tree shaking支持

响应式系统的升级

  • 使用ProxyReflect来代替vue2中的Object.defineproperty
  • 可以监听动态新增/删除的属性
  • 可以监听数组的索引和length属性

代码编译优化

  • 使用了组合式API来代替options api, 更方便维护
  • 组件内部不需要根节点了
  • vue3会标记和提升所有的静态根节点, diff只比较动态节点内容, 静态节点被提升到render方法之外, 对于动态组件, 会在其末尾添加patchFlag, 用于快速的找到动态节点, 而不用逐级遍历, 提高了dom diff的性能
  • 缓存时间处理函数cacheHandler, 避免了每次触发都要重新生成function去更新之前的函数
  • tree shaking 优化

全局API 变化

import { createApp } from 'vue'

const app = createApp({})

现在Vue不会再是一个全局的属性挂载, 而是创建一个应用实例.

原本的一些全局调用的api也就被迁移到了app实例上面:

2.x 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTipremoved (见下方)
Vue.config.ignoredElementsapp.config.isCustomElement (见下方)
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use (见下方)

所有其他的不改变全局行为的全局API现在都成为了exports的属性

config.productionTip移除

对于 ES 模块构建,由于它们是与 bundler 一起使用的,而且在大多数情况下,CLI 或样板已经正确地配置了生产环境,所以本技巧将不再出现

config.ignoredElements替换为config.isCustomElement

// before
Vue.config.ignoredElements = ['my-el', /^ion-/]

// after
const app = Vue.createApp({})
app.config.isCustomElement = tag => tag.startsWith('ion-')

并且, Vue.use已经停止使用, 因此现在需要开发者在应用程序实力上显示的指定使用该插件:

const app = createApp(MyApp)
app.use(VueRouter)

Teleport

用于把当前的组件的部分内容挂载到全局的任意位置, 例如下面这个例子:

app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>

<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})

与 Vue Components 一起使用

const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component />
`
})

app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#endofbody">
<child-component name="John" />
</teleport>
`
})

app.component('child-component', {
props: ['name'],
template: `
<div>Hello, {{ name }}</div>
`
})

在这个例子下面, 即使在不同的地方渲染child-component, 它任然是parent-component的子组件, 并将从中接受name的prop.

在多个目标上使用多个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>

片段

Vue3 现在支持多根组件了, 不过要求明确定义属性应该分布在哪里:

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

自定义事件

事件名

事件名不存在大小写转换, 因此推荐使用kebab-case的事件名.

定义自定义事件

app.component('custom-form', {
emits: ['in-focus', 'submit']
})

推荐定义所有发出的事件.

验证抛出的事件

app.component('custom-form', {
emits: {
// 没有验证
click: null,

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

返回的布尔值被用来指示该事件是否有效.

v-model

默认的, 组件现在使用modelValue属性和update:modeValue作为时间. 我们可以通过向v-model传递参数来修改这些名称:

<my-component v-model:foo="bar"></my-component>

对于这个组件, 相应的内容如下:

const app = Vue.createApp({})

app.component('my-component', {
props: {
foo: String
},
template: `
<input
type="text"
:value="foo"
@input="$emit('update:foo', $event.target.value)">
`
})

多个v-model

<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>

实现示例如下:

const app = Vue.createApp({})

app.component('user-name', {
props: {
firstName: String,
lastName: String
},
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">

<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
})

修饰符

在 3.x 中,添加到组件v-model的修饰符将通过modelModifiers prop提供给组件:

<my-component v-model.capitalize="bar"></my-component>

该修饰符实现如下:

app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
template: `
<input type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
`,
created() {
console.log(this.modelModifiers) // { capitalize: true }
}
})

对于带参的model, 则将model替换为对应的参数即可.

响应式API

ref

vue3中, 我们通过这个方法使一个变量变成一个响应式变量

import { ref } from 'vue'

const counter = ref(0)

ref接受参数并返回它包装在具有valueproperty的对象中, 然后可以使用该property访问或者更改响应式变量的值.

import { ref } from 'vue'

const counter = ref(0)

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

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

::: tip 因为在js中, 基础类型是值传递而不是引用传递的, 所以有必要将它放在一个对象中. ref的本质就是创建了一个响应式引用. :::

reactive

要为对象创建响应性, 可以使用reactive方法:

import { reactive } from 'vue'

// 响应式状态
const state = reactive({
count: 0
})

响应式解构

解构会导致property失去响应性. 对于这种情况, 我们可以将响应式对象转换为一组ref:

import { reactive, toRefs } from 'vue'

const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})

let { author, title } = toRefs(book)

title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'

readonly

readonly可以防止响应式对象被改变:

import { reactive, readonly } from 'vue'

const original = reactive({ count: 0 })

const copy = readonly(original)

// 在copy上转换original 会触发侦听器依赖

original.count++

// 转换copy 将导失败并导致警告
copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."

computed

类似于computed属性, 用于在vue组件外部创建计算属性:

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2

watchEffect

watchEffect会立即执行传入的一个函数, 同时响应式追踪其依赖, 并在依赖滨化时重新运行该函数:

const count = ref(0)

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

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

watchEffect会在组件卸载时自动停止, 也可以手动停止:

const stop = watchEffect(() => {
/* ... */
})

// later
stop()

此外, 当watchEffect中的提供了一个失效回调:

watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})

在执行数据请求的时候, 副作用函数往往是一个异步函数:

const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {...}) // 我们在Promise解析之前注册清除函数
data.value = await fetchData(props.id)
})

清理函数必须在Promiseresolve之前被注册, 另外, Vue依赖这个返回的promise来自动处理promise链上的潜在错误.

副作用的刷新时间

vue的向影心提供会缓存副作用函数, 并一步的刷新他们. 并在会在所有组件的update前执行.

如果你需要在组件update后执行watchEffect, 则需要传递带有flush选项的附加options:

// fire before component updates
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)

侦听器调试

  • onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。
watchEffect(
() => {
/* 副作用 */
},
{
onTrigger(e) {
debugger
}
}
)

watch

相比于watchEffect, watch可以:

  • 懒执行副作用
  • 具体的说明什么状态应该处罚侦听器重新运行
  • 访问侦听状态变化前后的值

就像watch选项一样, 可以在一个property上设置一个侦听器, 他接受三个参数:

  • 一个响应式引用或者我们想要的侦听的getter函数
  • 一个回调
  • 一个可选的配置项
import { ref, watch } from 'vue'

// 直接侦听ref
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})

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

或者侦听多个数据源:

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

对于停止侦听, 清除副作用, 副作用刷新时机和侦听器调试行为与watchEffect一致.

setup

setup执行在创建组件之前, props被解析之后, 并作为合成API的入口

因此这个时候setup是无法访问this的, 只能访问prop

setup接受propcontext, 返回的所有内容则会暴露给组件的其余部分

props

setup函数接受的第一个参数是props, 并且他是响应式的. 当新的prop被传入时, 就会更新.

::: tip 不要用es6结构prop, 这样会消解prop的响应性. :::

如果需要解构, 则应该使用toRefs来手动赋予响应性:

import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}

context

context是传递给setup函数的第二个参数, 主要暴露三个组件的property.

export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)

// 插槽 (非响应式对象)
console.log(context.slots)

// 触发事件 (方法)
console.log(context.emit)
}
}

context是普通的js对象, 因此可以安全的解构:

export default {
setup(props, { attrs, slots, emit }) {
...
}
}

访问组件的property

执行setup时, 可以访问:

  • props
  • attrs
  • slots
  • emit

无法访问:

  • data
  • computed
  • methods

结合template

setup返回的对象属性可以在template中直接访问, 并且对于refs是不需要手动解开的:

<template>
<div>{{ readersNumber }} {{ book.title }}</div>
</template>

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

export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })

// expose to template
return {
readersNumber,
book
}
}
}
</script>

使用渲染函数

也可以返回一个渲染函数, 该函数可以直接使用在同一作用域中声明的响应式状态

import { h, ref, reactive } from 'vue'

export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
return () => h('div', [readersNumber.value, book.title])
}
}

不要误用this

setup内部的this是不同于其他选项中的this的. 减少使用可以减少混淆

生命周期钩子

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

beforeCreatecreated生命周期钩子是不需要显示的定义的, 这两个钩子中的任何代码都可以在setup中直接编写.

这些函数接受一个回调:

export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}

Provide / Inject

setup中显示导入provide, prodive允许通过两个参数定义每个property:

  • property 的 name
  • property 的 value
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue

export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}
</script>

然后在子组件中显示调用inject, 同样的, inject也有两个参数:

  • 要注入的property的名称
  • 一个默认的值(可选)
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')

return {
userLocation,
userGeolocation
}
}
}
</script>

响应性

为了增加provideinject之间的响应性, 我们可以在provide里面使用ref或者reactive.

这样, 就可以为属性增加响应性:

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})

provide('location', location)
provide('geolocation', geolocation)
}
}

修改响应式property

建议尽可能,在provide内保持响应式 property 的任何更改

比如这样:

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})

provide('location', location)
provide('geolocation', geolocation)

return {
location
}
},
methods: {
updateLocation() {
this.location = 'South Pole'
}
}
}

如果需要在inject端修改输入的数据, 可以provide一个方法来负责修改响应式的property.

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})

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

provide('location', location)
provide('geolocation', geolocation)
provide('updateLocation', updateLocation)
}
}

在响应端:

import { inject } from 'vue'

export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')

return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}

最后, 如果要保证property不被注入的组件修改, 最好对property加上readonly:

import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
...
setup() {
...

provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}

模板引用

<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>这是根元素</div>
})

return {
root
}
}
}
</script>

在使用组合API的时候, 响应式引用和模板引用的概念是同一的.

这里我们在渲染上下文中暴露root, 并通过ref=root将之绑定到div作为其ref. 在virtual domdiff过程中, 如果vnoderef对应了上下文中的ref, 就会把vnode的元素或者组件的实例挂载到ref的值上

JSX

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

return () =>
h('div', {
ref: root
})

// with JSX
return () => <div ref={root} />
}
}

v-for

<template>
<div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
{{ item }}
</div>
</template>

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

export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])

// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
divs.value = []
})

return {
list,
divs
}
}
}
</script>

参考链接