您现在的位置是:首页 >技术杂谈 >使用Vue CLI脚手架网站首页技术杂谈

使用Vue CLI脚手架

阿瞒有我良计15 2024-06-17 10:14:00
简介使用Vue CLI脚手架

章节概述:

  • 使用Vue CLI脚手架
  • 初始化脚手架
  • 分析脚手架结构
  • render函数
  • 修改默认配置
  • ref属性
  • props配置项
  • mixin混入
  • plugin插件
  • scoped样式
  • Todo-List案例
  • WebStorage
  • 自定义事件
  • 绑定
  • 解绑
  • 全局事件总线
  • 消息的订阅与发布
  • $nextTick
  • 过渡与动画

提示(我没有使用markdown编译器,使用老版的使用惯了,教小白一个技巧,可以使用 ctrl+f 快速搜索一下) 

分析脚手架结构

.文件目录
├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   └── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
└── package-lock.json: 包版本控制文件

render函数

  • vue.js 与 vue.runtime.xxx.js的区别:
    • vue.js 是完整版的 Vue,包含:核心功能+模板解析器
    • vue.runtime.xxx.js 是运行版的 Vue,只包含核心功能,没有模板解析器
  • 因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 配置项,需要使用 render函数接收到的createElement 函数去指定具体内容
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    el:'#app',
    // 简写形式
	render: h => h(App),
    // 完整形式
	// render(createElement){
	//     return createElement(App)
	// }
})

修改默认配置

  • vue.config.js 是一个可选的配置文件,如果项目的(和 package.json 同级的)根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载
  • 使用 vue.config.js 可以对脚手架进行个性化定制,详见配置参考 | Vue CLI
module.exports = {
    pages: {
        index: {
            // 入口
            entry: 'src/index/main.js'
        }
    },
  // 关闭语法检查
  lineOnSave:false
}

ref属性:

  1. 被用来给元素或子组件注册引用信息(id的替代者)
  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上获取的是组件实例对象(vc)
  3. 使用方式:
  • 打标识:<h1 ref="xxx"></h1> 或 <School ref="xxx"></School>
  • 获取:this.$refs.xxx

App.vue 

<template>
  <div>
    <h1 v-text="msg" id="title"></h1>
    <h1 v-text="msg" ref="title2"></h1>
    <button ref="btn" @click="showDOM">点我输出上方DOM元素</button>
    <School ref="sch"></School>
    <School id="sch1"></School>
  </div>
</template>

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

export default {
  name: 'App',
  data() {
    return {
      msg: '欢迎学习Vue'
    }
  },
  components: {
    School
  },
  methods: {
    showDOM() {
      console.log(document.getElementById('title'))
      console.log(this.$refs.title2) //真实dom元素
      console.log(this.$refs)
      console.log(this.$refs.btn)//真实dom元素
      console.log(this.$refs.sch)//school组件的实例对象
      console.log(document.getElementById('sch1'))
    }
  }
}
</script>

 School.vue

<template>
  <div id='Demo'>
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址:{{ address }}</h2>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'School',
  data() {
    return {
      name: '尚硅谷',
      address: '北京'
    }
  },
}
</script>

<style>
#Demo {
  background: orange;
}
</style>

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

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

props配置项:

  1. 功能:让组件接收外部传过来的数据
  2. 传递数据:<Demo name="xxx"/>
  3. 接收数据:
  • 第一种方式(只接收):props:['name']
  • 第二种方式(限制数据类型):props:{name:String}
  • 第三种方式(限制类型、限制必要性、指定默认值):
     

props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据

App.vue 

<template>
  <div>
    <Student name="lisi" :age="23" sex="女"></Student>
    <hr>
    <Student name="wangwu" :age="23" sex="男"></Student>
  </div>
</template>

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

export default {
  name: 'App',
  data() {
    return {
      msg: '欢迎学习Vue'
    }
  },
  components: {
    Student
  }
}
</script>

Student.vue

<template>
  <div>
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
    <h2>学生年龄:{{ age }}</h2>
    <h2>学生年龄:{{ msg }}</h2>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Student',
  data() {
    return {
      msg: '我是一个学生',
    }
  },
  //简单声明接收
  // props:['name','age','sex']

  //接收的同时对数据类型进行限制
  // props: {
  //   name: String,
  //   age: Number,
  //   sex: String
  // }

  // 接收的同时对数据类型进行限制 + 指定默认值 + 限制必要性
  props: {
    name: {
      type: String,
      required: true,
    },
    age: {
      type: Number,
      default: 99
    },
    sex: {
      type: String,
      required: true
    }
  }
}
</script>

mixin(混入)

  1. 功能:可以把多个组件共用的配置提取成一个混入对象

  2. 使用方式:

    1. 定义混入:

      const mixin = {
          data(){....},
          methods:{....}
          ....
      }
      
    2. 使用混入:

    • 全局混入:Vue.mixin(xxx)
    • 局部混入:mixins:['xxx']

备注:

  1. 组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”,在发生冲突时以组件优先。

  2. 同名生命周期钩子将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

局部混入: 

App.vue

