【组件自定义事件+全局事件总线+消息订阅与发布+TodoList案例——编辑+过度与动画】

组件自定义事件+全局事件总线+消息订阅与发布+TodoList案例——编辑+过度与动画

  • 1 组件自定义事件
    • 1.1 绑定
    • 1.2 解绑
    • 1.3 总结
    • 1.4 TodoList案例——自定义事件
  • 2 全局事件总线
    • 2.1 理解
    • 2.2 步骤
    • 2.3 TodoList案例——事件总线
  • 3 消息订阅与发布
    • 3.1 理解
    • 3.2 TodoList案例——消息的订阅与发布
  • 4 TodoList案例——编辑
    • 4.1 $nextTick
    • 4.2 代码
  • 5 过度与动画
    • 5.1 理解
    • 5.2 TodoList案例——动画

1 组件自定义事件

  • 区别于JS中的内置事件(如:click、keyup等)用于html中的元素,自定义事件用于组件。

1.1 绑定

  • App.vue:
<template>
  <div class="app">
    <h1>{{msg}}</h1>
    <!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
    <School :getSchoolName="getSchoolName"/>

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
    <Student v-on:atguigu="getStudentName"/> <!-- 由于v-on在Student组件标签上,所以是给Student组件的实例对象vc身上绑定了一个事件atguigu,如果有人触发了此事件,那么getStudentName函数将会被调用 -->
    <!-- 若要让按钮只能触发一次 -->
    <!-- <Student v-on:atguigu.once="getStudentName"/> -->

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
    <!-- <Student ref="student"/> -->
  </div>
</template>

<script>
    // 引入Student组件
    import Student from './components/Student.vue' 
    // 引入School组件
    import School from './components/School.vue'


    
    export default {
      name:'App',
      components:{School, Student},
      data() {
        return {
          msg:"你好啊!"
        }
      },
      methods: {
        getSchoolName(name) {
          console.log('App收到了学校名:',name);
        },
        getStudentName(name) {
          console.log('App收到了学生名:',name);
        }
        /* getStudentName(name,...params) {
          console.log('App收到了学生名:',name,params); // params收集剩余参数
        } */
      },

      // 用于第二种写法ref
      // mounted() {
        // 绑定自定义事件
        // this.$refs.student是Student组件的实例对象
        // this.$refs.student.$on('atguigu',this.getStudentName)

        // 绑定自定义事件且让他等三秒钟返回
        /* setTimeout(()=>{
          this.$refs.student.$on('atguigu',this.getStudentName)
        },3000) */

        // 绑定自定义事件且让按钮只能触发一次
        // this.$refs.student.$once('atguigu',this.getStudentName)
      // }
    }
</script>

<style scoped>
  .app {
    background-color: gray;
    padding: 5px;
  }
</style>
  • School.vue:
<template>
  <div class="school">
    <h2>学校名称:{{name}}</h2>
    <h2>学校地址:{{address}}</h2>
    <button @click="sendSchoolName">把学校名给App</button>
  </div>
</template>

<script>
  export default {
    name:'School',
    props:['getSchoolName'],
    data() {
      return {
        name:'霍格沃兹魔法学院',
        address:'苏格兰高地'
      }
    },
    methods: {
      sendSchoolName() {
        this.getSchoolName(this.name)
      }
    }
  }
</script>

<style scoped>
  .school {
    background-color: pink;
    padding: 5px;
  }
</style>
  • Student.vue:
<template>
  <div class="student">
    <h2>学生姓名:{{name}}</h2>
    <h2>学生性别:{{sex}}</h2>
    <button @click="sendStudentName">点我把学生名给App</button>
  </div>
</template>

<script>
  export default {
    name:'Student',
    data() {
      return {
        name:'小王',
        sex:'女'
      }
    },
    methods: {
      sendStudentName() {
        // 触发Student组件实例身上的atguigu事件
        this.$emit('atguigu',this.name)
        // this.$emit('atguigu',this.name,666,888,900)
      }
    }
  }
</script>

<style scoped>
  .student{
    background-color: orange;
    padding: 5px;
    margin-top: 30px;
  }
</style>

在这里插入图片描述

1.2 解绑

  • App.vue:
<template>
  <div class="app">
    <h1>{{msg}}</h1>
    <!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
    <School :getSchoolName="getSchoolName"/>

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
    <!-- <Student v-on:atguigu="getStudentName"/> 由于v-on在Student组件标签上,所以是给Student组件的实例对象vc身上绑定了一个事件atguigu,如果有人触发了此事件,那么getStudentName函数将会被调用 -->
    <Student v-on:atguigu="getStudentName" @demo="m1"/>
    
    <!-- 若要让按钮只能触发一次 -->
    <!-- <Student v-on:atguigu.once="getStudentName"/> -->

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
    <!-- <Student ref="student"/> -->
  </div>
</template>

<script>
    // 引入Student组件
    import Student from './components/Student.vue' 
    // 引入School组件
    import School from './components/School.vue'


    
    export default {
      name:'App',
      components:{School, Student},
      data() {
        return {
          msg:"你好啊!"
        }
      },
      methods: {
        getSchoolName(name) {
          console.log('App收到了学校名:',name);
        },
        getStudentName(name) {
          console.log('App收到了学生名:',name);
        },
        /* getStudentName(name,...params) {
          console.log('App收到了学生名:',name,params); // params收集剩余参数
        } */
        m1() {
          console.log("demo事件被触发了");
        }
      },

      // 用于第二种写法ref
      // mounted() {
        // 绑定自定义事件
        // this.$refs.student是Student组件的实例对象
        // this.$refs.student.$on('atguigu',this.getStudentName)

        // 绑定自定义事件且让他等三秒钟返回
        /* setTimeout(()=>{
          this.$refs.student.$on('atguigu',this.getStudentName)
        },3000) */

        // 绑定自定义事件且让按钮只能触发一次
        // this.$refs.student.$once('atguigu',this.getStudentName)
      // }
    }
</script>

<style scoped>
  .app {
    background-color: gray;
    padding: 5px;
  }
</style>
  • Student.vue:
<template>
  <div class="student">
    <h2>学生姓名:{{name}}</h2>
    <h2>学生性别:{{sex}}</h2>
    <button @click="sendStudentName">点我把学生名给App</button>
    <button @click="unbind">解绑atguigu事件</button>
  </div>
</template>

<script>
  export default {
    name:'Student',
    data() {
      return {
        name:'小王',
        sex:'女'
      }
    },
    methods: {
      sendStudentName() {
        // 触发Student组件实例身上的atguigu事件
        this.$emit('atguigu',this.name)
        // this.$emit('atguigu',this.name,666,888,900)

        // this.$emit('demo')
      },
      unbind() {
        this.$off('atguigu') // 只适用于解绑一个自定义事件
        // this.$off(['atguigu','demo']) // 解绑多个自定义事件
        // this.$off() // 解绑所有的自定义事件
      }
    }
  }
</script>

<style scoped>
  .student{
    background-color: orange;
    padding: 5px;
    margin-top: 30px;
  }
</style>

1.3 总结

  • 一种组件间通信的方式,适用于:子组件 —> 父组件
  • 使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
  • 绑定自定义事件:
    1> 第一种方式,在父组件中:<Demo @atguigu="test"/><Demo v-on:atguigu="test"/>
    2> 第二种方式,在父组件中:
    在这里插入图片描述
    3> 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。
  • 触发自定义事件:this.$emit('atguigu',数据)
  • 解绑自定义事件:
    1> this.$off('atguigu'):只适用于解绑一个自定义事件
    2> this.$off(['atguigu','demo']):解绑多个自定义事件
    3> this.$off() :解绑所有的自定义事件
  • 注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中要么用箭头函数,否则this指向会出问题!
  • 组件上也可以绑定原生DOM事件,需要使用native修饰符。
  • App.vue:
<template>
  <div class="app">
    <h1>{{msg}},学生姓名是:{{studentName}}</h1>
    <!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
    <School :getSchoolName="getSchoolName"/>

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
    <!-- <Student v-on:atguigu="getStudentName"/> 由于v-on在Student组件标签上,所以是给Student组件的实例对象vc身上绑定了一个事件atguigu,如果有人触发了此事件,那么getStudentName函数将会被调用 -->
    <Student v-on:atguigu="getStudentName" @demo="m1"/>
    
    <!-- 若要让按钮只能触发一次 -->
    <!-- <Student v-on:atguigu.once="getStudentName"/> -->

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
    <!-- <Student ref="student"/> -->

    <!-- 组件上也可以绑定原生DOM事件 需要使用native修饰符 -->
    <!-- <Student ref="student" @click.native="show"/> -->
  </div>
</template>

<script>
    // 引入Student组件
    import Student from './components/Student.vue' 
    // 引入School组件
    import School from './components/School.vue'


    
    export default {
      name:'App',
      components:{School, Student},
      data() {
        return {
          msg:"你好啊!",
          studentName:''
        }
      },
      methods: {
        getSchoolName(name) {
          console.log('App收到了学校名:',name);
        },
        // 要么配置在methods中
        getStudentName(name) {
          console.log('App收到了学生名:',name);

          this.studentName = name
        },
        /* getStudentName(name,...params) {
          console.log('App收到了学生名:',name,params); // params收集剩余参数
        } */
        m1() {
          console.log("demo事件被触发了");
        },
        /* show() {
          alert(123)
        } */
      },

      // 用于第二种写法ref
      // mounted() {
        // 绑定自定义事件
        // this.$refs.student是Student组件的实例对象
        // this.$refs.student.$on('atguigu',this.getStudentName)

        // 要么用箭头函数
        /* this.$refs.student.$on('atguigu',(name,...params)=>{
          console.log('App收到了学生名:',name,params);
          console.log(this)
          this.studentName = name
        }) */

        // 绑定自定义事件且让他等三秒钟返回
        /* setTimeout(()=>{
          this.$refs.student.$on('atguigu',this.getStudentName)
        },3000) */

        // 绑定自定义事件且让按钮只能触发一次
        // this.$refs.student.$once('atguigu',this.getStudentName)
      // }
    }
</script>

<style scoped>
  .app {
    background-color: gray;
    padding: 5px;
  }
</style>

1.4 TodoList案例——自定义事件

  • App.vue:
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- <MyHeader :addTodo="addTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyHeader @addTodo="addTodo"/>
        <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/> <!--传递数据-->
        <!-- <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
      </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(todoObj) {
        // console.log('我是App组件,我收到了数据:',x);
        this.todos.unshift(todoObj)
      },
      // 勾选or取消勾选一个todo
      checkTodo(id) {
        this.todos.forEach((todo)=>{
          if(todo.id === id) todo.done = !todo.done
        })
      },
      // 删除一个todo
      deleteTodo(id) {
        this.todos = this.todos.filter((todo)=>{
          return todo.id !== id
        })
      },
      // 全选or取消全选
      checkAllTodo(done) {
        this.todos.forEach((todo)=>{
          todo.done = done
        })
      },
      // 清除所有已经完成的todo
      clearAllTodo() {
        this.todos = this.todos.filter((todo)=>{
          return !todo.done
        })
      }
    },
    watch: {
      todos: {
        // 深度监视
        deep:true,
        handler(value) {
          localStorage.setItem('todos',JSON.stringify(value))
        }
      }
    }
  }
</script>

