vue3 特点
- vue3 支持 vue2 的大多数特性
- 性能提升:
打包大小减少41%
初次渲染快55%,更新快133%
内存使用减少54% - Composition API
ref 和 reactive
computed 和 watch
新的生命周期函数
自定义函数——Hooks 函数 - 其他新增特性
Teleport——瞬移组建的位置
Suspense——异步加载组件的新福音
全局 API 的修改和优化
更多的试验性特性 - 更好的 Typescript 支持
为什么要有 vue3
- vue2 遇到的难题:同一逻辑分类的代码分散,不利于维护。
- mixins 难点:命名冲突、不清楚暴露出来变量的作用、组件复用时会遇到问题
- vue2 对于 typescript 的支持非常有限
应用和组件
-
创建应用:
- vue3:
const app = Vue.createApp({}) app.component('SearchInput', SearchInputComponent) app.directive('focus', FocusDirective) app.use(LocalePlugin) app.mount('#app') // 或 Vue.createApp({}) .component('SearchInput', SearchInputComponent) .directive('focus', FocusDirective) .use(LocalePlugin) .mount('#app')
- vue2:
new Vue({ el: '#app', data: obj })
组件是可复用的
Vue
实例,且带有一个名字,如
。我们可以在一个通过new Vue
创建的Vue
根实例中,把这个组件作为自定义元素来使用。
生命周期钩子函数
// 主要区别在于销毁时
beforeCreate() { console.log('实例刚刚被创建') },
created() { console.log('实例创建完成') },
beforeMount() { console.log('实例挂载之前') },
// 请求数据,操作dom , 放在这个里面
mounted() { console.log('实例挂载完成') },
// 数据更新时,虚拟 DOM 变化之前调用,这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
beforeUpdate() { console.log('数据更新之前') },
// 数据更新和虚拟 DOM 变化之后调用。请不要在此函数中更改状态,否则会触发死循环。
updated() { console.log('数据更新完毕') },
// vue3:
// 实例销毁之前调用,在这一步,实例仍然完全可用。一般在这里移除事件监听器、定时器等,避免内存泄漏
beforeUnmount() { console.log('实例销毁之前') },
// Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁。
unmounted() { console.log('实例销毁完成') },
// vue2:
beforeDestroy() { console.log('实例销毁之前') },
destroyed() { console.log('实例销毁完成') },
不要在选项
property
、回调上或生命周期函数上使用箭头函数,比如created: () => console.log(this.a)
或vm.$watch('a', newValue => this.myMethod())
。
因为箭头函数并没有this
,this
会作为变量一直向上级词法作用域查找,直至找到为止。经常导致Uncaught TypeError: Cannot read property of undefined
或Uncaught TypeError: this.myMethod is not a function
之类的错误。
不常用模板语法
v-once
指令:执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:
计算属性和侦听器
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message
还没有发生改变,多次访问 reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
通常更好的做法是使用计算属性而不是命令式的 watch
回调。
Class 与 Style 绑定
自动添加前缀
当v-bind:style
使用需要添加浏览器引擎前缀的CSS property
时,如transform
,Vue.js
会自动侦测并添加相应的前缀。多重值
从2.3.0
起你可以为style
绑定中的property
提供一个包含多个值的数组,常用于提供多个带前缀的值,如:。
这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的flexbox
,那么就只会渲染display: flex
。
条件渲染
用
key
管理可复用的元素
Vue
会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
这样也不总是符合实际需求,所以Vue
为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的key
即可。-
v-if
vsv-show
1、v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
2、v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
3、相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS
进行切换。
4、一般来说,v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。注意,
v-show
不支持元素,也不支持
v-else
。 v-if
与v-for
一起使用
当v-if
与v-for
一起使用时,v-for
具有比v-if
更高的优先级,这意味着v-if
将分别重复运行于每个v-for
循环中。不推荐同时使用v-if
和v-for
。若使用,eslint
会报错~~
数组更新检测
Vue
将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:push()
、pop()
、shift()
、unshift()
、splice()
、sort()
、reverse()
事件绑定
同时绑定多个事件
methods: {
one(event) {
// first handler logic...
},
two(event) {
// second handler logic...
}
}
修饰符
事件修饰符
.prevent
:等同于JavaScript
中的event.preventDefault()
,用于取消默认事件
.stop
:等同于JavaScript
中的event.stopPropagation()
,防止事件冒泡(由内到外)
.self
:只会触发自己范围内的事件,不包含子元素
.capture
:与事件冒泡的方向相反,事件捕获由外到内
.once
:事件将只会触发一次
.passive
:设置{passive: true}
,表示处理事件函数中不会调用preventDefault
函数,减少了额外的监听,从而提高了性能;所以不能和.prevent
修饰符一同使用,否则浏览器会报错。尤其能够提升移动端的性能
.native
:把一个vue
组件转化为一个普通的HTML
标签,并且该修饰符对普通HTML
标签是没有任何作用的。按键修饰符
.enter
、.tab
、.esc
、.space
、.up
、.down
、.left
、.right
、.delete
: 捕获“删除”和“退格”键系统修饰键
.ctrl
、.alt
、.shift
、.meta
鼠标按钮修饰符
.left
、.right
、.middle
-
精确修饰符
.exact
:允许你控制由精确的系统修饰符组合触发的事件。 -
表单修饰符
.lazy
:在默认情况下,v-model
在每次input
事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加lazy
修饰符,从而转为在change
事件之后进行同步。
.number
:自动将用户的输入值转为数值类型。
.trim
:自动过滤用户输入的首尾空白字符。修饰符可以串联,也可以只有修饰符。
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素自身的点击。
组件
全局组件:使用
app.component('search-input', SearchInputComponent)
定义。只要定义了,处处可以使用,性能不高,但是使用起来简单。建议小写字母开头中划线间隔命名。局部组件:使用
components
注册。定义了,要注册之后才能使用,性能较高,使用起来麻烦。建议大写字母开头驼峰命名。-
Prop
1、Prop
的大小写
HTML
中的attribute
名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用DOM
中的模板时,camelCase
(驼峰命名法) 的prop
名需要使用其等价的kebab-case
(短横线分隔命名) 命名:const app = Vue.createApp({}) app.component('blog-post', { // camelCase in JavaScript props: ['postTitle'], template: '
{{ postTitle }}
' })2、传入一个对象的所有
property
如果你想要将一个对象的所有property
都作为prop
传入,你可以使用不带参数的v-bind
(取代v-bind:prop-name
)。例如,对于一个给定的对象post
:post: { id: 1, title: 'My Journey with Vue' }
下面的模板:
等价于:
3、单项数据流
所有的prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。4、Prop 验证
app.component('my-component', { props: { // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证) propA: Number, // 多个可能的类型 propB: [String, Number], // 必填的字符串 propC: { type: String, required: true }, // 带有默认值的数字 propD: { type: Number, default: 100 }, // 带有默认值的对象 propE: { type: Object, // 对象或数组默认值必须从一个工厂函数获取 default: function () { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function (value) { // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } })
5、非 prop 的 attribute
组件可以接受任意的attribute
,而这些attribute
会被添加到这个组件的根元素上。
如果你不希望组件的根元素继承attribute
,你可以在组件的选项中设置inheritAttrs: false
。(不会影响style
和class
的绑定)app.component('date-picker', { inheritAttrs: false, template: `
当组件有多个根节点时:
// This will raise a warning app.component('custom-layout', { template: `
... ... ` }) // No warnings, $attrs are passed toelement app.component('custom-layout', { template: ` ... ... ` })
自定义事件
不同于组件和 prop
,事件名不存在任何自动化的大小写转换。因为 HTML
是大小写不敏感的,因此推荐你始终使用 kebab-case 的事件名。
- 验证发出的事件
app.component('custom-form', {
emits: {
// No validation
click: null,
// Validate submit event
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
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
`
})
- 同时绑定多个
v-model
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
`
})
-
v-model
增加自定义修饰符
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: `
`,
created() {
console.log(this.modelModifiers) // { capitalize: true }
},
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
})
插槽
编译作用域:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容:
// slot 标签内为后备内容,父组件提供了插槽内容,则展示;否则展示后备内容
- 具名插槽:
v-slot
只能添加在上(独占默认插槽的缩写语法除外)
Here might be a page title
A paragraph for the main content.
And another one.
Here's some contact info
- 作用域插槽
为了让插槽内容能够访问子组件中才有的数据,可以将data
作为
元素的一个attribute
绑定上去。
{{ user.lastName }}
{{ slotProps.user.firstName }}
- 独占默认插槽的缩写语法
被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用
{{ slotProps.user.firstName }}
provide / inject
使用场景:由于 vue
有 $parent
属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过 provide / inject
可以轻松实现跨级访问祖先组件的数据。
provide:Object | () => Object
inject:Array
// TodoList -> TodoListFooter -> TodoListStatistics
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})
提示:provide
和 inject
绑定并不是可响应的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
动态组件与异步组件
动态组件:
注意这个
要求被切换到的组件都有自己的名字,不论是通过组件的
name
选项还是局部 / 全局注册。
异步组件:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
// or
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
mixin
混入 mixin
提供了一种非常灵活的方式,来分发 Vue
组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
- 例子
// 定义一个混入对象
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
const app = Vue.createApp({
mixins: [myMixin]
})
app.mount('#app') // => "hello from mixin!"
-
选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。- 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
- 值为对象的选项,例如
methods
、components
和directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
全局混入
混入也可以进行全局注册。一旦使用全局混入,它将影响每一个之后创建的Vue
实例 (包括第三方组件)。
const app = Vue.createApp({
// 自定义属性
myOption: 'hello!'
})
// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
created() {
// 获取自定义属性 this.$options.XXX
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
// add myOption also to child component
app.component('test-component', {
myOption: 'hello from component!'
})
app.mount('#app')
// => "hello!"
// => "hello from component!"
- 自定义选项合并策略
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向app.config.optionMergeStrategies
添加一个函数:
const app = Vue.createApp({})
// 返回合并后的值
app.config.optionMergeStrategies.customOption = (mixinVal, appVal) => mixinVal || appVal
自定义指令
- 注册一个全局自定义指令
v-focus
:
const app = Vue.createApp({})
app.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})
- 注册局部指令:
app.directive('focus', {
mounted (el) {
el.focus()
}
})
// 若只有 mounted 和 updated,且逻辑一致,可简写
// binding.arg 获取参数;binding.value 获取值
app.directive('top', (el, binding) => {
el.style.top = binding.value + 'px'
})
-
钩子函数
- created: 在绑定元素的属性或事件侦听器被应用之前调用,只调用一次
- beforeMount: 当指令第一次绑定到元素上并且父组件挂载之前调用
- mounted: 当绑定元素的父组件挂载完成时调用
- beforeUpdate: 指令所在组件的 VNode 及其子 VNode 更新前调用
- updated: 指令所在组件的 VNode 及其子 VNode 更新后调用
- beforeUnmount: 当绑定元素的父组件销毁之前调用
- unmounted: 只调用一次,指令与元素解绑时调用
动态指令参数
Scroll down the page
Stick me {{ pinPadding + 'px' }} from the {{ direction }} of the page
const app = Vue.createApp({
data() {
return {
direction: 'right',
pinPadding: 200
}
}
})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
},
updated(el, binding) {
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
传送门 teleport
将插槽内容传送至指定位置,接受一个 to
的属性,它接受一个 css query selector
作为参数,这就是代表要把这个组件渲染到哪个 dom
元素中
const app = Vue.createApp({
template: `
Root
`
})
// 基础用法
app.component('modal-button', {
template: `
I'm a teleported modal! (My parent is "body")
`,
data() {
return {
modalOpen: false
}
}
})
// 与Vue components一起使用,如果包含Vue组件,则它仍将是父组件的逻辑子组件
app.component('parent-component', {
template: `
This is a parent component
`
})
app.component('child-component', {
props: [ 'name' ],
template: `Hello, {{ name }}`
})
// 在同一目标上使用多个teleport
// disabled 可以用于禁用teleport组件的功能,这意味着它的插槽内容将不会被移动到任何位置,而是在周围父组件中指定的地方渲染。
app.component('multiple-teleports', {
template: `
A
B
C
D
`
})
app.mount('#app')
Suspense
Suspense
组件用于在等待某个异步组件解析时显示后备内容。
AsyncShow.vue
{{item}}
index.vue
Loading !...