Vue(九) 全局事件总线、Todo案例应用全局事件总线、消息订阅与发布、Todo案例应用消息订阅、编辑Item、$nextTick

文章目录

一、全局事件总线 (GlobalEventBus)

1. 总线前言

全局事件总线可以实现任意组件间通信。总线是独立于所有组件之外的。

假设总线名为x,总线x应该满足这两个条件:所有组件都能看的见,且能调用$on(绑定),$off(解绑),$emit(触发)

问题1:绑在谁身上,才能让所有组件都能看的见

可以将总线绑在Vue原型:Vue.prototype上。之前学过有一个重要的内置关系:VueComponent.prototype.__proto__===Vue.prototype。有了这个关系,可以将组件实例对象访问到Vue原型上的属性、方法(即顺着图中绿色的线)。

javascript 复制代码
// main.js
Vue.prototype.x = {a:1,b:2}; //随便给总线x赋了个值

在School组件及Student组件中检验

javascript 复制代码
// School组件  
mounted () {
    console.log('School', this.x);
},
// Student组件
mounted () {
  console.log('Student', this.x);
},

确实所有组件都能看到x

问题二:总线得能调用$on/$emit/$off

$on/​$emit/$off都在Vue的原型对象上,x的值是对象时,无法调用这三个方法。所以x的值要么是vm,要么是vc

javascript 复制代码
Vue.prototype.x = vm/vc
  • 绑定vc
javascript 复制代码
// Vue.extend({}) 返回的是VueComponent函数,(就是刚学组件时,创建组件的写法)
const Demo = Vue.extend({})
// 通过构造函数得到组件实例,d就是组件实例,即vc
const d = new Demo()
Vue.prototype.x = d;
  • 绑定vm

由于main.js中有一个现成的vm,所以一般选择赋值vm:

写在(1)处,vm还为创建;写在(3)处,有点晚了,此时App整个组件已经放到页面上去了,组件里对于$on的调用已经执行了,会报错。正确写法:

2. 安装全局事件总线

一般名称不用x,用$bus

  • 方式一:安装vc
javascript 复制代码
const Demo = Vue.extend({})
const d = new Demo()
Vue.prototype.x = d;
  • 方式二:安装vm(更通用)
javascript 复制代码
new Vue({
  el: '#app',// 挂载容器
  render: h => h(App),
  beforeCreate () {
    // 安装全局事件总线
    Vue.prototype.$bus = this
  }
})

3. 使用总线事件

School组件与Student组件是兄弟组件。School需要获取到Student组件内的学生姓名(name)的值。

School需要借助总线得到数据,所以School组件得给总线绑定事件:

javascript 复制代码
mounted () {
    // 给总线绑定事件'getName',并写好事件的回调函数
    this.$bus.$on('getName', (name) => {
        console.log('School组件收到了学生姓名', name);
    })
},

Student组件触发总线上的事件,进而调用了School里的回调函数

javascript 复制代码
methods: {
    giveName () {
        this.x.$emit('getName', this.name)
    }
},

4. 解绑总线事件

组件在总线上绑定事件是想通过总线获取某些数据。所有组件都往这个总线bus上绑定事件,所以当组件被销毁时,该组件在总线$bus上绑定的事件也应该被解绑,不应该再占着了。

School组件

javascript 复制代码
 beforeDestroy () {
    // 在组件销毁之前,在总线中解绑该事件
    this.$bus.$off('getName') // 指明要解绑的事件
   // this.$bus.$off() 这样写会将所有人给总线绑定的事件都解绑了
  }

二、Todo案例应用全局事件总线

之前App组件与孙组件MyItem通信是通过MyList,以props的方式。现在改为全局事件总线的方式

1、安装全局事件总线

javascript 复制代码
// 创建vm实例
new Vue({
  // 将App组件放入容器中
  render: h => h(App),
  beforeCreate () {
    Vue.prototype.$bus = this     // 安装事件总线
  }
}).$mount('#app') // 挂载容器

2、App.vue

取消给MyList传递函数,MyList也取消接收,取消给MyItem传递函数。MyItem同样也取消props接收函数。

绑定事件

javascript 复制代码
  mounted () {
    // 事件总线上绑定事件
    this.$bus.$on('checkTodo', this.checkTodo)
    this.$bus.$on('deleteTodo', this.deleteTodo)
  },
  beforeDestroy () {
    // 组件销毁时,销毁总线上的事件
    this.$bus.$off(['checkTodo', 'deleteTodo'])
  },