<style>
  /*base*/
  body {
    background: #fff;
  }

  .btn {
    display: inline-block;
    padding: 4px 12px;
    margin-bottom: 5px;
    margin-left: 285px;
    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-danger:hover {
    color: #fff;
    background-color: #bd362f;
  }

  .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>
  • MyHeader.vue:
<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import {nanoid} from 'nanoid'
    export default {
        name:'MyHeader',
        // 采用自定义事件方法改为:
        // props:['addTodo'],
        data() {
            return{
                title:''
            }
        },
        methods: {
            add() {
                // 校验数据
                if(!this.title.trim()) return alert('输入不能为空') // 如果输入为空 敲回车就没反应 trim()去掉前后空格
                // console.log(e.target.value);
                // 将用户的输入包装成为一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                // console.log(todoObj)
                // 通知App组件去添加一个todo对象
                // this.addTodo(todoObj)
                // 采用自定义事件方法改为:
                this.$emit('addTodo',todoObj)
                // 清空输入
                this.title = ''
            }
        }
    }
</script>

<style scoped>
    /*header*/
    .todo-header input {
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 4px 7px;
    }

    .todo-header input:focus {
        outline: none;
        border-color: rgba(82, 168, 236, 0.8);
        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
    }
</style>
  • MyFooter.vue:
<template>
    <div class="todo-footer" v-show="total">
        <label>
            <!-- <input type="checkbox" :checked="doneTotal === total"/> -->
            <!-- 写法一 -->
            <!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
            <!-- 写法二 -->
            <input type="checkbox" v-model="isAll"/>
        </label>
        <span>
            <!-- <span>已完成{{doneTotal}}</span> / 全部{{todos.length}} -->
            <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        // props:['todos','checkAllTodo','clearAllTodo'],
        // 采用自定义事件方法改为:
        props:['todos'],
        computed:{
            total() {
                return this.todos.length
            },
            doneTotal() {
                /* const x =  this.todos.reduce((pre,current)=>{
                    console.log('@',pre,current)
                    return pre + (current.done ? 1 : 0)
                },0)
                console.log('###',x); */
                // 简写为:
                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
            },
            // 写法一
            /* isAll() {
                return this.doneTotal === this.total && this.total > 0
            }, */
            // 写法二
            isAll: {
                get(){
                    return this.doneTotal === this.total && this.total > 0
                },
                set(value){
                    // this.checkAllTodo(value)
                    // 采用自定义事件方法改为:
                    this.$emit('checkAllTodo',value)
                }
            }
        },
        methods: {
            /* checkAll(e) {
                // console.log(e.target.checked);
                this.checkAllTodo(e.target.checked)
            } */

            clearAll() {
                // this.clearAllTodo()
                // 采用自定义事件方法改为:
                this.$emit('clearAllTodo')
            }
        }
    }
</script>

<style scoped>
    /*footer*/
    .todo-footer {
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
    }

    .todo-footer label {
        display: inline-block;
        margin-right: 20px;
        cursor: pointer; /* 网页浏览时用户鼠标指针的样式或图形形状为一只手 */
    }
</style>

2 全局事件总线

2.1 理解

  • 全局事件总线:一种组件间通信的方式,适用于任意组件间通信
    在这里插入图片描述
  • Vue 原型对象上包含事件处理的方法:
    1> $on(eventName, listener): 绑定自定义事件监听
    2> $emit(eventName, data): 分发自定义事件
    3> $off(eventName): 解绑自定义事件监听
    4> $once(eventName, listener): 绑定事件监听, 但只能处理一次
  • 所有组件实例对象的原型对象的原型对象就是 Vue 的原型对象。
    1> 所有组件对象都能看到 Vue 原型对象上的属性和方法。
    2> Vue.prototype.$bus = new Vue(), 所有的组件对象都能看到$bus这个属性对象。

2.2 步骤

  • 安装全局事件总线:
    在这里插入图片描述
  • 使用事件总线:
    1> 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身
    最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
    在这里插入图片描述
    2> 提供数据:
    在这里插入图片描述

2.3 TodoList案例——事件总线

  • main.js代码:
// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false


// 创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})
  • App.vue代码:
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- <MyHeader :addTodo="addTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyHeader @addTodo="addTodo"/>
        <!-- <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/> 传递数据-->
        <!-- 采用全局事件总线方法改为: -->
        <MyList :todos="todos"/>
        <!-- <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
      </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(todoObj) {
        // console.log('我是App组件,我收到了数据:',x);
        this.todos.unshift(todoObj)
      },
      // 勾选or取消勾选一个todo
      checkTodo(id) {
        this.todos.forEach((todo)=>{
          if(todo.id === id) todo.done = !todo.done
        })
      },
      // 删除一个todo
      deleteTodo(id) {
        this.todos = this.todos.filter((todo)=>{
          return todo.id !== id
        })
      },
      // 全选or取消全选
      checkAllTodo(done) {
        this.todos.forEach((todo)=>{
          todo.done = done
        })
      },
      // 清除所有已经完成的todo
      clearAllTodo() {
        this.todos = this.todos.filter((todo)=>{
          return !todo.done
        })
      }
    },
    watch: {
      todos: {
        // 深度监视
        deep:true,
        handler(value) {
          localStorage.setItem('todos',JSON.stringify(value))
        }
      }
    },
    // 采用全局事件总线方法此处添加:
    mounted(){
      this.$bus.$on('checkTodo',this.checkTodo)
      this.$bus.$on('deleteTodo',this.deleteTodo)
    },
    beforeDestroy() {
      this.$bus.$off('checkTodo')
      this.$bus.$off('deleteTodo')
    }
  }
</script>

