业务场景:
A组件写一个回调demo方法,D组件可以通过X调用demo,并且传递参数666。A组件可以收到 666
实现思路如下:
配置一个中介
x
,x
主要用来完成各种组件之间的通信,同时x
应该具有如下特性:x
对于其他所有组件是可见的- 这个
x
能够调用$on()、$off()、$emit()
等方法
1- 实现依据如下图:vc身上找不到属性和方法,会去vm身上去找。
2- vc 上面的 VueComponent.prototype_proto_ ==== Vue.prototype , 如下图绿线
一、全局事件总线(GlobalEventBus)
全局事件总线在 Vue 中十分重要,因为利用它我们可以实现任意组件间的通信
使用步骤,如下 1- 1 ,1-2, 1-3
最好在beforeDestroy() ,钩子函数中销毁中,调用$off()去解绑函数
1-1 : main.js中 安装全局事件总线 $bus,如1 处代码
/*
main.js是整个项目的入口文件
*/
//引入 Vue
import Vue from 'vue'
//引入App组件,他是所有文件的父组件
import App from './App.vue'
// //构建 VueCompontent
// const Demo = Vue.extend({});
// const d = new Demo();
// // 赋值给vue(vc) 这样全局Vm都能访问到
// Vue.prototype.x = d;
Vue.config.productionTip = false
//创建Vue实例,vm
new Vue({
render: h => h(App),
beforeCreate(){
//1-安装全局事件总线
Vue.prototype.$bus = this;
}
})
.$mount('#app')
1-2:使用总线,如 2 处代码:在School组件上面绑定demo事件
<template>
<!--组件主体-->
<div class="school">
<h2>学校:{{ name }}</h2>
<h2>学校地址:{{ addr }}</h2>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "Vue学院",
addr: "海棠大道168号",
};
},
mounted() {
// 2- this.x 在 vc身上没有要到vm身上找到
this.$bus.$on("demo", (data) => {
console.log("我是school组件,接收到了数据: " + data);
});
},
beforeDestroy() {
this.$bus.$off("demo");
},
};
</script>
<style scoped>
/* 组件样式 */
.school {
background-color: red;
padding: 5px;
}
</style>
1-3 :在Studnet组件,如3处代码:$emit() 调用事件
<template>
<!--组件主体-->
<div class="student">
<h2>姓名:{{ name }}</h2>
<h2>性别:{{ sex }}</h2>
<button @click="sendStudentName()">调用全局的demo方法</button>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "正在学习Vue学生",
sex: "男",
};
},
methods: {
sendStudentName() {
//3-$emit 调用 传参
this.$bus.$emit("demo", this.name);
},
}
};
</script>
<style scoped>
/* 组件样式 */
.student {
background-color: greenyellow;
padding: 5px;
margin-top: 40px;
}
</style>
下面再去修改TODOLIst例子:
1-1-1安装总线路如上面 1-1 main.js
1-2-1 绑定事件,mounted() 挂载时候去绑定事件,并且在销毁前解绑
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader @addTodo="addTodo"></MyHeader>
<MyList :todos="todos"></MyList>
<MyFooter
:todos="todos"
@checkAllBut="checkAllBut"
@clearAll="clearAll"
></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: {
MyHeader,
MyList,
MyFooter,
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || [],
};
},
methods: {
//添加todo
addTodo(x) {
console.log("收到值:", x);
this.todos.unshift(x);
},
//勾选或者取消
checkTodo(id) {
this.todos.forEach((obj) => {
if (id == obj.id) {
obj.done = !obj.done;
}
});
},
// 删除方法
deleteTodo(id) {
// 过滤新数组返回
this.todos = this.todos.filter((obj) => {
return id !== obj.id;
});
},
//全选或者全不选
checkAllBut(isAll) {
this.todos.forEach((obj) => {
obj.done = isAll ? true : false;
});
},
// 清除所有
clearAll() {
this.todos = this.todos.filter((obj) => {
return !obj.done;
});
},
},
watch: {
todos: {
deep: true, // 开启深度监视,监视里面的每一个对象的 done 值
handler(value) {
localStorage.setItem("todos", JSON.stringify(value)); // 将 value 值转化为一个JSON字符串
},
},
},
mounted() {
// 2-在挂载时候绑定事件
this.$bus.$on("checkTodo", (id) => {
this.checkTodo(id);
});
this.$bus.$on("deleteTodo", (id) => {
this.deleteTodo(id);
});
},
// 4-销毁前解绑
beforeDestroy() {
this.$bus.$off("checkTodo");
this.$bus.$off("deleteTodo");
},
};
</script>
1-2-3 调用$emit() 方法,
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="changeCheck(todo.id)"
/>
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="handleDelte(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
//声明接受todo对象
props: ["todo"],
mounted() {
console.log(this.todo);
},
methods: {
//3-改变选中的状态
changeCheck(id) {
this.$bus.$emit("checkTodo", id);
},
//3-删除记录
handleDelte(id) {
if (confirm("确认删除吗?")) {
this.$bus.$emit("deleteTodo", id);
}
},
},
};
</script>
二、 消息订阅和发布
我们前面使用总局事件总线实现了所有的组件之间的通信,现在介绍另外一种技术,就是消息订阅与发布,具体流程如下:
第一步 : 订阅消息:消息名
第二步: 发布消息:消息内容
1- 安装依赖: npm i pubsub-js
2- 引入库 import pubsub from 'pubsub-js'
3- 订阅消息 (挂载时候)
//4- 订阅消息 第一种
this.pubid = pubsub.subscribe("demo", this.demo);
//4- 订阅消息 第二种
//箭头函数
// this.pubid = pubsub.subscribe("demo", (msgName, data)=>{
// console.log("消息名称", msg);
// console.log("数据", data);
// });4-发送消息
// 2-发送消息
pubsub.publish('demo', this.name)5- 取消订阅(销毁之前)
//3 取消订阅
pubsub.unsubscribe(this.pubid)
School 订阅消息 ,如 1、4、3处代码
<template>
<div class="school">
<h2>学校:{{ name }}</h2>
<h2>学校地址:{{ addr }}</h2>
</div>
</template>
<script>
// 1-
import pubsub from "pubsub-js";
export default {
name: "School",
data() {
return {
name: "Vue学院",
addr: "海棠大道168号",
};
},
methods: {
//注意是2个参数 demo(_, data) 第一个可用_占位
demo(msgName, data) {
console.log("消息名称", msg);
console.log("数据", data);
},
},
mounted() {
// this.$bus.$on("demo", (data) => {
// console.log("我是school组件,接收到了数据: " + data);
// });
//4- 订阅消息 第一种
this.pubid = pubsub.subscribe("demo", this.demo);
//4- 订阅消息 第二种
//箭头函数
// this.pubid = pubsub.subscribe("demo", (msgName, data)=>{
// console.log("消息名称", msg);
// console.log("数据", data);
// });
},
beforeDestroy() {
//3 取消订阅
pubsub.unsubscribe(this.pubid)
},
};
</script>
<style scoped>
/* 组件样式 */
.school {
background-color: red;
padding: 5px;
}
</style>
Student发送消息,如2处代码
<template>
<!--组件主体-->
<div class="student">
<h2>姓名:{{ name }}</h2>
<h2>性别:{{ sex }}</h2>
<button @click="sendStudentName()">调用全局的demo方法</button>
</div>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "Student",
data() {
return {
name: "正在学习Vue学生",
sex: "男",
};
},
methods: {
sendStudentName() {
// this.$bus.$emit("demo", this.name);
// 2-发送消息
pubsub.publish('demo', this.name)
},
}
};
</script>
<style scoped>
/* 组件样式 */
.student {
background-color: greenyellow;
padding: 5px;
margin-top: 40px;
}
</style>
三、修改todoList的编辑逻辑(事件总线实现)
1-点击编辑出现输入框
2-修改输入框文字修改并保存(失去焦点触发)
MyItem组件
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="changeCheck(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<!-- 2- 新增输入框 -->
<input
v-show="todo.isEdit"
type="text"
:value="todo.title"
@blur="handleBlur(todo, $event)"
/>
</label>
<button class="btn btn-danger" @click="handleDelte(todo.id)">删除</button>
<!-- 1-新增编辑按钮 -->
<button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
</li>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "MyItem",
props: ["todo"],
mounted() {
console.log(this.todo);
},
methods: {
changeCheck(id) {
this.$bus.$emit("checkTodo", id);
},
handleDelte(id) {
if (confirm("确认删除吗?")) {
pubsub.publish("deleteTodo", id);
}
},
// 3- 编辑操作(点击编辑显示输入框,判断是否有isEdit属性,无则 $set 绑定属性)
handleEdit(todo) {
if (!todo.hasOwnProperty("isEdit")) {
//判断todo没有isEdit属性 追加属性
this.$set(todo, "isEdit", true);
} else {
todo.isEdit = !todo.isEdit;
}
},
//4- 失去焦点操作:保存数据
handleBlur(todo, e) {
todo.isEdit = !todo.isEdit;
if(!e.target.value.trim()){
alert('输入不能为空!')
}
this.$bus.$emit('editTodo',todo.id,e.target.value)
},
},
};
</script>
<style scoped>
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: gray;
}
li:hover button {
display: block;
}
</style>
APP组件中定义操作数据方法,挂载前绑定 $on 事件,销毁前并解绑 $off 方法事件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader @addTodo="addTodo"></MyHeader>
<MyList :todos="todos"></MyList>
<MyFooter
:todos="todos"
@checkAllBut="checkAllBut"
@clearAll="clearAll"
></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
import pubsub from "pubsub-js";
export default {
name: "App",
components: {
MyHeader,
MyList,
MyFooter,
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || [],
};
},
methods: {
//添加todo
addTodo(x) {
console.log("收到值:", x);
this.todos.unshift(x);
},
//勾选或者取消
checkTodo(id) {
this.todos.forEach((obj) => {
if (id == obj.id) {
obj.done = !obj.done;
}
});
},
// 删除方法 (_,id) _ 是为了占位
deleteTodo(_, id) {
console.log(id);
// 过滤新数组返回
this.todos = this.todos.filter((obj) => {
return id !== obj.id;
});
},
//全选或者全不选
checkAllBut(isAll) {
this.todos.forEach((obj) => {
obj.done = isAll ? true : false;
});
},
// 清除所有
clearAll() {
this.todos = this.todos.filter((obj) => {
return !obj.done;
});
},
//7- 修改
editTodo(id, val) {
console.log("入参", id, val);
this.todos.forEach((obj) => {
if (id == obj.id) {
obj.title = val;
}
});
},
},
watch: {
todos: {
deep: true, // 开启深度监视,监视里面的每一个对象的 done 值
handler(value) {
localStorage.setItem("todos", JSON.stringify(value)); // 将 value 值转化为一个JSON字符串
},
},
},
mounted() {
// 3-在挂载时候绑定事件
this.$bus.$on("checkTodo", (id) => {
this.checkTodo(id);
});
// this.$bus.$on("deleteTodo", (id) => {
// this.deleteTodo(id);
// });
// 订阅删除消息
this.pubid = pubsub.subscribe("deleteTodo", this.deleteTodo);
// 5- 事件总线修改消息
this.$bus.$on("editTodo", (id,value) => {
this.editTodo(id,value);
});
},
// 销毁前解绑
beforeDestroy() {
this.$bus.$off("checkTodo");
this.$bus.$off("deleteTodo");
this.$bus.$off("editTodo");
//6- 取消订阅
pubsub.unsubscribe(this.pubid);
},
};
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-edit {
color: #fff;
background-color: green;
border: 1px solid rgb(2, 98, 2);
margin-right: 5px;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn-edit:hover {
color: #fff;
background-color: green;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
三-nextTick
1.语法: this.$nextTick(回调函数)
2.作用: 在下一次 DOM 更新结束后执行其指定的回。
3.什么时候用: 当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行
如3 处代码,修改编辑之后获取数据库焦点
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="changeCheck(todo.id)"
/>
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<!-- 2- 新增输入框 -->
<input
v-show="todo.isEdit"
type="text"
:value="todo.title"
@blur="handleBlur(todo, $event)"
ref="inputTitle"
/>
</label>
<button class="btn btn-danger" @click="handleDelte(todo.id)">删除</button>
<!-- 1-新增编辑按钮 -->
<button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
</li>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "MyItem",
props: ["todo"],
mounted() {
console.log(this.todo);
},
methods: {
changeCheck(id) {
this.$bus.$emit("checkTodo", id);
},
handleDelte(id) {
if (confirm("确认删除吗?")) {
pubsub.publish("deleteTodo", id);
}
},
// 3- 编辑操作(点击编辑显示输入框,判断是否有isEdit属性,无则 $set 绑定属性)
handleEdit(todo) {
if (!todo.hasOwnProperty("isEdit")) {
//判断todo没有isEdit属性 追加属性
this.$set(todo, "isEdit", true);
} else {
todo.isEdit = !todo.isEdit;
}
//dom 节点演示渲染
// setTimeout(() => {
// this.$refs.inputTitle.focus();
// }, timeout);
this.$nextTick(function(){
this.$refs.inputTitle.focus();
})
},
//4- 失去焦点操作:保存数据
handleBlur(todo, e) {
todo.isEdit = !todo.isEdit;
if(!e.target.value.trim()){
alert('输入不能为空!')
}
this.$bus.$emit('editTodo',todo.id,e.target.value)
},
},
};
</script>
<style scoped>
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: gray;
}
li:hover button {
display: block;
}
</style>