3、MyItem.vue

三、消息订阅与发布

消息订阅与发布是一个理念,不是技术。

1. 前言

以报纸的订阅与发布为例,包含两部分:

1、订阅报纸:需要提供住址

2、邮递员送报纸:报纸

消息的订阅与发布:

1、订阅消息:说明订阅什么消息 以及 回调函数

2、发布消息:发送消息内容

A组件订阅了C组件的demo消息,回调函数是test。C组件一发布demo消息,A由于订阅了该消息,其回调函数test就会被调用,消息内容666就以参数的形式传递给A组件。

需要数据的人----订阅消息

提供数据的人----发布消息。

2. 使用步骤

以School组件需要Student组件的学生姓名数据为例:

  1. 安装pubsub:npm i pubsub-js(用这个库去实现消息订阅与发布的理念)

  2. 引入(School与Student都要引入): import pubsub from 'pubsub-js'

  3. 接收数据:School组件想接收数据,则在School组件中订阅消息,订阅的回调留在A组件自身。

    javascript 复制代码
     mounted () {
        // 订阅消息(消息名,回调函数(消息名,参数))
        // msgName的值是消息名,data才是需要传递的参数
        // 将pubId放在vc上,方便后续取消订阅
        this.pubId = pubsub.subscribe('hello', (msgName, data) => {
          console.log('School订阅消息')
          console.log(data);
        })
      },
  4. Student提供数据

    javascript 复制代码
    giveInfo () {
        pubsub.publish('hello', this.name)
    }
  5. 最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)去取消订阅。

    javascript 复制代码
    beforeDestroy () {
        // 取消订阅
        pubsub.unsubscribe(this.pubId)
    }

四、Todo案例应用消息订阅

将MyItem里的删除改为消息订阅的形式:

App.vue