<style>
  /*base*/
  body {
    background: #fff;
  }

  .btn {
    display: inline-block;
    padding: 4px 12px;
    margin-bottom: 5px;
    margin-left: 285px;
    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-danger:hover {
    color: #fff;
    background-color: #bd362f;
  }

  .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>
  • MyList.vue代码:
<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" 
            :todo="todoObj" 
        /> <!--遍历数组并且传递数据--> <!--采用全局事件总线方法此处删除了 :checkTodo="checkTodo" 和 :deleteTodo="deleteTodo"-->
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue'

    export default {
        name:'MyList',
        components: {MyItem},
        // props:['todos','checkTodo','deleteTodo'] // 接收数据
        // 采用全局事件总线方法改为:
        props:['todos']
    }
</script>

<style scoped>
    /*main*/
    .todo-main {
        margin-left: 0px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0px;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>
  • MyItem代码:
<template>
    <li>
        <label>
            <!-- 写法一 -->
            <!-- <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>  :checked="true" 给input指定一个checked选项,如为true,拥有checked,如为false,则没有checked-->
            <!-- 写法二 -->
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 写法三 此写法直接合并后两项 不用App插件将数据传给MyList再传给MyItem 不推荐此写法 因为props是只读的 不建议修改-->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name:'MyItem',
        // 声明接收todo对象
        // props:['todo','checkTodo','deleteTodo'],
        // 采用全局事件总线方法改为:
        props:['todo'],
        methods:{
            // 勾选or取消勾选
            handleCheck(id) {
                // console.log(id);
                // 通知App组件将对应的todo对象的done值取反
                // this.checkTodo(id)
                // 采用全局事件总线方法改为:
                this.$bus.$emit('checkTodo',id)
            },
            // 删除
            handleDelete(id) {
                // confirm根据用户的交互 确定布尔值为真还是假
                if(confirm('确定删除吗?')) {
                    // console.log(id);
                    // this.deleteTodo(id)
                    // 采用全局事件总线方法改为:
                    this.$bus.$emit('deleteTodo',id)
                }
            }
        }
    }
</script>

<style scoped>
    /*item*/
    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: #ddd;
    }

    li:hover button {
        display: block;
    }
</style>

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3 消息订阅与发布

3.1 理解

  • 这种方式的思想与全局事件总线很相似,它包含以下操作:
    1> 订阅消息 --对应绑定事件监听
    2> 发布消息 --对应分发事件
    3> 取消消息订阅 --对应解绑事件监听
  • 需要引入一个消息订阅与发布的第三方实现库: PubSubJS
  • 报纸订阅与发布步骤:
    1> 订阅报纸:家庭住址
    2> 邮递员送报纸:报纸
  • 消息订阅与发布步骤:
    1> 订阅消息:消息名
    2> 发布消息:消息内容
    在这里插入图片描述
  • 一种组件间通信的方式,适用于任意组件间通信
  • 使用步骤:
    1> 安装pubsub:npm i pubsub-js
    2> 引入:import pubsub from 'pubsub-js'
    3> 接收数据(消息的订阅语法):A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
    在这里插入图片描述
    4> 提供数据(消息的发布语法):pubsub.publish('xxx',数据)(第一个形参代表消息名,第二个形参代表传递的数据)
    5> 最好在beforeDestroy钩子中,用Pubsub.unsubscribe(pid)取消订阅
  • main.js代码:
// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false

// 创建vm
new Vue({
    el:'#app',
    render: h => h(App),
})
  • School.vue代码:
<template>
  <div class="school">
    <h2>学校名称:{{name}}</h2>
    <h2>学校地址:{{address}}</h2>
  </div>
</template>

<script>
  import pubsub from 'pubsub-js'
  export default {
    name:'School',
    data() {
      return {
        name:'霍格沃兹魔法学院',
        address:'苏格兰高地'
      }
    },
    methods: {
      demo(msgName,data){
        console.log('有人发布了hello消息,hello消息的回调执行了',data)
      }
    },
    mounted(){
      this.pubId = pubsub.subscribe('hello',this.demo)
    },
    beforeDestroy(){
      pubsub.unsubscribe(this.pubId)
    }
  }
</script>

<style scoped>
  .school {
    background-color: pink;
    padding: 5px;
  }
</style>
  • Student.vue代码:
<template>
  <div class="student">
    <h2>学生姓名:{{name}}</h2>
    <h2>学生性别:{{sex}}</h2>
    <button @click="sendStudentName">把学生名给school组件</button>
  </div>
</template>

<script>
  import pubsub from 'pubsub-js'
  export default {
    name:'Student',
    data() {
      return {
        name:'小王',
        sex:'女'
      }
    },
    methods:{
      sendStudentName(){
        pubsub.publish('hello',666)
      }
    }
  }
</script>

<style scoped>
  .student{
    background-color: orange;
    padding: 5px;
    margin-top: 30px;
  }
</style>

3.2 TodoList案例——消息的订阅与发布

  • App.vue代码:
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- <MyHeader :addTodo="addTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyHeader @addTodo="addTodo"/>
        <!-- <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/> 传递数据-->
        <!-- 采用全局事件总线方法改为: -->
        <MyList :todos="todos"/>
        <!-- <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
      </div>
    </div>
  </div>
</template>

<script>
  // 引入pubsub库
  import pubsub from 'pubsub-js'
  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(todoObj) {
        // console.log('我是App组件,我收到了数据:',x);
        this.todos.unshift(todoObj)
      },
      // 勾选or取消勾选一个todo
      checkTodo(id) {
        this.todos.forEach((todo)=>{
          if(todo.id === id) todo.done = !todo.done
        })
      },
      // 删除一个todo
      // deleteTodo(msgName,id) {
      // 用下划线占个位
      deleteTodo(_,id) {
        this.todos = this.todos.filter((todo)=>{
          return todo.id !== id
        })
      },
      // 全选or取消全选
      checkAllTodo(done) {
        this.todos.forEach((todo)=>{
          todo.done = done
        })
      },
      // 清除所有已经完成的todo
      clearAllTodo() {
        this.todos = this.todos.filter((todo)=>{
          return !todo.done
        })
      }
    },
    watch: {
      todos: {
        // 深度监视
        deep:true,
        handler(value) {
          localStorage.setItem('todos',JSON.stringify(value))
        }
      }
    },
    // 采用全局事件总线方法此处添加:
    mounted(){
      this.$bus.$on('checkTodo',this.checkTodo)
      // this.$bus.$on('deleteTodo',this.deleteTodo) 
      // 采用消息订阅与发布方法此处改为:
      this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
    },
    beforeDestroy() {
      this.$bus.$off('checkTodo')
      // this.$bus.$off('deleteTodo')
      // 采用消息订阅与发布方法此处改为:
      pubsub.unsubscribe(this.pubId)
    }
  }
</script>

<style>
  /*base*/
  body {
    background: #fff;
  }

  .btn {
    display: inline-block;
    padding: 4px 12px;
    margin-bottom: 5px;
    margin-left: 285px;
    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-danger:hover {
    color: #fff;
    background-color: #bd362f;
  }

  .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>
  • MyItem.vue代码:
<template>
    <li>
        <label>
            <!-- 写法一 -->
            <!-- <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>  :checked="true" 给input指定一个checked选项,如为true,拥有checked,如为false,则没有checked-->
            <!-- 写法二 -->
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 写法三 此写法直接合并后两项 不用App插件将数据传给MyList再传给MyItem 不推荐此写法 因为props是只读的 不建议修改-->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        // 声明接收todo对象
        // props:['todo','checkTodo','deleteTodo'],
        // 采用全局事件总线方法改为:
        props:['todo'],
        methods:{
            // 勾选or取消勾选
            handleCheck(id) {
                // console.log(id);
                // 通知App组件将对应的todo对象的done值取反
                // this.checkTodo(id)
                // 采用全局事件总线方法改为:
                this.$bus.$emit('checkTodo',id)
            }, 
            // 删除
            handleDelete(id) {
                // confirm根据用户的交互 确定布尔值为真还是假
                if(confirm('确定删除吗?')) {
                    // console.log(id);
                    // this.deleteTodo(id)
                    // 采用全局事件总线方法改为:
                    // this.$bus.$emit('deleteTodo',id)
                    // 采用消息订阅与发布方法此处改为:
                    pubsub.publish('deleteTodo',id)
                }
            }
        }
    }
</script>

<style scoped>
    /*item*/
    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: #ddd;
    }

    li:hover button {
        display: block;
    }
</style>

4 TodoList案例——编辑

4.1 $nextTick

  • 语法:this.$nextTick(回调函数)
  • 作用:在下一次 DOM 更新结束后执行其指定的回调。
  • 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

4.2 代码

  • MyItem.vue代码:
<template>
    <li>
        <label>
            <!-- 写法一 -->
            <!-- <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>  :checked="true" 给input指定一个checked选项,如为true,拥有checked,如为false,则没有checked-->
            <!-- 写法二 -->
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 写法三 此写法直接合并后两项 不用App插件将数据传给MyList再传给MyItem 不推荐此写法 因为props是只读的 不建议修改-->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span v-show="!todo.isEdit">{{todo.title}}</span>
            <input type="text" v-show="todo.isEdit" :value="todo.title" @blur="handleBlur(todo,$event)" ref="inputTitle">
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        <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',
        // 声明接收todo对象
        // props:['todo','checkTodo','deleteTodo'],
        // 采用全局事件总线方法改为:
        props:['todo'],
        methods:{
            // 勾选or取消勾选
            handleCheck(id) {
                // console.log(id);
                // 通知App组件将对应的todo对象的done值取反
                // this.checkTodo(id)
                // 采用全局事件总线方法改为:
                this.$bus.$emit('checkTodo',id)
            }, 
            // 删除
            handleDelete(id) {
                // confirm根据用户的交互 确定布尔值为真还是假
                if(confirm('确定删除吗?')) {
                    // console.log(id);
                    // this.deleteTodo(id)
                    // 采用全局事件总线方法改为:
                    // this.$bus.$emit('deleteTodo',id)
                    // 采用消息订阅与发布方法此处改为:
                    pubsub.publish('deleteTodo',id)
                }
            },
            // 编辑
            handleEdit(todo) {
                // todo.isEdit = true // 此写法可以改值 但没有getter和setter
                // 利用列表渲染中的vue.set
                // this.$set(todo,'isEdit',true)
                // 第一次加上isEdit属性 后面无需再加 因此使用if语句
                // 如果todo身上有isEdit 直接改 如果todo身上没有isEdit 先添加此属性再赋值
                if('isEdit' in todo) {
                    todo.isEdit = true
                } else {
                    this.$set(todo,'isEdit',true)
                }
                this.$nextTick(function() {
                    // nextTick指定的回调 会在dom节点更新完毕后执行
                    this.$refs.inputTitle.focus() // 获取焦点
                }) 
            },
            // 失去焦点回调(真正执行修改逻辑)
            handleBlur(todo,e){
                todo.isEdit = false
                if(!e.target.value.trim()) return alert('输入不能为空!') // trim()函数用于删除字符串的头尾空白符
                this.$bus.$emit('updateTodo',todo.id,e.target.value)
            }
        }
    }
</script>

<style scoped>
    /*item*/
    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 input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none; 
        margin-top: 3px; 
        margin-left: 5px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: #ddd;
    }

    li:hover button {
        display: block;
    }
</style>
  • App.vue代码:
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- <MyHeader :addTodo="addTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyHeader @addTodo="addTodo"/>
        <!-- <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/> 传递数据-->
        <!-- 采用全局事件总线方法改为: -->
        <MyList :todos="todos"/>
        <!-- <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/> -->
        <!-- 采用自定义事件方法改为: -->
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
      </div>
    </div>
  </div>
</template>

<script>
  // 引入pubsub库
  import pubsub from 'pubsub-js'
  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(todoObj) {
        // console.log('我是App组件,我收到了数据:',x);
        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(msgName,id) {
      // 用下划线占个位
      deleteTodo(_,id) {
        this.todos = this.todos.filter((todo)=>{
          return todo.id !== id
        })
      },
      // 全选or取消全选
      checkAllTodo(done) {
        this.todos.forEach((todo)=>{
          todo.done = done
        })
      },
      // 清除所有已经完成的todo
      clearAllTodo() {
        this.todos = this.todos.filter((todo)=>{
          return !todo.done
        })
      }
    },
    watch: {
      todos: {
        // 深度监视
        deep:true,
        handler(value) {
          localStorage.setItem('todos',JSON.stringify(value))
        }
      }
    },
    // 采用全局事件总线方法此处添加:
    mounted(){
      this.$bus.$on('checkTodo',this.checkTodo)
      this.$bus.$on('updateTodo',this.updateTodo)
      // this.$bus.$on('deleteTodo',this.deleteTodo) 
      // 采用消息订阅与发布方法此处改为:
      this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
    },
    beforeDestroy() {
      this.$bus.$off('checkTodo')
      this.$bus.$off('updateTodo')
      // this.$bus.$off('deleteTodo')
      // 采用消息订阅与发布方法此处改为:
      pubsub.unsubscribe(this.pubId)
    }
  }
</script>

<style>
  /*base*/
  body {
    background: #fff;
  }

  .btn {
    display: inline-block;
    padding: 4px 12px;
    margin-bottom: 5px;
    margin-left: 285px;
    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: skyblue;
    border: 1px solid rgb(23, 99, 129);
  }

  .btn-danger:hover {
    color: #fff;
    background-color: #bd362f;
  }

  .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>

在这里插入图片描述
在这里插入图片描述

5 过度与动画

5.1 理解

  • 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。
  • 图示:
    在这里插入图片描述
  • 写法:
    1> 准备好样式:
    元素进入的样式:
    1. v-enter:进入的起点
    2. v-enter-active:进入过程中
    3. v-enter-to:进入的终点
    元素离开的样式:
    1. v-leave:离开的起点
    2. v-leave-active:离开过程中
    3. v-leave-to:离开的终点
    2> 使用<transition>包裹要过度的元素,并配置name属性:
    在这里插入图片描述
    3> 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key
  • 代码示例:
    1> main.js代码:
// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false


// 创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

2> App.vue代码:

<template>
  <div>
    <Test/>
    <Test2/>
    <Test3/>
  </div>
</template>

<script>
    import Test from './components/Test.vue'
    import Test2 from './components/Test2.vue'
    import Test3 from './components/Test3.vue'

    export default {
        name:'App',
        components:{Test,Test2,Test3},
    }
</script>

3> Test.vue代码:

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <transition name="hello" :appear="true"> <!-- appear控制h1一上来就滑入 -->
        <h1 v-show="isShow">你好啊!</h1>
    </transition>
  </div>
</template>

<script>
    export default {
        name:'Test',
        data() {
            return {
                isShow:true
            }
        }
    }
</script>

<style scoped>
    h1 {
        background-color: orange;
    }

    /* 动画样式 用动画写 */
    /* 来 */
    .hello-enter-active {
        animation: atguigu 1s linear; /* linear 匀速 */
    }
    /* 去 */
    .hello-leave-active {
        animation: atguigu 1s reverse; /* reverse 反转 */
    }
    
    @keyframes atguigu {
        from {
            transform: translateX(-100%);
        }
        to {
            transform: translateX(0);
        }
    }
</style>

在这里插入图片描述
4> Test2.vue代码:

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <!-- 单个元素过度 -->
    <!-- <transition name="hello" :appear="true"> --> <!-- appear控制h1一上来就滑入 -->
        <!-- <h1 v-show="isShow">你好啊!</h1> -->
    <!-- </transition> -->

    <!-- 多个元素过度 -->
    <transition-group name="hello" :appear="true"> <!-- appear控制h1一上来就滑入 -->
        <h1 v-show="isShow" key="1">你好啊!</h1>
        <h1 v-show="isShow" key="2">小王几点了!</h1>
    </transition-group>
  </div>
</template>

<script>
    export default {
        name:'Test',
        data() {
            return {
                isShow:true
            }
        }
    }
</script>

<style scoped>
    h1 {
        background-color: orange;
        /* transition: 1s linear;  放在下面*/
    }

    /* 动画样式 用过度写 */

    /* 来 */
    /* 进入的起点、离开的终点 */
    .hello-enter, .hello-leave-to {
        transform: translateX(-100%);
    }
    /* 进入过程中 */
    .hello-enter-active, .hello-leave-active {
        transition: 1s linear;
    }
    /* 进入的终点、离开的起点 */
    .hello-enter-to, .hello-leave {
        transform: translateX(0);
    }

    /* 去 */
    /* 离开的起点 */
    /* .hello-leave {
        transform: translateX(0);
    } */
    /* 离开的终点 */
    /* .hello-leave-to {
        transform: translateX(-100%);
    } */
    
</style>

在这里插入图片描述
5> Test3.vue代码:

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <!-- 多个元素过度 -->
    <transition-group
        :appear="true" 
        name="animate__animated animate__bounce" 
        enter-active-class="animate__swing"
        leave-active-class="animate__backOutUp"
    >
        <h1 v-show="isShow" key="1">你好啊!</h1>
        <h1 v-show="isShow" key="2">小王几点了!</h1>
    </transition-group>
  </div>
</template>

<script>
    // 引入第三方库
    import 'animate.css'
    
    export default {
        name:'Test',
        data() {
            return {
                isShow:true
            }
        }
    }
</script>

<style scoped>
    h1 {
        background-color: orange;
    }    
</style>

在这里插入图片描述
在这里插入图片描述

5.2 TodoList案例——动画

  • 方法一 修改MyItem.vue代码:
