使用Vue CLI脚手架

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

章节概述

  • 使用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.安装pubsubnpm 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
    }
})

运行结果

过渡与动画

Vue封装的过度与动画

  1. 作用在插入、更新或移除 DOM元素时在合适的时候给元素添加样式类
  2. 写法

        1.准备好样式

                元素进入的样式

                        v-enter进入的起点
                        v-enter-active进入过程中
                        v-enter-to进入的终点
                元素离开的样式

                        v-leave离开的起点
                        v-leave-active离开过程中
                        v-leave-to离开的终点
        2.使用<transition>包裹要过度的元素并配置name属性

<transition name="hello">
    <h1 v-show="isShow">你好啊</h1>
</transition>

        3.备注若有多个元素需要过度则需要使用<transition-group>且每个元素都要指定key值.

App.vue

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

<script>
import Test from "@/components/Test";
import Test2 from "@/components/Test2";
import Test3 from "@/components/Test3";

export default {
  name: 'App',
  // eslint-disable-next-line vue/no-unused-components
  components: {Test, Test2, Test3},
  data() {
    return {}
  },
}
</script>

 Test.vue

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <transition name="hello" :appear="true">
      <h1 v-show="isShow">你好啊</h1>
    </transition>
    <transition appear>
      <h1 v-show="isShow">你好啊</h1>
    </transition>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Test",
  data() {
    return {
      isShow: true
    }
  }
}
</script>

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

.v-enter-active {
  animation: atguigu 0.2s linear;
}

.v-leave-active {
  animation: atguigu 0.2s reverse linear;
}

.hello-enter-active {
  animation: atguigu 0.2s linear;
}

.hello-leave-active {
  animation: atguigu 0.2s reverse linear;
}

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

Test2.vue

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <transition-group name="hello" :appear="true">
      <h1 v-show="isShow" key="1">你好啊</h1>
      <h1 v-show="isShow" key="2">尚硅谷</h1>
    </transition-group>

<!--    <transition name="hello" :appear="true">-->
<!--      <div v-show="isShow">-->
<!--        <h1>你好啊</h1>-->
<!--        <h1>尚硅谷</h1>-->
<!--      </div>-->
<!--    </transition>-->
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Test2",
  data() {
    return {
      isShow: true
    }
  }
}
</script>

<style scoped>
h1 {
  background-color: orange;
  transition: 0.2s linear;
}

/*进入的起点*/
.hello-enter {
  transform: translateX(-100%);
}

/*进入的终点*/
.hello-enter-to {
  transform: translateX(0);
}

/*离开的起点*/
.hello-leave {
  transform: translateX(0);
}

/*离开的终点*/
.hello-leave {
  transform: translateX(-100%);
}

.hello-enter-active {

}

.hello-leave-active {

}
</style>

Test3.vue

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <transition-group
        :appear="true"
        name="animate__animated animate__bounce"
        enter-active-class="animate__bounce"
        leave-active-class="animate__shakeX">
      <h1 v-show="isShow" key="1">你好啊</h1>
      <h1 v-show="isShow" key="2">尚硅谷</h1>
    </transition-group>
  </div>
</template>

<script>
import 'animate.css'

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Test3",
  data() {
    return {
      isShow: true
    }
  }
}
</script>

<style scoped>
h1 {
  background-color: orange;
  transition: 0.2s linear;
}

</style>

运行结果

使用动画优化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)
    },
    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
    }
})

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">
    <transition-group name="todo" appear>
      <MyItem
          v-for="todo in todos"
          :key="todo.id"
          :todo="todo"
      />
    </transition-group>
  </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;
}

.todo-enter-active {
  animation: atguigu 0.2s linear;
}

.todo-leave-active {
  animation: atguigu 0.2s reverse linear;
}

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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue