文章目录
写在前面的话
虽然开发一直在使用Vue,但是必须要承认在实际开发中对于Vue的使用不尽人意,很多地方并没有能够利用Vue的优点来简化开发,比如父子组件的传值和修改,多个组件总是用到同一个方法,对象数据的响应式处理等,所以回炉重看了Vue的文档,看完Vue2(以下简称v2)再看Vue3(以下简称v3),在实际开发目前还用不到v3,所以自己记一下关于v2和v3之间的差异,以便自己后期使用。
Vue3的更新
关于ref在v-for时的处理
在v2中,如果在一个v-for
的DOM上使用了 ref
,那么这个ref值自动生成一个数组,包含循环的所有的DOM对象,在v3中,不再自动生成这样的一个数组(refs的数组),而是可以选择绑定一个函数,如下
<div v-for="item in list" :ref="setItemRef"></div>
此时的setItemRef
是一个函数,有一个参数是每一个DOM元素。
export default {
data() {
return {
itemRefs: []
}
},
methods: {
setItemRef(el) {
if (el) {
this.itemRefs.push(el)
}
}
}
}
可以处理每一个DOM,增加了ref在v-for使用的多样性。
异步组件
defineAsyncComponent
是个新增的重点方法,处理异步组件
= = 没看明白
Vue实例创建
v2
import Vue from 'vue'
new Vue({
render:h => h(App)
}).$mount('#app')
v3
import Vue from 'vue'
Vue.createApp(App).mount('#app')
且v3支持链式调用
如下
Vue.createApp({})
.component('SearchInput', SearchInputComponent)
.directive('focus', FocusDirective)
.use(LocalePlugin)
<template>上可使用v-for
因为<template>模板是被当作一个透明外壳渲染,既实际渲染时他是不存在的,所以在v2中不支持v-for
在其上面的使用,而在v3中,支持v-for
在<template>
上使用,且在渲染时<template>
依旧渲染为无内容。
v-if和v-for优先级
v2中,v-for
拥有更高的优先级;
v3中,v-if
拥有更高的优先级,这意味着,v-if
无权访问v-for
里面的变量,但是v-for
可以在<template>
上使用,所以可以在<template>
上使用v-for
,在内部元素中使用v-if
,同样可以实现依据列表项内容来选择是否渲染元素。
多事件处理器
事件处理器中可以有多个方法,这些方法由逗号分隔开,点击依次触发两个事件。
<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)">
Submit
</button>
// ...
methods: {
one(event) {
// first handler logic...
},
two(event) {
// second handler logic...
}
}
啊,,,这,我记得v2好像是没有的,而且这个方法的意义我还不知道在哪。。
键盘事件
键盘事件支持按键修饰符来绑定事件,在v2中,修饰符包括keyCode,在v3中,取消了这个修饰符。
Emits
property
之前确实没见过这个属性,可以检查组件抛出的所有事件
app.component('blog-post', {
props: ['title'],
emits: ['enlarge-text']
})
事件验证
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 })
}
}
})
以布尔值来判断是否有效
tip
:当在 emits 选项中定义了原生事件 (如 click) 时,将使用组件中的事件替代原生事件侦听器。
例子
// 父组件
<template>
<div>{{count}}</div>
// 把click原生事件传给子组件
<test-component @click="sendMessage"/>
</template>
<script>
export default {
name: 'App',
data(){
return {
count:0
}
},
methods:{
// 接收子组件触发时传递的一个参数。
sendMessage(count){
this.count += count
console.log('emit')
}
}
}
</script>
// 子组件
<template>
<div>
// 按钮双击触发click事件
<button @dblclick="$emit('click',2)">点</button>
<template v-for="item in 10" :key="item">
<h2>标题</h2>
<span>小标题</span>
<div>内容</div>
</template>
</div>
</template>
<script>
export default {
namespace:'TestComponent',
emits:['click']
};
</script>
通过上述实验代码,可以测试是单击/双击时父组件的sendMessage触发。测试可得是双击时,验证了组件中的事件替代了原生事件
v3支持v-model修饰符为自定义修饰符
添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件,
// 组件使用
<my-component v-model.capitalize="myText"></my-component>
// 组件定义
const app = Vue.createApp({
data() {
return {
myText: ''
}
}
})
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
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)
}
},
template: `<input
type="text"
:value="modelValue"
@input="emitValue">`
})
app.mount('#app')
对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + “Modifiers”:
<my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
// v-model的参数值为description,所以生成的prop名称为descriptionModifiers
props: ['description', 'descriptionModifiers'],
emits: ['update:description'],
template: `
<input type="text"
:value="description"
@input="$emit('update:description', $event.target.value)">
`,
created() {
console.log(this.descriptionModifiers) // { capitalize: true }
}
})
vue3的全局数据的绑定
// v2
import Vue from 'vue'
import axios from 'axios'
Vue.prototype.$axios = axios
// v3
let app = createApp(APP)
app.config.globalProperties.$axios = axios
组合式API
基础setup
新的setup
组件选项在创建组件之前执行,一旦props
被解析,并充当合成API的入口点。
由于在执行setup时,组件实例尚未创建,所以在setup中没有this。这意味着,除了props之外,你将无法访问组件声明的任何属性----本地状态、计算属性或方法。
setup方法应该是一个接受props和context的函数,我们从setup中返回的所有内容都将暴露给组件的其余部分(计算属性、方法、生命周期钩子等等)以及组件的模板。
export default {
components: { componentA, componentB, componentC},
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
带ref
的响应式变量
在v3中,我们可以通过一个新的ref
函数使任何响应式变量在任何地方起作用,
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) //{value:0}
counter.value ++
console.log(counter.value) //1
ref
接受参数并返回它包装在具有value属性的对象中,然后可以使用该属性访问或更改响应式变量的值。
在setup中注册生命周期钩子、watch、computed
生命周期钩子:钩子组合式API上的生命周期钩子与选项式API的名称相同,但前缀为on:即mounted
看起来像onMounted
。
watch:我们也可以从Vue中导入的watch函数只想相同的操作,它接受三个参数:
- 一个响应式引用或我们想要侦听的getter函数
- 一个回调
- 可选的配置选项
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
每当counter被修改时,watch将触发病执行回调(第二个参数)
computed:从Vue中导入computed函数,computed函数返回一个作为第一个参数传递的getter类回调的输出的一个制度的响应式引用。为了访问新创建的计算变量的value,我们需要像使用ref一样使用.value属性。
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
为了确保我们的侦听器能够对prop所作出的更改做出反应,我们可以使用toRefs(从Vue中引入)
toRefs(props)
setup
使用setup函数,他接受两个参数
- props
- context
props是第一个参数,setup函数中的props是响应式的,当传入新的prop时,它将被更新。
warning: 你不能使用es6解构props,因为他会消除prop的响应性。
如果需要解构,可以通过使用setup函数中的toRefs来完成操作:
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
// tilte 是ref
console.log(title.value)
}
第二个参数是context,其是一个普通的js对象,他暴露三个组件的属性:
context.attrs // Attribute 非响应式对象
context.slots //插槽 非响应式对象
context.emit //触发事件 方法
context是一个普通的js对象,他不是响应式的,你可以安全的对context使用es6解构。
export default {
setup(props, { attrs, slots, emit }) {
...
}
}
attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。你应该避免对它们进行解构,并始终以 attrs.x 或 slots.x 的方式引用 property。attrs 和 slots 是非响应式的,如果你打算根据 attrs 或 slots 更改应用副作用,那么应该在 onUpdated 生命周期钩子中执行此操作。
访问组件的property
执行setup时,组件实例尚未被创建。因为只能访问一下property:
- props
- attrs
- slots
- emit
你无法访问以下组件选项 - data
- computed
- methods
在setup内部调用生命周期钩子
beforeCreate和created无法被调用。因为setup就是围绕这两个钩子写的,在这两个钩子写的任何内容都会在setup中被调用。
在setup内部使用provide/inject
使用provide
provide函数允许你通过两个参数定义property:
- name
- value
import { provide } from 'vue'
import MyMarker from './MyMarker.vue
export default {
components: {
MyMarker
},
setup() {
//name //value
provide('location', 'North Pole')
//name
provide('geolocation', {//value
longitude: 90,
latitude: 135
})
}
}
</script>
使用inject
inject函数有两个参数
- 要inject的property的名称
- 一个默认的值(可选)
<script>
import { inject } from 'vue'
export default {
setup() {
//名称 //默认值
const userLocation = inject('location', 'The Universe')
// 名称
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
}
</script>
添加响应性
为了增加provide和inject值之间的响应性,我们可以在provide值时使用ref和reactive。
<script>
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)
}
}
</script>
修改响应式property
尽可能在provide内保持响应式的更改。
有时我们需要在注入数据的组件内部更新inject的数据,在这种情况下,我们建议provide一个方法来负责改变响应式property。
最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly。
模板引用
ref和模板的联系 没看明白啊ToT。
生命周期钩子
app = Vue.createApp(options)
app.mount(el)
// 初始化events
beforeCreate
// 初始化注入和reactivity,实例已经创建,可以访问this
created
//编译模板
beforeMount
// 创建模板实例,可以访问DOM(v-if下的DOM除外)
mounted
// 当数据更新
beforeUpdate
// 虚拟DOM重渲染
updated
// 组件销毁,销毁前,依旧可以访问实例对象
beforeUnmount
// 组件销毁
unmounted
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
}
}
})
使用了teleport
这个标签,并告诉Vue teleport这个HTML到body标签上,它可以突破组件间的现实位置,直接挂载到任意DOM上,只要这个DOM是存在的。
如果teleport
白哦韩Vue组件,则它仍将是teleport
父组件的逻辑子组件,仍能从父组件中接收props。
在同一个目标上使用多个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>
深入响应式原理
Vue如何追踪变化
当把一个普通的js对象作为data选项传给应用或组件实例时,Vue会使用带有getter和setter的处理程序遍历其所有的书香并将其转换为Proxy,这是ES6仅有的特性,但是在v3中也使用了Object.defineProperty来支持IE。Proxy版本二更精简,同时提高了性能。
Proxy是一个包含另一个对象或者函数并允许你对其进行拦截的对象。
new Proxy(target,handler)
在hander中,有get和set函数,分别用来返回和修改函数值,在内容发生变化时,我们在名为track的函数中处理这些值,为此,我们将通过触发trigger函数来更改新的值(其实我没懂= =)
响应性基础
声明响应式状态
import { reactive } from 'vue'
const state = reactive({
count:0
})
reactive相当于Vue2.x中的Vue.observable() API,其返回一个响应式的对象状态。该响应式转换时“深度转换”----- 它会影响嵌套对象传递的所有属性。
当响应式状态改变时视图回自动更新,这就是Vue响应式系统的本质。当组件中的data()返回一个对象的时候,他在内部由reactive()成为其响应式对象。模板会被编译成能够使用这些响应式属性的渲染函数。
创建独立的响应式值作为refs
import { ref } from 'vue'
const count = ref(0)
ref会返回一个可变的响应式对象,该对象作为他的内部值----一个响应式的引用,此对象只包含一个名为value的property:
import { ref } from 'vue'
const count = ref(0)
ref展开
当ref作为渲染上下文(setup中返回的对象)上的属性返回并可以在模板中被访问时,他将自动展开为内部值,不需要在模板中追加.value.
访问响应式对象
当ref作为响应式对象的属性被访问或更改时,为使其行为类似于普通属性,它会自动展开内部值:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
ref展开仅发生在被响应式Object嵌套的时候,当从Array或者原生集合类型如Map访问ref时,不会进行展开。
响应式状态解构
import { reactive } 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 } = book
如上,遗憾的是,解构的两个属性的响应式都会丢失,
那么,将响应式对象转换为一组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防止更改响应式对象
创建一个只读的proxy对象
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// 通过 original 修改 count,将会触发依赖 copy 的侦听器
original.count++
// 通过 copy 修改 count,将导致失败并出现警告
copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."
响应式计算和侦听
watchEffect
为了根据响应式状态自动应用和重新应用副作用,物品们可以使用watchEffect方法,它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
停止侦听
当watchEffect在组件的setup函数或生命周期钩子被调用时,侦听器会被连接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显示调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
v3处理边界情况少了很多鸭
记住规则
- 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。