<template>
    <transition name="todo" appear="true">
        <li>
            <label>
                <!-- 写法一 -->
                <!-- <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>  :checked="true" 给input指定一个checked选项,如为true,拥有checked,如为false,则没有checked-->
                <!-- 写法二 -->
                <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
                <!-- 写法三 此写法直接合并后两项 不用App插件将数据传给MyList再传给MyItem 不推荐此写法 因为props是只读的 不建议修改-->
                <!-- <input type="checkbox" v-model="todo.done"/> -->
                <span v-show="!todo.isEdit">{{todo.title}}</span>
                <input type="text" v-show="todo.isEdit" :value="todo.title" @blur="handleBlur(todo,$event)" ref="inputTitle">
            </label>
            <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
            <button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
        </li>
    </transition>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        // 声明接收todo对象
        // props:['todo','checkTodo','deleteTodo'],
        // 采用全局事件总线方法改为:
        props:['todo'],
        methods:{
            // 勾选or取消勾选
            handleCheck(id) {
                // console.log(id);
                // 通知App组件将对应的todo对象的done值取反
                // this.checkTodo(id)
                // 采用全局事件总线方法改为:
                this.$bus.$emit('checkTodo',id)
            }, 
            // 删除
            handleDelete(id) {
                // confirm根据用户的交互 确定布尔值为真还是假
                if(confirm('确定删除吗?')) {
                    // console.log(id);
                    // this.deleteTodo(id)
                    // 采用全局事件总线方法改为:
                    // this.$bus.$emit('deleteTodo',id)
                    // 采用消息订阅与发布方法此处改为:
                    pubsub.publish('deleteTodo',id)
                }
            },
            // 编辑
            handleEdit(todo) {
                // todo.isEdit = true // 此写法可以改值 但没有getter和setter
                // 利用列表渲染中的vue.set
                // this.$set(todo,'isEdit',true)
                // 第一次加上isEdit属性 后面无需再加 因此使用if语句
                // 如果todo身上有isEdit 直接改 如果todo身上没有isEdit 先添加此属性再赋值
                if('isEdit' in todo) {
                    todo.isEdit = true
                } else {
                    this.$set(todo,'isEdit',true)
                }
                this.$nextTick(function() {
                    // nextTick指定的回调 会在dom节点更新完毕后执行
                    this.$refs.inputTitle.focus() // 获取焦点
                }) 
            },
            // 失去焦点回调(真正执行修改逻辑)
            handleBlur(todo,e){
                todo.isEdit = false
                if(!e.target.value.trim()) return alert('输入不能为空!') // trim()函数用于删除字符串的头尾空白符
                this.$bus.$emit('updateTodo',todo.id,e.target.value)
            }
        }
    }
</script>

<style scoped>
    /*item*/
    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 input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none; 
        margin-top: 3px; 
        margin-left: 5px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: #ddd;
    }

    li:hover button {
        display: block;
    }

    /* 添加动画效果 */
    .todo-enter-active {
        animation: atguigu 0.5s linear;
    }
    .todo-leave-active {
        animation: atguigu 0.5s linear reverse;
    }

    @keyframes atguigu {
        from {
            transform: translateX(100%);
        }
        to {
            transform: translateX(0);
        }
    }
</style>
  • 方法二 修改MyList.vue代码:
<template>
    <ul class="todo-main">
    	<transition-group>
			<MyItem 
            	v-for="todoObj in todos" 
            	:key="todoObj.id" 
            	:todo="todoObj" 
        	/>
		</transition-group>
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue'

    export default {
        name:'MyList',
        components: {MyItem},
        // props:['todos','checkTodo','deleteTodo'] // 接收数据
        // 采用全局事件总线方法改为:
        props:['todos']
    }
</script>