<template>
  <div>
    <School></School>
    <hr>
    <Student></Student>
  </div>
</template>

<script>
import Student from './components/Student.vue'
import School from "@/components/School";

export default {
  name: 'App',
  data() {
    return {
      msg: '欢迎学习Vue'
    }
  },
  components: {
    Student,
    School
  }
}
</script>

 School.vue

<template>
  <div>
    <h2 @click="showName">学校名称:{{ name }}</h2>
    <h2>学生地址:{{ address }}</h2>
  </div>
</template>

<script>

// eslint-disable-next-line no-unused-vars
import {mixin, mixin2} from "../mixin";

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'School',
  data() {
    return {
      name: '尚硅谷',
      address: '北京'
    }
  },
  mounted() {
    console.log('组件钩子')
  },
  mixins: [mixin, mixin2]
}
</script>

 Student.vue

<template>
  <div>
    <h2 @click="showName">学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
  </div>
</template>

<script>
// eslint-disable-next-line no-unused-vars
import {mixin, mixin2} from "../mixin";

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Student',
  data() {
    return {
      name: 'Tom',
      sex: '男'
    }
  },
  mounted() {
    console.log('组件钩子')
  },
  mixins: [mixin,mixin2]
}
</script>

mixin.js

export const mixin = {
    methods: {
        showName() {
            alert(this.name)
        }
    },
    mounted() {
        console.log('我是混入js的钩子函数,mounted')
    }
}

export const mixin2 = {
    data() {
        return {
            x: 100,
            y: 200
        }
    }
}

运行结果:

全局混入: 

main.js

import Vue from 'vue'
import App from './App.vue'
import {mixin, mixin2} from "@/mixin";

Vue.config.productionTip = false

Vue.mixin(mixin)
Vue.mixin(mixin2)

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

App.vue

<template>
  <div>
    <School></School>
    <hr>
    <Student></Student>
  </div>
</template>

<script>
import Student from './components/Student.vue'
import School from "@/components/School";

export default {
  name: 'App',
  data() {
    return {
      msg: '欢迎学习Vue'
    }
  },
  components: {
    Student,
    School
  }
}
</script>

School.vue

<template>
  <div>
    <h2 @click="showName">学校名称:{{ name }}</h2>
    <h2>学生地址:{{ address }}</h2>
  </div>
</template>

<script>

// eslint-disable-next-line no-unused-vars
import {mixin, mixin2} from "../mixin";

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'School',
  data() {
    return {
      name: '尚硅谷',
      address: '北京'
    }
  },
  mounted() {
    console.log('组件钩子')
  }
}
</script>

Student.vue

<template>
  <div>
    <h2 @click="showName">学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
  </div>
</template>

<script>
// eslint-disable-next-line no-unused-vars
import {mixin, mixin2} from "../mixin";

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Student',
  data() {
    return {
      name: 'Tom',
      sex: '男'
    }
  },
  mounted() {
    console.log('组件钩子')
  }
}
</script>

plugin插件

  1. 功能:用于增强Vue

  2. 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据

  3. 定义插件:

    plugin.install = function (Vue, options) {
            // 1. 添加全局过滤器
            Vue.filter(....)
        
            // 2. 添加全局指令
            Vue.directive(....)
        
            // 3. 配置全局混入
            Vue.mixin(....)
        
            // 4. 添加实例方法
            Vue.prototype.$myMethod = function () {...}
            Vue.prototype.$myProperty = xxxx
        }
    

    4.使用插件:Vue.use(plugin)

代码示例: 

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

School.vue

<template>
  <div>
    <h2>学校名称:{{ name| mySlice }}</h2>
    <h2>学校地址:{{ address }}</h2>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data(){
    return{
      name:'尚硅谷atguigu',
      address:'北京'
    }
  }
}
</script>

Student.vue

<template>
  <div>
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
    <input type="text" v-fbind:value="name">
    <button @click="test">点我测试hello方法</button>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: 'Tom',
      sex: '男'
    }
  },
  methods: {
    test() {
      this.hello()
    }
  }
}
</script>

App.vue

<template>
  <div id="app">
    <school></school>
    <br>
    <student></student>
  </div>
</template>

<script>
import Student from "@/components/Student";
import School from "@/components/School";

export default {
  name: 'App',
  components: {
    School,
    Student
  }
}
</script>

main.js

import Vue from 'vue'
import App from './App.vue'
import plugin from "./plugin";

Vue.config.productionTip = false

Vue.use(plugin, 1, 2, 3)

new Vue({
    render: h => h(App),
}).$mount('#app')

plugin.js

export default {
    install(Vue, x, y, z) {
        console.log(x, y, z)

        //全局过滤器
        Vue.filter('mySlice', function (value) {
            return value.slice(0, 4)
        })

        //定义全局指令
        Vue.directive('fbind', {
            bind(element, binding) {
                element.value = binding.value
            },
            inserted(element) {
                element.focus()
            },
            update(element, binding) {
                element.value = binding.value
            }
        })

        //定义混入
        Vue.mixin({
            data() {
                return {
                    x: 100,
                    y: 200
                }
            }
        })

        //给Vue原型上添加一个方法(vm和vc就能用了)
        Vue.prototype.hello = () => alert('你好啊')
    }
}

