组件化编码流程 案例演示

  • 实现静态组件: 抽取组件,使用组件实现静态页面效果

    • 把js剔除,先保证页面正常的显示效果(比如css正常)
  • 展示动态数据

    • 数据的类型,名称是什么
    • 数据保存在哪个组件
  • 交互: 从绑定事件监听开始

  • "todo.png"页面组件化结构分析: 三个大组件,再嵌套子组件

- input框 可以命名为'MyHeader'组件(若命名为'Header',就和html <header>标签冲突)

- 备忘录一系列动作,可以命名为'MyList'组件

  - 每一个列表项,可以命名为'MyItem'组件

- 已/未完成,可以命名为'MyFooter'组件
  • 划分基本的组件结构
- components目录下,新建四个组件
  
  - MyHeader.vue
  - MyList.vue
  - MyItem.vue
  - MyFooter.vue

- 每块的内容如下

  <template>
  </template>

  <script>
    export default {
      name:'MyHeader' // 其他组件名称自己填
    }
  </script>

  <style>
  </style>

- 在 App.vue中注册

<template>
  <div id="root">
  
  </div>
</template>

<script>

import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'

export default {
  name: 'App',
  components: { // 注册
    MyHeader,
    MyList,
    MyFooter
  },
}
</script>

  • 把整体的html结构拆分后,各个组件代码如下
- App.vue

<template>
  <div id="root">

    <div class="todo-container">
        <div class="todo-wrap">
          <MyHeader/> <!--应用三个组件-->
          <MyList/>
          <MyFooter/>
        </div>
    </div>
  
  </div>
</template>

<script>

import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'

export default {
  name: 'App',
  components: {
    MyHeader,
    MyList,
    MyFooter
  },
}
</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-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="请输入你的任务名称,按回车键确认"/>
   </div>
</template>

<script>
  export default {
    name:'MyHeader'
  }
</script>

<style>
  /*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>

- MyList.vue

<template>
  <!--列表-->
  <ul class="todo-main"> 
    <MyItem/>
  </ul>
</template>

<script>
  import MyItem from './MyItem.vue'
  export default {
    name:'MyList',
    components:{MyItem}
  }
</script>

<style>
  /*list*/
  .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.vue

<template>
  <li> <!--列表项-->
    <label>
      <input type="checkbox"/>
      <span>打篮球</span>
    </label>
    <button class="btn btn-danger">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem'
  }
</script>

<style>
  /*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;
  }   
</style>

- MyFooter.vue

<template>
  <div class="todo-footer">
      <label>
        <input type="checkbox" />
      </label>
      <span>
        <span>已完成1</span><span>/全部3</span>
      </span>
      <button class="btn btn-danger">清除已完成任务</button>
  </div>
</template>

<script>
  export default {
    name:'MyFooter'
  }
</script>

<style>
  /*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;
   }

   .todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
   }

   .todo-footer button {
  float: right;
  margin-top: 5px;
   }
</style>

展示动态数据

  • 先把list列表的数据,放在MyList组件
### MyList.vue
<template>
  <ul class="todo-main">
    <!--渲染子项数据,并把子项的数据传递给子组件-->
    <MyItem  v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" />
  </ul>
</template>

<script>
  import MyItem from './MyItem.vue'
  export default {
    name:'MyList',
    data(){
      return {
        todos:[ // 准备数据
          {id:'001',title:'打篮球',done:true},
          {id:'002',title:'跑步',done:false},
          {id:'003',title:'健身',done:true},
        ]
      }
    },
    components:{MyItem}
  }
</script>

<style>
  ......
</style>


### MyItem.vue
<template>
  <li>
    <label>
      <!--渲染数据-->
      <input type="checkbox" :checked="todo.done"/>
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" style="display: none;">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem',
    props:['todo'] // 接收MyList组件传过来的子项数据
  }
</script>

<style>
  ......
</style>


- 至此,列表数据的渲染,暂告一段落

  • 表单收集用户数据的两种方法
- v-model

  <input type="text" v-model="title"/>
  ......
  <script>
  export default {
    name:'MyHeader',
    data(){
      return {
        title:'' // 收集用户数据
      }
    },
  }
</script>

- 键盘事件: @keyup.enter="add" ,再自定义add方法获取用户输入的数据
  
  <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
  ......
  <script>
  export default {
    name:'MyHeader',
    methods:{
      add(e){
        console.log(e.target.value) // 获取用户输入的数据
      }
    }
  }
</script>

  • 将用户的输入包装成一个对象

    • 先安装"uuid轻量版": npm i nanoid
<script>
  import {nanoid} from 'nanoid'
  export default {
    name:'MyHeader',
    ......
    methods:{
      add(e){
        // 包装对象
        const todoObj = {id:nanoid(),title:e.target.value,done:false}
      }
    }
  }
</script>
  • 把包装好的对象,发给 MyList组件

    • 要实现兄弟组件之间的通讯,以目前掌握的知识,还做不到(兄弟组件传值,,目前不可以)
    • 所以我们把数据放到 App.vue来处理,让App传给MyList组件(父子组件传值,可以)
### MyList.vue
<template>
  <ul class="todo-main">
    <MyItem  v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" />
  </ul>
</template>

<script>
  import MyItem from './MyItem.vue'
  export default {
    name:'MyList',
    data(){
      return {
        // 注释掉
        // todos:[
        //  {id:'001',title:'打篮球',done:true},
        //  {id:'002',title:'跑步',done:false},
        //  {id:'003',title:'健身',done:true},
        // ]
      }
    },
    components:{MyItem},
    props:['todos'] // 接收 App.vue传过来的值(遍历完后传给子组件)
  }
</script>

### APP.vue
<template>
  <div id="root">

    <div class="todo-container">
        <div class="todo-wrap">
          <MyHeader />
          <MyList :todos="todos" /> <!--传值-->
          <MyFooter/>
        </div>
    </div>
  
  </div>
</template>

<script>

// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'

export default {
  name: 'App',
  data(){
    return {
      todos:[ // 数据放在这里
        {id:'001',title:'打篮球',done:true},
        {id:'002',title:'跑步',done:false},
        {id:'003',title:'健身',done:true},
      ]
    }
  },
  components: {
    MyHeader,
    MyList,
    MyFooter
  },
  methods:{
   ......
  }
}
</script>

  • 在 App.vue中,传一个函数(可以携带参数)给 MyHeader 组件
    MyHeader组件收集完用户的数据,打包成一个obj,当成参数回传给该函数即可
### App.vue
<template>
  <div id="root">
  
    <div class="todo-container">
        <div class="todo-wrap">
            <MyHeader :receive="receive" /> <!--传一个函数对象过去-->
            <MyList :todos="todos" />
            <MyFooter/>
        </div>
    </div>
  </div>
</template>

<script>
// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'

export default {
  name: 'App',
  data(){
    ......
  },
  components: {
    MyHeader,
    MyList,
    MyFooter
  },
  methods:{
    receive(x){ // 用参数x来接收'包装对象'
      // console.log('我是App组件,我收到了',x)
      this.todos.unshift(x) // 把收到的包装对象,加入列表
    }
  }
}
</script>

### MyHeader.vue
<template>
  <div class="todo-header">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
   </div>
</template>

<script>
  import {nanoid} from 'nanoid'
  export default {
    name:'MyHeader',
    data(){
      return {
        title:''
      }
    },
    props:['receive'], // 接收
    methods:{
      add(e){
        const todoObj = {id:nanoid(),title:e.target.value,done:false}
        this.receive(todoObj); // 传值
        e.target.value = ''; // 清空输入框
      }
    }
  }
</script>

<style>
  ......
</style>


- 现在,可以测试效果了

  • 把 MyHeader 组件的输入逻辑小改一下(换成v-model,避免操作DOM[e.target.value就是操作DOM])
### MyHeader.vue
<template>
  <div class="todo-header">
    <!-- <input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/> -->
    <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
   </div>
</template>

<script>
  import {nanoid} from 'nanoid'
  export default {
    name:'MyHeader',
    data(){
      return {
        title:'' // 占坑
      }
    },
    props:['addTodo'],
    methods:{
      add(e){
        if(!this.title.trim()) return alert('输入不能为空');
        const todoObj = {id:nanoid(),title:e.target.value,done:false}
        this.addTodo(todoObj);
        // e.target.value = '';
        this.title = ''; // 修改成这样
      }
    }
  }
</script>

<style>
  ......
</style>

组件化编程重要思想

  • 数据存放在哪一个组件,该数据对应的增,删,改,查方法就写在该组件

勾选框处理

  • 思路: 获取该列表项的id,通知App.vue修改 done属性值,取反即可

    • App组件(爷爷)=>MyList组件(父亲)=>MyItem组件(孙子)

### App.vue
<template>
  <div id="root">
   
  <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader :addTodo="addTodo" />
        <!--传函数对象给MyList组件-->
        <MyList :todos="todos" :checkTodo="checkTodo" />
        <MyFooter/>
      </div>
  </div>
  
  </div>
</template>

<script>


// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'

export default {
  name: 'App',
  data(){
    return {
      todos:[
        {id:'001',title:'打篮球',done:true},
        {id:'002',title:'跑步',done:false},
        {id:'003',title:'健身',done:true},
      ]
    }
  },
  components: {
    ......
  },
  methods:{
    
    addTodo(x){
      ......
    },
    // 定义好函数以后,给孙组件调用
    checkTodo(id){
      this.todos.forEach((todo)=>{
        if(todo.id==id) todo.done=!todo.done // 取反
      })
    }
  }
}
</script>

### MyList.vue
<template>
  <ul class="todo-main">
    <MyItem  v-for="todoObj in todos" 
    :key="todoObj.id" 
    :todo="todoObj"
    :checkTodo="checkTodo" /> <!--传给子组件-->
  </ul>
</template>

<script>
  import MyItem from './MyItem.vue'
  export default {
    name:'MyList',
    data(){
      ......
    },
    components:{MyItem},
    props:['todos','checkTodo'] // 被动接收 checkTodo
  }
</script>

<style>
  ......
</style>

### MyItem.vue
<template>
  <li>
    <label>
                                                <!--点击事件处理勾选的逻辑-->
                                                <!--这里使用@change一模一样效果-->
      <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" style="display: none;">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem',
    props:['todo','checkTodo'], // 接收父组件传过来的函数对象...
    methods:{
      handleCheck(id){
        // console.log(id)
        this.checkTodo(id) // 调用爷组件函数
      }
    }
  }
</script>

- 测试勾选效果成功~

- 注意事项:这里还有一个简单粗暴的实现方式,但不推荐这么做
  因为会修改props配置项传过来的值(vue不推荐,vue只希望读,而不去修改)

  - 这种处理方式修改了props值,但是vue不会警告(vue只浅层检测对象整体变化才会警告)
    对于对象的部分属性修改,vue监测不到

<template>
  <li>
    <label>
      <!-- <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/> -->
      <!-- <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/> -->
                                  <!--利用v-model双向绑定,简单粗暴实现-->
      <input type="checkbox" :checked="todo.done" v-model="todo.done" />
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" style="display: none;">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem',
    props:['todo','checkTodo'],
    methods:{
      handleCheck(id){
        // console.log(id)
        this.checkTodo(id)
      }
    }
  }
</script>


删除列表项(逻辑和勾选类似)

  • 思路: 利用filter()过滤掉被删除的id,展示其他列表项即可
### app.vue
<template>
  <div id="root">
    
  <div class="todo-container">
      <div class="todo-wrap">
      <MyHeader :addTodo="addTodo" />                 <!--传给MyList-->
      <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" />
          <MyFooter/>
      </div>
  </div>
  
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import School from './components/School.vue'
import Student from './components/Student.vue'

// FirstDemo
import MyHeader from'./components/FirstDemo/MyHeader.vue'
import MyList from'./components/FirstDemo/MyList.vue'
import MyFooter from'./components/FirstDemo/MyFooter.vue'

export default {
  name: 'App',
  data(){
    return {
      todos:[
        {id:'001',title:'打篮球',done:true},
        {id:'002',title:'跑步',done:false},
        {id:'003',title:'健身',done:true},
      ]
    }
  },
  components: {
  MyHeader,
  MyList,
  MyFooter

  },
  methods:{
   
    addTodo(x){
      ......
    },
    checkTodo(id){
     ......
    },
    deleteTodo(id){
      // 利用过滤
      this.todos = this.todos.filter(todo=>todo.id!==id)
    }
  }
}
</script>

### MyList(被动接收)
<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',
    data(){
      return {
       ......
      }
    },
    components:{MyItem},
    props:['todos','checkTodo','deleteTodo'] // 被动接收
  }
</script>


### MyItem
<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
      <span>{{todo.title}}</span>
    </label>                        <!--删除的逻辑-->
    <button class="btn btn-danger" @click="handDelete(todo.id)">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem',
    props:['todo','checkTodo','deleteTodo'],
    methods:{
      handleCheck(id){
        this.checkTodo(id)
      },
      handDelete(id){
        if(confirm('确定删除吗?')){
          // console.log(id)
          this.deleteTodo(id) // 调用爷组件的逻辑
        }
        
      }
    }
  }
</script>

- 测试效果成功~

MyFooter组件功能实现

  • 实现 已完成xxx/全部yyy

- 思路: App组件把todos传给MyFooter组件

    - 全部: todos.length
    - 已完成: 定义一个变量i,遍历每一项,只有todo.done=true,i自加

### App.vue
......
<div class="todo-container">
    <div class="todo-wrap">
        <MyHeader ... />
        <MyList ... />
        <MyFooter :todos="todos" /> <!--传值-->
    </div>
</div>

### MyFooter.vue
......
<template>
    <div class="todo-footer">
        <label>
            ......
        </label>
        <span><!--用计算属性返回最终的结果-->            <!--全部数量-->
            <span>已完成{{doneTotal}}</span><span>/全部{{todos.length}}</span>
        </span>
        ......
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            doneTotal(){
                let i = 0; 
                this.todos.forEach((todo)=>{
                    if(todo.done == true)i++
                })
                return i;
            }
        }
    }
</script>

  • 计算'已完成'时,我们可以换一种'高大上'的写法

    • arr.reduce: 数组条件统计
### MyFooter.vue
......
<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            doneTotal(){
                // let i = 0;
                // this.todos.forEach((todo)=>{
                //  if(todo.done == true)i++
                // })
                // return i;
                return this.todos.reduce((pre,obj)=>{return pre + (obj.done? 1 : 0)},0)
            }
        }
    }
</script>
  • 实现'全选/全不选'

    • 当用户勾选所有的列表子项时,自动勾选'全选'
<template>
    <div class="todo-footer">
        <label>                     <!--使用计算属性-->
            <input type="checkbox" :checked="isAll"/>
        </label>
        <span>                                       <!--这里改造成'计算属性'-->
            <span>已完成{{doneTotal}}</span><span>/全部{{total}}</span>
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            total(){
                return this.todos.length
            },
            doneTotal(){
                return this.todos.reduce((pre,obj)=>{return pre + (obj.done? 1 : 0)},0)
            },
            isAll(){ // 如果子项数量和子项总数相等,那么就是true,从而实现'全选'
                return this.total === this.doneTotal && this.total > 0
            }
        }
    }
</script>

全选/全不选 功能实现

  • 场景分析

    • 当用户勾选子项的时候,若全部都勾选了,那么 checkbox 就全选

    • 当点击 checkbox 时,实现子项的'全选'/'全不选'

### MyFooter.vue
<template>
    <div class="todo-footer">
        <label>                     <!--关注之处-->
            <input type="checkbox" :checked="isAll" @change="checkAll" />
        </label>
        <span>
            <span>已完成{{doneTotal}}</span><span>/全部{{total}}</span>
        </span>
        <button class="btn btn-danger">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos','checkAllTodo'], // 接收
        computed:{
            total(){
                return this.todos.length
            },
            doneTotal(){
                return this.todos.reduce((pre,obj)=>{return pre + (obj.done? 1 : 0)},0)
            },
            isAll(){ // checkbox什么自动勾?满足如下条件就勾
                return this.total === this.doneTotal && this.total > 0
            }
        },
        methods:{
            checkAll(e){
                // console.log(e.target.checked)
                // 获取checkbox的状态值,传给爷组件处理
                this.checkAllTodo(e.target.checked)
            },
        }
    }
</script>


### App.vue
......
<div class="todo-container">
    <div class="todo-wrap">
        <MyHeader ...... />
        <MyList ...... />
                                <!--传函数给孙组件调用-->
        <MyFooter :todos="todos" :checkAllTodo="checkAllTodo"/>
    </div>
</div>

......
methods:{
     
      addTodo(x){
          ......
      },
      checkTodo(id){
          ......
      },
      deleteTodo(id){
          ......
      },
      checkAllTodo(done){
          this.todos.forEach((todo)=>{
              todo.done = done // 遍历修改done值
          })
      }
  }

  • 另一种实现 checkbox 全选/全不选 的方式,使用 v-model
<template>
    <div class="todo-footer">
        <label>
            <!-- <input type="checkbox" :checked="isAll" @change="checkAll" /> -->
                                <!--使用v-model实现读取/输出-->
            <input type="checkbox" v-model="isAll" />
        </label>
        ......
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos','checkAllTodo'],
        computed:{
            total(){
                ......
            },
            doneTotal(){
               ......
            },
            // isAll(){
            //  return this.total === this.doneTotal && this.total > 0
            //  // return this.total === this.doneTotal
            // }
            isAll:{
                get(){
                    // 把之前逻辑搞过来即可
                    return this.total === this.doneTotal && this.total > 0
                },
                set(value){ // value就是用户选中/未选中的值
                    return this.checkAllTodo(value)
                }
            }
        },
        // methods:{
        //    checkAll(e){
                // console.log(e.target.checked)
        //        this.checkAllTodo(e.target.checked)
        //    },
        // }
    }
</script>

清除已完成任务

  • 思路分析: 爷组件把obj.done为true的子项过滤出来,取反即可
    孙组件使用按钮点击事件,调用这段逻辑即可
### App.vue
<div class="todo-container">
    <div class="todo-wrap">
        <MyHeader ...... />
        <MyList ...... />
                            <!--传参-->
        <MyFooter ...... :clearTodoDone="clearTodoDone" />
    </div>
</div>
......
methods:{
      ......
      clearTodoDone(){
          this.todos = this.todos.filter((todo)=>{
              return !todo.done // 过滤取反
          })
      }
  }

### MyFooter.vue
......
<template>
    <div class="todo-footer">
        ......                          <!--绑定事件-->
        <button class="btn btn-danger" @click="clearDone">清除已完成任务</button>
    </div>
</template>
......
<script>
    export default {
        name:'MyFooter',
        props:['todos','checkAllTodo','clearTodoDone'], // 接收
        computed:{
            total(){
                ......
            },
            doneTotal(){
                ......
            },
    
            isAll:{
                ......
            }
        },
        methods:{
            checkAll(e){
                ......
            },
            clearDone(){
                this.clearTodoDone(); // 调用
            }
        }
    }
</script>

总结

  • 组件化编码流程

    • 拆分静态组件: 组件要按照功能点拆分,命名不要与html元素冲突

    • 实现动态组件: 考虑好数据存放的位置,数据是一个组件在用,还是多个组件在用:

      • 一个组件在用: 放在组件自身即可
      • 多个组件在用: 放在他们共同的父组件上(状态提升)
    • 实现交互: 从绑定事件开始

  • props适用于:

    • 父组件 ==> 子组件通信
    • 子组件 ==> 父组件通信(要求父先给子一个函数)
  • 使用 "v-model" 要切记: v-model 绑定的值不能是 props传过来的值(vue建议不可修改)

  • props传过来的若是对象类型的值,修改对象中的属性vue不会报错,但不推荐这样做

浏览器本地存储

  • 保存数据到浏览器

  • 读取浏览器存储的数据

  • 删除浏览器存储的数据

  • demo如下

    ......
    <body>
        <h2>浏览器存储Demo</h2>
        <button type="button" onclick="saveData()">保存数据</button>
        <button type="button" onclick="readData()">读取数据</button>
        <button type="button" onclick="delData()">删除数据</button>
    
        <script type="text/javascript">
            function saveData(){
                // 可以省略windows对象
                localStorage.setItem('name','JimGreen')
            };
            function readData(){
                console.log(localStorage.getItem('name'))
            };
            function delData(){
                localStorage.removeItem('name')
            }
        </script>
    </body>
    
  • 注意数据类型

    • 比如 Number会被转成 String
    • 存储对象类型的数据时,注意序列化
......
<script type="text/javascript">
    var obj = {name:'Kate Green',age:18}
    function saveData(){
        localStorage.setItem('name','JimGreen');
        localStorage.setItem('age',666); // 666被转换成字符串
        localStorage.setItem('obj',JSON.stringify(obj)); // 序列化对象
    };
    function readData(){
        ......
    };
    function delData(){
        ......
    }
</script>
  • 同样的,读取数据的时候,也要注意将对象字符串转换成js对象

    ......
    <script type="text/javascript">
        var obj = {name:'Kate Green',age:18}
        function saveData(){
            ......
            localStorage.setItem('obj',JSON.stringify(obj));
        };
        function readData(){
            console.log(localStorage.getItem('name'))
            console.log(localStorage.getItem('age'))
            console.log(localStorage.getItem('obj')) // 字符串对象
            var personObj = localStorage.getItem('obj')
            console.log(JSON.parse(personObj)) // js对象
        };
        function delData(){
            ......
        }
    </script>
    
  • 清空所有数据

    ......
    function clearAllData(){
    	localStorage.clear()
    }
    

sessionStorage 的API是一模一样的,不再演示(浏览器关闭,会话就消失)

WebStorage 小结

  • 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)

  • 浏览器端通过"Windows.sessionStorage"和"Windows.localStorage"属性来实现本地存储机制

  • 相关API

    - xxx.setItem(key,value);
    - xxx.getItem(key);
    - xxx.removeItem(key);
    - xxx.clear()
    
  • 备注:

    • SessionStorage存储的内容会随着浏览器窗口关闭而消失
    • LocalStorage存储的内容,需要手动清除才会消失
    • xxx.getItem(yyy),如果yyy对应的value获取不到,该API会返回 null
    • JSON.parse(null)的结果依然是null

自定义事件

  • 作用: 实现组件之间的数据传输(子传父)

    - 之前的实现方式: 父组件传给子组件一个函数类型的props
    
  • demo演示

    ### app.vue
    <template>
      <div id="root">
    		<h1>{{msg}}</h1>
    		<!--之前的写法-->
    		<MySchool :getSchoolName="getSchoolName"></MySchool>
    		<!--绑定peiqi事件到demo回调函数-->
    		<MyStudent v-on:peiqi="demo"></MyStudent>
      </div>
    </template>
    
    <script>
    
    // DefineEvent
    import MySchool from './components/DefineEvent/MySchool.vue'
    import MyStudent from './components/DefineEvent/MyStudent.vue'
    
    
    export default {
      name: 'App',
      data(){
    	  return {
    		  msg:'Welcome!'
    	  }
      },
      components: {
        MySchool, // 注册两个组件
    	MyStudent
      },
      methods:{
    	getSchoolName(name){
    		console.log('app收到了: ',name) // 之前的写法
    	},
    	demo(name){ // 接收子组件传过来的name参数
    		console.log('app的demo方法被调用了',name)
    	}
      }
    }
    </script>
    
    <style>
    	......
    </style>
    
    ### MyStudent.vue
    <template>
    	<div class="demo">
    		<h3>名字:{{studentName}}</h3>
    		<h3>性别:{{studentSex}}</h3>
    		<!--绑定点击事件-->
    		<button type="button" @click="sendStudentName">发送学生名字</button>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Student',
    		data(){
    			return {
    				studentName:'Jim Green',
    				studentSex:'male'
    			}
    		},
    		methods:{
    			sendStudentName(){
    				// 通过'$emit(自定义事件名,*参数)'传参
    				this.$emit('peiqi',this.studentName)
    			}
    		}
    	}
    </script>
    
    <style scoped>
    	......
    </style>
    
    
  • 自定义事件小结

    - 子组件通过'$emit(自定义事件名,*参数)' 自定义事件名并传参
    - 父组件在子组件标签中,通过v-on接收子组件的'自定义事件名称',通过自定义demo函数(回调函数)来接收其它参数,并处理逻辑
    	<MyStudent v-on:peiqi="demo"></MyStudent>
    
  • 自定义事件的另一种写法,通过定义'ref'属性实现

    ### App.vue
    <template>
      <div id="root">
    		<h1>{{msg}}</h1>
    		<MySchool :getSchoolName="getSchoolName"></MySchool>
    		<!-- <MyStudent v-on:peiqi="demo"></MyStudent> -->
    		<!--使用ref属性标识MyStudent组件-->
    		<MyStudent ref="student"></MyStudent>
      </div>
    </template>
    
    <script>
    
    // DefineEvent
    import MySchool from './components/DefineEvent/MySchool.vue'
    import MyStudent from './components/DefineEvent/MyStudent.vue'
    
    
    export default {
      name: 'App',
      data(){
    	  ......
      },
      components: {
        MySchool,
    	MyStudent
      },
      methods:{
    	getSchoolName(name){
    		......
    	},
    	getStudentName(name){
    		console.log('app的getStudentName方法被调用了',name)
    	}
      },
      mounted(){ // 组件挂载完立即执行
      		              // 接收子组件绑定的自定义事件 $on('子组件自定义事件',回调函数)
    	  this.$refs.student.$on('peiqi',this.getStudentName)
      }
    }
    </script>
    
    <style>
    	......
    </style>
    
    
  • 注意事项:若想让自定义事件只执行一次,可以这么写(使用'$once')

    ### app.vue
    ......
    mounted(){
    					// 变更之处
    	  this.$refs.student.$once('peiqi',this.getStudentName)
      }
    
    ### app.vue
    <template>
      <div id="root">
    		<h1>{{msg}}</h1>
    		<MySchool :getSchoolName="getSchoolName"></MySchool>
    		<!--之前的写法,用".once"也可以-->
    		<MyStudent @peiqi.once="getStudentName"></MyStudent>
    		<!-- <MyStudent v-on:peiqi="getStudentName"></MyStudent> -->
    		<!-- <MyStudent ref="student"></MyStudent> -->
      </div>
    </template>
    
  • 涉及到子组件传递多个参数的时候,父组件的回调函数可以这么设计

    ### MySchool.vue
    ......
    methods:{
        sendSchoolName(){
        							// 传递多个参数
        	this.getSchoolName(this.schoolName,111,222,333)
        }
    }
    
    ### App.vue
    ......
    methods:{
    				// 通过'...params'方式接收多个参数
    	getSchoolName(name,...params){
    								// params是一个list
    		console.log('app收到了: ',name,params)
    	},
    	......
      },
    
  • 自定义事件'this坑'演示(app接收子组件传过来的数据并展示)

    ### App.vue
    <template>
      <div id="root">
    		<h1>{{msg}}--{{title}}</h1>
    		<MySchool ......>
    		<!--使用ref方式-->
    		<MyStudent ref="student"></MyStudent>
      </div>
    </template>
    
    <script>
    
    // DefineEvent
    import MySchool from './components/DefineEvent/MySchool.vue'
    import MyStudent from './components/DefineEvent/MyStudent.vue'
    
    
    export default {
      name: 'App',
      data(){
    	  ......
      },
      components: {
        MySchool,
    	MyStudent
      },
      methods:{
    	getSchoolName(name,...params){
    		......
    	},
    	// StudentName(name){
    	// nsole.log('app的getStudentName方法被调用了',name)
    	// is.title = name
    	// }
      },
      mounted(){
    	  // 把回调函数写在里面,结果无效果,this指向子组件MyStudent
    	  this.$refs.student.$on('peiqi',function showName(name){
    		  console.log(name);
    		  console.log(this); // this指向子组件
    		  this.title=name; // 子组件没有title,所以赋值不会生效
    	  })
      }
    }
    </script>
    
    <style>
    	#root {
    		height: 400px;
    		background: red;
    	}
    </style>
    
    
  • this坑解决办法:使用 箭头函数 代替 普通函数 写法即可

    ### App.vue
    ......
    mounted(){
    	  // this.$refs.student.$once('peiqi',this.getStudentName)
    	  // this.$refs.student.$on('peiqi',function showName(name){
    		 //  console.log(name);
    		 //  console.log(this);
    		 //  this.title=name;
    	  // })
    	  this.$refs.student.$on('peiqi',(name)=>{
    		  console.log(name);
    		  console.log(this); // 指向app组件
    		  this.title=name; // 赋值成功
    	  })
      }
    
  • 给子组件绑定'@click事件',该事件会生效吗?答案是不会生效(vue不会当成原生的click事件,故不生效)

    ### App.vue
    <template>
      <div id="root">
    		......
    								<!--绑定click事件-->
    		<MyStudent ref="student" @click="show"></MyStudent>
      </div>
    </template>
    ......
    methods:{
    	getSchoolName(name,...params){
    		......
    	},
    	getStudentName(name){
    		......
    	},
    	show(){
    		alert('show message ...') // 写点逻辑,点击子组件区域块,该事件不会被触发
    	}
      }
    
  • 解决办法,加上'.native'即可

    ......
    <div id="root">
    		......
    								<!--增加之处-->
    		<MyStudent ref="student" @click.native="show"></MyStudent>
      </div>
    

自定义事件总结

- 作用: 一种组件间通信的方式,适用于: 子组件 ===> 父组件
- 使用场景:

	- 若子组件想给父组件传递数据,那么在父组件中给子组件绑定自定义事件(该事件的回调在父组件中)
	
- 两种绑定方式

	- 在父组件中, <Demo @peiqi="test"> 或 <Demo v-on:peiqi="test">
	
	- 在父组件中
		<Demo ref="demo">
		......
		mounted(){
			this.$refs.xxx.$on('peiqi',this.test)
		}
	- 若想让自定义事件只触发一次,可以使用'once'修饰符或者'$once'方法

- 触发自定义事件: this.$emit('peiqi',数据参数)

- 解绑自定义事件: this.$off('peiqi')

- 组件上也可以绑定原生DOM事件,需要使用'native'修饰符

- 注意事项: 通过"this.$refs.xxx.$on('peiqi',回调)"绑定自定义事件时,回调
	- 要么配置在 methods 中
	- 要么使用 箭头函数
  否则 this 指向会出问题

把toDoList案例改造成自定义事件

### App.vue
<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" :clearTodoDone="clearTodoDone" />
	   	</div>
	</div>
	
### MyHeader.vue
<template>
	<div class="todo-header">
		......
	 </div>
</template>

<script>
	import {nanoid} from 'nanoid'
	export default {
		name:'MyHeader',
		data(){
			return {
				title:''
			}
		},
		// props:['addTodo'], // 注释掉
		methods:{
			add(e){
				if(!this.title.trim()) return alert('输入不能为空');
				const todoObj = {id:nanoid(),title:e.target.value,done:false}
				this.$emit('addTodo',todoObj) // 触发自定义事件并传参
				// this.addTodo(todoObj); // 注释掉
				this.title = '';
			}
		}
	}
</script>
  • 注意事项: 如果是传数据,比如list之类的,就不可以使用自定义事件去处理

    ......
    <div class="todo-container">
    	   	<div class="todo-wrap">
    			......
    			<!--这里就不能改,传递数据给子组件-->          <!--这边两个都可以改-->
    	        <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearTodoDone="clearTodoDone" />
    	   	</div>
    </div>
    

    全局事件总线

  • 必须满足两个条件

    • 所有组件都能访问

    • 有'$on','$emit'方法

  • 准备场景(一个School组件和一个Student组件)

    ### School.vue
    <template>
    	<div class="demo">
    		<h3>名称:{{name}}</h3>
    		<h3>地址:{{address}}</h3>
    		
    	</div>
    </template>
    
    <script>
    	
    	export default {
    		name:'School',
    		data(){
    			return {
    				
    				name:'顶尖',
    				address:'厦门',
    			
    			}
    		},
    		mounted(){
    			console.log(Window.x)
    		}
    	}
    	
    	
    </script>
    
    <style scoped>
    	.demo {
    		background-color: #08C63E;
    	}
    </style>
    
    ### Student.vue
    <template>
    	<div class="demo">
    		<h3>名字:{{name}}</h3>
    		<h3>年龄:{{age}}</h3>
    	</div>
    </template>
    
    <script>
    	
    	export default {
    		name:'Student',
    		data(){
    			return {
    				name:'Jim Green',
    				age:20
    			}
    		},
    	}
    </script>
    
    <style scoped>
    	.demo {
    		background-color: skyblue;
    	}
    </style>
    
    
  • 设计思路一,绑定到 Window 上面

    • 所有组件都能访问,但是和事件相关的API无法调用(只有vm/vc实例才有这些API)
    ### main.js
    ......
    Window.x = {a:1,b:2} // 给Window对象绑定一个属性x,并赋值
    
    ### School.vue
    ......
    <script>
    	
    	export default {
    		name:'School',
    		data(){
    			......
    		},
    		mounted(){
    			console.log(Window.x) // 没毛病
    		}
    	}
    	
    	
    </script>
    
    
  • 解决方式:绑定到Vue原型对象上

    • 一般取名为'$bus'(翻译为: 总线/公交车)
    ### main.js
    ......
    Vue.prototype.$bus = {a:1,b:2} // 赋值
    
    ### School.vue
    ......
    <script>
    	
    	export default {
    		......
    		mounted(){
    			console.log(this.$bus) // 收到赋值
    		}
    	}
    	
    	
    </script>
    
    • 测试一下,绑定事件demo
    ### main.js
    ......
    // Window.x = {a:1,b:2}
    new Vue({
      render: h => h(App),
      beforeCreate(){
      	  // 安装全局事件总线	
    	  Vue.prototype.$bus = this // 定义全局事件总线$bus,赋值vm实例
      }
    }).$mount('#app')
    
    ### School.vue
    <template>
    	......
    </template>
    
    <script>
    	
    	export default {
    		name:'School',
    		data(){
    			......
    		},
    		mounted(){
    			// 绑定事件并接收数据参数
    			this.$bus.$on('sendStudentName',(name)=>{
    				console.log('School组件收到了数据',name)
    			})
    		},
    		// 组件被销毁之前,解绑事件(释放事件资源)
    		beforeDestroy(){
    			this.$bus.$off('sendStudentName')
    		}
    	}
    </script>
    
    ### Student.vue
    <template>
    	<div class="demo">
    		<h3>名字:{{name}}</h3>
    		<h3>年龄:{{age}}</h3>
    		<!--点击事件-->
    		<button type="button" @click="sendStudentName">发送学生名字</button>
    	</div>
    </template>
    
    <script>
    	
    	export default {
    		name:'Student',
    		data(){
    			return {
    				name:'Jim Green',
    				age:20
    			}
    		},
    		methods:{
    			sendStudentName(){
    				// 触发事件并传参
    				this.$bus.$emit('sendStudentName',this.name)
    			}
    		}
    	}
    </script>
    
    <style scoped>
    	.demo {
    		background-color: skyblue;
    	}
    </style>
    
    
    
    

全局事件总线(GlobalEventBus)

  • 一种组件间通信的方式,适用于任意组件间通信

  • 安装全局事件总线

    ### main.js
    ......
    new Vue({
      render: h => h(App),
      beforeCreate(){
    	  Vue.prototype.$bus = this // 关注之处
      }
    }).$mount('#app')
    
  • 使用事件总线

    • 接收数据: A组件想接收数据,则在A组件中给'$bus'绑定自定义事件,事件的回调函数留在A组件自身

      绑定事件以后,最好写一个 beforeDestroy() 解绑事件,以便释放资源

      ......
      	data(){
      			......
      		},
      		mounted(){
      			this.$bus.$on('sendStudentName',(name)=>{
      				console.log('School组件收到了数据',name)
      			})
      		},
      		beforeDestroy(){
      			this.$bus.$off('sendStudentName')
      		}
      
    • 提供数据

      this.$bus.$emit('sendStudentName',数据)
      

组件之间通讯方式 review

  • 父传子==>props

  • 子传父==>props/自定义事件/全局事件

  • 改造之前的"toDoList"案例,使用全局事件总线来处理 爷组件==>孙组件 之间传参(跳过父组件)

    ### main.js
    ......
    new Vue({
      ....
      beforeCreate(){
    	  Vue.prototype.$bus = this // 安装全局事件总线
      }
    }).$mount('#app')
    
    ### App.vue
    <template>
      <div id="root">	
    	<div class="todo-container">
    	   	<div class="todo-wrap">
    			......
    			<!-- <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" /> -->
    			<!--不再需要上面那种传数据的方式-->
    			<MyList :todos="todos" />
    	        ......
    	   	</div>
    	</div>
    	
      </div>
    </template>
    
    <script>
    
    
    import MyHeader from'./components/FirstDemo/MyHeader.vue'
    import MyList from'./components/FirstDemo/MyList.vue'
    import MyFooter from'./components/FirstDemo/MyFooter.vue'
    
    
    export default {
      name: 'App',
      data(){
    	  return {
    		  todos:[
    		  	......
    		  ]
    	  }
      },
      components: {
    	MyHeader,
    	MyList,
    	MyFooter
      },
      methods:{
    	  
    	  addTodo(x){
    		  ......
    	  },
    	  checkTodo(id){ // 回调函数
    		  this.todos.forEach((todo)=>{
    			  if(todo.id==id) todo.done=!todo.done
    		  })
    	  },
    	  deleteTodo(id){ // 回调函数
    		  this.todos = this.todos.filter(todo=>todo.id!==id)
    	  },
    	  checkAllTodo(done){
    		  ......
    	  },
    	  clearTodoDone(){
    		  ......
    	  }
      },
      mounted(){ // 绑定事件并指定回调函数
    	this.$bus.$on('checkTodo',this.checkTodo)
    	this.$bus.$on('deleteTodo',this.deleteTodo)
      }
    }
    </script>
    
    <style>
    	......
    </style>
    
    
    ### MyList.vue
    <template>
    	<ul class="todo-main">
    		<!-- <MyItem  v-for="todoObj in todos" 
    		:key="todoObj.id" 
    		:todo="todoObj"
    		:checkTodo="checkTodo" 
    		:deleteTodo="deleteTodo" /> -->
    		<!--不再使用之前的方式去传数据-->
    		<MyItem  v-for="todoObj in todos"
    		:key="todoObj.id" 
    		:todo="todoObj" />
    	</ul>
    </template>
    
    <script>
    	import MyItem from './MyItem.vue'
    	export default {
    		name:'MyList',
    		data(){
    			return {
    				......
    			}
    		},
    		components:{MyItem},
    		// props:['todos','checkTodo','deleteTodo']
    		// 不再接收父组件传过来的数据
    		props:['todos']
    	}
    </script>
    
    ### MyItem.vue
    <template>
    	......
    </template>
    
    <script>
    	export default {
    		name:'MyItem',
    		// props:['todo','checkTodo','deleteTodo'],
    		// 不再接收
    		props:['todo'],
    		methods:{
    			handleCheck(id){
    				
    				// this.checkTodo(id)
    				this.$bus.$emit('checkTodo',id) // 触发事件并传数据参数
    			},
    			handDelete(id){
    				if(confirm('确定删除吗?')){
    					// this.deleteTodo(id)
    					this.$bus.$emit('deleteTodo',id) // 触发事件并传数据参数
    				}
    				
    			}
    		}
    	}
    </script>
    

消息订阅与发布

  • 订阅消息: 消息名

  • 发布消息: 消息内容

  • 安装第三方"发布消息库"(这种库有很多) ==> pubsub-js

    npm i pubsub-js
    
  • 使用方式很简单,一个组件调动api发布,另外一个组件调用api接收即可

  • 测试环境如下: School组件和Student组件(演示Student组件给School组件传数据)

    ### school.vue
    <template>
    	<div class="demo">
    		<h3>名称:{{name}}</h3>
    		<h3>地址:{{address}}</h3>
    	</div>
    </template>
    
    <script>
    	
    	export default {
    		name:'School',
    		data(){
    			return {
    				
    				name:'顶尖',
    				address:'厦门',
    			
    			}
    		},
    	}
    	
    	
    </script>
    
    <style scoped>
    	.demo {
    		background-color: #08C63E;
    	}
    </style>
    
    ### Student.vue
    <template>
    	<div class="demo">
    		<h3>名字:{{name}}</h3>
    		<h3>年龄:{{age}}</h3>
    	</div>
    </template>
    
    <script>
    	export default {
    		name:'Student',
    		data(){
    			return {
    				name:'Jim Green',
    				age:20
    			}
    		},
    	}
    </script>
    
    <style scoped>
    	.demo {
    		background-color: skyblue;
    	}
    </style>
    
    
    ### Student.vue
    <template>
    	<div class="demo">
    		<h3>名字:{{name}}</h3>
    		<h3>年龄:{{age}}</h3>
    		<!--触发逻辑-->
    		<button type="button" @click="sendStudentName">发送学生名字</button>
    	</div>
    </template>
    
    <script>
    	import pubsub from 'pubsub-js' // 导入库
    	
    	export default {
    		name:'Student',
    		data(){
    			return {
    				name:'Jim Green',
    				age:20
    			}
    		},
    		methods:{
    			sendStudentName(){
    				// 发布消息
    				// 传过去的第一个参数是'事件名称',第二个参数才是 name
    				pubsub.publish('hello',this.name)
    			}
    		}
    	}
    </script>
    
    <style scoped>
    	.demo {
    		background-color: skyblue;
    	}
    </style>
    
    ### School.vue
    <template>
    	<div class="demo">
    		<h3>名称:{{name}}</h3>
    		<h3>地址:{{address}}</h3>
    	</div>
    </template>
    
    <script>
    	import pubsub from 'pubsub-js' // 导入库
    	
    	export default {
    		name:'School',
    		data(){
    			return {
    				name:'顶尖',
    				address:'厦门',
    			}
    		},
    		mounted(){
    			// 自定义pubId变量存储 消息ID,为了是销毁组件解绑消息的时候传参
    			// 接收消息
    			this.pubId = pubsub.subscribe('hello',function(msgName,data){
    				console.log('School','收到发布的消息了,收到的数据是',data)
    				console.log('该消息名称是',msgName)
    			})
    		},
    		beforeDestroy(){ // 销毁之前解绑消息
    			pubsub.unsubscribe(this.pubId)
    		}
    	}
    	
    	
    </script>
    
    <style scoped>
    	.demo {
    		background-color: #08C63E;
    	}
    </style>
    
    
  • this坑演示: 当在vue传入第三方库的时候,this的指向就不明确了

    ### Schoo.vue
    ......
    mounted(){
        this.pubId = pubsub.subscribe('hello',function(msgName,data){
            console.log(this) // undefined
        })
    },
    
    - 解决方式: 使用箭头函数/在methods里定义方法
    
    mounted(){
        // this.pubId = pubsub.subscribe('hello',function(msgName,data){
        // 	console.log('School','收到发布的消息了,收到的数据是',data)
        // 	console.log('该消息名称是',msgName)
        // 	console.log(this) // undefined
        // })
        									// 修改成箭头函数形式
        this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
        ......
        console.log(this) // vc实例
        })
    },
    

消息订阅与发布总结(pubsub)

  • 一种组件间通信的方式,适用于任意组件间通信

  • 使用步骤

    • 安装pubsub: npm i pubsub-js

    • 引入: import pubsub from 'pubsub-js'

    • 接收数据: A组件想接收数据,则在A组件中订阅消息,订阅的回调函数留在A组件自身

      methods(){
      	demo(data){......}
      },
      mounted(){
      	this.pubId = pubsub.subscribe('xxx',this.demo) // 订阅消息
      }
      
    • 提供数据: pubsub.publish('xxx',数据),注意第一个参数是消息名称,数据在第二个参数

    • 最好在beforeDestroy写一个取消订阅的逻辑

      mounted(){
          this.pubId = pubsub.subscribe('hello',......)
      },
      beforeDestroy(){ 
      	pubsub.unsubscribe(this.pubId)
      }
      

消息订阅运用到todo案例(改造删除功能)

### MyItem.vue
......
<script>
	import pubsub from 'pubsub-js' // 引入
	export default {
		name:'MyItem',
		props:['todo'],
		methods:{
			handleCheck(id){
				......
			},
			handDelete(id){
				if(confirm('确定删除吗?')){
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id) // 发布消息
				}
				
			}
		}
	}
</script>

### App.vue
<script>
......

import pubsub from 'pubsub-js' // 导入
export default {
  name: 'App',
  data(){
	  return {
		  todos:[
		  	......
		  ]
	  }
  },
  components: {
	MyHeader,
	MyList,
	MyFooter
  },
  methods:{
	
	 ......
	  // deleteTodo(id){
		 //  this.todos = this.todos.filter(todo=>todo.id!==id)
	  // },
	  deleteTodo(_,id){ // 第一个参数是'事件名',但我们不需要,所以使用'_'代替
	  		  this.todos = this.todos.filter(todo=>todo.id!==id)
	  },
	.......
  },
  mounted(){
	  // this.$bus.$on('deleteTodo',this.deleteTodo)
	  pubsub.subscribe('deleteTodo',this.deleteTodo) // 订阅消息
  }
}
</script>

todo案例编辑功能的实现

### MyItem.vue
<template>
	<li>
		<label>
			......
			<!--根据isEdit的值,要么显示input框,要么显示span-->
			<span v-show="!todo.isEdit">{{todo.title}}</span>
			<!--多了一个失去焦点事件,:value和使用v-model是一样的效果-->
			<input type="text" v-show="todo.isEdit" :value="todo.title" @blur="handBlur(todo,$event)" >
		</label>
		......
		<!--自定义btn-edit样式-->
		<!--绑定点击事件,当用户点击'编辑'按钮的时候,'编辑'按钮自动消失(根据需求来)-->
		<button class="btn btn-edit" @click="handEdit(todo)" v-show="!todo.isEdit" >编辑</button>
	</li>
</template>

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'MyItem',
		props:['todo'],
		methods:{
			handleCheck(id){
				......
			},
			handDelete(id){
				......
				}
				
			},
			handEdit(todo){
				// todo.isEdit = true; // 不是响应式属性
				// this.$set(todo,'isEdit',true)
				if(todo.hasOwnProperty('isEdit')){
					todo.isEdit = true;
				}else{
					this.$set(todo,'isEdit',true); // 创造响应式属性
				}
			},
			handBlur(todo,event){
				todo.isEdit = false; 
				if(!event.target.value.trim()) return alert('输入不能为空') // 空值判断
				// console.log(todo.title) // 打篮球
				// console.log(event.target.value) // 打篮球123
				this.$bus.$emit('updateTodo',todo.id,event.target.value) // 使用事件总线传值
			}
		}
	}
</script>

<style>
	......
</style>

### App.vue
<template>
 ......
</template>

<script>

......
import pubsub from 'pubsub-js'
export default {
  name: 'App',
  data(){
	  return {
		  todos:[
		  	......
		  ]
	  }
  },
  components: {
	MyHeader,
	MyList,
	MyFooter
  },
  methods:{
	  
	  ......
	  updateTodo(id,title){ // 接收两个参数
		  this.todos.forEach(todo=>{
			  if(todo.id==id) todo.title=title
		  })
	  }
  },
  mounted(){
	  ......
	  this.$bus.$on('updateTodo',this.updateTodo) // 绑定事件并指定回调
  }
}
</script>

<style>
	/*base*/
   ......
   
   .btn-edit { /*自定义样式*/
   	 color: #fff;
   	 background-color: skyblue;
   	 border: 1px solid #bd362f;
	 margin-right: 5px;
   }

   ......
</style>


todo案例实现功能: 当点击'编辑'按钮的时候,input框立即获得焦点

### MyItem.vue
<template>
	<li>
		<label>
			......
			<input type="text" 
			v-show="todo.isEdit" 
			v-model="todo.title" 
			@blur="handBlur(todo,$event)"
			ref="inputTitle" > <!--增加ref属性,以便找到这个元素-->
		......
	</li>
</template>
......
handEdit(todo){
   
    if(......){
        ......
    }else{
        ......
    }
    // 不能直接写,因为input中v-show的影响,元素还没有被展示出来
    // 既然元素还没有被展示出来,获取焦点肯定是行不通的
    // this.$refs.inputTitle.focus()
    setInterval(()=>{
        this.$refs.inputTitle.focus() // 等待一点时间再获取焦点
    },1000)
},
  • 官方推荐使用"$nextTick"(开发中经常用到)

    - 作用: 下一轮DOM元素被更新的时候,再执行里面的逻辑
    - API: 传入一个回调,执行你要的逻辑
    
    ......
    handEdit(todo){
        
        if(......){
            ......
        }else{
            ......
        }
        // this.$refs.inputTitle.focus()
    	// setInterval(()=>{
    	// 	this.$refs.inputTitle.focus()
    	// },1000)
    	
        this.$nextTick(function(){
            this.$refs.inputTitle.focus() // 推荐使用
        })
    },
    
  • 也有开发者这么搞,同样实现功能(vue中就使用vue推荐的...)

    handEdit(todo){
       
        if(......){
            ......
        }else{
            ......
        }
        
        // this.$refs.inputTitle.focus()
        // setInterval(()=>{
        //  this.$refs.inputTitle.focus()
        // },1000)
        
        // this.$nextTick(function(){
        //  this.$refs.inputTitle.focus()
        // })
        
        setInterval(()=>{ // 不传入时间
            this.$refs.inputTitle.focus()
        })
    },