<style scoped>
    /*main*/
    .todo-main {
        margin-left: 0px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0px;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
	/* 添加动画效果 */
    .todo-enter-active {
        animation: atguigu 0.5s linear;
    }
    .todo-leave-active {
        animation: atguigu 0.5s linear reverse;
    }

    @keyframes atguigu {
        from {
            transform: translateX(100%);
        }
        to {
            transform: translateX(0);
        }
    }
</style>

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/125768.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【vite】vite.defineConfig is not a function/npm无法安装第三方包问题

当使用vite命令 npm init vite-app 项目名称时配置 import vue from vitejs/plugin-vueexport default defineConfig({plugins: [vue()] })会报错vite.defineConfig is not a function 还有就是npm下载的时候也会报错 原因vite插件vitejs/plugin-vue和vite版本问题 解决 调…

微带线的ABCD矩阵的推导、转换与级联-Matlab计算实例

微带线的ABCD矩阵的推导、转换与级联-Matlab计算实例 散射参数矩阵有实际的物理意义&#xff0c;但是其无法级联计算&#xff0c;但是ABCD参数和传输散射矩阵可以级联计算&#xff0c;在此先简单介绍ABCD参数矩阵的基本用法。 1、微带线的ABCD矩阵的推导 其他的一些常用的二端…

R-install_miniconda()卸载 | conda命令行报错及解决方法

运行以下代码&#xff0c;突然报错&#xff1a; C:\Users\hp>conda info-e >>>>>>>>>>>>>>>>>>>>>> ERROR REPORT <<<<<<<<<<<<<<<<<<<<&…

开发一条公链多少钱

随着区块链技术的普及和发展&#xff0c;越来越多的企业和个人开始关注公链的开发和建设。那么&#xff0c;开发一条公链到底需要多少钱呢&#xff1f; 首先&#xff0c;我们需要了解公链开发的基本流程和成本构成。一般来说&#xff0c;开发一条公链需要考虑以下几个方面&…

数字政府!3DCAT实时云渲染助推上海湾区数字孪生平台

数字孪生&#xff0c;是一种利用物理模型、传感器数据、运行历史等信息&#xff0c;在虚拟空间中构建实体对象或系统的精确映射&#xff0c;从而实现对其全生命周期的仿真、优化和管理的技术。数字孪生可以应用于各个领域&#xff0c;如工业制造、智慧城市、医疗健康、教育培训…

AVD联网

AVD联网&#xff1a; 解决Android Studio模拟器无法联网_android studio模拟器没有网络-CSDN博客 挺好的&#xff0c;就是访问网站的时候只能用ip&#xff0c;而不能用域名。 AVD设置代理&#xff1a; android studio踩坑记 AVD模拟器代理设置_android studio avd 配置代理-…

《C++ Primer》第8章 IO库

参考资料&#xff1a; 《C Primer》第5版《C Primer 习题集》第5版 8.1 IO类&#xff08;P278&#xff09; 我们目前使用过的 IO 对象&#xff08;cin 、cout&#xff09;都是关联到控制台窗口、操纵 char 数据的。有时&#xff0c;我们需要对命名文件或者 string IO 操作。…

与set和map相关的OJ题练习

一、两个数组的交集 题目链接&#xff1a; 349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 给两个数组&#xff0c;求在数组里面共同出现的部分&#xff0c;就是求两个数组的交集&#xff0c;返回顺序不做要求 解题思路&#xff1a; …

Centos7安装配置中文输入法

Centos7安装配置中文输入法 在安装CentOS时&#xff0c;我们为了方便使用&#xff0c;语言选择了中文&#xff0c;但是我们发现&#xff0c;在Linux命令行或者是浏览器中输入时&#xff0c;我们只能输入英文&#xff0c;无法输入汉字。 来&#xff0c;跟随脚步&#xff0c;设…

第14章,lambda表达式与流处理例题

package 例题;import java.util.List; import java.util.stream.Collectors; import java.util. stream.Stream;public class 例题19 { public static void main(String[] args){List<例题14> list 例题14.get例题14List();//获取公共类的测试数据Stream<例题14>…

GZ038 物联网应用开发赛题第2套

2023年全国职业院校技能大赛 高职组 物联网应用开发 任 务 书 (第2套卷) 工位号:______________ 第一部分 竞赛须知 一、竞赛要求 1、正确使用工具,操作安全规范; 2、竞赛过程中如有异议,可向现场考评人员反映,不得扰乱赛场秩序; 3、遵守赛场纪律,尊重考评人员,…

Flink -- 事件时间 Watermark

1、事件时间&#xff1a; 指的是数据产生的时间或是说是数据发生的时间。 在Flink中有三种时间分别是&#xff1a; Event Time&#xff1a;事件时间&#xff0c;数据产生的时间&#xff0c;可以反应数据真实发生的时间 Infestion Time&#xff1a;事件接收时间 Processing Tim…

AIGC:使用生成对抗网络GAN实现MINST手写数字图像生成

1 生成对抗网络 生成对抗网络&#xff08;Generative Adversarial Networks, GAN&#xff09;是一种非常经典的生成式模型&#xff0c;它受到双人零和博弈的启发&#xff0c;让两个神经网络在相互博弈中进行学习&#xff0c;开创了生成式模型的新范式。从 2017 年以后&#x…

【MATLAB源码-第69期】基于matlab的LDPC码,turbo码,卷积码误码率对比,码率均为1/3,BPSK调制。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 本文章介绍了卷积码、Turbo码和LDPC码。以相同的码率仿真这三种编码&#xff0c;并对比其误码率性能 信源输出的数据符号&#xff08;二进制&#xff09;是相互独立和等概率的&#xff1b; 信道是加性白高斯噪声信道&#…

王道考研--》顺序表课后习题C语言代码实现(冲刺)

考研是许多计算机科学专业学生追求高学历、寻求更好就业前景的途径。在考研过程中&#xff0c;数据结构是一个非常重要的科目&#xff0c;而代码实现题更是其中的难点之一。在这篇文章中&#xff0c;我们将探讨如何通过实现数据结构代码问题来提升考研成绩。无论您是否有编程经…

Excel中功能区的存放位置很灵活,可以根据需要隐藏或显示

在这个简短的教程中,你将找到5种快速简单的方法来恢复Excel功能区,以防丢失,并学习如何隐藏功能区,为工作表腾出更多空间。 功能区是Excel中所有操作的中心点,也是大多数可用功能和命令所在的区域。你觉得功能区占用了你太多的屏幕空间吗?没问题,只需单击鼠标,它就被隐…

Apache APISIX Dashboard 未经认证访问导致 RCE(CVE-2021-45232)漏洞复现

漏洞描述 Apache APISIX 是一个动态、实时、高性能的 API 网关&#xff0c;而 Apache APISIX Dashboard 是一个简单易用的前端界面&#xff0c;用于管理 Apache APISIX。 在 2.10.1 之前的 Apache APISIX Dashboard 中&#xff0c;Manager API 使用了两个框架&#xff0c;并在…

本地生活餐饮视频怎么拍摄能有更多流量?如何批量生产呢?

本地生活近几年特别的火&#xff0c;所以到现在各类内容雷同性也比较高&#xff0c;视频缺少新的创意和玩法&#xff0c;像餐饮店的视频&#xff0c;大部分都是拍顾客进门、拍餐饮店座无虚席的实景……作为用户&#xff0c;其实早就已经看腻了。 今天推荐本地生活餐饮店商家拍…

11.9存储器实验总结(单ram,双ram,FIFO)

实验设计 单端口RAM实现 双端口RAM实现 FIFO实现 文件结构为

Linux nohup后台启动/ 后台启动命令中nohup 、、重定向的使用

文章目录 一、前言二、nohup&#xff08;不挂断&#xff09;简介三、nohup使用3.1、nohup启动3.2、nohup与&&#xff0c;后台运行3.3、nohup与>&#xff0c;日志重定向3.4、nohup后台启动-综合使用(推荐)2>&1 3.5、nohup后台启动(不生成日志) 四、查看进程五、知…