运行结果:

scoped样式

  1. 作用:让样式在局部生效,防止冲突
  2. 写法:<style scoped>

scoped样式一般不会在App.vue中使用 

 index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

School.vue

<template>
  <div class="demo">
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址:{{ address }}</h2>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data() {
    return {
      name: '尚硅谷',
      address: '北京'
    }
  }
}
</script>

<style scoped>
.demo {
  background-color: deepskyblue;
}
</style>

Student.vue

<template>
  <div class="demo">
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: 'Tom',
      sex: '男'
    }
  },
}
</script>

<style scoped>
.demo {
  background-color: green;
}
</style>

App.vue

<template>
  <div id="app">
    <div class="class1">
      hello1
      <div class="class2">hello2</div>
    </div>

    <school></school>
    <br>
    <student></student>
  </div>
</template>

<script>
import Student from "@/components/Student";
import School from "@/components/School";

export default {
  name: 'App',
  components: {
    School,
    Student
  }
}
</script>

<style lang="less">
.class1{
  background-color: orange;
  .class2{
    background-color: pink;
  }
}
</style>

main.js

import Vue from 'vue'
import App from './App.vue'


Vue.config.productionTip = false


new Vue({
    render: h => h(App),
}).$mount('#app')

运行结果:

Todo-List案例

  • 组件化编码流程:
    • 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
    • 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
      1. 一个组件在用:放在组件自身即可
      2. 一些组件在用:放在他们共同的父组件上(状态提升)
    • 实现交互:从绑定事件开始
  • props适用于:
    1. 父组件 ==> 子组件 通信
    2. 子组件 ==> 父组件 通信(要求父组件先给子组件一个函数)
  • 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的
  • props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做
     

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader :addTodo="addTodo"/>
        <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
        <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:[
        {id:'001',title:'抽烟',done:false},
        {id:'002',title:'喝酒',done:false},
        {id:'003',title:'烫头',done:false},
      ]
    }
  },
  methods:{
    //添加一个todo
    addTodo(todoObj){
      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 => todo.id !== id)
    },
    //全选or取消勾选
    checkAllTodo(done){
      this.todos.forEach(todo => todo.done = done)
    },
    //删除已完成的todo
    clearAllTodo(){
      this.todos = this.todos.filter(todo => !todo.done)
    }
  }
}
</script>

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

 MyFooter.vue

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" v-model="isAll"/>
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: 'MyFooter',
  props: ['todos', 'checkAllTodo', 'clearAllTodo'],
  computed: {
    doneTotal() {
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    total() {
      return this.todos.length
    },
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        this.checkAllTodo(value)
      }
    }
  },
  methods: {
    clearAll() {
      this.clearAllTodo()
    }
  }
}
</script>

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

MyHeader.vue

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

<script>
import {nanoid} from 'nanoid'

export default {
  name: 'MyHeader',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    add() {
      if (!this.title.trim()) return
      const todoObj = {id: nanoid(), title: this.title, done: false}
      console.log(this)
      this.addTodo(todoObj)
      this.title = ''
    }
  },
  props: ['addTodo']
}
</script>

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

MyItem.vue

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
  </li>
</template>

<script>
export default {
  name:'MyItem',
  props:['todo','checkTodo','deleteTodo'],
  methods:{
    handleCheck(id){
      this.checkTodo(id)
    },
    handleDelete(id,title){
      if(confirm("确定删除任务:"+title+"吗?")){
        this.deleteTodo(id)
      }
    }
  }
}
</script>

<style scoped>
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

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

li:before {
  content: initial;
}

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

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

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

MyList.vue

<template>
  <ul class="todo-main">
    <MyItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        :checkTodo="checkTodo"
        :deleteTodo="deleteTodo"
    />
  </ul>
</template>

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

export default {
  name: 'MyList',
  components: {MyItem},
  props: ['todos', 'checkTodo', 'deleteTodo']
}
</script>

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

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    render: h => h(App),
}).$mount('#app')


运行结果:

WebStorage

存储内容大小一般支持5MB左右(不同浏览器可能还不一样)浏览器端通过Window.sessionStorage和Window.localStorage属性来实现本地存储机制

相关API:

  • xxxStorage.setItem('key', 'value'):该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值
  • xxxStorage.getItem('key'):该方法接受一个键名作为参数,返回键名对应的值
  • xxxStorage.removeItem('key'):该方法接受一个键名作为参数,并把该键名从存储中删除
  • xxxStorage.clear():该方法会清空存储中的所有数据

备注:

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

localStorage.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>localStroage</h2>
<button onclick="saveData()">点我保存一个数据</button>
<button onclick="readData()">点我读取一个数据</button>
<button onclick="deleteData()">点我删除一个数据</button>
<button onclick="clearData()">点我清空一个数据</button>
<script type="text/javascript">
    function saveData() {
        let p = {name: '张三', age: 18}
        localStorage.setItem('msg', 'hello!')
        localStorage.setItem('msg1', 666)
        localStorage.setItem('person', JSON.stringify(p))
    }

    function readData() {
        console.log(localStorage.getItem('msg'));
        console.log(localStorage.getItem('msg1'));
        const result = localStorage.getItem('person')
        console.log(JSON.parse(result))
    }

    function deleteData() {
        localStorage.removeItem('msg')
    }

    function clearData(){
        localStorage.clear()
    }
