VUE学习笔记
day1
vue框架是什么?
Vue框架是一个MVVM的渐进式JavaScript框架,是初创项目的首选前端框架。它是轻量级的,有很多独立的功能或库,在vue里我们可以根据自己的项目来选用它的一些功能。Vue 的核心库只关注视图层,所以开发者关注的只是m-v的映射关系。
其中提到的“渐进式框架”和“自底向上增量开发的设计”是Vue开发的两个概念。
Vue可以在任意其他类型的项目中使用,使用成本较低,更灵活,主张较弱,在Vue的项目中也可以轻松融汇其他的技术来开发,并且因为Vue的生态系统特别庞大,可以找到基本所有类型的工具在vue项目中使用。
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。
创建Vue应用的方法
Vue3
// 创建应用的方法
const {createApp} = window.Vue
// 创建应用
const app = createApp({
data(){
// 定义初始化数据
// 在vue3中data写法是函数,返回一个对象,对象中写初始化数据
return {
msg:"hello 千锋html5大前端"
}
}
})
// 挂载应用
app.mount('#app')
Vue2
new Vue({
el:'#app', // 挂载在id为app的dom上
data:{ // 只有在new Vue时才写为对象,其他时候都写成函数返回对象
msg:'HELLO 千锋html5大前端'
}
})
new Vue({
data:{ // 只有在new Vue时才写为对象,其他时候都写成函数返回对象
msg:'HELLO 千锋html5大前端'
}
}).$mount('#app'); // $mount作用和el选项是一样的
Computed
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
- 类型
// 只读
function computed<T>(
getter: () => T,
// 查看下方的 "计算属性调试" 链接
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
示例
Vue.createApp({
data(){
return {
count:10
}
},
computed:{ // 计算属性 依赖data里面的变量
doubleCount(){
return this.count*2
}
}
}).mount('#app')
组合式Api
const {createApp,ref} = window.Vue;
createApp({
setup(){
const count = ref(10);
return { // 必不可少
count:count
}
},
}).mount('#app')
模板语法
文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
<span>Message: {{ msg }}</span>
双大括号标签会被替换为相应组件实例中 msg
属性的值。同时每次 msg
属性更改时它也会同步更新。
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html:
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
安全警告
在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。
Attribute 绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind
指令:
<div v-bind:id="dynamicId"></div>
v-bind
指令指示 Vue 将元素的 id
attribute 与组件的 dynamicId
属性保持一致。如果绑定的值是 null
或者 undefined
,那么该 attribute 将会从渲染的元素上移除。
注意隐式类型转换,非空即为真, 确保使用绑定属性。
<button :disabled="false">按钮</button>
(显示)
如果属性的值需要拼接字符串完成,且含有变量,可以使用绑定属性结合字符串拼接以及模板字符串
<div v-bind:id="'list'+id">111</div>
<div v-bind:id="`list-${id}`">222</div>
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:
const MyObj = {
id: 'one',
class: 'two'
}
通过不带参数的 v-bind
,你可以将它们绑定到单个元素上:
<div v-bind="MyObj"></div>
简写
因为 v-bind
非常常用,我们提供了特定的简写语法:
<div :id="dynamicId"></div>
tips
绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。
事件绑定
监听事件
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName"
或 @click="handler"
。
事件处理器的值可以是:
-
内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。内联事件处理器通常用于简单场景,例如:
js
const count = ref(0)
template
<button @click="count++">Add 1</button> <p>Count is: {{ count }}</p>
-
方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
<button v-on:click="add">加1</button>{{count}}<br/>
方法与内联事件判断
模板编译器会通过检查 v-on
的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo
、foo.bar
和 foo['bar']
会被视为方法事件处理器,而 foo()
和 count++
会被视为内联事件处理器。
在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event
变量,或者使用内联箭头函数:
template
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
js
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
.stop
(阻止冒泡).prevent
(提交事件将不再重新加载页面).self
(阻止元素及其子元素的所有点击事件的默认行为).capture
.once
.passive
template
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
TIP
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用
@click.prevent.self
会阻止元素及其子元素的所有点击事件的默认行为而@click.self.prevent
则只会阻止对元素本身的点击事件的默认行为。
.capture
、.once
和.passive
修饰符与原生addEventListener
事件相对应:
template
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>
<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
.passive
修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。
条件渲染
v-if
#
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
template
<h1 v-if="awesome">Vue is awesome!</h1>
v-else
#
你也可以使用 v-else
为 v-if
添加一个“else 区块”。
template
<button @click="awesome = !awesome">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
一个 v-else
元素必须跟在一个 v-if
或者 v-else-if
元素后面,否则它将不会被识别。
v-else-if
#
顾名思义,v-else-if
提供的是相应于 v-if
的“else if 区块”。它可以连续多次重复使用:
template
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
和 v-else
类似,一个使用 v-else-if
的元素必须紧跟在一个 v-if
或一个 v-else-if
元素后面。
<template>
上的 v-if
#
因为 v-if
是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template>
元素上使用 v-if
,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template>
元素。
template
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-else
和 v-else-if
也可以在 <template>
上使用。
v-show
#
另一个可以用来按条件显示一个元素的指令是 v-show
。其用法基本一样:
template
<h1 v-show="ok">Hello!</h1>
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display
的 CSS 属性。
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
v-if
vs v-show
#
v-if
是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if
也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show
简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display
属性会被切换。
总的来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if
会更合适。
v-if
和 v-for
警告
同时使用
v-if
和v-for
是不推荐的,因为这样二者的优先级不明显。请查看风格指南获得更多信息。当
v-if
和v-for
同时存在于一个元素上的时候,v-if
会首先被执行。请查看列表渲染指南获取更多细节。
v-if 优先级更高
列表渲染
v-for="(item,index) of list" :key="index"
list:要循环的数组的变量名
item:循环到的数组元素的变量名
index:循环到的数组元素的索引
列表渲染中的key需要使用列表数据的唯一值
实在不得已的情况下,可以使用数组的索引作为key值
Tip
同时使用 v-if
和 v-for
是不推荐的,因为这样二者的优先级不明显。
通过 key 管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for
渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key
attribute:
template
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
当你使用 <template v-for>
时,key
应该被放置在这个 <template>
容器上:
template
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>
注意
key
在这里是一个通过 v-bind
绑定的特殊 attribute。请不要和[在 v-for
中使用对象里所提到的对象属性名相混淆。
vue3里面v-if的优先级高于v-for
vue2里面v-for的优先级高于v-if
表单绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
template
<input
:value="text"
@input="event => text = event.target.value">
v-model
指令帮我们简化了这一步骤:
template
<input v-model="text">
另外,v-model
还可以用于各种不同类型的输入,<textarea>
、<select>
元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的
<input>
和<textarea>
元素会绑定value
property 并侦听input
事件; <input type="checkbox">
和<input type="radio">
会绑定checked
property 并侦听change
事件;<select>
会绑定value
property 并侦听change
事件:
注意
v-model
会忽略任何表单元素上初始的value
、checked
或selected
attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的
API 来声明该初始值。对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现
v-model
不会在 IME
输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的input
事件监听器和value
绑定而不要使用
v-model
。
多行文本 #
template
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>
注意在 <textarea>
中是不支持插值表达式的。请使用 v-model
来替代:
template
<!-- 错误 -->
<textarea>{{ text }}</textarea>
<!-- 正确 -->
<textarea v-model="text"></textarea>
复选框 #
单一的复选框,绑定布尔类型值:
template
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
也可以将多个复选框绑定到同一个数组或集合的值:
js
const checkedNames = ref([])
template
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
单选按钮 #
template
<div>Picked: {{ picked }}</div>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
选择器 #
单个选择器的示例如下:
template
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
如果
v-model
表达式的初始值不匹配任何一个选择项,<select>
元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change
事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
多选 (值绑定到一个数组):
template
<div>Selected: {{ selected }}</div>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
选择器的选项可以使用 v-for
动态渲染:
js
const selected = ref('A')
const options = ref([
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
])
template
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<div>Selected: {{ selected }}</div>
值绑定 #
对于单选按钮,复选框和选择器选项,v-model
绑定的值通常是静态的字符串 (或者对复选框是布尔值):
template
<!-- `picked` 在被选择时是字符串 "a" -->
<input type="radio" v-model="picked" value="a" />
<!-- `toggle` 只会为 true 或 false -->
<input type="checkbox" v-model="toggle" />
<!-- `selected` 在第一项被选中时为字符串 "abc" -->
<select v-model="selected">
<option value="abc">ABC</option>
</select>
但有时我们可能希望将该值绑定到当前组件实例上的动态数据。这可以通过使用 v-bind
来实现。此外,使用 v-bind
还使我们可以将选项值绑定为非字符串的数据类型。
复选框 #
template
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
true-value
和 false-value
是 Vue 特有的 attributes,仅支持和 v-model
配套使用。这里 toggle
属性的值会在选中时被设为 'yes'
,取消选择时设为 'no'
。你同样可以通过 v-bind
将其绑定为其他动态值:
template
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />
提示
true-value
和false-value
attributes 不会影响value
attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”)
的其中之一被表单提交,请使用单选按钮作为替代。
单选按钮 #
template
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
pick
会在第一个按钮选中时被设为 first
,在第二个按钮选中时被设为 second
。
选择器选项 #
template
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
v-model
同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected
会被设为该对象字面量值 { number: 123 }
。
修饰符 #
.lazy
#
默认情况下,v-model
会在每次 input
事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
template
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
.number
#
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符来管理输入:
template
<input v-model.number="age" />
如果该值无法被 parseFloat()
处理,那么将返回原始值。
number
修饰符会在输入框有 type="number"
时自动启用。
.trim
#
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model
后添加 .trim
修饰符:
template
<input v-model.trim="msg" />
组件上的 v-model
#
如果你还不熟悉 Vue 的组件,那么现在可以跳过这个部分。
HTML 的内置表单输入类型并不总能满足所有需求。幸运的是,我们可以使用 Vue 构建具有自定义行为的可复用输入组件,并且这些输入组件也支持 v-model
day 2
注册周期钩子 #
举例来说,onMounted
钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
vue
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log(`the component is now mounted.`)
})
</script>
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 onMounted
、onUpdated
和 onUnmounted
。所有生命周期钩子的完整参考及其用法请参考 API 索引。
当调用 onMounted
时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:
js
setTimeout(() => {
onMounted(() => {
// 异步注册时当前组件实例已丢失
// 这将不会正常工作
})
}, 100)
注意这并不意味着对 onMounted
的调用必须放在 setup()
或 <script setup>
内的词法上下文中。onMounted()
也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup()
就可以。
生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uai0MJAS-1664892924937)(C:\Users\czz\AppData\Roaming\Typora\typora-user-images\image-20221004203332702.png)]
Vue2
const vm = new Vue({
data:{
count:10
},
methods:{
add(){
this.count++;
if(this.count === 15){
this.$destroy()
}
}
},
beforeCreate(){
// 0个月-马上10个月
console.log('beforeCreate')
},
created(){
// 有的人在此处请求数据,修改状态 -- 不太建议(请求数据,修改状态 -教育 -- 胎教)
// 到了10个月
console.log('created')
},
beforeMount(){
console.log('beforeMount')
},
mounted(){
// 在此处请求数据 -- (请求数据-教育-早教)
// DOM操作
// 实例化 new Swiper()
// 计时器 延时定时器 (年龄从生下来开始计算)
console.log('mounted')
},
beforeUpdate(){
console.log('beforeUpdate')
},
updated(){
// DOM操作
// 实例化
// 不要请求数据,不要修改状态 会死循环
console.log('updated')
},
beforeDestroy(){
// 清除定时器,延时器,订阅等
console.log('beforeDestroy')
},
destroyed(){
console.log('destroyed')
}
}).$mount('#app')
Vue3
beforeUnmount(){
// 清除定时器,延时器,订阅等
console.log('beforeUnmount')
},
unmounted(){
console.log('unmounted')
}
事件侦听
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
注意,你不能直接侦听响应式对象的属性值,例如:
js
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
这里需要用一个返回该属性的 getter 函数:
js
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
深度侦听
深层侦听器 #
直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
js
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
你也可以给上面这个例子显式地加上 deep
选项,强制转成深层侦听器:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
案例
const {createApp} = Vue;
const app = createApp({
data(){
return {
user:{
firstName:'张',
lastName:'三',
fullName:''
},
count:100,
unwatch:null
}
},
watch:{
// 侦听失败
// user:function(newVal,oldVal){
// this.user.fullName = newVal.firstName + newVal.lastName;
// }
// 深度侦听
// user:{
// deep:true,
// handler:function(newVal,oldVal){
// console.log(this.$refs.test?.innerHTML)
// this.user.fullName = newVal.firstName + newVal.lastName;
// },
// // 强制立即执行回调函数handler ==> 自动执行一次侦听数据
// immediate:true,
// // 默认情况下,用户创建的侦听器回调,都会在vue组件更新之前调用
// // 这意味着在侦听回调中访问的dom是被vue更新之前的状态
// // 在侦听回调中能访问vue更新之后的dom,你需要指明:flush:'post' 选项
// flush:'post' // vue3中新增的
// },
// 下面的方法也适用于vue2
"user.firstName":function(newVal,oldVal){
this.user.fullName = newVal + this.user.lastName;
},
"user.lastName":function(newVal,oldVal){
this.user.fullName = this.user.firstName + newVal
}
},
methods:{
startWatch(){
// 开始侦听,赋值给一个函数,用于停止监听
this.unwatch = this.$watch('count',(newVal,oldVal)=>{
console.log(newVal,oldVal)
})
},
stopWatch(){
this.unwatch() // 停止侦听
}
}
})
app.mount('#app')
let box = document.querySelector('#box');
// 如果box有内容,就取他的innerHTML
// 如果box没有内容,就返回undefined
// console.log(box?.innerHTML)
模板引用
ref
是一个特殊的 attribute,和 v-for
章节中提到的 key
类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component。
定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue
文件中,这被叫做单文件组件 (简称 SFC):
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 或者 `template: '#my-template-element'`
}
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template>
元素),Vue 将会使用其内容作为模板来源。
上例中定义了一个组件,并在一个 .js
文件里默认导出了它自己,也可以通过具名导出在一个文件中导出多个组件。
使用组件
tip
接下来的指引中使用 SFC 语法,无论你是否使用构建步骤,组件相关的概念都是相同的。示例一节中展示了两种场景中的组件使用情况。
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
通过 <script setup>
,导入的组件都在模板中直接可用。
当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。
组件可以被重用任意多次:
template
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count
。这是因为每当你使用一个组件,就创建了一个新的实例。
在单文件组件中,推荐为子组件使用 PascalCase
的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 />
来关闭一个标签。
如果你是直接在 DOM 中书写模板 (例如原生 <template>
元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case
形式并显式地关闭这些组件的标签。
template
<!-- 如果是在 DOM 中书写该模板 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
全局注册
我们可以使用 Vue 应用实例的 app.component()
方法,让组件在当前 Vue 应用中全局可用。
js
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
如果使用单文件组件,你可以注册被导入的 .vue
文件:
js
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
app.component()
方法可以被链式调用:
js
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
template
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。
vue3.0 局部组件注册
const Header = {
template:'#header',
data(){
return {
msg:'hello vue'
}
},
computed:{
reverseMsg(){
return this.msg.split('').reverse().join('')
}
}
}
const {createApp} = Vue;
const app = createApp({
components:{ // 03 局部组件注册
// 组件名: 组件选项
MyHeader:Header
}
})
app.mount('#app')
vue2.0 局部组件注册
const Header = {
template:'#header',
data(){
return {
msg:'hello vue'
}
},
computed:{
reverseMsg(){
return this.msg.split('').reverse().join("")
}
}
}
new Vue({
components:{
// 03 局部组件注册
// 组件名:组件选项
MyHeader:Header
}
}).$mount('#app')
传递 props
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。
Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps
宏:
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
defineProps
是一个仅 <script setup>
中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps
会返回一个对象,其中包含了可以传递给组件的所有 props:
js
const props = defineProps(['title'])
console.log(props.title)
如果你没有使用 <script setup>
,props 必须以 props
选项的方式声明,props 对象会作为 setup()
函数的第一个参数被传入:
js
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。
当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:
template
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
js
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])
这种情况下,我们可以使用 v-for
来渲染它们:
template
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
留意我们是如何使用 v-bind
来传递动态 prop 值的。当事先不知道要渲染的确切内容时,这一点特别有用。
Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
要声明对 props 的校验,你可以向 defineProps()
宏提供一个带有 props 校验选项的对象,例如:
js
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
TIP
defineProps()
宏中的参数不可以访问<script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
一些补充细节:
- 所有 prop 默认都是可选的,除非声明了
required: true
。 - 除
Boolean
外的未传递的可选 prop 将会有一个默认值undefined
。 Boolean
类型的未传递 prop 将被转换为false
。你应该为它设置一个default
值来确保行为符合预期。- 如果声明了
default
值,那么在 prop 的值被解析为undefined
时,无论 prop 是未被传递还是显式指明的undefined
,都会改为default
值。
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
如果使用了基于类型的 prop 声明 ,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说,defineProps<{ msg: string }>
会被编译为 { msg: { type: String, required: true }}
。
运行时类型检查 #
校验选项中的 type
可以是下列这些原生构造函数:
String
Number
Boolean
Array
Object
Date
Function
Symbol
另外,type
也可以是自定义的类或构造函数,Vue 将会通过 instanceof
来检查类型是否匹配。例如下面这个类:
js
class Person {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
}
你可以将其作为一个 prop 的类型:
js
defineProps({
author: Person
})
Vue 会通过 instanceof Person
来校验 author
prop 的值是否是 Person
类的一个实例。
组件事件
触发与监听事件
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件 (例如:在 v-on
的处理函数中):
template
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
父组件可以通过 v-on
(缩写为 @
) 来监听事件:
template
<MyComponent @some-event="callback" />
同样,组件的事件监听器也支持 .once
修饰符:
template
<MyComponent @some-event.once="callback" />
像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。
TIP
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
事件参数
有时候我们会需要在触发事件时附带一个特定的值。举例来说,我们想要 <BlogPost>
组件来管理文本会缩放得多大。在这个场景下,我们可以给 $emit
提供一个额外的参数:
template
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>
然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:
template
<MyButton @increase-by="(n) => count += n" />
或者,也可以用一个组件方法来作为事件处理函数:
template
<MyButton @increase-by="increaseCount" />
该方法也会接收到事件所传递的参数:
js
function increaseCount(n) {
count.value += n
}
TIP
所有传入
$emit()
的额外参数都会被直接传向监听器。举例来说,$emit('foo', 1, 2, 3)
触发后,监听器函数将会收到这三个参数值。
声明触发的事件
组件要触发的事件可以显式地通过 defineEmits()
宏来声明:
vue
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
我们在 <template>
中使用的 $emit
方法不能在组件的 <script setup>
部分中使用,但 defineEmits()
会返回一个相同作用的函数供我们使用:
vue
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
defineEmits()
宏不能在子函数中使用。如上所示,它必须直接放置在 <script setup>
的顶级作用域下。
如果你显式地使用了 setup
函数而不是 <script setup>
,则事件需要通过 emits
选项来定义,emit
函数也被暴露在 setup()
的上下文对象上:
js
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}
与 setup()
上下文对象中的其他属性一样,emit
可以安全地被解构:
js
export default {
emits: ['inFocus', 'submit'],
setup(props, { emit }) {
emit('submit')
}
}
这个 emits
选项还支持对象语法,它允许我们对触发事件的参数进行验证:
vue
<script setup>
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>
如果你正在搭配 TypeScript 使用 <script setup>
,也可以使用纯类型标注来声明触发的事件:
vue
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
尽管事件声明是可选的,我们还是推荐你完整地声明所有要触发的事件,以此在代码中作为文档记录组件的用法。同时,事件声明能让 Vue 更好地将事件和透传 attribute 作出区分,从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况。
TIP
如果一个原生事件的名字 (例如
click
) 被定义在emits
选项中,则监听器只会监听组件触发的click
事件而不会再响应原生的click
事件。
配合 v-model
使用
自定义事件可以用于开发支持 v-model
的自定义表单组件。回忆一下 v-model
在原生元素上的用法:
template
<input v-model="searchText" />
上面的代码其实等价于下面这段 (编译器会对 v-model
进行展开):
template
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
而当使用在一个组件上时,v-model
会被展开为如下的形式:
template
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
要让这个例子实际工作起来,<CustomInput>
组件内部需要做两件事:
- 将内部原生
input
元素的value
attribute 绑定到modelValue
prop - 输入新的值时在
input
元素上触发update:modelValue
事件
这里是相应的代码:
vue
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
现在 v-model
也可以在这个组件上正常工作了:
template
<CustomInput v-model="searchText" />
另一种在组件内实现 v-model
的方式是使用一个可写的,同时具有 getter 和 setter 的计算属性。get
方法需返回 modelValue
prop,而 set
方法需触发相应的事件:
vue
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
v-model
的参数 #
默认情况下,v-model
在组件上都是使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。我们可以通过给 v-model
指定一个参数来更改这些名字:
template
<MyComponent v-model:title="bookTitle" />
在这个例子中,子组件应声明一个 title
prop,并通过触发 update:title
事件更新父组件值:
vue
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
多个 v-model
绑定 #
利用刚才在 v-model
参数小节中学到的技巧,我们可以在一个组件上创建多个 v-model
双向绑定,每一个 v-model
都会同步不同的 prop:
template
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
vue
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
处理 v-model
修饰符 #
在学习输入绑定时,我们知道了 v-model
有一些内置的修饰符,例如 .trim
,.number
和 .lazy
。在某些场景下,你可能想要一个自定义组件的 v-model
支持自定义的修饰符。
我们来创建一个自定义的修饰符 capitalize
,它会自动将 v-model
绑定输入的字符串值第一个字母转为大写:
template
<MyComponent v-model.capitalize="myText" />
组件的 v-model
上所添加的修饰符,可以通过 modelModifiers
prop 在组件内访问到。在下面的组件中,我们声明了 modelModifiers
这个 prop,它的默认值是一个空对象:
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
defineEmits(['update:modelValue'])
console.log(props.modelModifiers) // { capitalize: true }
</script>
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
注意这里组件的 modelModifiers
prop 包含了 capitalize
且其值为 true
,因为它在模板中的 v-model
绑定上被使用了。
有了 modelModifiers
这个 prop,我们就可以在原生事件侦听函数中检查它的值,然后决定触发的自定义事件中要向父组件传递什么值。在下面的代码里,我们就是在每次 <input>
元素触发 input
事件时将值的首字母大写:
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
对于又有参数又有修饰符的 v-model
绑定,生成的 prop 名将是 arg + "Modifiers"
。举例来说:
template
<MyComponent v-model:title.capitalize="myText">
相应的声明应该是:
js
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
day3
子传父
<div id="app">
<my-parent></my-parent>
</div>
<template id="child">
我是子组件
<!-- 02 通过 this.$emit('自定义事件名',传递给父组件的数据) ,在模板中可以省略this-->
<button @click="$emit('my-event',2000)">传2000</button>
<button @click="sendData(5000)">传5000</button>
</template>
<template id="parent">
我是父组件
<!-- 01 在父组调用子组件的地方,绑定一个自定义的事件,该事件处理函数由父组件实现,默认参数为子组件给父组件传入的值 -->
<my-child @my-event="getData"></my-child>
</template>
</body>
<script src="./lib/vue.global.js"></script>
<script>
const Child = {
template:'#child',
methods:{
sendData(val){
this.$emit('my-event',val)
}
}
}
const Parent = {
template:'#parent',
components:{
MyChild:Child
},
methods:{
getData(val){
console.log('接收到的子组件的数据:'+val)
}
}
}
const {createApp} = Vue;
const app = createApp({
components:{
MyParent:Parent
}
});
app.mount('#app')
</script>
<body>
<div id="app">
<my-parent></my-parent>
</div>
<template id="child">
我是子组件
<!-- 02 通过 this.$emit('自定义事件名',传递给父组件的数据) ,在模板中可以省略this-->
<button @click="$emit('my-event',2000)">传2000</button>
<button @click="sendData(5000)">传5000</button>
</template>
<template id="parent">
我是父组件
<!-- 01 在父组调用子组件的地方,绑定一个自定义的事件,该事件处理函数由父组件实现,默认参数为子组件给父组件传入的值 -->
<my-child @my-event="getData"></my-child>
</template>
</body>
<script src="./lib/vue.global.js"></script>
<script>
const Child = {
template:'#child',
// vue3新增了一个选项,叫做emits里面定义了要触发的父组件的事件名称集合
// emits数组语法
// emits:['my-event'],
// emits对象语法
emits:{
'my-event':function(payload){
// payload:触发自定义事件my-event的时候传递的值
return payload<3000
}
},
methods:{
sendData(val){
this.$emit('my-event',val)
}
}
}
const Parent = {
template:'#parent',
components:{
MyChild:Child
},
methods:{
getData(val){
console.log('接收到的子组件的数据:'+val)
}
}
}
const {createApp} = Vue;
const app = createApp({
components:{
MyParent:Parent
}
});
app.mount('#app')
</script>
透传 Attributes
Attributes 继承 #
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton>
组件,它的模板长这样:
template
<!-- <MyButton> 的模板 -->
<button>click me</button>
一个父组件使用了这个组件,并且传入了 class
:
template
<MyButton class="large" />
最后渲染出的 DOM 结果是:
html
<button class="large">click me</button>
这里,<MyButton>
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了 <MyButton>
的根元素上。
对 class
和 style
的合并 #
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton>
组件的模板改成这样:
template
<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
则最后渲染出的 DOM 结果会变成:
html
<button class="btn large">click me</button>
v-on
监听器继承 #
同样的规则也适用于 v-on
事件监听器:
template
<MyButton @click="onClick" />
click
监听器会被添加到 <MyButton>
的根元素,即那个原生的 <button>
元素之上。当原生的 <button>
被点击,会触发父组件的 onClick
方法。同样的,如果原生 button
元素自身也通过 v-on
绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
深层组件继承 #
有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 <MyButton>
,让它在根节点上渲染 <BaseButton>
:
template
<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />
此时 <MyButton>
接收的透传 attribute 会直接继续传给 <BaseButton>
。
Tip
- 透传的 attribute 不会包含
<MyButton>
上声明过的 props 或是针对emits
声明事件的v-on
侦听函数,换句话说,声明过的 props 和侦听函数被<MyButton>
“消费”了。- 透传的 attribute 若符合声明,也可以作为 props 传入
<BaseButton>
。
禁用 Attributes 继承 #
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
。
const Base = {
template:'#base',
inheritAttrs:false, // 不想要一个组件自动继承attributes,可以在组件选项中设置
}
ref用到自定义组件,可以获取到子组件的实例
<my-child ref="childRef"></my-child>
console.log(this.$refs.childRef.count);// 10
console.log(this.$refs.childRef.doubleCount);// 10
$parent
当前组件可能存在的父组件实例,如果当前组件是顶层组件,则为 null
。
- 类型
$root
当前组件树的根组件实例。如果当前实例没有父组件,那么这个值就是它自己。
兄弟组件传值 - 中央事件总线
<body>
<div id="app">
<my-content></my-content>
<my-footer></my-footer>
</div>
<script src="./lib/vue.js"></script>
<script>
const bus = new Vue();// 中央事件总线 - 电信公司
const Content = {
template:'<div>{{type}}</div>',
data(){
return {
type:'首页'
}
},
mounted(){
// $on用于监听一个自定义事件 - 去电信公司买一个电话,随时接受发给我的短信
bus.$on('change-type',(val)=>{
this.type = val;
})
}
}
const Footer = {
template:`<ul>
<li @click='changeType("首页")'>首页</li>
<li @click='changeType("分类")'>分类</li>
<li @click='changeType("购物车")'>购物车</li>
<li @click='changeType("我的")'>我的</li>
</ul>`,
methods:{
changeType(val){
// $emit用于触发一个指定的事件 - 给指定的电话发短信/打电话
bus.$emit('change-type',val)
}
}
}
new Vue({
components:{
MyContent:Content,
MyFooter:Footer
}
}).$mount('#app')
</script>
</body>
兄弟组件传值 - 状态提升
<body>
<div id="app">
<my-content :type="type"></my-content>
<my-footer @change-type="changeType"></my-footer>
</div>
<script src="./lib/vue.global.js"></script>
<script>
const Content = {
template:'<div>{{type}}</div>',
props:['type']
}
const Footer = {
template:`<ul>
<li @click='changeType("首页")'>首页</li>
<li @click='changeType("分类")'>分类</li>
<li @click='changeType("购物车")'>购物车</li>
<li @click='changeType("我的")'>我的</li>
</ul>`,
methods:{
changeType(val){
this.$emit('change-type',val)
}
}
}
const {createApp} = Vue;
const app = createApp({
components:{
MyFooter:Footer,
MyContent:Content
},
data(){
return {
type:'首页'
}
},
methods:{
changeType(val){
this.type = val;
}
}
})
app.mount('#app')
</script>
</body>
插槽 Slots
插槽内容与出口 #
在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
举例来说,这里有一个 <FancyButton>
组件,可以像这样使用:
template
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton>
的模板是这样的:
template
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
html
<button class="fancy-btn">Click me!</button>
通过使用插槽,<FancyButton>
仅负责渲染外层的 <button>
(以及相应的样式),而其内部的内容由父组件提供。
理解插槽的另一种方式是和下面的 JavaScript 函数作类比,其概念是类似的:
js
// 父元素传入插槽内容
FancyButton('Click me!')
// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}
插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:
template
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
通过使用插槽,<FancyButton>
组件更加灵活和具有可复用性。现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。
默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton>
组件:
template
<button type="submit">
<slot></slot>
</button>
如果我们想在父组件没有提供任何插槽内容时在 <button>
内渲染“Submit”,只需要将“Submit”写在 <slot>
标签之间来作为默认内容:
template
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
现在,当我们在父组件中使用 <SubmitButton>
且没有提供任何插槽内容时:
template
<SubmitButton />
“Submit”将会被作为默认内容渲染:
html
<button type="submit">Submit</button>
但如果我们提供了插槽内容:
template
<SubmitButton>Save</SubmitButton>
那么被显式提供的内容会取代默认内容:
html
<button type="submit">Save</button>
具名插槽 #
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout>
组件中,有如下模板:
template
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>
对于这种场景,<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
在父组件中使用 <BaseLayout>
时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令:
template
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot
有对应的简写 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
下面我们给出完整的、向 <BaseLayout>
传递插槽内容的代码,指令均使用的是缩写形式:
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- 隐式的默认插槽 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
现在 <template>
元素中的所有内容都将被传递到相应的插槽。最终渲染出的 HTML 如下:
html
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
使用 JavaScript 函数来类比可能更有助于你来理解具名插槽:
js
// 传入不同的内容给不同名字的插槽
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> 渲染插槽内容到对应位置
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
day4
动态插槽名 #
动态指令参数在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名:
template
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
注意这里的表达式和动态指令参数受相同的语法限制。
$slots
一个表示父组件所传入插槽的对象。
- 类型
ts
interface ComponentPublicInstance {
$slots: { [name: string]: Slot }
}
type Slot = (...args: any[]) => VNode[]
详细信息
通常用于手写渲染函数,但也可用于检测是否存在插槽。
每一个插槽都在 this.$slots
上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.$slots.default
。
如果插槽是一个作用域插槽,传递给该插槽函数的参数可以作为插槽的 prop 提供给插槽。
组合式 API:依赖注入
provide() #
提供一个值,可以被后代组件注入。
- 类型
ts
function provide<T>(key: InjectionKey<T> | string, value: T): void
详细信息
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
当使用 TypeScript 时,key 可以是一个被类型断言为 InjectionKey
的 symbol。InjectionKey
是一个 Vue 提供的工具类型,继承自 Symbol
,可以用来同步 provide()
和 inject()
之间值的类型。
与注册生命周期钩子的 API 类似,provide()
必须在组件的 setup()
阶段同步调用。
示例
-
vue
<script setup> import { ref, provide } from 'vue' import { fooSymbol } from './injectionSymbols' // 提供静态值 provide('foo', 'bar') // 提供响应式的值 const count = ref(0) provide('count', count) // 提供时将 Symbol 作为 key provide(fooSymbol, count) </script>
inject() #
注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值。
- 类型
ts
// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined
// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
// 使用工厂函数
function inject<T>(
key: InjectionKey<T> | string,
defaultValue: () => T,
treatDefaultAsFactory: true
): T
详细信息
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject()
将返回 undefined
,除非提供了一个默认值。
第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false
作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。
与注册生命周期钩子的 API 类似,inject()
必须在组件的 setup()
阶段同步调用。
当使用 TypeScript 时,key 可以是一个类型为 InjectionKey
的 symbol。InjectionKey
是一个 Vue 提供的工具类型,继承自 Symbol
,可以用来同步 provide()
和 inject()
之间值的类型。
示例
假设有一个父组件已经提供了一些值,如前面 provide()
的例子中所示:
vue
<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'
// 注入值的默认方式
const foo = inject('foo')
// 注入响应式的值
const count = inject('count')
// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)
// 注入一个值,若为空则使用提供的默认值
const bar = inject('foo', 'default value')
// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('foo', () => new Map())
// 注入时为了表明提供的默认值是个函数,需要传入第三个参数
const fn = inject('function', () => {}, false)
</script>
动态组件
<body>
<div id="app">
<!-- 动态组件 -->
<!-- is属性,值为组件标签名 -->
<component :is="currentType"></component>
<footer>
<ul>
<li @click="currentType='Home'">首页</li>
<li @click="currentType='Kind'">分类</li>
<li @click="currentType='Cart'">购物车</li>
<li @click="currentType='User'">我的</li>
</ul>
</footer>
</div>
</body>
<script src="./lib/vue.global.js"></script>
<script>
const Home = {
template:`<div>首页</div>`
}
const Kind = {
template:`<div>
分类
<input type='text'/>
</div>`
}
const Cart = {
template:`<div>购物车</div>`
}
const User = {
template:`<div>我的</div>`
}
const {createApp} = Vue;
const app = createApp({
components:{
Home,Kind,Cart,User
},
data(){
return {
currentType:'Home'
}
}
})
app.mount('#app')
</script>
KeepAlive
<KeepAlive>
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
基本使用 #
在组件基础章节中,我们已经介绍了通过特殊的 <component>
元素来实现动态组件的用法:
template
<component :is="activeComponent" />
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态 —— 当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
在下面的例子中,你会看到两个有状态的组件——A 有一个计数器,而 B 有一个通过 v-model
同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:
A
B
Current component: A
count: 0
你会发现在切回来之后,之前已更改的状态都被重置了。
在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive>
内置组件将这些动态组件包装起来:
template
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
现在,在组件切换时状态也能被保留了:
A
B
Current component: A
count: 0
TIP
在 DOM 模板中使用时,它应该被写为
<keep-alive>
。
包含/排除 #
<KeepAlive>
默认会缓存内部的所有组件实例,但我们可以通过 include
和 exclude
prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
template
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
它会根据组件的 name
选项进行匹配,所以组件如果想要条件性地被 KeepAlive
缓存,就必须显式声明一个 name
选项。
TIP
在 3.2.34 或以上的版本中,使用
<script setup>
的单文件组件会自动根据文件名生成对应的name
选项,无需再手动声明。
最大缓存实例数 #
我们可以通过传入 max
prop 来限制可被缓存的最大组件实例数。<KeepAlive>
的行为在指定了 max
后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
template
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
缓存实例的生命周期 #
当一个组件实例从 DOM 上移除但因为被 <KeepAlive>
缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
一个持续存在的组件可以通过 onActivated()
和 onDeactivated()
注册相应的两个状态的生命周期钩子:
vue
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
Tip
onActivated
在组件挂载时也会调用,并且onDeactivated
在组件卸载时也会调用。- 这两个钩子不仅适用于
<KeepAlive>
缓存的根组件,也适用于缓存树中的后代组件。
案例
<script>
// 假如首页需要保留组件的状态,跳转到新的页面的时候,然后跳转回来的时候,要确保首页数据的及时性和有效性
// 以前mounted中请求数据,使用keep-alive时再回到首页的时候mounted不再执行
// 如果需要获取最新的数据,请在activated中获取
const Home = {
template:`<div>首页</div>`,
data(){
return {
distance:0
}
},
created(){
console.log("首页 created")
},
mounted(){
console.log('首页 mounted')
},
unmounted(){
console.log('首页 unmounted')
},
activated(){
console.log('首页 activated')
console.log('回来的时候,滚动到'+this.distance+"位置")
},
deactivated(){
console.log('首页 deactivated')
// 假设首页长列表,点击某一项可以进入详情,返回首页,还希望滚动条在原来的位置
// 以前销毁组件时记录滚动条位置,现在有keep-alive,在deactivated中记录滚动位置
this.distance = 900;
}
}
const Kind = {
template:`<div>
分类
<input type='text'/>
</div>`,
created(){
console.log("分类 created")
},
mounted(){
console.log('分类 mounted')
},
unmounted(){
console.log('分类 unmounted')
},
activated(){
console.log('分类 activated')
},
deactivated(){
console.log('分类 deactivated')
}
}
const Cart = {
template:`<div>购物车</div>`,
created(){
console.log("购物车 created")
},
mounted(){
console.log('购物车 mounted')
},
unmounted(){
console.log('购物车 unmounted')
},
activated(){
console.log('购物车 activated')
},
deactivated(){
console.log('购物车 deactivated')
}
}
const User = {
template:`<div>我的</div>`,
created(){
console.log("我的 created")
},
mounted(){
console.log('我的 mounted')
},
unmounted(){
console.log('我的 unmounted')
},
activated(){
console.log('我的 activated')
},
deactivated(){
console.log('我的 deactivated')
}
}
const {createApp} = Vue;
const app = createApp({
components:{
Home,Kind,Cart,User
},
data(){
return {
currentType:'Home'
}
}
})
app.mount('#app')
</script>
<!-- 动态组件 -->
<!-- is属性,值为组件标签名 -->
<!-- 可以通过keep-alive保留组件的状态,避免组件的销毁和重建 -->
<!-- include可以设置哪些组件需要缓存 -->
<!-- 用逗号分隔组件名,注意不要加空格 -->
<!-- <keep-alive include="kindb,userd"> -->
<!-- 正则表达式 使用 v-bind -->
<!-- <keep-alive :include="/^(kindb|userd)$/"> -->
<!-- 数组,使用v-bind -->