使用Vue CLI脚手架

news2024/11/22 8:54:24

章节概述:

  • 使用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
    }
})

运行结果:

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++ 常用总结(二)

1. ① 可变参数... 、__VA_ARGS__与##__VA_ARGS__ 结论&#xff1a;##__VA_ARGS__中##的作用就是去掉前面多余的,号 &#xff0c;在使用自定义打印的时候&#xff0c;推荐##__VA_ARGS__而不是__VA_ARGS__ C语言##__VA_ARGS__的用法_fengwang0301的博客-CSDN博客 例1 __VA…

良心推荐!数学建模基础知识-MATLAB快速上手,最适合新手学习的Matlab快速入门教程

目录 1. 如何打开matlab的文件 第一种方法&#xff1a; 第二种创建脚本文件的方法。 2. 如何运行一段代码 写在命令行 写在脚本 3.some tips about matlab 工作区储存的数据 如何加入断点&如何终止运行&代码分节 1. 如何打开matlab的文件 我们写的源代码可以…

探索对抗样本生成方法:保护机器学习模型的安全性

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

Comparable和Comparator的用法和区别

文章目录 前言 在这里给大家整理了一下comparable和comparator的用法和区别,这些在以后代码和面试中可能也会出现,那么,就跟着我一起去看看吧! 一 .Comparable 1.Comparable是什么? public interface Comparable<T> comparable是个接口,此接口强行对实现它的每个类的对…

22 memcpy 的调试

前言 同样是一个 很常用的 glibc 库函数 不管是 用户业务代码 还是 很多类库的代码, 基本上都会用到 内存数据的拷贝 不过 我们这里是从 具体的实现 来看一下 它的实现 主要是使用 汇编 来进行实现的, 因此 理解需要一定的基础 测试用例 就是简单的使用了一下 memcpy,…

【Linux】10. 进程地址空间

1. 虚拟地址的引出 2. 感性理解 3. 区域划分 在理解虚拟地址空间之前首先了解区域划分是什么 在小学期间的三八线&#xff0c;让桌子分割成两个区域&#xff0c;类比到地址空间也是这样划分的。 操作系统需要对进程管理&#xff0c;进程存在不同的区域映射不同的虚拟地址 这…

【GAMES101】作业0学习总结

本系列博客为记录笔者在学习GAMES101课程时遇到的问题与思考。 GAMES101&#xff1a;课程官网GAMES101&#xff1a;B站视频GAMES101&#xff1a;相关文件下载(百度网盘) 一、环境搭建 以下说明两种环境搭建方法&#xff0c;一种为用原视频所提及的VirtualBox一键搭建环境&…

随笔-涨薪了

突然想起来上个月工资&#xff0c;绩效部分是按照1.01发的&#xff0c;多了10块钱&#xff0c;这也是一年半来第一次涨薪了&#xff0c;就去小龙、小虎我仨的小群里面嘚瑟一下&#xff1a; 我&#xff1a;两年来第一次涨薪&#xff0c;涨了12。 小龙&#xff1a;羡慕。 小虎…

11-CSS-概述、与HTML的结合方式

一、概述 CSS&#xff08;层叠样式表&#xff09;是一种用于控制网页外观和布局的样式语言。它可以独立于 HTML 或 XHTML 文档&#xff0c;以及任何标记语言使用&#xff0c;因此可以用于设计不同类型的文档&#xff0c;如 XML、SVG、XUL 等。CSS 提供了广泛的样式选择器&…

6.Java流在Android中的应用

字节流有哪些? 以输出流为例,输入流除PrintStream外,和输出流是一一对应的 OutputStream ByteArrayOutputStreamPipedOutputStreamFilterOutputStream BufferedOutputStreamDataOutputStreamPrintStream FileOutputStreamObjectOutputStream 使用案例 DataOutputStream dos…

生信刷题之ROSALIND——Part 5 (PERM, PRTM, REVP)

公众号搜索《生信er》&#xff0c;内容更多&#xff0c;更精彩~ 目录 公众号搜索《生信er》&#xff0c;内容更多&#xff0c;更精彩~1、Enumerating Gene OrdersProblemSample DatasetSample OutputexampleCodeOutput 2、Calculating Protein MassProblemSample DatasetSample…

产品经理如何分析业务需求

目录 背景方案一&#xff1a;汇总所有推课的功能二&#xff1a;设置推课机器人的方法三&#xff1a;方法之间的关系四&#xff1a;方法合并五&#xff1a;方法汇总 总结 背景 现在我们开始设计第三版AR***&#xff0c;我负责的部分是推课部分&#xff0c;在领导的一步步引导之…

51单片机(十二)AT24C02(I2C)

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

车企招聘高薪车载开发岗位,如何抓住机会进入该领域?

随着智能化、电动化和网联化的趋势不断加强&#xff0c;车载应用正在成为汽车和信息技术产业的一个重要领域。未来的车载应用将会实现智能驾驶、舒适性、智能信息娱乐等领域的创新&#xff0c;为车辆和车主带来更好的用户体验。 从行业来看&#xff0c;车载应用的相关企业不断…

Azkaban学习——单机版安装与部署

目录 1.解压改名 2.修改装有mysql的虚拟机的my.cnf文件 3.重启装有mysql的虚拟机 4.Datagrip创建azkaban数据库&#xff0c;执行脚本文件 5.修改/opt/soft/azkaban-exec/conf/azkaban.properties文件 6.修改commonprivate.properties 7.传入mysql-connector-java-8.0.29…

最简单的helm教程

最简单的Helm教程 学习前置条件 你得了解Kubernetes&#xff0c;拥有实际的使用经验那是最好不过了 Helm是什么&#xff1f; 我们打开Helm的官网&#xff1a;Helm的官网 可以看到官网的第一页就告诉了我们Helm是什么。 **Helm是Kubernetes&#xff08;k8s&#xff09;的包…

第十二届蓝桥杯青少组省赛Python真题,包含答案

目录 一、选择题 二、编程题 第十二届蓝桥杯青少组省赛Python真题 一、选择题 第 1 题 单选题 设s="Hello Lan Qiao,执行print(s[4:11])输出的结果为 () 答案:D 第 2 题 单选题 循环语句for iin range (8,4,2) : 执行了几次循环 ()

进腾讯了,38k....

大家好&#xff0c;最近有一位老同学成功去了腾讯&#xff0c;特意找他要了一些面试相关的资料&#xff0c;内容涵盖测试理论、Linux基础、MySQL基础、Web测试、接口测试、App测试、管理工具、Python基础、Selenium相关、性能测试、LordRunner相关等质量非常高&#xff01;&…

SM2椭圆曲线公钥密码算法--密钥对与数字签名

1. SM2国密算法介绍 SM2算法全称是SM2椭圆曲线公钥密码算法&#xff08;SM是商用密码的拼音缩写&#xff09;&#xff0c;是一种基于“椭圆曲线”的密码ECC(Elliptic Curve Cryptography)。2016年&#xff0c;SM2成为中国国家密码标准。 在商用密码体系中&#xff0c;SM2主要用…

超详细,多图 PVE 安装 OpenWRT 教程(个人记录)

前言 - 写这个的目的是因为本人健忘所以做个记录以便日后再折腾时查阅。 - 本人笔拙如有选词&#xff0c;错字&#xff0c;语法&#xff0c;标点错误请忽视&#xff0c;大概率不会修改&#xff0c;我自己能看懂就好。 - 内容仅适用于本人的使用环境&#xff0c;不同环境请忽…