</script>
</body>
</html>

 sessionStorage.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>sessionStroage</h2>
<button onclick="saveData()">点我保存一个数据</button>
<button onclick="readData()">点我读取一个数据</button>
<button onclick="deleteData()">点我删除一个数据</button>
<button onclick="clearData()">点我清空一个数据</button>
<script type="text/javascript">
    function saveData() {
        let p = {name: '张三', age: 18}
        sessionStorage.setItem('msg', 'hello!')
        sessionStorage.setItem('msg1', 666)
        sessionStorage.setItem('person', JSON.stringify(p))
    }

    function readData() {
        console.log(sessionStorage.getItem('msg'));
        console.log(sessionStorage.getItem('msg1'));
        const result = sessionStorage.getItem('person')
        console.log(JSON.parse(result))
    }

    function deleteData() {
        sessionStorage.removeItem('msg')
    }

    function clearData() {
        sessionStorage.clear()
    }
</script>
</body>
</html>

使用本地存储优化Todo-List:

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader :addTodo="addTodo"/>
        <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
        <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) {
      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 => todo.id !== id)
    },
    //全选or取消勾选
    checkAllTodo(done) {
      this.todos.forEach(todo => todo.done = done)
    },
    //删除已完成的todo
    clearAllTodo() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  },
  watch: {
    todos: {
      deep: true,
      handler(value) {
        localStorage.setItem('todos', JSON.stringify(value))
      }
    }
  }
}
</script>

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

自定义事件

组件的自定义事件:

1.一种组件间通信的方式,适用于:==子组件 > 父组件

2.使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)

3.绑定自定义事件:

        1.第一种方式,在父组件中:<Demo @atguigu="test"/> 或 <Demo v-on:atguigu="test"/>

        2.第二种方式,在父组件中:

<Demo ref="demo"/>
...
mounted(){
    this.$refs.demo.$on('atguigu',data)
}


        3.若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法

4.触发自定义事件:this.$emit('atguigu',数据)

5.解绑自定义事件:this.$off('atguigu')

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

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

绑定

App.vue

<template>
  <div class="app">
    <h1>{{ msg }}</h1>
    <School :getSchoolName="getSchoolName"></School>
    <!--    第一种写法-->
    <Student v-on:atguigu="getStudentName"></Student>
    <!--    第二种写法-->
    <Student @atguigu="getStudentName"></Student>
    <!--    <Student @atguigu.once="getStudentName"></Student>-->
    <!--    第三种写法-->
    <Student ref="student"></Student>
  </div>
</template>

<script>
import School from "@/components/School";
import Student from "@/components/Student";

export default {
  name: 'App',
  components: {Student, School},
  comments: {School, Student},
  data() {
    return {
      msg: "你好啊!"
    }
  },
  methods: {
    getSchoolName(name) {
      console.log('App收到了学校名:', name)
    },
    // getStudentName(name, x, y, z) {
    //   console.log('App收到了学生名:', name, x, y, z)
    // },
    getStudentName(name, ...params) {
      console.log('App收到了学生名:', name, params)
    }
  },
  mounted() {
    // this.$refs.student.$on('atguigu', this.getStudentName)
    setTimeout(() => {
      // this.$refs.student.$on('atguigu', this.getStudentName)
      this.$refs.student.$once('atguigu', this.getStudentName)
    }, 3000)
  }
}

</script>

<style>
.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 {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data() {
    return {
      name: '尚硅谷atguigu',
      address: '北京'
    }
  },
  props: ['getSchoolName'],
  methods: {
    sendSchoolName() {
      this.getSchoolName(this.name)
    }
  }
}
</script>

<style scoped>
.school {
  background-color: skyblue;
  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 {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: '张三',
      sex: '男'
    }
  },
  methods: {
    sendStudentName() {
      //触发Student组件实例身上的atguigu事件
      this.$emit('atguigu', this.name, 666, 888, 900)
    }
  }
}
</script>

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

运行结果:

解绑 

app.vue

<template>
  <div class="app">
    <h1>{{ msg }}</h1>
    <School :getSchoolName="getSchoolName"></School>
    <!--    &lt;!&ndash;    第一种写法&ndash;&gt;-->
    <!--    <Student v-on:atguigu="getStudentName"></Student>-->
    <!--    第二种写法-->
    <Student @atguigu="getStudentName" @demo="m1"></Student>
    <!--    &lt;!&ndash;    第三种写法&ndash;&gt;-->
<!--    <Student ref="student"></Student>-->
  </div>
</template>

<script>
import School from "@/components/School";
import Student from "@/components/Student";

