本文转载自专栏《1小时掌握vue3》:https://fangcaicoding.cn/course/12/65
大家好!我是方才,目前是8人后端研发团队的负责人,拥有6年后端经验&3年团队管理经验。 系统学习践行者!近期在系统化输出前端入门相关技术文章,期望能帮大家构建一个完整的知识体系。 如果对你有所帮助,记得一键三连! 我创建了一个编程学习交流群(扫码关注后即可加入),秉持“一群人可以走得更远”的理念,期待与你一起 From Zero To Hero! 茫茫人海,遇见即是缘分!方才兄送你ElasticSearch系列知识图谱、前端入门系列知识图谱、系统架构师备考资料!
Hello,大家好!我是方才,前面已经学习条件渲染和列表渲染的指令,今天我们一口气把剩下的内容搞定。包括监听事件v-on
、动态绑定v-bind
、双向绑定v-model
以及不常用的指令和自定义指令。
v-on
监听事件 给元素绑定事件监听器。这部分vue
官方(vue官网)的内容,写得还是非常清楚的。方才兄在这里重点结合实际场景做个简单的讲解。
@
Function | Inline Statement | Object (不带参数)
类似【内联事件处理器】、【事件修饰符】、【鼠标按键修饰符】大家可以直接移步官网。方才兄在这里演示下最常用的:方法事件处理器和按键修饰符。
基于文章列表页的示例代码,监听元素的click
点击事件,语法@click="方法名"
和 v-on:click="方法名"
都是可以的:
<template>
<p>文章列表总数:{{ articleList.length }}</p>
<button @click="addArticle">@click添加文章 </button>
<button v-on:click="addArticle">v-on:click添加文章</button>
<div v-for="(article,index) in articleList">
<h1>序号:{{index+1}}</h1>
<h2>欢迎来到:{{ article.title }}</h2>
<h4>摘要:{{ article.summary }}</h4>
<p>作者:{{ article.author }} 阅读量:{{ article.readCt }}</p>
</div>
</template>
<script setup>
import {ref} from "vue";
const articleList = ref([{
title: "一小时构建Vue知识体系-default",
summary: "关注方才兄,一小时构建Vue知识体系-default",
author: "方才",
readCt: 12
}
]
)
const addArticle = () => {
articleList.value.push({
title: "一小时构建Vue知识体系-" + Math.floor(Math.random() * 100),
summary: "关注方才兄,一小时构建Vue知识体系-3",
author: "方才3",
readCt: Math.floor(Math.random() * 100)
})
}
</script>
<style scoped>
</style>
实现的效果就是,每点击一次按钮,文章列表就会新增一篇文章:
在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on
或 @
监听按键事件时添加按键修饰符。
按键修饰符主要用于自定义快捷键,比如说vue官网的ctr+k
快速搜索就是基于该机制实现的。
image-20241114205835314
语法结构为:@按键行为.按键名称
,比如说@keydown.enter
,当enter
键被按下时触发。
keydown
,一个是松开按键时触发 keyup
;.ctrl
.alt
.shift
.meta
.enter
.tab
.delete
(捕获“Delete”和“Backspace”两个按键).esc
.space
.up
.down
.left
.right
简单演示下keydown
和keyup
的区别,参考示例代码:
<!-- 省略了和上面代码重复的代码,方便阅读 -->
<input type="text" placeholder="按下a键 添加文章" @keydown.a="addArticle"/>
<br/>
<br/>
<input type="text" placeholder="松开a键 添加文章" @keyup.a="addArticle"/>
效果如下,对于@keyup.a
如果一直按住a
键,虽然输入框会输入多个字符,但只会调用一次添加文章的方法,因为a
键只被松开了一次;
对于@keydown.a
如果一直按住a
键,输入框会输入多少个a字符,就会调用多少次添加文章的方法,一直按住a
键就等于不停的按a
键。
组合快捷键,比如说ctrl+a
:
<input type="text" placeholder="组合键ctrl+a键 添加文章" @keyup.ctrl.a="addArticle"/>
按键修饰符的自定义快捷键,默认情况下,仅当绑定快捷键的元素获得焦点时,键盘事件才能被正确的触发。看@keyup.ctrl.a
的效果示例,包括@keydown.a
和 @keyup.a
也是一样的:
若需要在当前页面全局实现快捷键,需要做一些额外的实现。方才兄在这里演示基于 window.addEventListener
全局监听快捷键的方式实现一个demo(若全局快捷键比较多,可以考虑使用 Vue
插件 vue-shortkey
):
<script setup>
标签中,基于vue的生命周期函数onMounted
注册监听按键事件 window.addEventListener('keydown', handleKeydown)
;handleKeydown
中,去实现我们预期的快捷键行为。<script setup>
...
onMounted(() => {
console.log("mounted")
// 在全局监听键盘事件
window.addEventListener('keydown', handleKeydown);
})
const handleKeydown = (event) => {
if (event.key === 'a' && event.ctrlKey) {
// 阻止默认行为,例如浏览器的快捷键
event.preventDefault();
addArticle();
}
}
</script>
现在就可以在当前页面使用ctr+a
快速添加文章了,不需要聚焦到input
组件中。
大家也可以访问方才兄的博客,搜索快捷 ctr+k
就是基于该机制实现的,体验下效果。
了解了以上内容,不知道大家有没有灵光乍现的感觉。我们基于自定义快捷键和全局监听机制,是可以实现很多自定义功能的。
比如说基于该机制,去修改一些默认的快捷键的行为。
一个例子:博客文章内容,在用户未登录时,ctrl+c
默认弹出登录页,阻塞默认的复制行为(效果这里就不截图了,有兴趣的可以去站点测试:https://fangcaicoding.cn/course/12)。
<script setup>
onMounted(() => {
console.log("mounted")
// 在全局监听键盘事件
window.addEventListener('keydown', handleKeydown);
})
const handleKeydown = async (event) => {
// 监听 ctrl+k 打开搜索框
if (event.key === 'c' && event.ctrlKey) {
if (!userStore.isLogin()) {
// 阻止默认行为,例如浏览器的快捷键
event.preventDefault();
// 打开登录对话框
toLogin();
} else {
ElMessage.success('内容复制成功!')
}
}
}
</script>
系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。
.ctrl
.alt
.shift
.meta
举例来说:
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />
<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>
请注意,系统按键修饰符和常规按键不同。与 keyup
事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,**keyup.ctrl
只会在你仍然按住 ctrl
但松开了另一个键时被触发。若你单独松开 ctrl
键将不会触发。**
.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>
v-bind
动态绑定 动态的绑定一个或多个 attribute,也可以是组件的 prop。用于绑定 class
、 style
或组件的 attribute。
:
或者 .
(当使用 .prop
修饰符)any (带参数) | Object (不带参数)
attrOrProp (可选的)
就以绑定图片地址为例,<img :src="article.imgUrl" alt="图片" style="width: 100px" />
:
<div v-for="(article,index) in articleList">
<h1>序号:{{ index + 1 }}</h1>
<img :src="article.imgUrl" alt="图片" style="width: 100px" />
<h2>欢迎来到:{{ article.title }}</h2>
<h4>摘要:{{ article.summary }}</h4>
<p>作者:{{ article.author }} 阅读量:{{ article.readCt }}</p>
</div>
官方也提供了示例:
<img v-bind:src />
<!-- 缩写形式的动态 attribute 名 (3.4+),扩展为 :src="src" -->
<img :src />
<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />
注意,如果你不用v-bind
,程序会认为你写的内容是一个文件:
<img src="article.imgUrl" alt="图片" style="width: 100px" />
。
所以你的图片是本地资源,是不需要v-bind
的。示例:
<h2>本地图片</h2>
<img src="./assets/img.png" alt="图片" style="width: 100px" />
Html css
v-bind
还可以用于动态绑定样式。这个日常使用频率也是比较高的。就以博客的教程列表来说,针对当前查看的文章列表实现一个特殊的选中样式,就用到了该机制。
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class:
<div :class="{ activeClass: isActive }"></div>
上面的语法表示 activeClass
是否存在 取决于数据属性 isActive
的真假值。
我们可以在对象中写多个字段来操作多个 class。此外,:class
指令也可以和一般的 class
attribute 共存。
方才兄这里还是以文章列表的渲染为例,期望有一个默认的通用样式,同时奇偶行有不同的样式:
<p>文章列表总数:{{ articleList.length }}</p>
<button @click="addArticle">添加文章</button>
<div v-for="(article,index) in articleList">
<p class="defaultClass" :class="(index+1) % 2 === 0? 'evenClass' : 'oddClass'">序号:{{ index + 1 }}</p>
</div>
配合以下样式:
<style scoped>
.defaultClass {
font-size: 16px;
color: black;
}
.evenClass{
color: red;
}
.oddClass{
color: green;
}
</style>
渲染的结果会是:
/* 偶数的样式 */
<p class="defaultClass evenClass"></p>
/* 奇数的样式 */
<p class="defaultClass oddClass"></p>
以下内容,方才兄就直接引用官网的内容,做一个简单介绍
:style
支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style
属性:
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
ps:样式中,加载响应式数据,可以用于干嘛呢?头像绘制,比如说飞书的群头像,允许用户选择背景色和输入文字,就可以基于该机制实现。
尽管推荐使用 camelCase
,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称),例如:
<div :style="{ 'font-size': fontSize + 'px' }"></div>
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
template
<div :style="styleObject"></div>
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
v-model
双向绑定 在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
<input
:value="text"
@input="event => text = event.target.value">
v-model
指令帮我们简化了这一步骤:
<input v-model="text">
v-model
指令的作用就是在表单输入元素或组件上创建双向绑定。
什么叫双向绑定呢?两个对象:响应式数据变量 和 表单组件的值,双向绑定:互相影响,更改其中一个对象的值,另一个对象的值也会变更。
方才兄这里以文章信息的表单编辑为例,来体验下“双向绑定”:
input
标签修改文章的标题。<template>
<h1>文章详情展示</h1>
<h2>欢迎来到:{{ article.title }}</h2>
<h4>摘要:{{ article.summary }}</h4>
<p>作者:{{ article.author }} 阅读量:{{ article.readCt }}</p>
<h1 style="color:red;">编辑文章</h1>
标题: <input type="text" v-model="article.title" placeholder="edit me"/><br>
<button @click="saveArticle">保存文章</button>
</template>
<script setup>
import {ref} from "vue";
const article = ref({
title: "一小时构建Vue知识体系-default",
summary: "关注方才兄,一小时构建Vue知识体系-default",
author: "方才",
imgUrl: "https://fangcaicoding.cn/oss/cover/css_learn.png",
readCt: 12
}
)
const saveArticle = () => {
alert(article.value.title + " 文章已保存!")
}
</script>
<style scoped>
input, textarea {
width: 500px;
height: 50px;
padding: 5px;
margin-bottom: 10px;
}
</style>
效果如下:
v-model
和表单的其他组件使用也是类似的,方才兄在这里就不冗余展示了,后续我们会基于element-plus
去实现博客系统的各种功能,在实践中去学习更佳!
若想要提前了解其他组件的绑定,可以直接阅读vue官网
:https://cn.vuejs.org/guide/essentials/forms.html。
可以简单浏览下官网:https://cn.vuejs.org/api/built-in-directives,了解所有的内置指令。
v-pre
:元素内具有 v-pre
,所有 Vue 模板语法都会被保留并按原样渲染。最常见的用例就是显示原始双大括号标签及内容。v-once
:仅渲染元素和组件一次,并跳过之后的更新。v-memo
:缓存一个模板的子树。在元素和组件上都可以使用。v-memo
仅用于性能至上场景中的微小优化,应该很少需要。Vue
支持用户注册自定义的指令 (Custom Directives)。
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}
指令的钩子会传递以下几种参数:
el
:指令绑定到的元素。这可以用于直接操作 DOM。binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在 v-my-directive="1 + 1"
中,值是 2
。oldValue
:之前的值,仅在 beforeUpdate
和 updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo
中,参数是 "foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar
中,修饰符对象是 { foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。vnode
:代表绑定元素的底层 VNode。prevVnode
:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate
和 updated
钩子中可用。我们可以基于自定义指令来实现一个懒加载图片的指令。
ImgLazyLoad.js
:export default {
mounted(el, binding) {
const options = {
root: null, // 视口作为根
rootMargin: '0px', // 不设置外边距
threshold: 0.1, // 元素进入视口 10% 时触发
};
// 设置占位符图片(避免在网络加载完成前显示空白)
el.setAttribute(
'src',
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
);
const loadImage = () => {
const imageSrc = binding.value; // 获取真实图片地址
if (imageSrc) {
el.src = imageSrc; // 设置真实图片地址
el.dataset.loaded = 'true'; // 标记为已加载
observer.unobserve(el); // 停止观察
}
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadImage(); // 进入视口时加载真实图片
}
});
}, options);
observer.observe(el); // 开始观察元素
},
};
main.js
中注册 v-lazy-load
指令:import { createApp } from 'vue'
import App from './App.vue'
import lazyLoadDirective from './components/directives/ImgLazyLoad.js';
createApp(App)
.directive('lazy-load', lazyLoadDirective)
.mount('#app')
App.vue
使用自定义指令:<template>
<img
v-for="(image, index) in images"
:key="index"
v-lazy-load="image.src"
alt="Blog Image"
class="lazy-image"
/>
</template>
<script setup>
import {ref} from 'vue';
const images = ref([
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241113225240775.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241113192259847.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241021225853769.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241021225853769.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241113220459108.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241113191757798.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241113222135766.png'},
{src: 'https://fangcaicoding.cn/oss/assets/14-Vue%E6%9C%80%E5%B8%B8%E7%94%A8%E7%9A%84%E6%8C%87%E4%BB%A4/image-20241113191757798.png'},
])
</script>
<style>
.lazy-image {
width: 100%;
height: auto;
opacity: 0; /* 初始透明度为0,隐藏图片以便在加载后显示 */
transition: opacity 0.5s ease-in-out; /* 添加过渡效果,图片透明度变化会在 0.5秒内平滑进行 */
}
/* 定义当图片加载完成后应用的样式 */
.lazy-image[data-loaded] {
opacity: 1; /* 将透明度设置为1,显示图片 */
}
</style>
运行效果如下,最初只加载第一张图片,随着滚动条的下拉,逐步加载出现在视图中的图片文件:
可以打开【开发者工具】,查看【network】的图片加载顺序图,体验懒加载的效果:
以上,就是vue
常用的指令,截止目前,我们已经掌握了vue
的工程化构建、生命周期函数以及常用的内置指令,我相信大家已经可以基于单文件组件去完成一些项目的开发了。
后续方才兄将继续分享vue3
的组件化,将重复代码封装为组件,提升代码的可维护性。欢迎大家点赞关注。
近期更新计划(有需要的小伙伴,记得点赞关注哟!)
vue、router、elementplus
等前端框架技术文章,期望能帮助大家快速建立相关的知识体系;“学编程,一定要系统化”——若你也是系统学习的践行者,记得点赞关注,期待与你一起 From Zero To Hero!