javascript 复制代码
import pubsub from 'pubsub-js'
methods:{
   // 因为回调函数的第一个参数是msgName,函数里用不到这个参数,
   // 所以采用一个_占位,表示接收参数但不用
    deleteTodo (_, id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
 }
  mounted () {
    // 订阅消息
    this.pubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
  },
  beforeDestroy () {
    // 取消订阅
    pubsub.unsubscribe(this.pubId)
  },

MyItem.vue

javascript 复制代码
import pubsub from 'pubsub-js'
 // 处理删除
 handleDelete (id) {
   if (confirm('确定要删除吗?')) {
     pubsub.publish('deleteTodo', id)
   }
 }

五、Todo案例编辑Item

1、鼠标移到item上,显示编辑按钮

2、 点击编辑按钮时,前边变成input框,框里的内容是todo.title。

3、编辑完成之后,input框失去焦点时,又变成文字展示。

解决:给todo身上加一个isEdit属性。

1、 鼠标悬浮在MyItem上,显示编辑按钮

html 复制代码
<!--MyItem.vue-->
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<button class="btn btn-edit"">编辑</button>
<style>
/* 按钮的样式是写在App.vue里的
 因为同一个项目中,不同组件的删除按钮都是同一个风格的,因此只需要写一遍即可。不需要再每个组件里都写一遍。
 */
.btn-edit {
  color: #fff;
  background-color: skyblue;
  border: 1px solid rgb(98, 182, 216);
  margin-right: 5px;
}
</style>

2、点击编辑按钮时,前边变成input框,框里的内容是todo.title。

添加点击事件,给todo添加isEdit属性。如果是这样添加属性:todo.isEdit = true。这个属性确实加进todo里了,但是,这不是响应式的,点击编辑按钮时,这个属性值确实改变了,但不会被Vue监测到,也不会引起页面的更新。所以应该添加响应式的属性

html 复制代码
<!--当处于编辑状态时,不应该有编辑按钮,因此采用v-show-->
 <button class="btn btn-edit" @click="handleEdit(todo) v-show="!todo.isEdit"">编辑</button>
 <script>
   handleEdit (todo) {
   // 防止每次点击编辑,都给todo加一下isEdit属性,因此用if判断一下是否需要添加该属性
   //  if里的条件判断还可以写成:if ('isEdit' in todo)
      if (todo.isEdit !== undefined) {
        console.log('todo里有isEdit属性了');
        todo.isEdit = true
      } else {
        console.log('todo没有isEdit属性');
        this.$set('todo', 'isEdit', true)
      }
    },
</script>

页面的变化:文字内容与输入框只能存在一个

html 复制代码
  <span v-show="!todo.isEdit">{{ todo.title }}</span>
  <input type="input" :value="todo.title" v-show="todo.isEdit" />

3、编辑完成之后,input框失去焦点时,又变成文字展示。

给输入框添加一个失去焦点事件

html 复制代码
  <input
    type="input"
    :value="todo.title"
    v-show="todo.isEdit"
    @blur="handleBlur(todo,$event)"
  />
<script>
// 这个函数里不用再考虑给todo添加isEdit属性了,既然失去焦点,说明曾经获得过焦点,也就是能够编辑,有isEdit属性
    handleBlur (todo, e) {
      todo.isEdit = false
      // 判断输入是否为空,为空的话不能够进行修改
      if (!e.target.value.trim()) return alert('输入为空')
      // 触发全局总线的事件,真正修改todo数据
      this.$bus.$emit('updateTodo', todo.id, e.target.value)
    }
</script>

App.vue

javascript 复制代码
    // 编辑todo内容
    updateTodo (id, title) {
      this.todos.forEach((todo) => {
        if (todo.id === id) {
          todo.title = title
        }
      })
    },
  mounted () {
    this.$bus.$on('updateTodo', this.updateTodo)
  },
  beforeDestroy () {
    // 组件销毁时,销毁总线上的事件
    this.$bus.$off(['updateTodo'])
  },

仍然存在的问题:

点击编辑按钮后,若不想编辑了,需要先点击输入框(因为变成输入框的时候,输入框并没有自动获取焦点),再让数据框失去焦点,才能够让其不是编辑状态。

六、$nextTick

需要:一点击编辑按钮,输入框自动获取焦点。

html 复制代码
 <input
   type="input"
   :value="todo.title"
   v-show="todo.isEdit"
   @blur="handleBlur(todo, $event)"
   ref="inputTitle"
 />
 <script>
   // 处理编辑
    handleEdit (todo) {
      if (todo.isEdit !== undefined) {
        console.log('todo里有isEdit属性了');
        todo.isEdit = true
      } else {
        console.log('todo没有isEdit属性');
        this.$set(todo, 'isEdit', true)
      }
      // 让输入框获取焦点
      this.$refs.inputTitle.focus()
    },
</script>

这样写发现并不起作用,原因是:

vue会将handleEdit这个回调函数的所有代码都执行完后,再去重新解析模板,而不是执行一条代码,重新解析一次模板。

案例中是用v-show控制input框的出现,执行完19行之前的代码时,isEdit的值发生了变化,v-show的值为true,但是模板并未重新解析,此时input框虽然确实存在,但是还是隐藏状态。所以执行19行代码,对input进行的操作并不起作用 (如果一个input框隐藏了,再调用input框的focus(),input框不会获取焦点)

解决办法一:定时器

javascript 复制代码
 // 处理编辑
  handleEdit (todo) {
     ...if...
    // 让输入框获取焦点
    setTimeout(() => {
      this.$refs.inputTitle.focus()
    }, 200)
  },

解决方法二(官方方法):$nextTick

  • 语法:this.$nextTick(回调函数)
  • 作用:在下一次DOM更新结束后执行其指定的回调
  • 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在$nextTick所指定的回调函数中执行。
javascript 复制代码
  handleEdit (todo) {
     ...if...
    // 让输入框获取焦点
    this.$nextTick(() => {
      this.$refs.inputTitle.focus()
    })
  },

简单来说,$nextTick所指定的回调,会在DOM节点更新之后再执行。

修改后的Todo完整代码

MyItem.vue

html 复制代码
<template>
  <li>
    <label>
      <!-- 添加checked属性使得复选框被勾选上 -->
      <input
        type="checkbox"
        :checked="todo.done"
        @change="handleCheck(todo.id)"
      />
       <!-- 展示todo内容 -->
      <span v-show="!todo.isEdit">{{ todo.title }}</span>
      <input
        type="input"
        :value="todo.title"
        v-show="todo.isEdit"
        @blur="handleBlur(todo, $event)"
        ref="inputTitle"
      />
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    <button  class="btn btn-edit"  @click="handleEdit(todo)"  v-show="!todo.isEdit">编辑</button>
  </li>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
  name: 'MyItem',
  // 接收MyList组件传递的对象
  props: ["todo"],
  methods: {
    // 处理是否勾选
    handleCheck (id) {
      // this.checkTodo(id)
      this.$bus.$emit('checkTodo', id)
    },

    // 处理删除
    handleDelete (id) {
      if (confirm('确定要删除吗?')) {
        // 通知App,删除对应的todo
        // this.deleteTodo(id)   props方法
        // this.$bus.$emit('deleteTodo', id) 全局事件总线
        // 消息订阅
        pubsub.publish('deleteTodo', id)
      }
    },

    // 处理编辑
    handleEdit (todo) {
      // 这个属性确实加进todo里了,但是不是响应式的,这个属性的修改不会被Vue监测到,进而不会引起页面的更新
      // todo.isEdit = true
      // 防止每次点击编辑,都给todo加一下isEdit属性,因此用if判断一下是否需要添加该属性
      // 'isEdit' in todo  也可以这样判断
      if (todo.isEdit !== undefined) {
        console.log('todo里有isEdit属性了');
        todo.isEdit = true
      } else {
        console.log('todo没有isEdit属性');
        this.$set(todo, 'isEdit', true)
      }
      this.$nextTick(() => {
        this.$refs.inputTitle.focus()
      })
      // 自动获取焦点
      this.$nextTick(() => {
        this.$refs.inputTitle.focus()
      })
    },
    // 输入框失去焦点回调函数(真正执行修改逻辑)
    handleBlur (todo, e) {
      todo.isEdit = false
      if (!e.target.value.trim()) return alert('输入为空')
      this.$bus.$emit('updateTodo', todo.id, e.target.value)
    }
  }
}
</script>

