MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。
MVVM 新增了 VM 类
MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性
Tips 1: 为什么官方要说 Vue 没有完全遵循 MVVM 思想呢?
严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
Tips 2: MVVM模型框架
View 层
<div id="app">
<p>{{message}}p>
<button v-on:click="showMessage()">Click mebutton>
div>
ViewModel 层
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
// Ajax 获取 Model 层的数据
ajax({
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})
Model 层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
缺点:
优点:
更好的搜索引擎优化 (SEO)。因为搜索引擎爬虫会直接读取完整的渲染出来的页面。
注意,目前 Google 和 Bing 已经可以很好地为同步加载的 JavaScript 应用建立索引。在这里同步加载是关键。如果应用起始状态只是一个加载中的效果,而通过 API 调用获取内容,则爬虫不会等待页面加载完成。这意味着如果你的页面有异步加载的内容且 SEO 很重要,那么你可能需要 SSR。
更快的内容呈现,尤其是网络连接缓慢或设备运行速度缓慢的时候。服务端标记不需要等待所有的 JavaScript 都被下载并执行之后才显示,所以用户可以更快看到完整的渲染好的内容。这通常会带来更好的用户体验,同时对于内容呈现时间和转化率呈正相关的应用来说尤为关键。
缺点:
在应用中使用 SSR 之前,你需要问自己的第一个问题是:你是否真的需要它?它通常是由内容呈现时间对应用的重要程度决定的。例如,如果你正在搭建一个内部管理系统,几百毫秒的初始化加载时间对它来说无关紧要,这种情况下就没有必要使用 SSR。然而,如果内容呈现时间非常关键,SSR 可以助你实现最佳的初始加载性能。
Tips 1: SSR vs 预渲染
如果你仅希望通过 SSR 来改善一些推广页面 (例如 /
、/about
、/contact
等) 的 SEO,那么预渲染也许会更合适。和使用动态编译 HTML 的 web 服务器相比,预渲染可以在构建时为指定的路由生成静态 HTML 文件。它的优势在于预渲染的设置更加简单,且允许将前端保持为一个完全静态的站点。
组件的 data
选项是一个函数。Vue 会在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也会直接通过组件实例暴露出来:
const app = Vue.createApp({
data() {
return { count: 4 }
}
})
const vm = app.mount('#app')
console.log(vm.$data.count) // => 4
console.log(vm.count) // => 4
// 修改 vm.count 的值也会更新 $data.count
vm.count = 5
console.log(vm.$data.count) // => 5
// 反之亦然
vm.$data.count = 6
console.log(vm.count) // => 6
这些实例 property 仅在实例首次创建时被添加,所以你需要确保它们都在 data
函数返回的对象中。必要时,要对尚未提供所需值的 property 使用 null
、undefined
或其他占位的值。
直接将不包含在 data
中的新 property 添加到组件实例是可行的。但由于该 property 不在背后的响应式 $data
对象内,所以 Vue 的响应性系统不会自动跟踪它。
Vue 使用 $
前缀通过组件实例暴露自己的内置 API。它还为内部 property 保留 _
前缀。你应该避免使用这两个字符开头的的顶级 data
property 名称。
Tips 1:为什么 data 是一个函数
组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果
我们用 methods
选项向组件实例添加方法,它应该是一个包含所需方法的对象:
const app = Vue.createApp({
data() {
return { count: 4 }
},
methods: {
increment() {
// `this` 指向该组件实例
this.count++
}
}
})
const vm = app.mount('#app')
console.log(vm.count) // => 4
vm.increment()
console.log(vm.count) // => 5
Vue 自动为 methods
绑定 this
,以便于它始终指向组件实例。这将确保方法在用作事件监听或回调时保持正确的 this
指向。在定义 methods
时应避免使用箭头函数,因为这会阻止 Vue 绑定恰当的 this
指向。
这些 methods
和组件实例的其它所有 property 一样可以在组件的模板中被访问。在模板中,它们通常被当做事件监听使用:
<button @click="increment">Up votebutton>
在上面的例子中,点击 时,会调用
increment
方法。
也可以直接从模板中调用方法。就像下一章节即将看到的,通常换做计算属性会更好。但是,在计算属性不可行的情况下,使用方法可能会很有用。你可以在模板支持 JavaScript 表达式的任何地方调用方法:
<span :title="toTitleDate(date)">
{{ formatDate(date) }}
span>
如果 toTitleDate
或 formatDate
访问了任何响应式数据,则将其作为渲染依赖项进行跟踪,就像直接在模板中使用过一样。
从模板调用的方法不应该有任何副作用,比如更改数据或触发异步进程。如果你想这么做,应该使用生命周期钩子来替换。
Vue 没有内置支持防抖和节流,但可以使用 Lodash 等库来实现。
如果某个组件仅使用一次,可以在 methods
中直接应用防抖:
<script src="https://unpkg.com/[email protected]/lodash.min.js">script>
<script>
Vue.createApp({
methods: {
// 用 Lodash 的防抖函数
click: _.debounce(function() {
// ... 响应点击 ...
}, 500)
}
}).mount('#app')
script>
但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的 created
里添加该防抖函数:
app.component('save-button', {
created() {
// 使用 Lodash 实现防抖
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 移除组件时,取消定时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 响应点击 ...
}
},
template: `
`
})
完整的使用示例
Document
{{counter}}
组件传递数据给父组件是通过$emit 触发事件来做到的
<div id="emit-example-argument">
<advice-component v-on:advise="showAdvice">advice-component>
div>
const app = createApp({
methods: {
showAdvice(advice) {
alert(advice)
}
}
})
app.component('advice-component', {
emits: ['advise'],
data() {
return {
adviceText: 'Some advice'
}
},
template: `
`
})
app.mount('#emit-example-argument')
父组件向子组件传递数据是通过 prop 传递的
字符串数组形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
对象形式的prop可以指定值类型
props:{
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或任何其他构造函数
}
<blog-post title="My journey with Vue">blog-post>
<blog-post :title="post.title">blog-post>
<blog-post :title="post.title + ' by ' + post.author.name">blog-post>
如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 v-bind
(用 v-bind
代替 :prop-name
)。例如,对于一个给定的对象 post
:
post: {
id: 1,
title: 'My Journey with Vue'
}
下面的模板:
<blog-post v-bind="post">blog-post>
等价于:
<blog-post v-bind:id="post.id" v-bind:title="post.title">blog-post>
获取父组件
尽管存在 prop 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为此,可以使用 ref
attribute 为子组件或 HTML 元素指定引用 ID。例如:
<input ref="input" />
例如,你希望在组件挂载时,以编程的方式 focus 到这个 input 上,这可能有用:
const app = Vue.createApp({})
app.component('base-input', {
template: `
`,
methods: {
focusInput() {
this.$refs.input.focus()
}
},
mounted() {
this.focusInput()
}
})
此外,还可以向组件本身添加另一个 ref
,并使用它从父组件触发 focusInput
事件:
<base-input ref="usernameInput">base-input>
this.$refs.usernameInput.focusInput()
Tips 1: 注意
$refs
只会在组件渲染完成之后生效。这仅作为一个用于直接操作子元素的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
。
const app = Vue.createApp({})
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
user: 'John Doe'
},
template: `
{{ todos.length }}
`
})
app.component('todo-list-statistics', {
inject: ['user'],
created() {
console.log(`Injected property: ${this.user}`) // > 注入的 property: John Doe
}
})
但是,如果我们尝试在此处 provide 一些组件的实例 property,这将是不起作用的:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
todoLength: this.todos.length // 将会导致错误 `Cannot read property 'length' of undefined`
},
template: `
...
`
})
要访问组件实例 property,我们需要将 provide
转换为返回对象的函数:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide() {
return {
todoLength: this.todos.length
}
},
template: `
...
`
})
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。
实际上,你可以将依赖注入看作是“长距离的 prop”,除了:
✨Tips 1: 处理响应性
默认情况下,provide/inject
绑定并不是响应式的。我们可以通过传递一个 ref
property 或 reactive
对象给 provide
来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength
分配一个组合式 API computed
property:
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}`) // > 注入的 property: 5
}
})
在这种情况下,任何对 todos.length
的改变都会被正确地反映在注入 todoLength
的组件中。
当组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。例如,在 date-picker 组件的实例中:
app.component('date-picker', {
template: `
`
})
如果我们需要通过 data-status
attribute 定义
组件的状态,它将应用于根节点 (即 div.date-picker
)。
<date-picker data-status="activated">date-picker>
<div class="date-picker" data-status="activated">
<input type="datetime-local" />
div>
同样的规则也适用于事件监听器:
<date-picker @change="submitChange">date-picker>
app.component('date-picker', {
created() {
console.log(this.$attrs) // { onChange: () => {} }
}
})
当一个具有 change
事件的 HTML 元素作为 date-picker
的根元素时,这可能会有帮助。
app.component('date-picker', {
template: `
`
})
在这种情况下,change
事件监听器将从父组件传递到子组件,它将在原生 的
change
事件上触发。我们不需要显式地从 date-picker
发出事件:
<div id="date-picker" class="demo">
<date-picker @change="showChange">date-picker>
div>
const app = Vue.createApp({
methods: {
showChange(event) {
console.log(event.target.value) // 将打印所选选项的值
}
}
})
如果你不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false
。
禁用 attribute 继承的常见场景是需要将 attribute 应用于根节点之外的其他元素。
通过将 inheritAttrs
选项设置为 false
,你可以使用组件的 $attrs
property 将 attribute 应用到其它元素上,该 property 包括组件 props
和 emits
property 中未包含的所有属性 (例如,class
、style
、v-on
监听器等)。
使用上一节中的 date-picker 组件示例,如果需要将所有非 prop 的 attribute 应用于 input
元素而不是根 div
元素,可以使用 v-bind
缩写来完成。
app.component('date-picker', {
inheritAttrs: false,
template: `
`
})
有了这个新配置,data-status
attribute 将应用于 input
元素!
<date-picker data-status="activated">date-picker>
<div class="date-picker">
<input type="datetime-local" data-status="activated" />
div>
与单个根节点组件不同,具有多个根节点的组件不具有自动 attribute allthrough (隐式贯穿行为)。如果未显式绑定 $attrs
,将发出运行时警告。
<custom-layout id="custom-layout" @click="changeValue">custom-layout>
// 这将发出警告
app.component('custom-layout', {
template: `
...
...
`
})
// 没有警告,$attrs 被传递到 元素
app.component('custom-layout', {
template: `
...
...
`
})
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
这里有两种常见的试图变更一个 prop 的情形:
props: ['initialCounter'],
data() {
return {
counter: this.initialCounter
}
}
props: ['size'],
computed: {
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态,且 Vue 无法为此向你发出警告。作为一个通用规则,应该避免修改任何 prop,包括对象和数组,因为这种做法无视了单向数据绑定,且可能会导致意料之外的结果。
v-if
是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-if
和 v-for
一起使用永远不要在一个元素上同时使用 v-if
和 v-for
。
一般我们在两种常见的情况下会倾向于这样做:
v-for="user in users" v-if="user.isActive"
)。在这种情形下,请将 users
替换为一个计算属性 (比如 activeUsers
),返回过滤后的列表。v-for="user in users" v-if="shouldShowUsers"
)。这种情形下,请将 v-if
移动至容器元素上 (比如 ul
、ol
)。反面例子
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
li>
ul>
正面例子
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
li>
ul>
<ul>
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">
{{ user.name }}
li>
template>
ul>
Tips 1: 详解
当 Vue 处理指令时,v-if
比 v-for
具有更高的优先级,所以这个模板:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
li>
ul>
将抛出一个错误,因为 v-if
指令将首先被执行,而迭代的变量 user
此时还不存在。
这可以通过遍历一个计算属性来解决,像这样:
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
li>
ul>
另外,我们也可以使用 标签和
v-for
来包裹 元素。
<ul>
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">
{{ user.name }}
li>
template>
ul>
.lazy
在默认情况下,v-model
在每次 input
事件触发后将输入框的值与数据进行同步 。你可以添加 lazy
修饰符,从而转为在 change
事件之后进行同步:
.number
如果想自动将用户的输入值转为数值类型,可以给 v-model
添加 number
修饰符:
<input v-model.number="age" type="text" />
当输入类型为 text
时这通常很有用。如果输入类型是 number
,Vue 能够自动将原始字符串转换为数字,无需为 v-model
添加 .number
修饰符。如果这个值无法被 parseFloat()
解析,则返回原始的值。
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model
添加 trim
修饰符:
<input v-model.trim="msg" />
当我们学习表单输入绑定时,我们看到 v-model
有内置修饰符——.trim
、.number
和 .lazy
。但是,在某些情况下,你可能还需要添加自己的自定义修饰符。
让我们创建一个示例自定义修饰符 capitalize
,它将 v-model
绑定提供的字符串的第一个字母大写。
添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers
prop。
请注意,当组件的 created
生命周期钩子触发时,modelModifiers
prop 会包含 capitalize
,且其值为 true
——因为 capitalize
被设置在了写为 v-model.capitalize="myText"
的 v-model
绑定上。
<my-component v-model.capitalize="myText">my-component>
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: `
`,
created() {
console.log(this.modelModifiers) // { capitalize: true }
}
})
现在我们已经设置了 prop,我们可以检查 modelModifiers
对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 元素触发
input
事件时,我们都将字符串大写。
<div id="app">
<my-component v-model.capitalize="myText">my-component>
{{ myText }}
div>
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: ``
})
app.mount('#app')
对于带参数的 v-model
绑定,生成的 prop 名称将为 arg + "Modifiers"
:
<my-component v-model:description.capitalize="myText">my-component>
app.component('my-component', {
props: ['description', 'descriptionModifiers'],
emits: ['update:description'],
template: `
`,
created() {
console.log(this.descriptionModifiers) // { capitalize: true }
}
})
Tips 1: Vue 的双向数据绑定原理
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Vue 3.0与Vue 2.0的区别仅是数据劫持的方式由Object.defineProperty更改为Proxy代理,其他代码不变。
⚖ Tips 2: v-model
是语法糖
-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现
v-model
不会在输入法组织文字过程中得到更新。如果你也想响应这些更新,请使用input
事件监听器和value
绑定来替代v-model
。
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
Class 可以通过对象语法和数组语法进行动态绑定:
data: {
isActive: true,
hasError: false
}
<div v-bind:class="[isActive ? activeClass : '', errorClass]">div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
Style 也可以通过对象语法和数组语法进行动态绑定:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">div>
data: {
activeColor: 'red',
fontSize: 30
}
<div v-bind:style="[styleColor, styleSize]">div>
data: {
styleColor: {
color: 'red'
},
styleSize:{
fontSize:'23px'
}
}
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据
即:
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。
vue采用数据劫持结合发布者-订阅者模式的方式实现双向绑定
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Proxy() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将
元素作为承载分发内容的出口。
它允许你像这样合成组件:
<todo-button>
Add todo
todo-button>
然后在
的模板中,你可能有:
<button class="btn-primary">
<slot>slot>
button>
当组件渲染的时候,
将会被替换为“Add todo”。
<button class="btn-primary">
Add todo
button>
不过,字符串只是开始!插槽还可以包含任何模板代码,包括 HTML:
<todo-button>
<i class="fas fa-plus">i>
Add todo
todo-button>
或其他组件:
<todo-button>
<font-awesome-icon name="plus">font-awesome-icon>
Add todo
todo-button>
如果
的 template 中没有包含一个
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
<button class="btn-primary">
Create a new item
button>
<todo-button>
Add todo
todo-button>
当你想在一个插槽中使用数据时,例如:
<todo-button>
Delete a {{ item.name }}
todo-button>
该插槽可以访问与模板其余部分相同的实例 property (即相同的“作用域”)。
插槽不能访问
的作用域。例如,尝试访问 action
将不起作用:
<todo-button action="delete">
Clicking here will {{ action }} an item
todo-button>
有时为一个插槽指定备用 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个
组件中:
<button type="submit">
<slot>slot>
button>
我们可能希望这个 内绝大多数情况下都渲染“Submit”文本。为了将“Submit”作为备用内容,我们可以将它放在
标签内:
<button type="submit">
<slot>Submitslot>
button>
现在当我们在一个父级组件中使用
并且不提供任何插槽内容时:
<submit-button>submit-button>
备用内容“Submit”将会被渲染:
<button type="submit">
Submit
button>
但是如果我们提供内容:
<submit-button>
Save
submit-button>
则这个提供的内容将会被渲染从而取代备用内容:
<button type="submit">
Save
button>
有时我们需要多个插槽。例如对于一个带有如下模板的
组件:
<div class="container">
<header>
header>
<main>
main>
<footer>
footer>
div>
对于这样的情况,
元素有一个特殊的 attribute:name
。通过它可以为不同的插槽分配独立的 ID,也就能够以此来决定内容应该渲染到什么地方:
<div class="container">
<header>
<slot name="header">slot>
header>
<main>
<slot>slot>
main>
<footer>
<slot name="footer">slot>
footer>
div>
一个不带 name
的
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 元素上使用
v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page titleh1>
template>
<template v-slot:default>
<p>A paragraph for the main content.p>
<p>And another one.p>
template>
<template v-slot:footer>
<p>Here's some contact infop>
template>
base-layout>
现在 元素中的所有内容都将会被传入相应的插槽。
渲染的 HTML 将会是:
<div class="container">
<header>
<h1>Here might be a page titleh1>
header>
<main>
<p>A paragraph for the main content.p>
<p>And another one.p>
main>
<footer>
<p>Here's some contact infop>
footer>
div>
有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。
要使 item
在父级提供的插槽内容上可用,我们可以添加一个
元素并将其作为一个 attribute 绑定:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item">slot>
li>
ul>
可以根据自己的需要将任意数量的 attribute 绑定到 slot
上:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute">slot>
li>
ul>
绑定在
元素上的 attribute 被称为插槽 prop。现在,在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check">i>
<span class="green">{{ slotProps.item }}span>
template>
todo-list>
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
<base-layout>
<template #header>
<h1>Here might be a page titleh1>
template>
<template #default>
<p>A paragraph for the main content.p>
<p>And another one.p>
template>
<template #footer>
<p>Here's some contact infop>
template>
base-layout>
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
<todo-list #="{ item }">
<i class="fas fa-check">i>
<span class="green">{{ item }}span>
todo-list>
如果希望使用缩写的话,你必须始终以明确的插槽名取而代之:
<todo-list #default="{ item }">
<i class="fas fa-check">i>
<span class="green">{{ item }}span>
todo-list>
基本例子
<div id="computed-basics">
<p>Has published books:p>
<span>{{ publishedBooksMessage }}span>
div>
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 计算属性的 getter
publishedBooksMessage() {
// `this` 指向 vm 实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}).mount('#computed-basics')
Tips 1: 计算属性缓存 vs 方法
我们可以将同样的函数定义为一个方法,而不是一个计算属性。从最终结果来说,这两种实现方式确实是完全相同的。然而,不同的是计算属性将基于它们的响应依赖关系缓存。计算属性只会在相关响应式依赖发生改变时重新求值。这就意味着只要 author.books
还没有发生改变,多次访问 publishedBookMessage
时计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将永远不会更新,因为 Date.now ()
不是响应式依赖:
computed: {
now() {
return Date.now()
}
}
相比之下,每当触发重新渲染时,调用方法将始终会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 list
,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 list
。如果没有缓存,我们将不可避免的多次执行 list
的 getter!如果你不希望有缓存,请用 method
来替代。
Tips 2: 计算属性的 Setter
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
现在再运行 vm.fullName = 'John Doe'
时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
基本例子
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question" />
p>
<p>{{ answer }}p>
div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js">script>
<script>
const watchExampleVM = Vue.createApp({
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// 每当 question 发生变化时,该函数将会执行
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...'
axios
.get('https://yesno.wtf/api')
.then(response => {
this.answer = response.data.answer
})
.catch(error => {
this.answer = 'Error! Could not reach the API. ' + error
})
}
}
}).mount('#watch-example')
script>
Tips 1: 计算属性 vs 侦听器
Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,watch
很容易被滥用——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。细想一下这个例子:
<div id="demo">{{ fullName }}div>
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
watch: {
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
}).mount('#demo')
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
}).mount('#demo')
好很多了,不是吗?
当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的。
既然我们知道了为什么,我们就可以知道怎么做。为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup
。
setup
组件选项使用 setup
函数时,它将接收两个参数:
props
context
etup
函数中的第一个参数是 props
。正如在一个标准组件中所期望的那样,setup
函数中的 props
是响应式的,当传入新的 prop 时,它将被更新。
// MyBook.vue
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
但是,因为 props
是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。
如果需要解构 prop,可以在 setup
函数中使用 toRefs
函数来完成此操作:
// MyBook.vue
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
如果 title
是可选的 prop,则传入的 props
中可能没有 title
。在这种情况下,toRefs
将不会为 title
创建一个 ref 。你需要使用 toRef
替代它:
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
传递给 setup
函数的第二个参数是 context
。context
是一个普通 JavaScript 对象,暴露了其它可能在 setup
中有用的值:
// MyBook.vue
export default {
setup(props, context) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)
// 触发事件 (方法,等同于 $emit)
console.log(context.emit)
// 暴露公共 property (函数)
console.log(context.expose)
}
}
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构。
// MyBook.vue
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}
attrs
和 slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x
或 slots.x
的方式引用 property。请注意,与 props
不同,attrs
和 slots
的 property 是非响应式的。如果你打算根据 attrs
或 slots
的更改应用副作用,那么应该在 onBeforeUpdate
生命周期钩子中执行此操作。
Tips 1:使用 this
在 setup()
内部,this
不是该活跃实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这使得 setup()
在和其它选项式 API 一起使用时可能会导致混淆。
setup
添加到组件// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
Tips 1: 这里repositories
变量是非响应式的
ref
的响应式变量在 Vue 3.0 中,我们可以通过一个新的 ref
函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue'
const counter = ref(0)
ref
接收参数并将其包裹在一个带有 value
property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,Number
或 String
等基本类型是通过值而非引用传递的。
在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。
Tips 1: 换句话说,ref
为我们的值创建了一个响应式引用。
回到我们的例子,让我们创建一个响应式的 repositories
变量:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
}
完成!现在,每当我们调用 getUserRepositories
时,repositories
都将发生变化,视图也会更新以反映变化。我们的组件现在应该如下所示:
// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
},
data () {
return {
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
setup
内注册生命周期钩子为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup
中注册生命周期钩子的方法。这要归功于 Vue 导出的几个新函数。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on
:即 mounted
看起来会像 onMounted
。
这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。
让我们将其添加到 setup
函数中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
watch
响应式更改就像我们在组件中使用 watch
选项并在 user
property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。它接受 3 个参数:
下面让我们快速了解一下它是如何工作的
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
每当 counter
被修改时,例如 counter.value=5
,侦听将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5'
记录到控制台中。
以下是等效的选项式 API:
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
}
现在我们将其应用到我们的示例中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'
// 在我们组件中
setup (props) {
// 使用 `toRefs` 创建对 `props` 中的 `user` property 的响应式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `prop.user` 到 `user.value` 访问引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在 user prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
computed
属性基本例子
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
将搜索功能移到 setup
中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'
// 在我们的组件中
setup (props) {
// 使用 `toRefs` 创建对 props 中的 `user` property 的响应式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `props.user ` 到 `user.value` 访问引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在 user prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(
repository => repository.name.includes(searchQuery.value)
)
})
return {
repositories,
getUserRepositories,
searchQuery,
repositoriesMatchingSearchQuery
}
}
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 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 |
⛲ Tips 1: Vue 的父子组件生命周期钩子函数执行顺序
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
父 beforeUpdate->父 updated
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
Tips 2: 各个生命周期的作用
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
生命周期示意图
在 setup()
中使用 provide
时,我们首先从 vue
显式导入 provide
方法。这使我们能够调用 provide
来定义每个 property。
provide
函数允许你通过两个参数定义 property:
类型)使用 MyMap
组件后,provide 的值可以按如下方式重构:
在 setup()
中使用 inject
时,也需要从 vue
显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。
inject
函数有两个参数:
使用 MyMarker
组件,可以使用以下代码对其进行重构:
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive。
使用 MyMap
组件,我们的代码可以更新如下:
现在,如果这两个 property 中有任何更改,MyMarker
组件也将自动更新!
当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。
例如,在需要更改用户位置的情况下,我们最好在 MyMap
组件中执行此操作。
然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。
最后,如果要确保通过 provide
传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly
。
is
attribute有的时候,在不同组件之间进行动态切换是非常有用的
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>
{{ tab }}
button>
<component v-bind:is="currentTabComponent" class="tab">component>
div>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
app.component('tab-home', {
template: `Home component`
})
app.component('tab-posts', {
template: `Posts component`
})
app.component('tab-archive', {
template: `Archive component`
})
app.mount('#dynamic-component-demo')
在上述示例中,currentTabComponent
可以包括:
keep-alive
我们之前在一个多标签的界面中使用 is
attribute 来切换不同的组件:
<component :is="currentTabComponent">component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复渲染导致的性能问题。
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent
实例。
重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个
元素将其动态组件包裹起来。
<keep-alive>
<component :is="currentTabComponent">component>
keep-alive>
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了实现这个效果,Vue 有一个 defineAsyncComponent
方法:
const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
const AsyncComp = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: 'I am async!'
})
})
)
app.component('async-example', AsyncComp)
如你所见,此方法接受一个返回 Promise
的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve
回调。你也可以调用 reject(reason)
,来表示加载失败。
你也可以在工厂函数中返回一个 Promise
,把 webpack 2 及以上版本和 ES2015 语法相结合后,我们就可以这样使用动态地导入:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
当在局部注册组件时,你也可以使用 defineAsyncComponent
:
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
基本例子
const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})
如果想注册局部指令,组件中也接受一个 directives
的选项:
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
:当指令与元素解除绑定且父组件已卸载时,只调用一次。
Tips 1: directive
参数:
{string} name
{Function | Object} [definition]
返回值:
definition
参数,则返回应用实例。definition
参数,则返回指令定义。用法:
注册或检索全局指令。
import { createApp } from 'vue'
const app = createApp({})
// 注册
app.directive('my-directive', {
// 指令具有一组生命周期钩子:
// 在绑定元素的 attribute 或事件监听器被应用之前调用
created() {},
// 在绑定元素的父组件挂载之前调用
beforeMount() {},
// 在绑定元素的父组件挂载之后调用
mounted() {},
// 在包含组件的 VNode 更新之前调用
beforeUpdate() {},
// 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
updated() {},
// 在绑定元素的父组件卸载之前调用
beforeUnmount() {},
// 在绑定元素的父组件卸载之后调用
unmounted() {}
})
// 注册 (函数指令)
app.directive('my-directive', () => {
// 这将被作为 `mounted` 和 `updated` 调用
})
// getter, 如果已注册,则返回指令定义
const myDirective = app.directive('my-directive')
指令绑定到的元素。这可用于直接操作 DOM。
包含以下 property 的对象。
instance
:使用指令的组件实例。value
:传递给指令的值。例如,在 v-my-directive="1 + 1"
中,该值为 2
。oldValue
:先前的值,仅在 beforeUpdate
和 updated
中可用。无论值是否有更改都可用。arg
:传递给指令的参数(如果有的话)。例如在 v-my-directive:foo
中,arg 为 "foo"
。modifiers
:包含修饰符(如果有的话) 的对象。例如在 v-my-directive.foo.bar
中,修饰符对象为 {foo: true,bar: true}
。dir
:一个对象,在注册指令时作为参数传递。例如,在以下指令中app.directive('focus', {
mounted(el) {
el.focus()
}
})
dir
将会是以下对象:
{
mounted(el) {
el.focus()
}
}
一个真实 DOM 元素的蓝图,对应上面收到的 el 参数。
上一个虚拟节点,仅在 beforeUpdate
和 updated
钩子中可用。
除了 el
之外,你应该将这些参数视为只读,并且永远不要修改它们。
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
const app = Vue.createApp({
data() {
return {
direction: 'right'
}
}
})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
// binding.arg 是我们传递给指令的参数
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
app.mount('#dynamic-arguments-example')
我们的自定义指令现在已经足够灵活,可以支持一些不同的用例。为了使其更具动态性,我们还可以允许修改绑定值。让我们创建一个附加属性 pinPadding
,并将其绑定到 。
Scroll down the page
Stick me {{ pinPadding + 'px' }} from the {{ direction || 'top' }} 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'
}
})
你可能想在 mounted
和 updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
和非 prop 的 attribute 类似,当在组件中使用时,自定义指令总是会被应用在组件的根节点上。
app.component('my-component', {
template: `
// v-demo 指令将会被应用在这里
My component content
`
})
和 attribute 不同,指令不会通过 v-bind="$attrs"
被传入另一个元素。
有了片段支持以后,组件可能会有多个根节点。当被应用在一个多根节点的组件上时,指令会被忽略,并且会抛出一个警告。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
让我们深入一个简单的例子,这个例子里 render
函数很实用。假设我们要生成一些带锚点的标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
a>
h1>
锚点标题的使用非常频繁,我们应该创建一个组件:
Hello world!
当开始写一个只能通过 level
prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:
const { createApp } = Vue
const app = createApp({})
app.component('anchored-heading', {
template: `
`,
props: {
level: {
type: Number,
required: true
}
}
})
这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了
。当我们添加锚元素时,我们必须在每个 v-if/v-else-if
分支中再次重复它。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
const { createApp, h } = Vue
const app = createApp({})
app.component('anchored-heading', {
render() {
return h(
'h' + this.level, // 标签名
{}, // prop 或 attribute
this.$slots.default() // 包含其子节点的数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
render()
函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot
指令的子节点时,比如 anchored-heading
中的 Hello world!
,这些子节点被存储在组件实例中的 $slots.default
中。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return h('h1', {}, this.blogTitle)
h()
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
Tips 1: h()
参数
h()
函数是一个用于创建 VNode 的实用程序。也许可以更准确地将其命名为 createVNode()
,但由于频繁使用和简洁,它被称为 h()
。它接受三个参数:
// @returns {VNode}
h(
// {String | Object | Function} tag
// 一个 HTML 标签名、一个组件、一个异步组件、或
// 一个函数式组件。
//
// 必需的。
'div',
// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 这会在模板中用到。
//
// 可选的。
{},
// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 VNode" 或者
// 有插槽的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)
如果没有 prop,那么通常可以将 children 作为第二个参数传入。如果会产生歧义,可以将 null
作为第二个参数传入,将 children 作为第三个参数传入。
完整实例
const { createApp, h } = Vue
const app = createApp({})
/** 递归地从子节点获取文本 */
function getChildrenTextContent(children) {
return children
.map(node => {
return typeof node.children === 'string'
? node.children
: Array.isArray(node.children)
? getChildrenTextContent(node.children)
: ''
})
.join('')
}
app.component('anchored-heading', {
render() {
// 从 children 的文本内容中创建短横线分隔 (kebab-case) id。
const headingId = getChildrenTextContent(this.$slots.default())
.toLowerCase()
.replace(/\W+/g, '-') // 用短横线替换非单词字符
.replace(/(^-|-$)/g, '') // 删除前后短横线
return h('h' + this.level, [
h(
'a',
{
name: headingId,
href: '#' + headingId
},
this.$slots.default()
)
])
},
props: {
level: {
type: Number,
required: true
}
}
})
Tips 2: 约束 ------ VNodes 必须唯一
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render() {
const myParagraphVNode = h('p', 'hi')
return h('div', [
// 错误 - 重复的 Vnode!
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render() {
return h('div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
Tips 3: 虚拟 DOM 有什么优缺点
由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。
优点:
缺点:
Tips 4: 虚拟 DOM 实现原理
虚拟 DOM 的实现原理主要包括以下 3 部分:
要为某个组件创建一个 VNode,传递给 h
的第一个参数应该是组件本身。
render() {
return h(ButtonCounter)
}
如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent
:
const { h, resolveComponent } = Vue
// ...
render() {
const ButtonCounter = resolveComponent('ButtonCounter')
return h(ButtonCounter)
}
resolveComponent
是模板内部用来解析组件名称的同一个函数。
render
函数通常只需要对全局注册的组件使用 resolveComponent
。而对于局部注册的却可以跳过,请看下面的例子:
// 此写法可以简化
components: {
ButtonCounter
},
render() {
return h(resolveComponent('ButtonCounter'))
}
我们可以直接使用它,而不是通过名称注册一个组件,然后再查找:
render() {
return h(ButtonCounter)
}
v-if
和 v-for
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if
和 v-for
:
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}li>
ul>
<p v-else>No items found.p>
这些都可以在渲染函数中用 JavaScript 的 if
/else
和 map()
来重写:
props: ['items'],
render() {
if (this.items.length) {
return h('ul', this.items.map((item) => {
return h('li', item.name)
}))
} else {
return h('p', 'No items found.')
}
}
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些 prop:
props: ['modelValue'],
emits: ['update:modelValue'],
render() {
return h(SomeComponent, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}
v-on
我们必须为事件处理程序提供一个正确的 prop 名称,例如,要处理 click
事件,prop 名称应该是 onClick
。
render() {
return h('div', {
onClick: $event => console.log('clicked', $event.target)
})
}
对于 .passive
、.capture
和 .once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
实例:
render() {
return h('input', {
onClickCapture: this.doThisInCapturingMode,
onKeyupOnce: this.doThisOnce,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode
})
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop |
event.stopPropagation() |
.prevent |
event.preventDefault() |
.self |
if (event.target !== event.currentTarget) return |
按键: .enter , .13 |
if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 |
修饰键: .ctrl , .alt , .shift , .meta |
if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey , shiftKey , 或 metaKey ) |
这里是一个使用所有修饰符的例子:
render() {
return h('input', {
onKeyUp: event => {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果向上键不是回车键,则终止
// 没有同时按下按键 (13) 和 shift 键
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件传播
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
})
}
你可以通过 [this.$slots
] 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render() {
// ` `
return h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
// ` `
return h('div', {}, this.$slots.default({
text: this.message
}))
}
要使用渲染函数将插槽传递给子组件,请执行以下操作:
const { h, resolveComponent } = Vue
render() {
// `{{ props.text }} `
return h('div', [
h(
resolveComponent('child'),
{},
// 将 `slots` 以 { name: props => VNode | Array } 的形式传递给子对象。
{
default: (props) => Vue.h('span', props.text)
}
)
])
}
插槽以函数的形式传递,允许子组件控制每个插槽内容的创建。任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。相反,对 resolveComponent
的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。
// ` {{ text }} `
render() {
// 应该是在插槽函数外面调用 resolveComponent。
const Button = resolveComponent('MyButton')
const Icon = resolveComponent('MyIcon')
return h(
Button,
null,
{
// 使用箭头函数保存 `this` 的值
default: (props) => {
// 响应式 property 应该在插槽函数内部读取,
// 这样它们就会成为 children 渲染的依赖。
return [
h(Icon, { name: this.icon }),
this.text
]
}
}
)
}
如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件。
render() {
return h(Panel, null, this.$slots)
}
也可以根据情况单独传递或包裹住。
render() {
return h(
Panel,
null,
{
// 如果我们想传递一个槽函数,我们可以通过
header: this.$slots.header,
// 如果我们需要以某种方式对插槽进行操作,
// 那么我们需要用一个新的函数来包裹它
default: (props) => {
const children = this.$slots.default ? this.$slots.default(props) : []
return children.concat(h('div', 'Extra child'))
}
}
)
}
和 is
在底层实现里,模板使用 resolveDynamicComponent
来实现 is
attribute。如果我们在 render
函数中需要 is
提供的所有灵活性,我们可以使用同样的函数:
const { h, resolveDynamicComponent } = Vue
// ...
// ` `
render() {
const Component = resolveDynamicComponent(this.name)
return h(Component)
}
就像 is
, resolveDynamicComponent
支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。
通常这种程度的灵活性是不需要的。通常 resolveDynamicComponent
可以被换做一个更直接的替代方案。
例如,如果我们只需要支持组件名称,那么可以使用 resolveComponent
来代替。
如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给 h
:
// ` `
render() {
return h(this.bold ? 'strong' : 'em')
}
同样,如果传递给 is
的值是一个组件选项对象,那么不需要解析什么,可以直接作为 h
的第一个参数传递。
与 标签一样,
标签仅在模板中作为语法占位符需要,当迁移到 render
函数时,应被丢弃。
可以使用 withDirectives
将自定义指令应用于 VNode:
const { h, resolveDirective, withDirectives } = Vue
// ...
//
render () {
const pin = resolveDirective('pin')
return withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
}
resolveDirective
是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。
诸如
、
、
和
等内置组件默认并没有被全局注册。这使得打包工具可以 tree-shake,因此这些组件只会在被用到的时候被引入构建。不过这也意味着我们无法通过 resolveComponent
或 resolveDynamicComponent
访问它们。
在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。当我们编写自己的 render
函数时,需要自行导入它们:
const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
return h(Transition, { mode: 'out-in' }, /* ... */)
}
在我们目前看过的所有示例中,render
函数返回的是单个根 VNode。但其实也有别的选项。
返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素:
render() {
return 'Hello world!'
}
我们也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment):
// 相当于模板 `Hello
world!`
render() {
return [
'Hello',
h('br'),
'world!'
]
}
可能是因为数据依然在加载中的关系,组件不需要渲染,这时它可以返回 null
。这样我们在 DOM 中会渲染一个注释节点。
Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render() {
return (
Hello world!
)
}
})
app.mount('#demo')
函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。
我们使用的是一个简单函数,而不是一个选项对象,来创建函数式组件。该函数实际上就是该组件的 render
函数。而因为函数式组件里没有 this
引用,Vue 会把 props
当作第一个参数传入:
const FunctionalComponent = (props, context) => {
// ...
}
第二个参数 context
包含三个 property:attrs
、emit
和 slots
。它们分别相当于实例的 $attrs
、$emit
和 $slots
这几个 property。
大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 props
和 emits
作为 property 加入,以达到定义它们的目的:
FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']
如果这个 props
选项没有被定义,那么被传入函数的 props
对象就会像 attrs
一样会包含所有 attribute。除非指定了 props
选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。
函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h
,它将会被当作一个函数式组件来对待。
Tips 1: 模板编译原理
Vue 的模板实际上被编译成了渲染函数。
分为三步:
为了能够在数值变化时,随时运行我们的总和,我们首先要做的是将其包裹在一个函数中。
const updateSum = () => {
sum = val1 + val2
}
但我们如何告知 Vue 这个函数呢?
Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它。
为了更好地理解这一点,让我们尝试脱离 Vue 实现类似的东西,以看看它如何工作。
我们需要的是能够包裹总和的东西,像这样:
createEffect(() => {
sum = val1 + val2
})
我们需要 createEffect
来跟踪和执行。我们的实现如下:
// 维持一个执行副作用的栈
const runningEffects = []
const createEffect = fn => {
// 将传来的 fn 包裹在一个副作用函数中
const effect = () => {
runningEffects.push(effect)
fn()
runningEffects.pop()
}
// 立即自动执行副作用
effect()
}
当我们的副作用被调用时,在调用 fn
之前,它会把自己推到 runningEffects
数组中。这个数组可以用来检查当前正在运行的副作用。
副作用是许多关键功能的起点。例如,组件的渲染和计算属性都在内部使用副作用。任何时候,只要有东西对数据变化做出奇妙的回应,你就可以肯定它已经被包裹在一个副作用中了。
当我们从一个组件的 data
函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get
和 set
处理程序的Proxy中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。
Tips 1: Proxy
A Proxy
is created with two parameters:
target
: 要代理的原始对象handler
: 一个对象,它定义了哪些操作将被拦截以及如何重新定义被拦截的操作。const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property) {
console.log('intercepted!')
return target[property]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// intercepted!
// tacos
除了控制台日志,我们可以在这里做任何我们想做的事情。如果我们愿意,我们甚至可以不返回实际值。这就是为什么 Proxy 对于创建 API 如此强大。
使用 Proxy 的一个难点是 this
绑定。我们希望任何方法都绑定到这个 Proxy,而不是目标对象,这样我们也可以拦截它们。值得庆幸的是,ES6 引入了另一个名为 Reflect
的新特性,它允许我们以最小的代价消除了这个问题:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
Vue 实现响应性的关键步骤:
get
处理函数中 track
函数记录了该 property 和当前副作用。set
处理函数。trigger
函数查找哪些副作用依赖于该 property 并执行它们。const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
trigger(target, property)
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
从 Vue 3 开始,我们的响应性现在可以在一个独立包中使用。将 $data
包裹在一个代理中的函数被称为 reactive
。我们可以自己直接调用这个函数,允许我们在不需要使用组件的情况下将一个对象包裹在一个响应式代理中。
Tips 1: reactive和ref的区别
reactive 和 ref 都是用来定义响应式数据的 reactive更推荐去定义复杂的数据类型 ref 更推荐定义基本类型
ref 和 reactive 本质我们可以简单的理解为ref是对reactive的二次包装, ref定义的数据访问的时候要多一个.value
使用ref定义基本数据类型,ref也可以定义数组和对象。
Tips 2: reactive
将解包所有深层的 refs,同时维持 ref 的响应性。
const count = ref(1)
const obj = reactive({ count })
// ref 会被解包
console.log(obj.count === count.value) // true
// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3
Tips 3: 当将 ref 分配给 reactive
property 时,ref 将被自动解包。
const count = ref(1)
const obj = reactive({})
obj.count = count
console.log(obj.count) // 1
console.log(obj.count === count.value) // true
Vue 在内部跟踪所有已经被转成响应式的对象,所以它总是为同一个对象返回相同的代理。
当从一个响应式代理中访问一个嵌套对象时,该对象在被返回之前也被转换为一个代理:
const handler = {
get(target, property, receiver) {
track(target, property)
const value = Reflect.get(...arguments)
if (isObject(value)) {
// 将嵌套对象包裹在自己的响应式代理中
return reactive(value)
} else {
return value
}
}
// ...
}
Proxy 的使用确实引入了一个需要注意的新警告:在身份比较方面,被代理对象与原始对象不相等 (===
)。例如:
const obj = {}
const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
其他依赖严格等于比较的操作也会受到影响,例如 .includes()
或 .indexOf()
。
这里的最佳实践是永远不要持有对原始对象的引用,而只使用响应式版本。
const obj = reactive({
count: 0
}) // 未引用原始
这确保了等值的比较和响应性的行为都符合预期。
请注意,Vue 不会在 Proxy 中包裹数字或字符串等原始值,所以你仍然可以对这些值直接使用 ===
来比较:
const obj = reactive({
count: 0
})
console.log(obj.count === 0) // true
一个组件的模板被编译成一个 render
函数。渲染函数创建 VNodes,描述该组件应该如何被渲染。它被包裹在一个副作用中,允许 Vue 在运行时跟踪被“触达”的 property。
一个 render
函数在概念上与一个 computed
property 非常相似。Vue 并不确切地追踪依赖关系是如何被使用的,它只知道在函数运行的某个时间点上使用了这些依赖关系。如果这些 property 中的任何一个随后发生了变化,它将触发副作用再次运行,重新运行 render
函数以生成新的 VNodes。然后这些举动被用来对 DOM 进行必要的修改。
要为 JavaScript 对象创建响应式状态,可以使用 reactive
方法:
import { reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
reactive
相当于 Vue 2.x 中的 Vue.observable()
API,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响传递对象的所有嵌套 property。
Vue 中响应式状态的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。
这就是 Vue 响应性系统的本质。当从组件中的 data()
返回一个对象时,它在内部交由 reactive()
使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。
refs
ref
会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref
名称的来源。该对象只包含一个名为 value
的 property:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
当 ref 作为渲染上下文 (从 setup 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次解包内部值。只有访问嵌套的 ref 时需要在模板中添加 .value
:
<template>
<div>
<span>{{ count }}span>
<button @click="count ++">Increment countbutton>
<button @click="nested.count.value ++">Nested Increment countbutton>
div>
template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
script>
当 ref
作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1
Ref 解包仅发生在被响应式 Object
嵌套的时候。当从 Array
或原生集合类型如 Map
访问 ref 时,不会进行解包:
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
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)///如果直接用ES6结构会失去响应性
title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
Tips 1: vue2 和 vue3 对数组的响应性对比
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 测试数组响应式实例title>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js">script>
head>
<body>
<div id="app">
<h1>一维数组(length={{message.length}})h1>
<p v-for="item in message">{{item}}p>
<h1>JSON数组(length={{message1.length}})h1>
<p v-for="item in message1">{{item.name}}p>
div>
<script>
console.log(typeof(Vue))
if(typeof(Vue) === 'function'){//vue2走这,可更换引入的js切换到vue3
new Vue({
el: '#app',
data() {
return {
message: [
1,
],
message1: [
{name:'name'},
],
}
},
mounted() {
setTimeout(()=>{
//操作一维数组, 一个个试,不要一次性打开多个
this.message[0] = 111 //不触发更新
// this.message.length = 99 //不触发更新
// this.message.push(222) //更新
// this.$set(this.message,2,333) //更新
//操作JSON数组
// this.message1[0] = {name:'name111'} //不触发更新
// this.message1.length = 99 //不触发更新
// this.message1.push({name:'name222'}) //更新
// this.$set(this.message1,2,{name:'name333'}) //更新
console.log('一维数组:',this.message)
console.log('JSON数组:',this.message1)
},2000)
},
})
}else if( typeof(Vue) ==='object'){ //vue3走这,可更换引入的js切换到vue2
const app = {
data() {
return {
message: [
1,
],
message1: [
{name:'name'},
],
}
},
mounted() {
setTimeout(()=>{
//操作一维数组, 一个个试,不要一次性打开多个
// this.message[0] = 111 //会更新
// this.message.length = 99 //会更新
// this.message.push(222) //会更新
// this.$set(this.message,2,333) //报错,vue3没有set方法
//操作JSON数组
// this.message1[0] = {name:'name111'} //会更新
// this.message1.length = 99 //会更新, 会报错可以先把{{item.name}}
这块注释掉
// this.message1.push({name:'name222'}) //会更新
// this.$set(this.message1,2,{name:'name333'}) //报错,vue3没有set方法
console.log('一维数组:',this.message)
console.log('JSON数组:',this.message1)
},2000)
},
}
Vue.createApp(app).mount('#app')
}
script>
body>
html>