export default {
  name: 'App',
  components: {Student, School},
  comments: {School, Student},
  data() {
    return {
      msg: "你好啊!"
    }
  },
  methods: {
    getSchoolName(name) {
      console.log('App收到了学校名:', name)
    },
    // getStudentName(name, x, y, z) {
    //   console.log('App收到了学生名:', name, x, y, z)
    // },
    getStudentName(name, ...params) {
      console.log('App收到了学生名:', name, params)
    },
    m1(){
      console.log('demo事件被触发了')
    }
  },
  mounted() {
    // this.$refs.student.$on('atguigu', this.getStudentName)
    // setTimeout(() => {
    //   // this.$refs.student.$on('atguigu', this.getStudentName)
    //   this.$refs.student.$once('atguigu', this.getStudentName)
    // }, 3000)
  }
}

</script>

<style>
.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 {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data() {
    return {
      name: '尚硅谷atguigu',
      address: '北京'
    }
  },
  props: ['getSchoolName'],
  methods: {
    sendSchoolName() {
      this.getSchoolName(this.name)
    }
  }
}
</script>

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

Student.vue

<template>
  <div class="student">
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
    <h2>当前求和为:{{ number }}</h2>
    <button @click="add">点我number++</button>
    <button @click="sendStudentName">把学生名给App</button>
    <button @click="unbind">解绑atguigu事件</button>
    <button @click="death">销毁当前Student组件的实例(vc)</button>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: '张三',
      sex: '男',
      number: 0
    }
  },
  methods: {
    add() {
      console.log('add回调被调用了!')
      this.number++
    },
    sendStudentName() {
      //触发Student组件实例身上的atguigu事件
      this.$emit('atguigu', this.name, 666, 888, 900)
      this.$emit('demo')
    },
    unbind() {
      // this.$off('atguigu')//解绑一个自定义事件
      // this.$off(['atguigu', 'demo'])//解绑多个自定义事件
      this.$off()//解绑所有自定义事件
    },
    death() {
      this.$destroy()//销毁当前了Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效。
    }
  }
}
</script>

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

运行结果:

使用@click.native

App.vue

<template>
  <div class="app">
    <h1>{{ msg }},学生姓名是:{{ StudentName }}</h1>
    <School :getSchoolName="getSchoolName"></School>
    <!--    &lt;!&ndash;    第一种写法&ndash;&gt;-->
    <!--    <Student v-on:atguigu="getStudentName"></Student>-->
    <!--    第二种写法-->
    <!--    <Student @atguigu="getStudentName" @demo="m1"></Student>-->
    <!--    &lt;!&ndash;    第三种写法&ndash;&gt;-->
    <Student ref="student" @click.native="show"></Student>
<!--    <Student ref="student" @click="show"></Student>-->
  </div>
</template>

<script>
import School from "@/components/School";
import Student from "@/components/Student";

export default {
  name: 'App',
  components: {Student, School},
  data() {
    return {
      msg: "你好啊!",
      StudentName: ""
    }
  },
  methods: {
    getSchoolName(name) {
      console.log('App收到了学校名:', name)
    },
    getStudentName(name, ...params) {
      console.log('App收到了学生名:', name, params)
      this.StudentName = name
    },
    m1() {
      console.log('demo事件被触发了')
    },
    show() {
      alert("hello")
    }
  },
  mounted() {
    this.$refs.student.$on('atguigu', this.getStudentName) //这三种方法推荐这一种

    // this.$refs.student.$on('atguigu', function (name, ...params) {
    //   console.log('App收到了学生名:', name, params)
    //   console.log(this)//this是Student.vue实例对象
    //   this.StudentName = name
    // })

    // this.$refs.student.$on('atguigu', (name, ...params) => {
    //   console.log('App收到了学生名:', name, params)
    //   console.log(this)//this是Student.vue实例对象
    //   this.StudentName = name
    // })
  }
}

</script>

<style>
.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 {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data() {
    return {
      name: '尚硅谷atguigu',
      address: '北京'
    }
  },
  props: ['getSchoolName'],
  methods: {
    sendSchoolName() {
      this.getSchoolName(this.name)
    }
  }
}
</script>

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

Student.vue

<template>
  <div class="student">
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
    <h2>当前求和为:{{ number }}</h2>
    <button @click="add">点我number++</button>
    <button @click="sendStudentName">把学生名给App</button>
    <button @click="unbind">解绑atguigu事件</button>
    <button @click="death">销毁当前Student组件的实例(vc)</button>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: '张三',
      sex: '男',
      number: 0
    }
  },
  methods: {
    add() {
      console.log('add回调被调用了!')
      this.number++
    },
    sendStudentName() {
      //触发Student组件实例身上的atguigu事件
      this.$emit('atguigu', this.name, 666, 888, 900)
      // this.$emit('demo')
      // this.$emit('click')
    },
    unbind() {
      this.$off('atguigu')//解绑一个自定义事件
      // this.$off(['atguigu', 'demo'])//解绑多个自定义事件
      // this.$off()//解绑所有自定义事件
    },
    death() {
      this.$destroy()//销毁当前了Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效。
    }
  }
}
</script>

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