App.vue

html 复制代码
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 给子组件传函数 -->
        <MyHeader @addTodo="addTodo"></MyHeader>
        <!-- 给子组件传数据 -->
        <MyList :todos="todos"></MyList>
        <MyFooter
          :todos="todos"
          @checkAllTodo="checkAllTodo"
          @clearAllTodo="clearAllTodo"
        ></MyFooter>
      </div>
    </div>
  </div>
</template>

<script>
import pubsub from 'pubsub-js'
import MyHeader from './components/MyHeader'
import MyFooter from './components/MyFooter'
import MyList from './components/MyList'
export default {
  name: 'App',
  components: {
    MyHeader,
    MyFooter,
    MyList
  },
  data () {
    return {
      // 3.读取数据
      todos: JSON.parse(localStorage.getItem('todos')) || []
    }
  },
  methods: {
    // 添加todoObj
    addTodo (todoObj) {
      this.todos.unshift(todoObj)
    },
    // 勾选or取消勾选一个todo
    checkTodo (id) {
      this.todos.forEach((todo) => {
        if (todo.id === id) {
          todo.done = !todo.done
        }
      })
    },
    // 编辑todo内容
    updateTodo (id, title) {
      this.todos.forEach((todo) => {
        if (todo.id === id) {
          todo.title = title
        }
      })
    },
    // 删除一个todo
    deleteTodo (_, id) {
      this.todos = this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    // 勾选or取消勾选所有todo
    checkAllTodo (done) {
      this.todos.forEach((todo) => {
        todo.done = done
      })
    },
    // 清除所有已完成的todo
    clearAllTodo () {
      this.todos = this.todos.filter((todo) => {
        return !todo.done
      })
    }
  },
  mounted () {
    // 事件总线上绑定事件
    this.$bus.$on('checkTodo', this.checkTodo)
    this.$bus.$on('updateTodo', this.updateTodo)
    // 订阅消息
    this.pubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
  },
  beforeDestroy () {
    // 组件销毁时,销毁总线上的事件
    this.$bus.$off(['checkTodo', 'updateTodo'])
    // 取消订阅
    pubsub.unsubscribe(this.pubId)
  },
  // 1. 将最新的数据存到localStorage,数据会发生增删改查,只需监视todos数据,有变化时将新的重新存入即可
  watch: {
    todos: {
      // 2. 开启深度监视,todos是个对象数组,不开启深度监视,当某一个对象的元素发生变化时,watch监听不到
      deep: true,
      handler (value) {
        localStorage.setItem('todos', JSON.stringify(value))
      }
    }
  }
  </script>
相关推荐
来恩10039 分钟前
jQuery选择器
前端·javascript·jquery
前端繁华如梦11 分钟前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo44 分钟前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE1 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家1 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班1 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
threelab2 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
爱怪笑的小杰杰2 小时前
Leaflet 高性能大数据量图圆:彻底解决缩放/拖拽偏移问题
大数据·前端·vue.js·贴图
失眠的咕噜3 小时前
PDA 安卓设备上传多张图片
android·前端·javascript
掰头战士3 小时前
深入了解JS原型及原型继承链机制
javascript