@[TOC](自学前端开发 - VUE 框架 (二) 事件处理、表单输入绑定、生命周期、侦听器、组件基础)
模板引用
模板引用就是直接访问模板对象实例。最经常使用的例子就是使用元素对象或组件对象的方法。如果使用的是原生 DOM 对象,还可以使用 JS 的方式来获取元素对象(例如使用 getElementById
方法),对于 VUE 组件对象,就需要使用模板引用了。
设置模板引用
使用模板引用时,需要在元素或组件上设置一个特殊属性 ref
,它能够在元素或组件挂载后,获取其直接引用。
<input ref="input">
<el-table ref="table">
访问模板引用
设置了特殊属性 ref
后,可以通过应用对象的 $refs
属性进行访问
<script>
export default {
mounted() {
this.$refs.input.focus()
}
}
</script>
<template>
<input ref="input" />
</template>
注意,只可以在组件挂载后才能访问模板引用。
使用数组
当在 v-for
中使用模板引用时,相应的引用中包含的值是一个数组
<script>
export default {
data() {
return {
list: [
/* ... */
]
}
},
mounted() {
console.log(this.$refs.items)
}
}
</script>
<template>
<ul>
<li v-for="item in list" ref="items">
{
{ item }}
</li>
</ul>
</template>
需要注意的是,ref 数组并不保证与源数组相同的顺序。
函数模板引用
除了使用字符串值作名字,ref 属性还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
<input :ref="el => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
这样就能够在使用数组时保证顺序正确了
<li v-for="(item, index) in list" :ref="el => items[index] = el">
{
{ item }}
</li>
需要注意的是使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。也可以绑定一个组件方法而不是内联函数。
另外,绑定 ref 属性的值可以没有经过定义就进行使用。实际上会将此值作为 this.$refs
这个对象的 key 来保存引用。而使用 :ref
则必须使用函数模板引用,而函数内部使用的变量必须是定义并暴露的,否则将无法正常访问引用。
使用对象
使用函数也可以将模板引用赋值给对象的成员,这样可以将若干组件来分类
<script>
export default {
data() {
return {
inputRefs: ref({
})
}
},
mounted() {
console.log(this.$refs.inputRefs)
}
}
</script>
<template>
<input name='address' :ref="el => inputRefs['address'] = el" />
<input name='age' :ref="el => inputRefs['age'] = el" />
</template>
直接定义为对象或数组成员
直接将对象成员设置为 ref
的属性是不行的,例如
<input name='age' ref="input['age']" />
vue 会将 input['age']
看作一个字符串整体而不是一个对象和其属性,调用时就成了 this.$refs."input['age']"
。对于数组也是同理。
事件处理
可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName"
或 @click="handler"
。事件处理器的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
- 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
内联事件处理器
内联事件处理器通常用于简单场景,例如:
data() {
return {
count: 0
}
}
<button @click="count++">Add 1</button>
<p>Count is: {
{ count }}</p>
方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on
也可以接受一个方法名或对某个方法的调用。
data() {
return {
name: 'Vue.js'
}
},
methods: {
greet(event) {
// 方法中的 `this` 指向当前活跃的组件实例
alert(`Hello ${
this.name}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>
方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target.tagName
访问到该 DOM 元素。
传递参数给方法事件处理器
除了直接绑定方法名,还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
methods: {
say(message) {
alert(message)
}
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>
在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event
变量,或者使用内联箭头函数:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
methods: {
warn(message, event) {
// 这里可以访问 DOM 原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}
事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
- .stop ——调用
event.stopPropagation()
。 - .prevent ——调用
event.preventDefault()
。 - .self ——只有事件从元素本身发出才触发处理函数。
- .capture ——在捕获模式添加事件监听器。
- .once ——最多触发一次处理函数。
- .passive ——通过
{ passive: true }
附加一个 DOM 事件。
<!-- 单击事件将停止传递 -->
<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>
按键修饰符
在监听键盘事件时,经常需要检查特定的按键。Vue 允许在 v-on
或 @
监听按键事件时添加按键修饰符。
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />
按键别名
Vue 为一些常用的按键提供了别名:
- .enter
- .tab
- .delete (捕获“Delete”和“Backspace”两个按键)
- .esc
- .space
- .up
- .down
- .left
- .right
系统按键修饰符
系统按键修饰符用来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。
- .ctrl
- .alt
- .shift
- .meta
举例来说
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />
<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>
.exact
修饰符
.exact
修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>
鼠标按键修饰符
这些修饰符将处理程序限定为由特定鼠标按键触发的事件:
- .left
- .right
- .middle
表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
<input
:value="text"
@input="event => text = event.target.value">
v-model
指令帮我们简化了这一步骤:
<input v-model="text">
v-model
指令可以用于大多数表单元素,它会根据所使用的元素自动使用对应的 DOM 属性和事件组合。v-model
会忽略任何表单元素上初始的 value、checked 或 selected 属性。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用 data
选项来声明该初始值。
值绑定
对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值):
<!-- `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 还使我们可以将选项值绑定为非字符串的数据类型。
复选框
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
true-value
和 false-value
是 Vue 特有的属性,仅支持和 v-model
配套使用。这里 toggle 属性的值会在选中时被设为 'yes'
,取消选择时设为 'no'
。你同样可以通过 v-bind
将其绑定为其他动态值:
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />
true-value
和 false-value
属性不会影响 value 属性,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”) 的其中之一被表单提交,请使用单选按钮作为替代。
单选按钮
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。
选择器选项
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
v-model
同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }
。
修饰符
v-model
可以使用的修饰符有:
- .lazy
默认情况下,v-model
会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。可以添加.lazy
修饰符来改为在每次 change 事件后更新数据:
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
- .number
如果你想让用户输入自动转换为数字,你可以在v-model
后添加 .number 修饰符来管理输入。如果该值无法被parseFloat()
处理,那么将返回原始值。.number
修饰符会在输入框有type="number"
时自动启用。
<input v-model.number="age" />
- .trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在v-model
后添加.trim
修饰符:
<input v-model.trim="msg" />
组件上的 v-model
生命周期
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
vue 应用组件的生命周期
vue 应用的生命周期为:
- 设置(创建代码,还未进入生命周期)
- 初始化事件(生命周期的开始)
- 创建对象并注入响应式状态(Init Options API)
- 模板预处理(pre-compile template)
- 初始化并渲染模板,替换数据(initial render)
- 组件挂载完成(Mounted),根据需要重渲染(re-render)
- 组件卸载(Unmounted)
图示中的红色框即生命周期中的钩子。
注册周期钩子
举例来说,mounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
export default {
mounted() {
console.log(`the component is now mounted.`)
}
}
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 created、mounted、updated 和 unmounted。
所有生命周期钩子函数的 this 上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话将无法在函数中通过 this 获取组件实例。
created
created 钩子在初始化完成状态后调用,此时已经能够获取初始化的 data 等状态信息了。通常会在此阶段进行 ajax 请求(methods 中的方法),并加载需要的数据到 data 中。
mounted
mounted 钩子在组件挂载完成后被调用,此时组件已经渲染完成(在此之前组件未渲染,没有创建 $el
),能够获取渲染后的元素和节点信息。所以一般在此阶段编写初始化页面的操作代码。
updated
updated 钩子在响应式状态被改动后调用。此时状态已经被改变,并且也将结果重渲染至页面中。一般在此执行数据改动后同步保存至数据库的 ajax 代码。
如果需要在渲染前完成某些操作,例如判断数据是否有效,可以在 beforeUpdate 钩子中进行。
unmounted
unmounted 钩子在组件被卸载时被调用,主要用来回收资源。
侦听器
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。侦听器会监听指定的数据的变化情况,并触发一个回调函数。
基本侦听
可以使用 watch 选项在每次响应式属性发生变化时触发一个函数。此函数函数名为监听的数据变量名,参数接收该变量改动前和改动后的值。
export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
this.answer = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = 'Error! Could not reach the API. ' + error
}
}
}
}
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{
{ answer }}</p>
watch 选项也支持把键设置成用 .
分隔的路径:
export default {
data:{
return {
a: {
b: {
c: 'hello'
}
}
}},
watch: {
// 注意:只能是简单的路径,不支持表达式。
a.b.c(newValue, oldValue) {
// ...
}
}
}
深层侦听器
watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,则需要使用深层侦听器:
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// 注意:在嵌套的变更中,
// 只要没有替换对象本身,
// 那么这里的 `newValue` 和 `oldValue` 相同
},
deep: true
}
}
}
需注意,深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此只在必要时才使用它,并且要留意性能。
即时回调的侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,希望在创建侦听器时,立即执行一遍回调。举例来说,想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以用一个对象来声明侦听器,这个对象有 handler
方法和 immediate: true
选项,这样便能强制回调函数立即执行:
export default {
// ...
watch: {
question: {
handler(newQuestion, oldQuestion) {
// 在组件实例创建时会立即调用
// 立即调用时,oldQuestion为 null
},
// 强制立即执行回调
immediate: true
}
}
// ...
}
回调函数的初次执行就发生在 created 钩子之前。Vue 此时已经处理了 data、computed 和 methods 选项,所以这些属性在第一次调用时就是可用的。
回调的触发时机
当更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post'
选项:
export default {
// ...
watch: {
key: {
handler() {
},
flush: 'post'
}
}
}
this.$watch()
也可以使用组件实例的 $watch()
方法来命令式地创建一个侦听器:
export default {
created() {
this.$watch('question', (newQuestion) => {
// ...
})
}
}
如果要在特定条件下设置一个侦听器,或者只侦听响应用户交互的内容,这方法很有用。它还允许提前停止该侦听器。
停止侦听器
用 watch 选项或者 $watch()
实例方法声明的侦听器,会在宿主组件卸载时自动停止。因此,在大多数场景下,无需关心怎么停止它。在少数情况下,的确需要在组件卸载之前就停止一个侦听器,这时可以调用 $watch()
API 返回的函数:
const unwatch = this.$watch('foo', callback)
// ...当该侦听器不再需要时
unwatch()
组件基础
组件(component)允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。即使重复使用同一个组件,组件与组件之间也是相互独立的,这使得各组件能够独立维护自身的状态。
定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<template>
<button @click="count++">You clicked me {
{ count }} times.</button>
</template>
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
// 声明组件
// 注意,Vue 对象不是 vue 框架的类对象,而应该是 mount 后的 vm 应用实例对象。
Vue.component('组件名', {
data() {
// 组件内部的数据
...
},
methods: {
// 组件内部的方法
...
},
template: ``, // 组件的 html 代码
})
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template>
元素),Vue 将会使用其内容作为模板来源。
上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但也可以通过具名导出在一个文件中导出多个组件。
组件的使用
组件的使用分为全局注册和局部注册。
全局注册
全局注册是在父组件中直接注册使用,例如
// 定义一个子组件
const cpt = {
data() {
... },
methods: {
... },
template: ``,
}
// 定义根组件(父组件)
let app = Vue.createApp({
data() {
... },
methods: {
... },
});
// 注册子组件到父组件内
app.component('子组件名', cpt);
// 挂载根组件
app.mount('#app');
<!-- 在模板中渲染子组件 -->
<div id='app'>
<子组件名></子组件名>
</div>
局部注册
要局部使用一个子组件,需要在父组件中导入它。假设把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
<script>
import ButtonCounter from './ButtonCounter.vue'
export default {
components: {
// 局部注册
'ButtonCounter': ButtonCounter // 组件名称
// ButtonCounter // js 中属性名和属性值都是同一个变量名,可以简写
}
}
</script>
<template>
<h1>Here is a child component!</h1>
<!-- 使用时会转换成小写加 - 的形式,需注意 -->
<button-counter/>
</template>
若要将导入的组件暴露给模板,需要在 components 选项上注册它。这个组件将会以其注册时的名字作为模板中的标签名。
内置组件 Transition
<Transition>
会在一个元素或组件进入和离开 DOM 时应用动画。<Transition>
是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素
<component>
切换的动态组件
基本用法
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
当一个 <Transition>
组件中的元素被插入或移除时,会发生下面这些事情:
- Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class 会在适当的时机被添加和移除。
- 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。
- 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。
基于 CSS 的过渡效果
Transition 组件一共有 6 个应用于进入与离开过渡效果的 CSS class。
- v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
- v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
- v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
- v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
- v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
- v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。
可以给 <Transition>
组件传一个 name 属性来声明一个过渡效果名:
<Transition name="fade">
...
</Transition>
对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀。比如,上方例子中被应用的 class 将会是 fade-enter-active 而不是 v-enter-active
。这个“fade”过渡的 class 应该是这样:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
<Transition>
一般都会搭配原生 CSS 过渡一起使用,正如上面的例子。这个 transition CSS 属性是一个简写形式,可以一次定义一个过渡的各个方面,包括需要执行动画的属性、持续时间和速度曲线。
<Transition name="slide-fade">
<p v-if="show">hello</p>
</Transition>
/*
进入和离开动画可以使用不同
持续时间和速度曲线。
*/
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}