组合式API
需要用到的API 都需要import,这个操作会被插件自动补全
import { defineProps, reactive, computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'
响应性API
1 响应性基础API
reactive
返回对象的响应式副本,将全部属性和方法打包为一个对象
转而集合到一个对象之内,使对象 data 内的属性可以响应外部的更改
const data = reactive({
counter: 1,
doubleCounter: computed(() => data.counter * 2)
})
let timer;
onMounted(()=> {
timer = setInterval(() => {
data.counter++
}, 1000);
})
onUnmounted(()=> {
clearInterval(timer);
})
return toRefs(data);
通过 data.counter 和 data.doubleCountert 读取内部属性,让内部的数据可以响应外部方法的更改
打包组合函数
为了代码更有组织性,我们可以将组合函数打包为一个函数,并将属性 return 出来,通过声明函数名调用函数得到对象
const data = useCounter();
function useCounter(()=> {
const data = reactive({
counter: 1,
doubleCounter: computed(() => data.counter * 2)
})
let timer;
onMounted(()=> {
timer = setInterval(() => {
data.counter++
}, 1000);
})
onUnmounted(()=> {
clearInterval(timer);
})
return data;
})
相比Vue2之下, data、computed、watch 被拆分的写法被破坏,避免了之前的一个操作在这些API之间反复横跳
2 Refs
ref
接受一个单独的值,返回一个响应式且可变的 ref 对象,ref 对象具有指向内部的单个属性 .value
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
如果需要将对象分配为 ref 值,则是需要通过 reactive 方法使该对象具有高度的响应式
unref()
如果参数是一个 ref 则返回内部值,否则返回参数本身的值,这是 val = isRef(val) ? val.value : val 的语法糖函数
function useFoo (x: number | Ref<number>) {
const unwrapped = unref(x) // 现在一定是数字类型
}
toRef()
可以用来为源响应式对象的某个 属性 新创建一个 ref,然后 ref 可以被传递, 它会保持对其源属性的响应式链接
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
以上,toRef() 方法内部第一个参数为源对象,第二个参数为内部属性的 key 字符串,所创建的ref对象的value被更改,源对象内的值也将被更改
当需要将响应式对象中的一个属性传递给其他函数使用时,toRef 很有用
setup(props) {
useSomeFeature(toRef(props, 'foo'))
}
toRefs()
这个方法将响应式对象转换为普通对象,得到的对象之中的每个属性都指向原始对象相应的ref
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
当组合式函数返回响应式对象时,toRefs 非常有用,
比如上面的reactive打包整个任务为一个函数,我们每次引用其中的属性,都需要 state.foo
这时可以用 toRefs() 方法返回当前data,外部解构这个函数得到对应的ref,
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
// 操作 state 的逻辑
// 返回时转换为ref
return toRefs(state)
}
// 可以在不失去响应性的情况下解构
const { foo, bar } = useFeatureX()
isRef()
检查值是否为一个 ref 对象
3 computed 与 watch
computed
接受一个 getter 函数作为参数,并从 getter 返回的值返回一个不变的响应式 ref 对象
const count = ref(1)
const plusOne = computed(()=> count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 报错
或者,他也可以使用具有 get 和 set 函数的 对象 来创建可写的 ref 对象
const count = ref(1)
const plusOne = computed({
get: ()=> count.value + 1,
set: val => count.value = val -1
})
// 当对plusOne 赋值相当于传入了一个参数,val形参等于1,对ref返回函数结果
plusOne.value = 1
console.log(count.value) // 0
watchEffect
在响应式的跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它
const count = ref(0)
watchEffect(()=> {
console.log(count.value)
}) // logs 0
setTimeout(()=> {
count.value++
},1000)
// logs 1
// logs 2
// logs 3
// ...
watch
watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效,需要侦听特定的数据源,并在单独的回调函数中执行副作用,默认情况下也是惰性的,即回调仅在侦听源发生更改时被调用
- 与 watchEffect 比较,watch 允许我们:
- 惰性的执行副作用
- 更具体的说明应触发侦听器重新运行的状态
- 访问被侦听状态的 newVal 和 oldVal
侦听一个单一源
侦听器数据源可以是具有返回值的 getter 函数,也可以是 ref
// 侦听一个 getter
const state = reactive({ count: 0})
watch(
() => state.count,
(newVal, oldVal) => {
// ...
}
)
// 侦听一个 ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
// ...
})
侦听多个源
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([newFoo, newBar], [oldFoo, oldBar]) =>{
// ...
})
Teleport 传送门
属性
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件
to - string — 通过条件判断渲染,将当前组件 teleport 标签内的 dom 挂载到指定类名的 dom 下
<teleport to="#some-id">...</teleport>
<teleport to=".some-class">...</teleport>
<teleport to="[data-teleport]">...</teleport>
disable - boolean — 此属性可用于禁用 的功能,这意味着其插槽内容将不会移动到任何位置,而是在你在周围父组件中指定了 的位置渲染
<teleport to='#popup' :disabled='displayVideoInline'>
<video src='./my-movie.mp4'/>
</teleport>
请注意,这将移动实际的 dom 节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的实例活动状态,所有状态的 HTML 元素 ( 即播放的视频 ) 都将保持其状态
在同一个目标上使用多个 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>
动态创建组件
在父组件内,我们可以通过 控制,动态的创建组件插入当前dom,当前需要渲染的组件需要 import 引入
<template>
<component :is='bloolear ? Foo : Bar'/>
</template>
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
自定义指令
1. 编写和导入
除了默认的内置指令(v-modul 和 v-show),当你需要对普通DOM元素进行底层操作,vue3也允许导入。自定义指令
我们可以像导入一个组件一样,导入一个指令,需要注意的是,要求命名方式里加一个 v ,这是一个标识提高代码可读性
例如我们创建一个vFocus.js , mounted 生命周期是同调用的组件对齐的
export default {
// 当被绑定指令的元素挂载到 DOM 时
mounted(el){
// 当前元素获取焦点
el.focus()
console.log('获取焦点成功!');
}
}
在需要的组件内 import , input标签内 v-focus 直接调用
<template>
<input type="text" v-focus>
</template>
<script setup>
import vFocus from '../vFocus.js';
</script>
另一个例子:vHighlight.js
export default {
// 当被绑定指令的元素挂载到 DOM 时
beforeMount(el, binding, vnode){
el.style.background = binding.value
}
}
<template>
<p v-highlight="color">这是一段文字</p>
</template>
这样在元素被渲染前,就获取到当前组件内的对应变量,将颜色传递给指令来设置元素的背景色
2. 自定义指令API 的钩子函数
api和组件保持一致,具体表现在:
-
created:在绑定元素的属性或事件监听器被应用之前调用,在指令需要附加须要在普通的
v-on事件监听器前调用的事件监听器时,这很有用。 -
bind → beforeMount —— 当指令第一次绑定到元素,并且在挂载父组件之前调用
-
inserted → mounted —— 在绑定元素的父组件被挂载后调用
-
beforeUpdate: 新的 —— 在更新包含组件的 VNode 之前调用
-
componentUpdated → update —— 在包含组件的 VNode 及其子组件的 VNode 更新后调用
-
beforUnmount → 新的 —— 组件或元素将要移除之前调用
-
unbind → unmounted —— 组件或元素移除之后调用
Fragments
Vue3中可以拥有多个根
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
组件通信
Props 父传子
基本同 vue2,在父组件声明的属性需要传递到子组件,需要在组件上绑定 :子组件属性=‘父组件属性’,在子组件内通过 defindProps({ key: 值类型 }) 接收
// 父组件
<template>
<!-- 1.父组件内声明的事件,通过在子组件标签上绑定 -->
<Props :fatherProps="props"></Props>
</template>
<script>
import { ref } from 'vue'
const props = ref('abc')
</script>
// 子组件
<template>
{{ fatherProps }}
</template>
<script setup>
import { withDefaults } from "vue";
// 2.子组件内通过defineProps获取
// 2.1 只声明props变量
defineProps(['fatherProps'])
// 2.2 指定类型
defineProps({
props1: String;
props2: Number;
})
// 2.3 为props设定默认值
defineProps({
count1: {
type: String,
default: '默认值'
},
count2: {
type: Number,
default: 321
}
})
</script>
Emit 子传父
Vue3 提供一个 emits 选项,和 props 父传子类似,这个选项用来触发父组件事件 (子传父)
// 子组件
<template>
<!-- 2.在子组件内绑定原生事件,触发自定义事件名,需要传值的话逗号隔开写入变量名 -->
<button @click="$emit('my-click', str)">触发父组件事件</button>
</template>
<script setup>
import { ref } from 'vue'
// 1.声明自定义事件
const str = ref('字符串')
const emit = defineEmites(['my-click'])
</script>
// 父组件
<template>
<!-- 3.父组件内,子组件标签上绑定触发事件名,属性值为当前组件内的方法 -->
<Foo @my-click='useFoo'></Foo>
</template>
<script setup>
import Foo from './Foo.vue'
// 3.触发当前方法
function useFoo(str) {
console.log(str)
}
</script>
获取、调用、更改子组件内的状态或方法
子组件标签 绑定ref属性,在父组件获取子组件实例,通过在子组件 definExpose 将变量或函数暴露出来,通过 ref.value.* 获取
// 父组件
<template>
<!—— 3.ref绑定子组件 ——>
<Foo ref='foo'></Foo>
<button @click="getCount">getCount</button>
</template>
<script setup>
import Foo from "./components/Foo.vue";
import { ref } from 'vue'
// 2.声明 foo 响应对象
const foo = ref(null)
const bb = ref('bb')
function getCount() {
// 4.通过 foo.value 获取子组件整个实例对象,内部会包含第一步暴露出来的内容
bb.value = foo.value.a;
foo.value.a = bbb;
console.log(foo.value.a);
foo.value.getName()
}
</script>
// 子组件
// 1.子组件内声明的变量或函数,通过 defindExpose 暴露出去
<script setup>
const a = ref('aaa')
function getName() {
console.log('name');
}
defineExpose({
a,
getName
})
</script>
另一个从子组件导出的方法
可以通过 import 的方法将子组件内的变量或方法导入到父组件
impor Foo,{ useFoo } from './Foo.vue'
只是,因为 setup 语法糖的关系,子组件抛出的是一个 setup对象,{ useFoo } 解构是不行的
这时我们可以在
// 子组件-------------------------
<script>
export function useFoo(){
return 'useFoo'
}
export default {
name: 'Foo',
obj: {
count: 100
}
}
</script>
<script setup>
// 这里是setup语法糖代码,上方为另起一个script脚本
</script>
// 父组件--------------------------
<template>
<button @click='getFoo'>getFoo</button>
</template>
<script setup>
import Foo, { useFoo, a } from './Foo.vue'
function getFoo() {
console.log(useFoo()) // useFoo
console.log(Foo.name) // Foo
// ...
}
</script>
自定义渲染器 custome renderer
Vue3.0支持自定义渲染器,这个API 可以用来自定义渲染逻辑,比如将数据渲染到canvas上
用到的时候再了解........
全局API改为实例方法调用
实例方法定义组件
vue3中使用 createApp返回app实例,由它暴露一些列全局api
// main.js
import { createApp, h } from 'vue'
import App from './App.vue'
createApp(App)
.component('comp', {
render() {
return h('div', 'I am comp')
}
})
.mount('#app')
在组件中通过标签直接调用该组件
<template>
<comp/>
</template>
Global and internal APIs 重构为可做摇树优化
vue2中不少全局api是作为静态函数直接挂在构造函数之上的,例如Vue.nextTick(),如果我们从未在代码中使用过它们,就会形成所谓的 dead code,这类全局api造成的无用代码无法使用 webpack的tree-shaking排除掉
import Vue from 'vue'
Vue.nextTick(() => {
//...
})
Vue3中做了相应变化,将它们抽取成为独立函数,这样打包工具的摇树优化可以将这些dead code排除掉
import { nextTick } from 'vue'
nextTick(() => {
//...
})
受影响api:
- Vue.nextTick
- Vue.observable (替换为 Vue.reactive) —— 响应式数据定义方法
- Vue.version —— 判断版本方法
- Vue.compile (仅在完整版本中) —— 用来做编译的方法
- Vue.set (仅在兼容版中)
- Vue.delete (仅在兼容版中)
v-model使用的变化
子组件的双向绑定
vue2中 .sync 和 v-model 功能有重叠,容易混淆,vue3做了统一
在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model 指令时的混淆并且在使用 v-model 指令时可以更加灵活。
父传子的同时,又允许子组件内修改这个数值传回父组件
// 父组件
<template>
<comp v-model='counter'></comp>
</template>
<script setup>
import {ref} from 'vue'
const counter = ref(0)
</script>
// 子组件 —— 通过 v-model="xxx" 传递的数据,内部必须用modelValue接收
<template>
<div @click="$emit('update:modelValue', modelVlaue + 1)">
counter: {{modelValue}}
</div>
</template>
<script setup>
import { definProps } from 'vue'
definProps({
modelValue:{
type: Number,
default: 0
}
})
</script>
在 vue3中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:
<ChildComponent v-model="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
v-model 参数
若果需要修改 modelValue 名称,作为组件内 modelValue 选项的替代,现在我们可以将一个 argument传递给 v-model
v-model:xxx="xxx"
<ChildComponent v-model:title="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
这也可以作为 .sync 修饰符的替代,而且允许我们在自定义组件上使用多个 v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 是以下的简写: -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
这时候在子组件内,通过事件触发这个和传入的参数并更新到父组件
<template>
<div @click="$emit('update:pageTitle', pageTitle + 1)">
counter: {{pageTitle}}
</div>
</template>
<script setup>
import { definProps } from 'vue'
definProps({
pageTitle:{
type: Number,
default: 0
}
})
</script>
异步组件使用变化
为了减少程序的体积,优化加载速度,仅在需要渲染组件的时候加载组件,就需要对组件进行异步导入
因为vu3中函数式组件必须定义为纯函数,异步组件定义时就有了变化:
- 必须明确使用 defineAsyncComponent 包裹
- component 选项重命名为 loader
- Loader 函数不再接收 resolve 和 reject,且必须返回一个 Promise
定义一个不带配置的异步组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncCompomemt(() =>('./components/AsyncComp.vue'))
定义一个带配置的异步组件,loader选项是以前的 component
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./components/AsyncComp.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
自定义组件白名单
vue3中自定义元素检测发生在模板编译时,如果要添加一些vue之外的自定义元素,需要在编译器选项中设置 isCustomElement 选项
使用构建工具时,模板都会用 vue-loader 预编译,设置它提供的 compilerOptions 即可,vue.config.js
rules: [
{
test: /\.vue$/,
use: 'vue=loader',
options: {
compolerOptions: {
//接收一个标签,这个标签等于xxx就忽略掉这个问题,消除警告提示
isCustomElement: tab => tag === 'plastic-button'
}
}
}
]
在vite项目中,在 vite.config.js 中配置 vueCompilerOptions 即可:
module.exports = {
vueCompilerOptions: {
isCustomElement: tag => tag === 'plastic-button'
}
}
$scopedSlots 属性被移除,都用 $slots 代替
vue3中统一普通插槽和作用域插槽到 $slots ,具体变化如下:
- 插槽均以函数形式暴露
- $scopdSlots 移除
函数形式访问插槽内容,MyLink.vue
<script>
import { h } from 'vue'
export default {
props: {
to: {
type: String,
required: true
},
},
render() {
return h("a", { href: this to }, this.$slots.default());
},
}
</script>
transition类名变更
vue2 写法不统一,vue3做了更正
- v-enter → v-enter-from — 对应 — v-enter-to ---- v-enter-active ( 过渡 )
- v-leave → v-leave-from — 对应 — v-leave-to ---- v-leave-active ( 过渡 )
组件watch选项和实例方法$watch不再支持点分隔符串路径
以 . 分隔的表达式不再被 watch 和 watch 参数实现
this.$watch(() => this.foo.bar, (v1, v2) => {
console.log(this.foo.bar)
})
keyCode 作为 v-on 修饰符被移除
vue2中可以使用keyCode指代某个按键,由于可读性差,vue3不再支持
<!-- keyCode方式不再被支持 -->
<input v-on:keyup.13="submit" />
<!-- 只能使用alias方式 -->
<input @keyup.enter="submit" />
$on, $off 和 $once 移除
上述3个方法被认为不应该由vue提供,因此移除了,事件的派发和监听可以使用其他第三方库实现
npm i mitt -S
// 创建emitter
const emitter = mitt()
// 发送事件
emitter.emit('foo', 'fooooooo')
// 监听事件
emitter.on('foo', msg => console.log(msg))
filter移除
vue3中移除了过滤器,请调用方法或者计算属性代替