通过自定义事件优化ToDoList

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader @addTodo="addTodo"/>
        <MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
        <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) {
      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 => todo.id !== id)
    },
    //全选or取消勾选
    checkAllTodo(done) {
      this.todos.forEach(todo => todo.done = done)
    },
    //删除已完成的todo
    clearAllTodo() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  },
  watch: {
    todos: {
      deep: true,
      handler(value) {
        localStorage.setItem('todos', JSON.stringify(value))
      }
    }
  }
}
</script>

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

MyFooter.vue

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" v-model="isAll"/>
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: 'MyFooter',
  props: ['todos', 'checkAllTodo', 'clearAllTodo'],
  computed: {
    doneTotal() {
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    total() {
      return this.todos.length
    },
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        // this.checkAllTodo(value)
        this.$emit('checkAllTodo', value)
      }
    }
  },
  methods: {
    clearAll() {
      // this.clearAllTodo()
      this.$emit('clearAllTodo')
    }
  }
}
</script>

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

MyHeader.vue

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

<script>
import {nanoid} from 'nanoid'

export default {
  name: 'MyHeader',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    add() {
      if (!this.title.trim()) return
      const todoObj = {id: nanoid(), title: this.title, done: false}
      console.log(this)
      this.$emit('addTodo', todoObj)
      this.title = ''
    }
  }
}
</script>

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

MyItem.vue

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
  </li>
</template>

<script>
export default {
  name:'MyItem',
  props:['todo','checkTodo','deleteTodo'],
  methods:{
    handleCheck(id){
      this.checkTodo(id)
    },
    handleDelete(id,title){
      if(confirm("确定删除任务:"+title+"吗?")){
        this.deleteTodo(id)
      }
    }
  }
}
</script>

<style scoped>
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

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

li:before {
  content: initial;
}

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

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

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

MyList.vue

<template>
  <ul class="todo-main">
    <MyItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        :checkTodo="checkTodo"
        :deleteTodo="deleteTodo"
    />
  </ul>
</template>

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

export default {
  name: 'MyList',
  components: {MyItem},
  props: ['todos', 'checkTodo', 'deleteTodo']
}
</script>

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

全局事件总线

全局事件总线是一种可以在任意组件间通信的方式,本质上就是一个对象。它必须满足以下条件:1. 所有的组件对象都必须能看见他

2. 这个对象必须能够使用$on$emit$off方法去绑定、触发和解绑事件

全局事件总线(GlobalEventBus):

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

  • 安装全局事件总线:

    new Vue({
       	...
       	beforeCreate() {
       		Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
       	},
        ...
    }) 
    

使用事件总线:

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

    export default {
        methods(){
            demo(data){...}
        }
        ...
        mounted() {
            this.$bus.$on('xxx',this.demo)
        }
    }
    

  2. 提供数据:this.$bus.$emit('xxx',data)

4.最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件

main.js

import Vue from 'vue'
import App from './App.vue'

// window.x = {a: 1, b: 2}

Vue.config.productionTip = false

// const Demo = Vue.extend({})
// const d = new Demo()
// Vue.prototype.x = d

new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})



School.vue

<template>
  <div class="school">
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址:{{ address }}</h2>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data() {
    return {
      name: '尚硅谷atguigu',
      address: '北京'
    }
  },
  mounted() {
    // console.log('School', window.x)
    this.$bus.$on('hello', (data) => {
      console.log('我是School组件,收到了数据', data)
    })
  },
  beforeDestroy() {
    this.$bus.$off('hello')
  }
}
</script>

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

Student.vue

<template>
  <div class="student">
    <h2>学生姓名:{{ name }}</h2>
    <h2>学生性别:{{ sex }}</h2>
    <button @click="sendStudentName">把学生名给School组件</button>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: '张三',
      sex: '男'
    }
  },
  mounted() {
    // console.log('Student', window.x)
  },
  methods: {
    sendStudentName() {
      this.$bus.$emit('hello', 666)
    }
  }
}
</script>

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

App.vue

<template>
  <div class="app">
    <h1>{{ msg }}</h1>
    <School></School>
    <Student></Student>
  </div>
</template>
<script>


import Student from "@/components/Student";
import School from "@/components/School";

export default {
  name: 'App',
  components: {Student, School},
  data() {
    return {
      msg: "你好啊!",
    }
  },
}
</script>
<style>
.app {
  background-color: gray;
  padding: 5px;
}
</style>

运行结果:

使用全局事件总线优化Todo-List:

MyFooter.vue

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" v-model="isAll"/>
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: 'MyFooter',
  props: ['todos', 'checkAllTodo', 'clearAllTodo'],
  computed: {
    doneTotal() {
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    total() {
      return this.todos.length
    },
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        // this.checkAllTodo(value)
        this.$emit('checkAllTodo', value)
      }
    }
  },
  methods: {
    clearAll() {
      // this.clearAllTodo()
      this.$emit('clearAllTodo')
    }
  }
}
</script>

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

MyHeader.vue

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

<script>
import {nanoid} from 'nanoid'

export default {
  name: 'MyHeader',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    add() {
      if (!this.title.trim()) return
      const todoObj = {id: nanoid(), title: this.title, done: false}
      console.log(this)
      this.$emit('addTodo', todoObj)
      this.title = ''
    }
  }
}
</script>

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

