vue3 变更
main.js
vue2:
vue3:
vue3 新特性
- 更好的 ts 支持:源码全部用 ts 重写
- tree-shaking
- 打包后体积更小(没有用到的代码在打包时会删除)
- fragments 支持多个根节点
- teleport 传送门
- custom renderer 自定义渲染器(小程序、iOS等等平台)- canvas
- composition api
一、组合式 API
介绍
组合式 API:将同一个逻辑关注点的相关代码收集到一起。(为了不引入全新的概念,采用独立的函数来创建和监听响应式的状态等)
组合 API 基础
setup 基础
setup
选项是一个接收 props
和 context
的函数
。
在组件创建之前执行(发生在 data
、computed
或 methods
被解析之前)。
setup
返回一个对象,该对象的 property 以及 传递给 setup
的 props
参数中的 property 都可以在模板中访问到。
<template>
<div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板
// 必须 return 才能访问
return {
readersNumber,
book
}
}
}
</script>
在 setup 中你应该避免使用
this
,因为它不会找到组件实例。setup
的调用发生在data
、computed
或methods
被解析之前,所以它们无法在 setup 中被获取。
响应式对象
ref
在 Vue 3.0 中,我们可以通过一个新的 ref
函数 使任何响应式变量在任何地方起作用。换句话说,ref
为我们的值创建了一个响应式引用
。
ref
接收参数 并 将其包裹在一个带有 value
property 的对象中返回,然后可以使用 .value
访问或更改响应式变量的值:
<template>
<!-- 响应式对象:模板自动解构(模板中不需要写 `.value`) -->
<div>count:{{ count }}</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup(props) {
const count = ref(0)
onMounted(() => {
console.log(count); // { value: 0 }
console.log(counter.value); // 0
counter.value++;
console.log(counter.value); // 1
})
return {
count
}
}
}
</script>
从
setup
返回的refs
在模板中访问时是被自动浅解包的,因此不应在模板中使用.value
。
reactive
<template>
<div>count:{{ count }}</div>
<div>user-name:{{ user.name }}</div>
<div>user-age:{{ user.age }}</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
setup(props) {
const count = ref(0)
const user = reactive({
name: 'una',
age: 23
})
onMounted(() => {
console.log(count); // { value: 0 }
console.log(user); // { name: 'una', age: 23 }
// 心智负担
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
// user-> reactive (proxy) 本质上是一个代理对象
console.log(user.age); // 23 不需要 `.value`
})
// 暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板
return {
count,
user
}
}
}
</script>
ref
vs reactive
为什么要用 ref
?因为数字、布尔值、字符串等值类型改变不容易监听,使用 ref
包裹一下,变为引用类型,保持 JavaScript 中不同数据类型的行为统一。
ref
也可以包裹引用类型,在底层的时候会自动把他转为 reactive
,但使用时还是要用 .value
。
所以推荐:使用值类型用 ref
包裹,引用类型用 reactive
代理。
readonly
从控制台的显示来看,readonly
是只读的 reactive
。
<template>
<div>user-name:{{ useronly.name }}</div>
<div>user-age:{{ useronly.age }}</div>
</template>
<script>
import { readonly, onMounted } from 'vue'
export default {
setup(props) {
const useronly = readonly({
name: 'una',
age: 23
})
onMounted(() => {
console.log(useronly);
useronly.age = 20; // 失败,只读类型不能修改
})
// 暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板
return {
useronly
}
}
}
</script>
使用场景
props
provide/inject
(后面会介绍)
计算属性 computed
需要给 computed
函数传递一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用
。我们需要像 ref
一样使用 .value
。
<template>
<div>count:{{ count }}</div>
<div>count * 2:{{ double }}</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(1)
const double = computed(() => count.value * 2)
onMounted(() => {
console.log(double);
console.log(double.value); // 2
})
return {
count,
double
}
}
}
</script>
侦听
watch
<template>
<div>count: {{ count }}</div>
<div>double : {{ double }}</div>
<div>num : {{ num }}</div>
<div>user - age: {{ user.age }}</div>
<div>user - name: {{ user.name }}</div>
<button @click="handleNumAdd">num add</button>
<button @click="handleUserAgeAdd">user age add</button>
<button @click="handleCountAdd">count add </button>
</template>
<script>
import { ref, reactive, computed, watch } from "vue";
export default {
setup() {
const count = ref(0)
const num = ref(2)
const user = reactive({
name: 'una',
age: 23
})
const double = computed(() => count.value * 2)
const handleCountAdd = () => {
console.log('count.value++');
count.value++;
};
const handleNumAdd = () => {
console.log('num.value++');
num.value++;
};
const handleUserAgeAdd = () => {
console.log('user.age++');
user.age++;
};
// ref
watch(num, (newVal, oldVal) => {
console.log("watch - num ");
console.log(newVal, oldVal);
});
// reactive
// user.age
watch(
() => user.age, // 如果观察的是一个 对象里面的某个key 的话, 需要用一个函数来 return
(newVal, oldVal) => {
console.log("user age", newVal, oldVal);
},
{
immediate: true, // 立即执行
}
);
// 侦听多个
watch([count, double], (val) => {
console.log('侦听多个', val);
});
// watch([count, double], (newVal, oldVal) => {
// console.log('侦听多个', newVal, oldVal);
// });
return {
count,
num,
user,
double,
handleCountAdd,
handleNumAdd,
handleUserAgeAdd
}
}
};
若侦听多个数据且返回新旧值的话,会返回2个数组,分别存放相应的新值和旧值:
watchEffect
特点:
- 立即执行
- 不需指定具体的响应式对象,只要里面依赖的值变化就执行
- 没有新旧值
- 返回一个
stop
函数,用于 手动停止侦听 - 组件销毁时,
watchEffect
自动销毁
<template>
<div>count: {{ count }}</div>
<div>double : {{ double }}</div>
<div>num : {{ num }}</div>
<div>user - age: {{ user.age }}</div>
<div>user - name: {{ user.name }}</div>
<button @click="handleNumAdd">num add</button>
<button @click="handleUserAgeAdd">user age add</button>
<button @click="handleCountAdd">count add </button>
<button @click="stopWatchEffect">stop-watch-effect</button>
</template>
<script>
import { ref, reactive, computed, watchEffect } from "vue";
export default {
setup(props) {
const count = ref(0)
const num = ref(2)
const user = reactive({
name: 'una',
age: 23
})
const double = computed(() => count.value * 2)
const handleCountAdd = () => {
console.log('count.value++');
count.value++;
};
const handleNumAdd = () => {
console.log('num.value++');
num.value++;
};
const handleUserAgeAdd = () => {
console.log('user.age++');
user.age++;
};
// 不需指定具体的响应式对象,只要里面依赖的值变化就执行
// 没有新旧值
watchEffect(() => {
console.log("watch - effect - 自动停止");
// getter
console.log(num.value);
console.log(user.age);
});
// 手动停止,使用 watchEffect 返回的 stop 函数
const stop = watchEffect(() => {
console.log("watch - effect - 手动停止");
console.log(count.value);
console.log(double.value);
});
const stopWatchEffect = () => {
stop();
};
return {
count,
num,
user,
double,
handleCountAdd,
handleNumAdd,
handleUserAgeAdd,
stopWatchEffect
}
}
};
</script>
watch
VS watchEffect
watchEffect
立即执行;watch
不立即执行,当watch
设置immediate: true
才会立即执行。watch
需要设置要侦听的响应式对象;watchEffect
不需指定具体的响应式对象,只要里面依赖的值变化就执行。watchEffect
没有新旧值;watch
有新旧值。watchEffect
返回一个stop
函数,用于 手动停止侦听;而watch
只能在组件销毁时,自动销毁。
Setup
组合式 API 入口(可以实际使用它的地方): setup
。
参数
使用 setup 函数时,它将接收两个参数:
props
context
props
setup
函数中的 props
是响应式的,当传入新的 prop
时,它将被更新。
但是,因为 props
是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。如果需要解构 prop
,可以在 setup
函数中使用 toRefs
函数来完成此操作。toRefs
将会为 prop
创建一个 ref
响应式引用:
import { toRefs } from 'vue'
export default {
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
}
如果 title 是可选的 prop
,则传入的 props
中可能没有 title 。在这种情况下,toRefs
将不会为 title 创建一个 ref
,你需要使用 toRef
替代它:
import { toRef } from 'vue'
export default {
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
}
context
context
是一个普通的 JavaScript 对象(非响应式,可解构),它暴露组件的三个 property:
attrs
slots
emit
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
}
// 解构
export default {
setup(props, { attrs, slots, emit }) {
...
}
}
attrs
和 slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x
或 slots.x
的方式引用 property。
请注意,与 props
不同,attrs
和 slots
是非响应式的。如果你打算根据 attrs
或 slots
更改应用副作用,那么应该在 onUpdated
生命周期钩子中执行此操作。
// 待补充
使用渲染函数
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态.
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 请注意这里我们需要显式调用 ref 的 value
return () => h('div', [readersNumber.value, book.title])
}
}
使用 this
在 setup()
内部,this
不是该活跃实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这使得 setup()
在和其它选项式 API 一起使用时可能会导致混淆。因此,最好从头到尾使用同一种 API 进行开发。
生命周期钩子 ???
你可以通过在生命周期钩子前面加上 “on
” 来访问组件的生命周期钩子。支持多个,谁先注册谁先执行。
下表包含如何在 setup ()
内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed |
created | Not needed |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
errorCaptured 错误捕获,只要报错就走这里。
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
Provide / Inject
我们也可以在组合式 API 中使用 provide/inject
。两者都只能在 setup()
调用。
- provide:
name
(<String>
类型)value
- inject:
- 要
inject
的 property 的name
- 默认值 (可选):当
provide
该 property 为undefined
时使用
- 要
vue2:
// ./src/App.vue
export default {
components: {
Foo
},
provide: {
location: 'North Pole'
}
}
// ./src/components/Foo.vue
export default {
inject: ['location']
}
vue3:
// ./src/App.vue
import { provide } from "vue";
export default {
components: {
Foo
},
setup() {
provide("location", "North Pole");
// 当不传值的时候,如果 inject 有默认值就用默认值
// provide("location");
},
};
// ./src/components/Foo.vue
import { inject } from 'vue'
export default {
setup() {
// const userLocation = inject('location');
// 设置默认值
const userLocation = inject('location', 'The Universe');
return {
userLocation
}
}
}
响应性
添加响应性
为了增加 provide
值和 inject
值之间的响应性,我们可以在设置 provide
值时使用 ref
或 reactive
。
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
}
}
修改响应式 property
当使用响应式 provide / inject
值时,建议尽可能将对响应式 property 的所有修改限制在 定义 provide
的组件 内部。
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)
const updateLocation = () => {
location.value = 'South Pole'
}
return {
location,
updateLocation
}
},
}
然而,有时我们需要在注入数据的组件内部更新 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
}
}
}
</script>
如果要确保通过 provide
传递的数据不会被 inject
的组件更改,我们建议对提供者的 property 使用 readonly
(只读)。
import { provide, reactive, readonly, ref } from 'vue'
export default {
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', readonly(location)) // 只读
provide('geolocation', readonly(geolocation)) // 只读
provide('updateLocation', updateLocation)
}
}
模板引用(refs)
在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref
并从 setup()
返回。
在虚拟 DOM 补丁算法中,如果 VNode 的 ref
键对应于渲染上下文中的 ref
,则 VNode 的相应元素或组件实例将被分配给该 ref
的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
// ./src/components/Foo.vue
<template>
<input type="text" ref="input" />
</template>
import { ref } from "vue";
export default {
setup() {
// refs
const input = ref(null); // 1. 变量名要与 ref 设置的名称一致
// 2. DOM 元素将在初始渲染后分配给 ref,挂载后才能获取到值
onMounted(() => {
console.log(input.value);
});
return {
input
};
},
};
作为模板使用的 ref
的行为与任何其他 ref
一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。
JSX 中的用法
export default {
setup() {
const root = ref(null)
return () =>
h('div', {
ref: root
})
// with JSX
return () => <div ref={root} />
}
}
v-for
中的用法
组合式 API 模板引用在 v-for 内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:
<template>
<!-- 通过一个函数,动态挂载 ref -->
<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([]) // refs 数组
// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
侦听模板引用
watch()
和 watchEffect()
在 DOM 挂载或更新之前运行,所以当侦听器运行时,模板引用还未被更新。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
// 在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。
console.log(root.value) // => null
})
return {
root
}
}
}
</script>
使用模板引用的侦听器应该用 flush: 'post'
选项来定义,这将在 DOM 更新后运行,确保模板引用与 DOM 保持同步,并引用正确的元素。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
console.log(root.value) // => <div></div>
},
{
flush: 'post'
})
return {
root
}
}
}
</script>
二、 Mixin
混入
与 vue2 基本一致。
基础
Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个 mixin
对象可以包含任意组件选项。当组件使用 mixin
对象时,所有 mixin
对象的选项将被“混合”进入该组件本身的选项。
// define a mixin object
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}
// define an app that uses this mixin
const app = Vue.createApp({
mixins: [myMixin]
})
app.mount('#mixins-basic') // => "hello from mixin!"
选项合并
当组件和 mixin
对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,每个 mixin
可以拥有自己的 data
函数。每个 data
函数都会被调用,并将返回结果合并。在数据的 property 发生冲突时,会以组件自身的数据为优先。
const myMixin = {
data() {
return {
message: 'hello',
foo: 'abc'
}
}
}
const app = Vue.createApp({
mixins: [myMixin],
data() {
return {
message: 'goodbye',
bar: 'def'
}
},
created() {
console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" }
}
})
同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin
对象的钩子将在组件自身钩子之前
调用。
const myMixin = {
created() {
console.log('mixin 对象的钩子被调用')
}
}
const app = Vue.createApp({
mixins: [myMixin],
created() {
console.log('组件钩子被调用')
}
})
// => "mixin 对象的钩子被调用"
// => "组件钩子被调用"
值为对象的选项,例如 methods
、components
和 directives
,将被合并为同一个对象。两个对象键名冲突时,取组件
对象的键值对。
const myMixin = {
methods: {
foo() {
console.log('foo')
},
conflicting() {
console.log('from mixin')
}
}
}
const app = Vue.createApp({
mixins: [myMixin],
methods: {
bar() {
console.log('bar')
},
conflicting() {
console.log('from self')
}
}
})
const vm = app.mount('#mixins-basic')
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
全局 mixin
Mixin 也可以进行全局注册。使用时格外小心!一旦使用全局 mixin
,它将影响每一个之后创建的组件 (例如,每个子组件)。
const app = Vue.createApp({
myOption: 'hello!'
})
// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
created() {
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
// 将myOption也添加到子组件
app.component('test-component', {
myOption: 'hello from component!'
})
app.mount('#mixins-global')
// => "hello!"
// => "hello from component!"
大多数情况下,只应当应用于自定义选项,不推荐使用全局 mixin
。推荐将其作为插件发布,以避免重复应用 mixin
。
自定义选项合并策略
自定义选项在合并时,默认策略为简单地覆盖已有值。如果想让某个自定义选项以自定义逻辑进行合并,可以在 app.config.optionMergeStrategies
中添加一个函数:
const app = Vue.createApp({})
app.config.optionMergeStrategies.customOption = (toVal, fromVal) => {
// return mergedVal
}
合并策略接收在父实例
和子实例
上定义的该选项的值,分别作为第一个和第二个参数。让我们来检查一下使用 mixin 时,这些参数有哪些:
const app = Vue.createApp({
custom: 'hello!'
})
app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
console.log(fromVal, toVal)
// 父实例,子实例 => "goodbye!", undefined
// 父实例,子实例 => "hello", "goodbye!"
return fromVal || toVal
}
app.mixin({
custom: 'goodbye!',
created() {
console.log(this.$options.custom) // => "hello!"
}
})
如你所见,在控制台中,我们先从 mixin
打印 toVal
和 fromVal
,然后从 app
打印。如果存在,我们返回 fromVal
,this.$options.custom
控制台打印的为hello!
。如果我们尝试将策略更改为始终从子实例
返回值,控制台打印的为 goodbye!
不足
在 Vue 2 中,mixin
是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
- Mixin 很容易发生冲突:因为每个
mixin
的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。 - 可重用性是有限的:我们不能向
mixin
传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。
为了解决这些问题,我们添加了一种通过逻辑关注点组织代码的新方法:组合式 API。
动机与目的
-
更好的代码组织与逻辑复用
- 代码组织:基于功能而不是基于选项
vue2 分散:
vue3 集合:
或者分出去作为一个函数,用的时候再导进来
- 逻辑复用:本身就是函数,天然支持复用 vue2 mixin:
mixin 的弊端:
- 命名冲突
- 来源不清晰
vue3:
也可以使用 toRefs 方法:
响应式丢失问题:解构有可能会导致返回给 tempalte 的是普通对象。
vueuse
-
更好的类型推导:ts
三、自定义指令
全局指令:
const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})
局部指令:
directives: {
focus: {
// 指令的定义
mounted(el) {
el.focus()
}
}
}
然后你可以在模板中任何元素上使用新的 v-focus
attribute:
<input v-focus />
钩子函数(改动)
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
created
:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加须要在普通的v-on
事件监听器前调用的事件监听器时,这很有用。beforeMount
:当指令第一次绑定到元素并且在挂载父组件之前调用。mounted
:在绑定元素的父组件被挂载后调用。beforeUpdate
:在更新包含组件的 VNode 之前调用。updated
:在包含组件的 VNode 及其子组件的 VNode 更新后调用。beforeUnmount
:在卸载绑定元素的父组件之前调用unmounted
:当指令与元素解除绑定且父组件已卸载时,只调用一次。
钩子函数参数
与 vue2 相同
函数简写
在前面的例子中,你可能想在 mounted
和 updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
和非 prop 的 attribute 类似,当在组件中使用时,自定义指令总是会被应用在组件的根节点上。
但和 attribute 不同,指令不会通过 v-bind="$attrs"
被传入另一个元素。
<my-component v-demo="test"></my-component>
app.component('my-component', {
template: `
<div> // v-demo 指令将会被应用在这里
<span>My component content</span>
</div>
`
})
有了片段支持以后,组件可能会有多个根节点。当被应用在一个多根节点的组件上时,指令会被忽略,并且会抛出一个警告。
四、Teleport
有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置。
一个常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
让我们修改 modal-button 以使用 <teleport>
,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”。
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<!-- 将模态框内容渲染为 body 标签的子级 -->
<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 将正确地将模态框内容渲染为 body 标签的子级。
与 Vue components 一起使用
如果 <teleport>
包含 Vue 组件,则它仍将是 <teleport>
父组件的逻辑子组件:
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。
这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置。
在同一目标上使用多个 teleport
一个常见的用例场景是一个可重用的 <Modal>
组件,它可能同时有多个实例处于活动状态。对于这种情况,多个 <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 添加全局级功能。它可以是公开 install()
方法的 object
,也可以是 function
。
插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property。如:
vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如:
vue-touch
- 通过全局
mixin
来添加一些组件选项。如:vue-router
- 添加全局实例方法,通过把它们添加到
config.globalProperties
上实现。 - 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如
vue-router
编写插件
为了更好地理解如何创建自己的 Vue.js 版插件,我们将创建一个非常简化的插件版本。
每当这个插件被添加到应用程序中时,如果它是一个对象,就会调用 install
方法。如果它是一个 function
,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的 createApp
生成的 app
对象和用户传入的选项。
让我们从设置插件对象开始。建议在单独的文件中创建它并将其导出,以保持包含的逻辑和分离的逻辑。
// plugins/i18n.js
export default {
install: (app, options) => {
// Plugin code goes here
}
}
我们想要一个函数来翻译整个应用程序可用的键,因此我们将使用 app.config.globalProperties
暴露它。
该函数将接收一个 key 字符串,我们将使用它在用户提供的选项中查找转换后的字符串。
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = key => {
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
}
}
我们假设用户使用插件时,将在 options
参数中传递一个包含翻译后的键的对象。我们的 $translate
函数将使用诸如 greetings.hello
之类的字符串,查看用户提供的配置内部并返回转换后的值-在这种情况下为 Bonjour!
。
greetings: {
hello: 'Bonjour!'
}
插件还允许我们使用 inject
为插件的用户提供功能或 attribute。例如,我们可以允许应用程序访问 options
参数以能够使用翻译对象。
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = key => {
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
app.provide('i18n', options)
}
}
插件用户现在可以将 inject[i18n]
注入到他们的组件并访问该对象。
另外,由于我们可以访问 app
对象,因此插件可以使用所有其他功能,例如使用 mixin
和 directive
。
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = (key) => {
return key.split('.')
.reduce((o, i) => { if (o) return o[i] }, options)
}
app.provide('i18n', options)
app.directive('my-directive', {
mounted (el, binding, vnode, oldVnode) {
// some logic ...
}
...
})
app.mixin({
created() {
// some logic ...
}
...
})
}
}
使用插件
在使用 createApp()
初始化 Vue 应用程序后,你可以通过调用 use()
方法将插件添加到你的应用程序中。
我们将使用在编写插件部分中创建的 i18nPlugin 进行演示。
use()
方法有两个参数。第一个是要安装的插件,在这种情况下为 i18nPlugin。
它还会自动阻止你多次使用同一插件,因此在同一插件上多次调用只会安装一次该插件。
第二个参数是可选的,并且取决于每个特定的插件。在演示 i18nPlugin 的情况下,它是带有转换后的字符串的对象。
import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from './plugins/i18n'
const app = createApp(Root)
const i18nStrings = {
greetings: {
hi: 'Hallo!'
}
}
app.use(i18nPlugin, i18nStrings)
app.mount('#app')
其他
reactive 直接赋值失去响应式:blog.csdn.net/qq_43750656…
vue-script-setup官方资料:github.com/vuejs/rfcs/…
vue-script-setup:github.com/zhixinpeng/…