写在最前:本文转自掘金
Composition API
Composition API 主要解决了,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。
setup
setup 是 Vue3.x新增的一个选项,他是组件内使用 Compsition API
的入口。
setup 执行时机
setup 执行时机是在 beforeCreate
之前执行
setup 参数
setup接收两个参数:
- props: 组件传入的属性
- context
setup 中接收的props
是响应式的,当传入的新的props时,会及时被更新。由于是响应式的,所以不可以使用ES6结构,结构会消除它的响应式。
// 错误代码示例,这代代码会让props不再支持响应式
export default defineComponent({
setup(props, context){
const {name} = props
console.log(name)
},
})
在toRefs
学习的地方为大家解答。接下来我们来说一下setup
接受的第二个参数context
,我们前面说了setup
中不能访问Vue2中最常用的this
对象,所以context
中提供了this
中最常用的是三个属性:attrs
、slot
、emit
,分别对应Vue2.x中的$attr
属性、slot
插槽和$emit
触发事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新的值。
reactive、 ref 与toRefs
在Vue3.x中,定义数据可以使用reactive
和ref
来进行数据定义。那么ref
和reactive
他们有什么区别呢?分别什么时候使用呢?ref
可以处理js基础类型的双向绑定,也可以定义对象类型双向绑定。但是reactive
函数只能代理对象类型。
我们在模板中使用
user.name
,user.age
这样写感觉很繁琐,如果需要解构出来,就需要toRefs
。toRefs
用于将一个reactive
对象转化为属性全部为ref
对象的普通对象。具体使用方法如下:
第 {{ year }} 年
姓名: {{ nickname }}
年龄: {{ age }}
生命周期钩子
我们可以直接看生命周期图来认识有哪些生命周期钩子
从图中我们可以看到Vue3.0新增了setup
,然后是将Vue2.x中的beforeDestroy
名称变更为beforeUnmount
;destroyed
名称变更为unmounted
。其它Vue2中的生命周期仍然保留。上边生命周期图并没有包含全部的生命周期钩子,还有其它几个,全部声明周期钩子如下图所示:
我们可以看到beforeCreate
和created
被setup
替换了。其次,钩子函数都增加了on
前缀;Vue3.x还新增了用于调试的钩子函数onRenderTriggered
和onRenderTricked
下面我们就简单使用几个钩子,方便大家学习如何使用,Vue3.x中的钩子函数是需要从vue中导入的
import {defineComponent, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmout, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered} from 'vue';
export default defineComponent({
beforeCreate(){
console.log("beforeCreated")
},
created(){
console.log("created")
},
setup(){
console.log("setup");
// vue3.x生命周期写在setup中
onBeforeMount(()=>{
console.log("onBeforeMount")
});
onMounted(()=>{
console.log("onMounted")
});
// 调试哪些数据发生了变化
onRenderTriggered((event)=>{
console.log("onRenderTriggered", event)
});
}
})
watch 与 watchEffect 的用法
watch函数用来侦听特定的数据源,并在回调函数中执行逻辑。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。
watch(source, callback, [options])
参数说明
- source: 可以支持string, Object, Function, Array;用于指定要侦听的响应式变量
- callback: 执行的回调函数
- options: 支持deep, immediate 和 flush 选项
侦听reactive定义的数据
import {defineComponent, ref, reactive, toRefs, watch} from 'vue';
export default defineComponent({
setup(){
const state = reactive({nickname: "xiaofan", age: 20});
setTimeout(()=>{
state.age++;
}, 1000);
// 修改age值时会触发watch的回调
watch(()=> state.age, (curAge, preAge)=>{console.log(curAge, preAge)});
return {
...toRefs(state),
}
}
})
侦听ref定义的数据
const year = ref(0);
setTimeout(()=>{
year.value++;
}, 1000);
watch(year, (newVal, oldVal)=>{
console.log("新值", newVal, "老值", oldVal);
})
侦听多个数据
watch([()=>test.age,year],([curAge,newVal],[preAge, oldVal])=>{console.log(curAge,preAge);console.log(newVal,oldVal)})
侦听复杂的嵌套对象
const state = reactive({
room: {
id: 100,
attrs: {
size: "140平方米",
type: "三室两厅",
},
},
});
watch(
() => state.room,
(newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
},
{ deep: true }
);
如果不适用第三个参数deep:true
,是无法监听到数据变化的。前面我们提到,默认情况下,watch是惰性的,那什么情况下不是惰性的,可以立即执行回调函数呢?给第三个参数中设置 immediate:true
即可。关于flush
配置,还在学习,后期补充。
stop停止监听
我们在组件中创建的watch
监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听,可以调用watch()
函数的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止监听
stopWatchRoom()
}, 3000)
watchEffect 的用法
import {defineComponent, ref, reactive, toRefs, watchEffect} from 'vue';
export default defineComponent({
setup() {
const state = reactive({nickname:"xiaofan", age: 20});
let year = ref(0);
setInterval(()=>{
state.age++
year.value++
},1000)
watchEffect(()=>{
console.log(state);
console.log(year);
});
return {...toRefs(state)}
},
})
执行结果首先打印一次state
和year
值;然后每隔一秒,打印state
和year
值。
从上面的代码可以看出, 并没有像watch
一样需要先传入依赖,watchEffect
会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以总结对比如下:
- watchEffect 不需要手动传入依赖
- watchEffect 会先执行一次用来自动收集依赖
- watchEffect 无法获取到变化前的值, 只能获取变化后的值
computed 计算属性
setup() {
let name = ref('xiaofan')
let age = ref(21)
//计算属性
let getInfo = computed(() => {
return `我的名字:${name.value},今年${age.value},请多多指教`
})
return {
name,
age,
getInfo,
}
}
自定义 Hooks
我们来写了一个实现加减的例子,这里将其封装成hook,我们约定这些[自定义的hook]以use作为前缀,和普通的函数加以区分。useCount.ts
实现:
import { ref, Ref, computed } from 'vue'
interface CountResultProps {
count: Ref;
multiple: Ref;
increase: (delta?: number) => void;
decrease: (delta?: number) => void;
}
export default function useCount(initValue = 1): CountResultProps {
const count = ref(initValue);
const increase = (delta?: number): void => {
if (typeof delta !== 'undefined') {
count.value += delta;
} else {
count.value += 1
}
}
const multiple = computed(() => count.value * 2);
const decrease = (delta?: number): void => {
if (typeof delta !== 'undefined') {
count.value -= delta;
} else {
count.value -= 1
}
}
return {
count,
multiple,
increase,
decrease,
}
}
接下来看下在组件中使用useCount
:
count: {{ count }}
倍数: {{ multiple }}
teleport
Teleport 是什么呢?
Teleport
就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到 Teleport
的特性呢,看一个小例子:
在子组件Header
中使用到Dialog
组件,我们实际开发中经常会在类似的情形下使用到 Dialog
,此时Dialog
就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index
和样式都变得困难。
Dialog
从用户感知的层面,应该是一个独立的组件,从 dom
结构应该完全剥离Vue
顶层组件挂载的 DOM
;同时还可以使用到 Vue
组件内的状态(data
或者props
)的值。简单来说就是,即希望继续在组件内部使用Dialog
, 又希望渲染的 DOM
结构不嵌套在组件的 DOM
中。
此时就需要 Teleport
上场,我们可以用
包裹Dialog
, 此时就建立了一个传送门,可以将Dialog
渲染的内容传送到任何指定的地方。
接下来就举个小例子,看看 Teleport
的使用方式
Teleport 的使用
我们希望 Dialog
渲染的 dom
和顶层组件是兄弟节点关系, 在index.html
文件中定义一个供挂载的元素:
定义一个Dialog
组件Dialog.vue
, 留意 to
属性, 与上面的id
选择器一致:
{{ title }}
最后在一个子组件Header.vue
中使用Dialog
组件, 这里主要演示 Teleport
的使用,不相关的代码就省略了。header
组件
...
...
可以看到,我们使用
teleport
组件,通过 to
属性,指定该组件渲染的位置与
同级,也就是在 body
下,但是 Dialog
的状态dialogVisible
又是完全由内部 Vue 组件控制.
Suspense
暂未学习
片段(Fragment)
在 Vue2.x 中, template
中只允许有一个根节点,
但是在 Vue3.x 中,你可以直接写多个根节点, 是不是很爽。
更好的 Tree - Shaking
Vue3.x 在考虑到tree-shaking
的基础上重构了全局和内部API,表现结果就是现在的全局API需要通过ESMoudle
的引用方式进行具名引用,比如在Vue2.x中,我们要使用nextTick
:
// vue2.x
import Vue from 'vue'
Vue.nextTick(()=>{
...
})
Vue.nextTick()
是一个从Vue对象直接暴露出来的API,其实$nextTick()
只是Vue.nextTick()
的一个简易包装,只是为了方便而把后者的回调函数的this
绑定到了当前实例。虽然我们借助webpack
的tree-shaking
,但是不管我们实际上是否使用Vue.nextTick()
,最终都会进入我们的生产代码,因为Vue实例是作为单个对象导出的,打包器无法检测出代码中使用对象的哪些属性。在Vue3.x中改成这样写:
import {nextTick} from 'vue'
nextTick(()=>{
...
})
受影响的API
- Vue.nextTick
- Vue.observable(用 Vue.reactive 替换)
- Vue.version
- Vue.compile(仅限完整版本时可用)
- Vue.set(仅在 2.x 兼容版本中可用)
- Vue.delete(与上同)
变更
slot 具名插槽语法
在vue2.x中,具名插槽的写法:
在父组件中使用:
歌曲
如果我们要在slot上面绑定数据,可以使用作用域插槽,实现如下:
// 子组件
export default {
data(){
return{
data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"]
}
}
}
{{item}}
在Vue2.x中具名插槽的作用域插槽分别使用slot
和slot-scope
来实现,在Vue3.x中将slot
和slot-scope
进行了合并统一使用。Vue3.x中 v-slot
{{item}}
{{item}}
自定义指令
首先回顾下 Vue2 中实现一个自定义指令:
// 注册一个全局自定义指令 v-focus
Vue.directive('focus', {
inserted: function (el){
// 聚焦
el.focus()
}
})
在vue2.x中,自定义指令通过以下几个可选钩子创建:
- bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
- update: 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated: 指令所在组件的VNode 及其子VNode 全部更新后调用
-
unbind: 只调用一次,指令与元素解绑时调用
在Vue3 中对自定义的API进行了更加语义化的修改。
所以在 Vue3 中, 可以这样来自定义指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
然后可以在模板中任何元素上使用新的v-focus
指令, 如下:
v-model 升级
在使用Vue3 之前就了解到 v-model
发生了很大的变化,下面来了解下发生了哪些变化:
- 变更:在自定义组件上使用
v-model
时,属性以及事件的默认名称变了 - 变更:
v-bind
的.sync
修饰符在Vue3中去掉,合并到v-model
里 - 新增: 同一组件可以同时设置多个
v-model
- 新增: 开发者可以自定义
v-model
修饰符
在vue2中,在组件上使用 v-model
其实就相当于传递了 value属性,并触发了input
事件:
这时 v-model
只能绑定在组件的value
属性上,如果我们想给自己组件用一个别的属性,并且不想通过input
来更新值,在.sync
出来之前,Vue2是这样实现的:
// 子组件:searchInput.vue
export default {
model:{
prop: 'search',
event:'change'
}
}
修改后, searchInput
组件使用v-model
就相当于这样:
但是在实际开发中,有些场景我们可能需要对一个 prop
进行 “双向绑定”, 这里以最常见的 modal
为例子:modal
挺合适属性双向绑定的,外部可以控制组件的visible
显示或者隐藏,组件内部关闭可以控制 visible
属性隐藏,同时visible
属性同步传输到外部。组件内部, 当我们关闭modal
时, 在子组件中以 update:PropName
模式触发事件:
this.$emit('update:visible', false)
然后在父组件中可以监听这个事件进行数据更新:
此时我们也可以使用v-bind.sync
来简化实现:
上面回顾了 Vue2 中v-model
实现以及组件属性的双向绑定,那么在 Vue 3 中应该怎样实现的呢?在vue3中,在自定义组件上使用v-model
,相当于传递一个modelValue
属性,同时触发一个update:modelValue
事件:
如果要绑定属性名,只需要给v-model
传递一个参数就行,同时可以绑定多个v-model
不知道你有没有发现,这个写法完全没有.sync
什么事儿了, 所以啊,Vue 3 中又抛弃了.sync
写法, 统一使用v-model
异步组件
Vue3 中使用defineAsyncComponent
定义异步组件,配置选项component
替换为loader
,Loader函数本身不在接收resolve 和 reject 参数,且必须返回一个Promise ,用法如下