MyItem.vue

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
  </li>
</template>

<script>
export default {
  name: 'MyItem',
  props: ['todo'],
  methods: {
    handleCheck(id) {
      // this.checkTodo(id)
      this.$bus.$emit('checkTodo', id)
    },
    handleDelete(id, title) {
      if (confirm("确定删除任务:" + title + "吗?")) {
        // this.deleteTodo(id)
        this.$bus.$emit('deleteTodo', id)
      }
    }
  }
}
</script>

<style scoped>
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

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

li:before {
  content: initial;
}

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

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

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

MyList.vue 

<template>
  <ul class="todo-main">
    <MyItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
    />
  </ul>
</template>

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

export default {
  name: 'MyList',
  components: {MyItem},
  props: ['todos']
}
</script>

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

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader @addTodo="addTodo"/>
        <MyList :todos="todos"/>
        <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) {
      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 => todo.id !== id)
    },
    //全选or取消勾选
    checkAllTodo(done) {
      this.todos.forEach(todo => todo.done = done)
    },
    //删除已完成的todo
    clearAllTodo() {
      this.todos = this.todos.filter(todo => !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>
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>

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false
new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})


运行结果:

消息的订阅与发布

消息订阅与发布(pubsub):

  • 消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信
  • 使用步骤:

                1.安装pubsub:npm i pubsub-js

                2.引入:import pubsub from 'pubsub-js'

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

export default {
    methods(){
        demo(data){...}
    }
    ...
    mounted() {
		this.pid = pubsub.subscribe('xxx',this.demo)
    }
}


4.提供数据:pubsub.publish('xxx',data)

5.最好在beforeDestroy钩子中,使用pubsub.unsubscribe(pid)取消订阅
 

School.vue

<template>
  <div class="school">
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址:{{ address }}</h2>
  </div>
</template>

<script>
import pubsub from "pubsub-js";

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "School",
  data() {
    return {
      name: '尚硅谷atguigu',
      address: '北京'
    }
  },
  methods: {
    demo(msgName, data) {
      console.log(this)//vc
      console.log('有人发布了hello消息,hello消息的回调执行了', msgName, data)
    }
  },
  mounted() {
    // this.pubid = pubsub.subscribe('hello', function (msgName, data) {
    //   console.log(this) undefined
    //   console.log('有人发布了hello消息,hello消息的回调执行了', msgName, data)
    // })

    // this.pubid = pubsub.subscribe('hello', (msgName, data) => {
    //   console.log(this)//vc
    //   console.log('有人发布了hello消息,hello消息的回调执行了', msgName, data)
    // })

    this.pubid = pubsub.subscribe('hello', this.demo)
  },
  beforeDestroy() {
    pubsub.unsubscribe(this.pubid)
  }
}
</script>

<style scoped>
.school {
  background-color: skyblue;
  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 {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Student",
  data() {
    return {
      name: '张三',
      sex: '男'
    }
  },
  methods: {
    sendStudentName() {
      pubsub.publish('hello',666)
    }
  }
}
</script>

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

App.vue

<template>
  <div class="app">
    <h1>{{ msg }}</h1>
    <School></School>
    <Student></Student>
  </div>
</template>
<script>


import Student from "@/components/Student";
import School from "@/components/School";

export default {
  name: 'App',
  components: {Student, School},
  data() {
    return {
      msg: "你好啊!",
    }
  },
}
</script>
<style>
.app {
  background-color: gray;
  padding: 5px;
}
</style>

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})



使用消息的订阅与发布优化Todo-List:

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader @addTodo="addTodo"/>
        <MyList :todos="todos"/>
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
      </div>
    </div>
  </div>
</template>

<script>
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) {
      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 => todo.id !== id)
    },
    //全选or取消勾选
    checkAllTodo(done) {
      this.todos.forEach(todo => todo.done = done)
    },
    //删除已完成的todo
    clearAllTodo() {
      this.todos = this.todos.filter(todo => !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('checkTodo', this.checkTodo)
    this.pubId2 = pubsub.subscribe('deleteTodo', this.deleteTodo)
  },
  beforeDestroy() {
    this.$bus.$off('checkTodo')
    // this.$bus.$off('deleteTodo')
    pubsub.unsubscribe(this.pubId)
    pubsub.unsubscribe(this.pubId2)
  }
}
</script>

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

MyFooter.vue

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" v-model="isAll"/>
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: 'MyFooter',
  props: ['todos', 'checkAllTodo', 'clearAllTodo'],
  computed: {
    doneTotal() {
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    total() {
      return this.todos.length
    },
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        // this.checkAllTodo(value)
        this.$emit('checkAllTodo', value)
      }
    }
  },
  methods: {
    clearAll() {
      // this.clearAllTodo()
      this.$emit('clearAllTodo')
    }
  }
}
</script>

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

MyHeader.vue

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

<script>
import {nanoid} from 'nanoid'

export default {
  name: 'MyHeader',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    add() {
      if (!this.title.trim()) return
      const todoObj = {id: nanoid(), title: this.title, done: false}
      console.log(this)
      this.$emit('addTodo', todoObj)
      this.title = ''
    }
  }
}
</script>

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

