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>
相关推荐
ggdpzhk2 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
小曲曲3 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•4 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS5 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜6 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点6 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow6 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o6 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
刚刚好ā7 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
yqcoder9 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript