1. 进阶语法
1.1 v-model 简化代码
App.vue
<template><!-- 11-src-下拉封装 --><div class="app"><!-- <BaseSelect :cityId="selectId" @changeId="handleChangeId"></BaseSelect> --><!-- v-model 简化代码 → 父组件 v-model 简化代码,实现 子组件 和 父组件数据 双向绑定 --><!-- 1. 子组件中:props 通过 value 接收,事件触发 input --><!-- 2. 父组件中:v-model 给组件直接绑数据 --><BaseSelect :cityId="selectId" v-model="selectId"></BaseSelect></div>
</template><script>
import BaseSelect from "./components/BaseSelect.vue";
export default {data() {return {selectId: "102",};},components: {BaseSelect,},methods: {/* handleChangeId(id) {this.selectId = id;}, */},/* watch: {selectId(newval) {console.log(newval);},}, */
};
</script><style>
</style>
BaseSelect.vue
<template><div><!-- 表单类组件封装 & v-model 简化代码实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)下拉菜单 → value 和 input 事件的语法糖model 不能用 → 双向绑定,代表要修改数据 → cityId来自于父组件,子组件不能直接修改--><select :value="cityId" @change="changeId"><option value="101">北京</option><option value="102">上海</option><option value="103">武汉</option><option value="104">广州</option><option value="105">深圳</option></select></div>
</template><script>
export default {props: {cityId: String,},methods: {changeId(e) {// 通知父组件修改数据 → 当前下拉菜单的value值// this.$emit("changeId", e.target.value);this.$emit("input", e.target.value);},},
};
</script><style>
</style>
1.2 sync 修饰符
App.vue
<template><!-- 12-src-sync修饰符 --><div class="app"><button @click="isShow = true">退出按钮</button><!-- isShow.sync => :isShow="isShow" @update:isShow="isShow=$event" --><!-- <BaseDialog v-show="isShow"></BaseDialog> --><!-- <BaseDialog :isShow="isShow" @closeDialog="handleClose"></BaseDialog> --><!--.sync 修饰符 → 可以实现 子组件 与 父组件数据 的 双向绑定,简化代码应用场景:封装弹框类的基础组件, visible属性 true显示 false隐藏.sync修饰符 就是 :属性名 和 @update:属性名 合写--><BaseDialog :isShow.sync="isShow"></BaseDialog></div>
</template><script>
import BaseDialog from "./components/BaseDialog.vue";
export default {data() {return {isShow: false,};},methods: {/* handleClose(newVal) {this.isShow = newVal;}, */},components: {BaseDialog,},
};
</script><style>
</style>
BaseDialog.vue
<template><div class="base-dialog-wrap" v-show="isShow"><div class="base-dialog"><div class="title"><h3>温馨提示:</h3><button class="close" @click="close">x</button></div><div class="content"><p>你确认要退出本系统么?</p></div><div class="footer"><button>确认</button><button>取消</button></div></div></div>
</template><script>
export default {props: {isShow: Boolean,},methods: {close() {// this.$emit("closeDialog", false);this.$emit("update:isShow", false);},},
};
</script><style scoped>
.base-dialog-wrap {width: 300px;height: 200px;box-shadow: 2px 2px 2px 2px #ccc;position: fixed;left: 50%;top: 50%;transform: translate(-50%, -50%);padding: 0 10px;
}
.base-dialog .title {display: flex;justify-content: space-between;align-items: center;border-bottom: 2px solid #000;
}
.base-dialog .content {margin-top: 38px;
}
.base-dialog .title .close {width: 20px;height: 20px;cursor: pointer;line-height: 10px;
}
.footer {display: flex;justify-content: flex-end;margin-top: 26px;
}
.footer button {width: 80px;height: 40px;
}
.footer button:nth-child(1) {margin-right: 10px;cursor: pointer;
}
</style>
1.3 ref 和 $refs
1.3.1 获取 dom
App.vue
<template><!-- 13-src-ref-DOM --><div class="app"><div class="base-chart-box">父组件</div><BaseChart></BaseChart></div>
</template><script>
import BaseChart from "./components/BaseChart.vue";
export default {components: {BaseChart,},
};
</script><style>
.base-chart-box {width: 300px;height: 200px;
}
</style>
BaseChart.vue
<template><!-- ref 和 $refs 作用:利用 ref 和 $refs 可以用于 获取 dom 元素, 或 组件实例特点:查找范围 → 当前组件内 (更精确稳定)--><!-- 1. 目标标签 – 添加 ref 属性 --><div class="base-chart-box" ref="chartEl">子组件</div>
</template><script>
import * as echarts from "echarts";export default {mounted() {// 基于准备好的dom,初始化echarts实例// document.querySelector 会查找项目中所有的元素// $refs只会在当前组件查找盒子// var myChart = echarts.init(document.querySelector('.base-chart-box'))// 2. 恰当时机, 通过 this.$refs.xxx, 获取目标标签var myChart = echarts.init(this.$refs.chartEl);// 绘制图表myChart.setOption({title: {text: "ECharts 入门示例",},tooltip: {},xAxis: {data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],},yAxis: {},series: [{name: "销量",type: "bar",data: [5, 20, 36, 10, 10, 20],},],});},
};
</script><style scoped>
.base-chart-box {width: 400px;height: 300px;border: 3px solid #000;border-radius: 6px;
}
</style>
1.3.2 获取组件
App.vue
<template><!-- 14-src-ref-组件 --><div class="app"><!-- 通过这种方式,可以在父组件中直接操作子组件的方法和数据,这在需要跨组件通信时非常有用。 --><!-- 1. 目标组件 – 添加 ref 属性 --><BaseForm ref="myForm"></BaseForm><button @click="getValues">获取数据</button><button @click="resetValues">重置数据</button></div>
</template><script>
import BaseForm from "./components/BaseForm.vue";
export default {components: {BaseForm,},methods: {getValues() {// 2. 恰当时机, 通过 this.$refs.xxx, 获取目标组件,就可以调用组件对象里面的方法// this.$refs.myForm → BaseForm 组件的 Vue 实例对象console.log(this.$refs.myForm); // BaseForm vue对象console.log(this.$refs.myForm.getValues()); // {username: '12', password: '12'}},resetValues() {this.$refs.myForm.resetValues();},},
};
</script><style>
.app {width: 300px;height: 200px;border: 1px solid #000;border-radius: 10px;padding-top: 30px;padding-left: 30px;
}
button {margin-top: 50px;margin-left: 30px;
}
</style>
BaseForm.vue
<template><form action="">账号:<input type="text" v-model="username" /><br /><br />密码:<input type="text" v-model="password" /></form>
</template><script>
export default {data() {return {username: "",password: "",};},methods: {getValues() {return {username: this.username,password: this.password,};},resetValues() {this.username = "";this.password = "";},},
};
</script><style>
</style>
1.4 Vue异步更新、$nextTick
<template><!-- 15-src-$nextTick --><div class="app"><div v-if="isShowEdit"><input type="text" v-model="editValue" ref="ipt" /><button @click="hide">确认</button></div><div v-else><span>{{ title }}</span><button @click="show">编辑</button></div></div>
</template><script>
export default {data() {return {title: "大标题",editValue: "",isShowEdit: false,};},methods: {// 需求:编辑标题, 编辑框自动聚焦show() {// 点击编辑,显示编辑框this.isShowEdit = true;// 让编辑框,立刻获取焦点// 1. 报错 Cannot read properties of undefined (reading 'focus')// ! Vue 是 异步更新 DOM (提升性能)// this.$refs.ipt.focus();// 2. 延迟慢 体验差/* setTimeout(()=>{this.$refs.ipt.focus();},1500) */// 3. Vue异步更新、$nextTick// → 等 DOM 更新后, 才会触发执行此方法里的函数体// 语法: this.$nextTick(函数体)this.$nextTick(() => {this.$refs.ipt.focus();});},hide() {this.isShowEdit = false;},},
};
</script><style>
span {font-size: 30px;margin-right: 30px;
}
</style>
2. 自定义指令
2.1 全局&局部注册
main.js
import Vue from "vue";
import App from "./App.vue";Vue.config.productionTip = false;/* 指令介绍内置指令:v-html、v-if、v-bind、v-on... 这都是Vue给咱们内置的一些指令,可以直接使用自定义指令:同时Vue也支持让开发者,自己注册一些指令。这些指令被称为自定义指令每个指令都有自己各自独立的功能
*/// 自定义指令
// 自定义指令:自己定义的指令,可以封装一些 dom 操作,扩展额外功能
// 1. 全局注册 - 语法
/* Vue.directive("指令名", {inserted(el) {// 对el标签扩展额外功能},
}); */
/* Vue.directive("focus", {// inserted 指令的生命周期钩子 → 当指令被加到标签上面自动执行的代码// el 使用指令的那个DOM元素// 自带一个形参 → 添加当前指令的标签inserted(el) {el.focus();},
}); */new Vue({render: (h) => h(App),
}).$mount("#app");
App.vue
<template><!-- 01-src-自定义指令-focus --><div id="app"><h1>自定义指令</h1><!-- 使用指令注意:在使用指令的时候,一定要先注册,再使用,否则会报错使用指令语法:v-指令名 如:<input type="text" v-focus/> 注册指令时不用加v-前缀,但使用时一定要加v-前缀--><input type="text" v-focus /></div>
</template><script>
export default {// 2. 局部注册 – 语法 指令名/'指令名'/* directives: {指令名: {inserted(el) {// 可以对 el 标签,扩展额外功能el.focus();},},}, */directives: {focus: {inserted(el) {el.focus();},},},
};
</script><style>
</style>
2.2 指令的值
<template><!-- 02-src-自定义指令-值 --><!-- 1. 通过指令的值相关语法,可以应对更复杂指令封装场景2. 指令值的语法:① v-指令名 = "指令值",通过 等号 可以绑定指令的值② 通过 binding.value 可以拿到指令的值③ 通过 update 钩子,可以监听指令值的变化,进行dom更新操作--><div id="app"><!-- 语法:在绑定指令时,可以通过"等号"的形式为指令 绑定 具体的参数值 --><h1 v-color="color1">自定义指令<button @click="color1 = 'green'">修改颜色</button></h1><h1 v-color="color2">自定义指令</h1></div>
</template><script>
export default {data() {return {color1: "red",color2: "pink",};},// 需求:实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色directives: {color: {// binding 对象会包含所有与该指令相关的信息inserted(el, binding) {// console.log(el);// console.log(binding);// 通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数// console.log(binding.value);el.style.color = binding.value;},update(el, binding) {el.style.color = binding.value;},},},
};
</script><style>
</style>
2.3 v-loading 指令封装
main.js
import Vue from "vue";
import App from "./App.vue";Vue.config.productionTip = false;// // 1. 全局注册指令
// Vue.directive('focus', {
// // inserted 会在 指令所在的元素,被插入到页面中时触发
// inserted (el) {
// // el 就是指令所绑定的元素
// // console.log(el);
// el.focus()
// }
// })new Vue({render: (h) => h(App),
}).$mount("#app");
App.vue
<template><!-- 03-src-封装loading指令 --><!-- 场景:实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好需求:封装一个 v-loading 指令,实现加载中的效果--><div class="box" v-loading="isLoading"><ul><li v-for="item in list" :key="item.id" class="news"><div class="left"><div class="title">{{ item.title }}</div><div class="info"><span>{{ item.source }}</span><span>{{ item.time }}</span></div></div><div class="right"><img :src="item.img" alt="" /></div></li></ul></div>
</template><script>
// 安装axios => yarn add axios
import axios from "axios";// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {data() {return {list: [],isLoading: true,};},watch: {list: {deep: true,handler(newList) {// newList !== [] ? (this.isLoading = false) : (this.isLoading = true);this.isLoading = newList.length === 0;},},},async created() {// 1. 发送请求获取数据const res = await axios.get("http://hmajax.itheima.net/api/news");setTimeout(() => {// 2. 更新到 list 中,用于页面渲染 v-forthis.list = res.data.data;// this.isLoading = false;}, 1000);},directives: {loading: {inserted(el, binding) {binding.value? el.classList.add("loading"): el.classList.remove("loading");},update(el, binding) {binding.value? el.classList.add("loading"): el.classList.remove("loading");},},},
};
</script><style>
/* 通过css添加一个伪元素 → 假的标签 */
/* ::before:这是一个伪元素选择器,用于在目标元素的内容之前插入额外的内容。这个伪元素并不存在于文档树中,但它会显示在目标元素的前面。常用于为正在加载的资源(如图片、视频或音频)添加自定义的加载样式,以提供更丰富的用户体验和视觉反馈。 */
/* 当将.loading和::before组合使用时,就表示在具有loading类的每个元素的内容之前插入额外的内容。 */
.loading::before {/* 必填属性 */content: "";position: absolute;left: 0;top: 0;width: 100%;height: 100%;background: #fff url("./loading.gif") no-repeat center;
}.box {width: 800px;min-height: 500px;border: 3px solid orange;border-radius: 5px;position: relative;margin: 0 auto;
}
.news {display: flex;height: 120px;width: 600px;margin: 0 auto;padding: 20px 0;cursor: pointer;
}
.news .left {flex: 1;display: flex;flex-direction: column;justify-content: space-between;padding-right: 10px;
}
.news .left .title {font-size: 20px;
}
.news .left .info {color: #999999;
}
.news .left .info span {margin-right: 20px;
}
.news .right {width: 160px;height: 120px;
}
.news .right img {width: 100%;height: 100%;object-fit: cover;
}
</style>
3. 插槽
3.1 默认插槽
3.2 后备内容
3.3 具名插槽
App.vue
<template><!-- 04-src-封装对话框组件-默认和具名插槽 --><div><!-- 一、默认插槽 --><!-- <MyDialog>文本内容1</MyDialog> --><!-- <MyDialog><h3>标题3</h3></MyDialog> --><!-- 二、后备内容 --><!-- <MyDialog></MyDialog> --><!-- 三、具名插槽 --><MyDialog><template v-slot:head><h4>温馨提示</h4></template><template #content>确定要删除吗?</template></MyDialog></div>
</template><script>
import MyDialog from "./components/MyDialog.vue";
export default {data() {return {};},components: {MyDialog,},
};
</script><style>
body {background-color: #b3b3b3;
}
</style>
MyDialog.vue
<template><div class="dialog"><div class="dialog-header"><!-- <h3>友情提示</h3> --><slot name="head"></slot><span class="close">✖️</span></div><div class="dialog-content"><!-- 我是文本内容 --><!-- 一、插槽 - 默认插槽 → 让组件内部的一些 结构 支持 自定义插槽基本语法:1. 组件内需要定制的结构部分,改用<slot></slot>占位2. 使用组件时, <MyDialog></MyDialog>标签内部, 传入结构替换slot3. 给插槽传入内容时,可以传入纯文本、html标签、组件--><!-- <slot></slot> --><!-- 二、插槽 - 后备内容(默认值)插槽后备内容:封装组件时,可以为预留的 `<slot>` 插槽提供后备内容(默认内容)。在 <slot> 标签内,放置内容, 作为默认显示内容 → 外部使用组件时,不传东西,则slot会显示后备内容--><!-- <slot>默认内容</slot> --><!-- 三、插槽 - 具名插槽 → 一个组件内有多处结构,需要外部传入标签,进行定制具名插槽语法:1. 多个slot使用name属性区分名字2. template配合v-slot:名字来分发对应标签3. v-slot:插槽名 可以简化成 #插槽名--><slot name="content"></slot></div><div class="dialog-footer"><button>取消</button><button>确认</button></div></div>
</template><script>
export default {data() {return {};},
};
</script><style scoped>
* {margin: 0;padding: 0;
}
.dialog {width: 470px;height: 230px;padding: 0 25px;background-color: #ffffff;margin: 40px;border-radius: 5px;
}
.dialog-header {height: 70px;line-height: 70px;font-size: 20px;border-bottom: 1px solid #ccc;position: relative;
}
.dialog-header .close {position: absolute;right: 0px;top: 0px;cursor: pointer;
}
.dialog-content {height: 80px;font-size: 18px;padding: 15px 0;
}
.dialog-footer {display: flex;justify-content: flex-end;
}
.dialog-footer button {width: 65px;height: 35px;background-color: #ffffff;border: 1px solid #e1e3e9;cursor: pointer;outline: none;margin-left: 10px;border-radius: 3px;
}
.dialog-footer button:last-child {background-color: #007acc;color: #fff;
}
</style>
3.4 作用域插槽
App.vue
<template><!--05-src-封装表格组件-作用域插槽场景:封装表格组件1. 父传子,动态渲染表格内容2. 利用默认插槽,定制操作列3. 删除或查看都需要用到 当前项的 id,属于组件内部的数据, 通过 作用域插槽 传值绑定,进而使用--><div><MyTable :data="list"><template #default="obj"><button @click="del(obj.id)">删除</button></template></MyTable><MyTable :data="list2"><template #default="{ id, msg }"><button @click="fn(id, msg)">查看</button></template></MyTable></div>
</template><script>
import MyTable from "./components/MyTable.vue";
export default {data() {return {list: [{ id: 1, name: "张小花", age: 18 },{ id: 2, name: "孙大明", age: 19 },{ id: 3, name: "刘德忠", age: 17 },],list2: [{ id: 1, name: "赵小云", age: 18 },{ id: 2, name: "刘蓓蓓", age: 19 },{ id: 3, name: "姜肖泰", age: 17 },],};},components: {MyTable,},methods: {del(id) {this.list = this.list.filter((item) => item.id !== id);},fn(id, msg) {console.log(id, msg);},},
};
</script>
MyTable.vue
<template><table class="my-table"><thead><tr><th>序号</th><th>姓名</th><th>年纪</th><th>操作</th></tr></thead><!-- 四、插槽 - 作用域插槽作用域插槽: 定义 slot 插槽的同时, 是可以传值的。给 插槽 上可以 绑定数据,将来 使用组件时可以用。基本使用步骤:1. 给 slot 标签, 以 添加属性的方式传值2. 所有添加的属性, 都会被收集到一个对象中3. 在template中, 通过 ` #插槽名= "obj" ` 接收,默认插槽名为 default--><tbody><!-- <tr><td>1</td><td>小张</td><td>8</td><td><button>删除</button></td></tr> --><tr v-for="(item, index) in data" :key="item.id"><td>{{ index + 1 }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td><td><!-- <button>删除</button> --><slot :id="item.id" msg="普通数据"></slot></td></tr></tbody></table>
</template><script>
export default {props: {data: Array,},
};
</script><style scoped>
.my-table {width: 450px;text-align: center;border: 1px solid #ccc;font-size: 24px;margin: 30px auto;
}
.my-table thead {background-color: #1f74ff;color: #fff;
}
.my-table thead th {font-weight: normal;
}
.my-table thead tr {line-height: 40px;
}
.my-table th,
.my-table td {border-bottom: 1px solid #ccc;border-right: 1px solid #ccc;
}
.my-table td:last-child {border-right: none;
}
.my-table tr:last-child td {border-bottom: none;
}
.my-table button {width: 65px;height: 35px;font-size: 18px;border: 1px solid #ccc;outline: none;border-radius: 3px;cursor: pointer;background-color: #ffffff;margin-left: 5px;
}
</style>
4. 综合案例 - 商品列表
App.vue
<template><!-- 06-src-商品列表 --><!-- 需求说明:1. my-tag 标签组件封装(1) 双击显示输入框,输入框获取焦点(2) 失去焦点,隐藏输入框(3) 回显标签信息(4) 内容修改,回车 → 修改标签信息2. my-table 表格组件封装(1) 动态传递表格数据渲染(2) 表头支持用户自定义(3) 主体支持用户自定义--><div class="table-case"><MyTable :data="goods"><template #head><th>编号</th><th>图片</th><th>名称</th><th width="100px">标签</th></template><!-- 拿到插槽传入的数据 解构 --><template #con="{ item, index }"><td>{{ index + 1 }}</td><td><img :src="item.picture" /></td><td>{{ item.name }}</td><td><MyTag v-model="item.tag"></MyTag></td></template></MyTable></div>
</template><script>
import MyTable from "./components/MyTable.vue";
import MyTag from "./components/MyTag.vue";
export default {name: "TableCase",data() {return {goods: JSON.parse(localStorage.getItem("newGoods")) || [{id: 101,picture:"https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg",name: "梨皮朱泥三绝清代小品壶经典款紫砂壶",tag: "茶具",},{id: 102,picture:"https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg",name: "全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌",tag: "男鞋",},{id: 103,picture:"https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png",name: "毛茸茸小熊出没,儿童羊羔绒背心73-90cm",tag: "儿童服饰",},{id: 104,picture:"https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg",name: "基础百搭,儿童套头针织毛衣1-9岁",tag: "儿童服饰",},],};},components: {MyTable,MyTag,},watch: {goods: {deep: true,handler(newGoods) {localStorage.setItem("newGoods", JSON.stringify(newGoods));},},},
};
</script><style lang="less" scoped>
.table-case {width: 1000px;margin: 50px auto;img {width: 100px;height: 100px;object-fit: contain;vertical-align: middle;}
}
</style>
MyTable.vue
<template><table class="my-table"><thead><tr><slot name="head"></slot></tr></thead><tbody><tr v-for="(item, index) in data" :key="item.id"><slot name="con" :item="item" :index="index"></slot></tr></tbody></table>
</template><script>
// import MyTag from "./MyTag.vue";
export default {components: {// MyTag,},props: {data: Array,},
};
</script><style lang="less" scoped>
.my-table {width: 100%;border-spacing: 0;img {width: 100px;height: 100px;object-fit: contain;vertical-align: middle;}th {background: #f5f5f5;border-bottom: 2px solid #069;}td {border-bottom: 1px dashed #ccc;}td,th {text-align: center;padding: 10px;transition: all 0.5s;&.red {color: red;}}.none {height: 100px;line-height: 100px;color: #999;}
}
</style>
MyTag.vue
<template><div class="my-tag"><inputclass="input"type="text"placeholder="输入标签"v-if="isEdit"v-focus@blur="isEdit = false"@keyup.enter="changeTag":value="value"/><!-- :value 是html标签自带的属性 "value" 是props数据 --><!-- dbl - double, 双击 - dblclick --><div class="text" v-else @dblclick="isEdit = true">{{ value }}</div></div>
</template><script>
// 分类标签的功能:
// input和文字互斥 → 布尔数据 v-if
// 双击文字div 显示input标签 → 自动获得焦点
// input 要数据回显 - 父子通信
// v-model = value属性和input事件的缩写
// 用户输入后,回车 → 把新的内容渲染到页面 → 数据应该同步
export default {props: {value: String,},data() {return {isEdit: false,};},methods: {changeTag(e) {// 通知父组件保存用户输入的数据 $emit(事件名, 用户输入的数据)this.$emit("input", e.target.value);// 显示div,隐藏输入框this.isEdit = false;},},
};
</script><style lang="less" scoped>
// lang="less" 表示的是less语法.my-tag {cursor: pointer;.input {appearance: none;outline: none;border: 1px solid #ccc;width: 100px;height: 40px;box-sizing: border-box;padding: 10px;color: #666;&::placeholder {color: #666;}}
}
</style>