MyItem.vue

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</button>
  </li>
</template>

<script>
import pubsub from "pubsub-js";

export default {
  name: 'MyItem',
  props: ['todo'],
  methods: {
    handleCheck(id) {
      // this.checkTodo(id)
      // this.$bus.$emit('checkTodo', id)
      pubsub.publish('checkTodo', id)
    },
    handleDelete(id, title) {
      if (confirm("确定删除任务:" + title + "吗?")) {
        // this.deleteTodo(id)
        // this.$bus.$emit('deleteTodo', id)
        pubsub.publish('deleteTodo', id)
      }
    }
  }
}
</script>

<style scoped>
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

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

li:before {
  content: initial;
}

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

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

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

 MyList.vue

<template>
  <ul class="todo-main">
    <MyItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
    />
  </ul>
</template>

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

export default {
  name: 'MyList',
  components: {MyItem},
  props: ['todos']
}
</script>

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

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false
new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})

使用$nextTick优化Todo-List:(实现编辑功能)

$nextTick(回调函数)可以将回调延迟到下次 DOM 更新循环之后执行

MyFooter.vue

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" v-model="isAll"/>
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: 'MyFooter',
  props: ['todos', 'checkAllTodo', 'clearAllTodo'],
  computed: {
    doneTotal() {
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    total() {
      return this.todos.length
    },
    isAll: {
      get() {
        return this.total === this.doneTotal && this.total > 0
      },
      set(value) {
        // this.checkAllTodo(value)
        this.$emit('checkAllTodo', value)
      }
    }
  },
  methods: {
    clearAll() {
      // this.clearAllTodo()
      this.$emit('clearAllTodo')
    }
  }
}
</script>

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

 MyHeader.vue

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

<script>
import {nanoid} from 'nanoid'

export default {
  name: 'MyHeader',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    add() {
      if (!this.title.trim()) return
      const todoObj = {id: nanoid(), title: this.title, done: false}
      console.log(this)
      this.$emit('addTodo', todoObj)
      this.title = ''
    }
  }
}
</script>

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

MyItem.vue

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
      <span v-show="!todo.isEdit">{{ todo.title }}</span>
      <input v-show="todo.isEdit"
             type="text"
             :value="todo.title"
             @blur="handleBlur(todo,$event)"
             ref="inputTitle"
      >
    </label>

    <button class="btn btn-danger" @click="handleDelete(todo.id,todo.title)">删除</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',
  props: ['todo'],
  methods: {
    handleCheck(id) {
      // this.checkTodo(id)
      // this.$bus.$emit('checkTodo', id)
      pubsub.publish('checkTodo', id)
    },
    handleDelete(id, title) {
      if (confirm("确定删除任务:" + title + "吗?")) {
        // this.deleteTodo(id)
        // this.$bus.$emit('deleteTodo', id)
        pubsub.publish('deleteTodo', id)
      }
    },
    handleEdit(todo) {
      // todo.isEdit = true
      // eslint-disable-next-line no-prototype-builtins
      if (todo.hasOwnProperty('isEdit')) {
        todo.isEdit = true
      } else {
        this.$set(todo, 'isEdit', true)
      }
      this.$nextTick(function () {
        this.$refs.inputTitle.focus()
      })
    },
    handleBlur(todo, e) {
      todo.isEdit = false
      if (!e.target.value.trim()) return alert('输入不能为空!')
      this.$bus.$emit('updateTodo', todo.id, e.target.value)
    }
  }
}
</script>

<style scoped>
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

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

li:before {
  content: initial;
}

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

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

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

MyList.vue

<template>
  <ul class="todo-main">
    <MyItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
    />
  </ul>
</template>

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

export default {
  name: 'MyList',
  components: {MyItem},
  props: ['todos']
}
</script>

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

App.vue

<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader @addTodo="addTodo"/>
        <MyList :todos="todos"/>
        <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
      </div>
    </div>
  </div>
</template>

<script>
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) {
      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 => todo.id !== id)
    },
    //全选or取消勾选
    checkAllTodo(done) {
      this.todos.forEach(todo => todo.done = done)
    },
    //删除已完成的todo
    clearAllTodo() {
      this.todos = this.todos.filter(todo => !todo.done)
    },
    updateTodo(id, title) {
      this.todos.forEach((todo) => {
        if (todo.id === id) todo.title = title
      })
    }
  },
  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.$bus.$on('updateTodo', this.updateTodo)
    this.pubId = pubsub.subscribe('checkTodo', this.checkTodo)
    this.pubId2 = pubsub.subscribe('deleteTodo', this.deleteTodo)
  },
  beforeDestroy() {
    this.$bus.$off('checkTodo')
    // this.$bus.$off('deleteTodo')
    this.$bus.$off('updateTodo')
    pubsub.unsubscribe(this.pubId)
    pubsub.unsubscribe(this.pubId2)
  }
}
</script>

<style>
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-edit {
  color: #fff;
  background-color: skyblue;
  border: 1px solid #87ceea;
}

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

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false
new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})

运行结果:

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。