第3章 使用 Vue 脚手架

第3章 使用 Vue 脚手架

  • 3.1 初始化脚手架
    • 3.1.1 说明
    • 3.1.2. 具体步骤
    • 3.1.3 分析脚手架结构
      • 1 总结
      • 2 细节分析
        • 1 配置文件
        • 2 src文件
          • 1 文件结构分析
          • 2 例子
      • 3 public文件
      • 4 最终效果
  • 3.2 ref属性
  • 3.3 props配置项
  • 3.4 mixin混入
  • 3.5 插件
  • 3.6 scoped样式
  • 3.7 Todo-list 案例
    • 3.7.1 组件化编码流程
    • 3.7.2 实现静态组件
    • 3.7.3 展示动态数据
      • 3.7.3.1 初始化列表
      • 3.7.3.2 添加功能
      • 3.7.3.3 勾选功能
      • 3.7.3.4 删除功能
      • 3.7.3.5 底部统计
      • 3.7.3.5 底部交互
      • 3.7.3.6 案例总结
  • 3.8 浏览器本地存储
    • 3.8.1 什么是本地存储
    • 3.8.2 WebStorage
      • 总结
      • LocalStorage
      • sessionStorage
    • 3.8.3 Todolist案例本地存储
  • 3.9 组件自定义事件
    • 3.9.1 总结
    • 3.9.2 绑定和销毁自定义事件
      • 1 源代码
      • 2 绑定自定义事件
      • 3 解绑自定义事件
    • 3.9.3 Todo-list自定义事件
  • 3.10 全局事件总线
    • 3.10.1 图解
    • 3.10.2 谁适合当X
    • 3.10.3 一个例子
    • 3.10.4 Todo-list案例
  • 3.11 消息订阅与发布
    • 3.11.1 总结
    • 3.11.2 具体说明
    • 3.11.3 Todo-list案例
  • 3.12 Todo-list案例和nextTick
    • 3.12.1 源代码
    • 3.12.1 代码细节问题
    • 3.12.2 nextTick
  • 3.13 动画与过度
    • 3.13.1 源代码
    • 3.13.2 动画代码解析
    • 3.13.3 过渡代码解析
    • 3.13.4 第三方动画代码解析

3.1 初始化脚手架

3.1.1 说明

  • Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)
  • 最新的版本是 4.x
  • 文档:https://cli.vuejs.org/zh/

3.1.2. 具体步骤

第一步(仅第一次执行):全局安装@vue/cli。

使用命令:win + r,并打开 cmd
在这里插入图片描述

使用命令:npm install -g @vue/cli,进行下载

  • 注意:如出现下载缓慢请配置 npm 淘宝镜像:npm config set registry https://registry.npmmirror.com

这个过程可能会发生警告,可以直接忽略。如果卡住了,则直接按回车即可
在这里插入图片描述
如图所示,安装完毕
在这里插入图片描述
第二步:切换到你要创建项目的目录,然后使用命令创建项目

在桌面创建项目,使用命令:vue create xxxx
在这里插入图片描述
此时会出现如图所示的选项:要求选择Vue的版本(不是脚手架的版本)

我们选择Vue 2

  • babel:将ES6转换为ES5。eslint:语法检查。

在这里插入图片描述
开始加载,如果卡住了,按回车即可
在这里插入图片描述
成功创建项目
在这里插入图片描述
第三步:启动项目

打开文件夹vue_test,在里面打开终端,执行命令:npm run serve

接下来就开始编译代码,该代码为Vue为其配置的hello word例子
在这里插入图片描述
编译完成,如图所示,其还开启了一个内置服务器,端口号为8080,直接访问该服务器即可
在这里插入图片描述
访问服务器,可以得到如下页面
在这里插入图片描述
如果想停止项目,按ctrl+c即可
在这里插入图片描述

3.1.3 分析脚手架结构

1 总结

.文件目录
├── 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: 包版本控制文件

2 细节分析

用一个vue例子作为例子来展示脚手架的效果,在讲解文件结构的过程中,逐步说明vue代码

1 配置文件

在这里插入图片描述
.gitignore

  • git版本管制忽略的配置、

babel.config.js

  • 作用:babel的配置文件,这个文件涉及到ES6转ES5

  • 直接用官方配置好的即可,如图所示:
    在这里插入图片描述

  • 如果想知道其它的配置规则,可以参考babel官网

package.json和package-lock.json

只要打开的工程符合npm规范的,就一定会有这两个文件

package.json: 应用包配置文件

  • 里面保存了包的名字,版本,常用的命令等。当输入命令:npm run serve,真正执行的命令式如图所示的"vue-cli-service serve"。

  • build命令作用:项目完成时,要把项目转变成浏览器认识的东西(如.html文件.css文件.js文件)

  • lint命令作用:语法检查(极少用,因为有语法检查插件)

在这里插入图片描述
package-lock.json: 包版本控制文件

  • 例如如图的compat-data包,会记录下载地址,版本等。以后想安装compat-data包,可以以最快速度安装到指定版本,如果没有这个,以后可能会安装到其它版本的包(例如8.0版本之类的)
    在这里插入图片描述

README.md

  • README.md: 应用描述文件,对整个工程进行描述
2 src文件
1 文件结构分析

文件结构如图所示:
在这里插入图片描述
main.js

  • 运行时机:执行了"npm run serve"后,就直接运行main.js。因此,该文件是整个项目的入口文件

main.js的代码和总结如下

/* 
	该文件是整个项目的入口文件
*/
//引入Vue
import Vue from 'vue'
//引入App组件,它是所有组件的父组件
import App from './App.vue'
//关闭vue的生产提示
Vue.config.productionTip = false
 
/* 
	关于不同版本的Vue:
	
		1.vue.js与vue.runtime.xxx.js的区别:
				(1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
				(2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
		2.因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用
			render函数接收到的createElement函数去指定具体内容。
*/
 
//创建Vue实例对象---vm
new Vue({
	el:'#app',
	//render函数完成了这个功能:将App组件放入容器中
  render: h => h(App),
	// render:q=> q('h1','你好啊')
 
	// template:`<h1>你好啊</h1>`,
	// components:{App},
})

render函数的完整形式和简写形式

// 简写形式
render: h => h(App),
// 完整形式
// render(createElement){
//     return createElement(App)
// }

细节分析
分析import Vue from 'vue’中的vue来自于哪里

按住 ctrl 并点击 vue。来到node_modules下的vue包
在这里插入图片描述
打开vue包的package.json,查看第六行,发现引入了runtime.esm.js,这是残缺版的vue,没有模板解析器,因此需要使用render函数(如果使用完整版的vue,直接用template即可)
在这里插入图片描述
在dist目录下,完整版的vue如图所示:
在这里插入图片描述
分析render函数
createElement是一个函数,借助这个函数,可以渲染内容。其需要两个参数,分别是标签和内容如图所示:
在这里插入图片描述
也可以写成箭头函数,如图所示:
在这里插入图片描述
createElement太长,可以换个名字:
在这里插入图片描述
特别注意:如果传的是组件,只需把组件传进去即可,并且组件不能加引号,如果加一个引号,会解析成html元素
在这里插入图片描述
asset文件夹
存放静态资源,例如放一下logo图

component文件夹
存放除 App.vue 外的所有组件

2 例子

把之前单文件组件的School组件和Student组件放入component
在这里插入图片描述
School.vue

//School.vue
<template>
	<div class="demo">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="showName">点我提示学校名</button>	
	</div>
</template>
 
<script>
	 export default {
		name:'School',
		data(){
			return {
				name:'尚硅谷',
				address:'北京昌平'
			}
		},
		methods: {
			showName(){
				alert(this.name)
			}
		},
	}
</script>
 
<style>
	.demo{
		background-color: orange;
	}
</style>

Student.vue

//Student.vue
<template>
	<div>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生年龄:{{age}}</h2>
	</div>
</template>
 
<script>
	 export default {
		name:'Student',
		data(){
			return {
				name:'张三',
				age:18
			}
		}
	}
</script>

App.vue
App.vue:负责汇总所有组件

<template>
	<div>
		<img src="./assets/logo.png" alt="logo">
		<School></School>
		<Student></Student>
	</div>
</template>
 
<script>
	//引入组件
	import School from './components/School'
	import Student from './components/Student'
 
	export default {
		name:'App',
		components:{
			School,
			Student
		}
	}
</script>
 

3 public文件

public文件:存放网站页签图标和index.html(整个应用的界面),如图所示:
在这里插入图片描述

  • 注意:本例子使用了bootstrap.css,不使用css也无所谓,只是为了美观,不影响脚手架的使用
<!-- index.html -->
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
		<!-- 针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的渲染级别渲染页面 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
		<!-- 开启移动端的理想视口 -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
		<!-- 配置页签图标 -->
    <!-- <%= BASE_URL %>指的是public的路径  -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
		<!-- 引入第三方样式 -->
		<link rel="stylesheet" href="<%= BASE_URL %>css/bootstrap.css">
		<!-- 配置网页标题 -->
    <title>硅谷系统</title>
  </head>
  <body>
		<!-- 当浏览器不支持js时noscript中的元素就会被渲染 -->
    <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>

4 最终效果

打开终端,输入命令:npm run serve

这里会产生一些错误,有两种解决方法

在这里插入图片描述
解决方法1

修改组件名,使其符合命名规范。例如:StudentNameStudentName或者student-name

解决方法2

修改配置项,关闭语法检查

在项目的根目录找到(没有就自行创建)vue.config.js文件
在这里插入图片描述
在文件中添加如下代码

在这里插入图片描述
结果如下:
在这里插入图片描述
打开上述的服务器
在这里插入图片描述

3.2 ref属性

总结
ref属性:

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

代码
App组件

<template>
	<div>
		<h1 v-text="msg" ref="title"></h1>
		<button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
		<School ref="sch"/>
	</div>
</template>
 
<script>
	//引入School组件
	import School from './components/School'
 
	export default {
		name:'App',
		components:{School},
		data() {
			return {
				msg:'欢迎学习Vue!'
			}
		},
		methods: {
			showDOM(){
				console.log(this.$refs.title) //真实DOM元素
				console.log(this.$refs.btn) //真实DOM元素
				console.log(this.$refs.sch) //School组件的实例对象(vc)
			}
		},
	}
</script>

school组件

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

<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京·昌平'
			}
		},
	}
</script>

<style>
	.school{
		background-color: gray;
	}
</style>

3.3 props配置项

props配置项:

  1. 功能:让组件接收外部传过来的数据

  2. 传递数据:<Demo name=“xxx”/>

  3. 接收数据:

    1. 第一种方式(只接收):props:[‘name’]

    2. 第二种方式(限制数据类型):props:{name:String}

    3. 第三种方式(限制类型、限制必要性、指定默认值):

props:{
    name:{
    	type:String, //类型
        required:true, //必要性
        default:'star' //默认值
    }
}

props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告。

  • 若业务需求确实需要修改,可以复制props的内容到data中一份,然后去修改data中的数据

例子
src/App.vue组件

  • 这里有个细节,age要用数据绑定,以确保18不是字符串
<template>
	<div>
		<Student name="李四" sex="女" :age="18"/>
	</div>
</template>
 
<script>
	import Student from './components/Student'
 
	export default {
		name:'App',
		components:{Student}
	}
</script>

src/components/Student.vue

<template>
	<div>
		<h1>{{msg}}</h1>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<h2>学生年龄:{{myAge+1}}</h2>
		<button @click="updateAge">尝试修改收到的年龄</button>
	</div>
</template>
 
<script>
	export default {
		name:'Student',
		data() {
			console.log(this)
			return {
				msg:'我是一个尚硅谷的学生',
				myAge:this.age
			}
		},
		methods: {
			updateAge(){
				this.myAge++
			}
		},
		//简单声明接收
		// props:['name','age','sex'] 
 
		//接收的同时对数据进行类型限制
		/* props:{
			name:String,
			age:Number,
			sex:String
		} */
 
		//接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
		props:{
			name:{
				type:String, //name的类型是字符串
				required:true, //name是必要的
			},
			age:{
				type:Number,
				default:99 //默认值
			},
			sex:{
				type:String,
				required:true
			}
		}
	}
</script>

3.4 mixin混入

总结

mixin(混入):

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

  2. 使用方式:

定义混入

const mixin = {
    data(){....},
    methods:{....}
    ....
}

使用混入

  • 全局混入:Vue.mixin(xxx)
  • 局部混入:mixins:[‘xxx’]
  1. 备注:

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

var mixin = {
	data: function () {
		return {
    		message: 'hello',
            foo: 'abc'
    	}
  	}
}
 
new Vue({
  	mixins: [mixin],
  	data () {
    	return {
      		message: 'goodbye',
            bar: 'def'
    	}
    },
  	created () {
    	console.log(this.$data)
    	// => { message: "goodbye", foo: "abc", bar: "def" }
  	}
})

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

var mixin = {
  	created () {
    	console.log('混入对象的钩子被调用')
  	}
}
 
new Vue({
  	mixins: [mixin],
  	created () {
    	console.log('组件钩子被调用')
  	}
})
 
// => "混入对象的钩子被调用"
// => "组件钩子被调用"

具体使用方法例子

src/mixin.js

export const hunhe = {
	methods: {
		showName(){
			alert(this.name)
		}
	},
	mounted() {
		console.log('你好啊!')
	},
}
export const hunhe2 = {
	data() {
		return {
			x:100,
			y:200
		}
	},
}

src/components/School.vue

<template>
	<div>
		<h2 @click="showName">学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>
 
<script>
	//引入一个hunhe
	// import {hunhe,hunhe2} from '../mixin'
 
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
				x:666
			}
		},
		// mixins:[hunhe,hunhe2],
	}
</script>

src/components/Student.vue

<template>
	<div>
		<h2 @click="showName">学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
	</div>
</template>
 
<script>
	// import {hunhe,hunhe2} from '../mixin'
 
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		},
		// mixins:[hunhe,hunhe2]
	}
</script>

src/App.vue

<template>
	<div>
		<School/>
		<hr>
		<Student/>
	</div>
</template>
 
<script>
	import School from './components/School'
	import Student from './components/Student'
 
	export default {
		name:'App',
		components:{School,Student}
	}
</script>

src/main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
import {hunhe,hunhe2} from './mixin'
//关闭Vue的生产提示
Vue.config.productionTip = false
 
Vue.mixin(hunhe)
Vue.mixin(hunhe2)
 
 
//创建vm
new Vue({
	el:'#app',
	render: h => h(App)
})

3.5 插件

总结

  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
    }
  1. 使用插件:Vue.use(plugin)

具体使用方法例子
src/plugin.js

export default {
	install(Vue,x,y,z){
		console.log(x,y,z) //输出123
		//全局过滤器
		Vue.filter('mySlice',function(value){
			return value.slice(0,4)
		})
 
		//定义全局指令
		Vue.directive('fbind',{
			//指令与元素成功绑定时(一上来)
			bind(element,binding){
				element.value = binding.value
			},
			//指令所在元素被插入页面时
			inserted(element,binding){
				element.focus()
			},
			//指令所在的模板被重新解析时
			update(element,binding){
				element.value = binding.value
			}
		})
 
		//定义混入
		Vue.mixin({
			data() {
				return {
					x:100,
					y:200
				}
			},
		})
 
		//给Vue原型上添加一个方法(vm和vc就都能用了)
		Vue.prototype.hello = ()=>{alert('你好啊')}
	}
}

src/main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//引入插件
import plugins from './plugins'
//关闭Vue的生产提示
Vue.config.productionTip = false
 
//应用(使用)插件
Vue.use(plugins,1,2,3)
//创建vm
new Vue({
	el:'#app',
	render: h => h(App)
})

src/components/School.vue

<template>
	<div>
		<h2>学校名称:{{name | mySlice}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="test">点我测试一个hello方法</button>
	</div>
</template>
 
<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷atguigu',
				address:'北京',
			}
		},
		methods: {
			test(){
				this.hello()
			}
		},
	}
</script>

src/components/Student.vue

<template>
	<div>
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<input type="text" v-fbind:value="name">
	</div>
</template>
 
<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男'
			}
		},
	}
</script>

src/App.vue

<template>
	<div>
		<School/>
		<hr>
		<Student/>
	</div>
</template>
 
<script>
	import School from './components/School'
	import Student from './components/Student'
 
	export default {
		name:'App',
		components:{School,Student}
	}
</script>

3.6 scoped样式

总结
scoped样式:

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

注意:scoped样式一般不会在App.vue中使用,会导致一些错误

例子
以school组件为例,这样即使其它组件使用demo这个名字也不会冲突

<template>
	<div class="demo">
		<h2 class="title">学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>
 
<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷atguigu',
				address:'北京',
			}
		}
	}
</script>
 
<style scoped>
	.demo{
		background-color: skyblue;
	}
</style>

3.7 Todo-list 案例

3.7.1 组件化编码流程

  1. 实现静态组件:抽取组件,使用组件实现静态页面效果
  2. 展示动态数据:
    • 数据的类型、名称是什么?
    • 数据保存在哪个组件?
  3. 交互——从绑定事件监听开始

3.7.2 实现静态组件

回顾组件定义:用来实现局部功能的代码和资源的集合(html/css/js/image…)
因此我们根据局部功能来划分组件,如图所示:
为了防止重名,除了App,其余名字都加上My
在这里插入图片描述
src/components/MyHeader.vue

<template>
	<div class="todo-header">
		<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
	</div>
</template>
 
<script>
	export default {
		name:'MyHeader',		
	}
</script>
 
<style scoped>
	/*header*/
	.todo-header input {
		width: 560px;
		height: 28px;
		font-size: 14px;
		border: 1px solid #ccc;
		border-radius: 4px;
		padding: 4px 7px;
	}
 
	.todo-header input:focus {
		outline: none;
		border-color: rgba(82, 168, 236, 0.8);
		box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
	}
</style>

src/components/MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox"/>
			<span>xxxxx</span>
		</label>
		<button class="btn btn-danger" style="display:none">删除</button>
	</li>
</template>
 
<script>
	export default {
		name:'MyItem',
	}
</script>
 
<style scoped>
	/*item*/
	li {
		list-style: none;
		height: 36px;
		line-height: 36px;
		padding: 0 5px;
		border-bottom: 1px solid #ddd;
	}
 
	li label {
		float: left;
		cursor: pointer;
	}
 
	li label li input {
		vertical-align: middle;
		margin-right: 6px;
		position: relative;
		top: -1px;
	}
 
	li button {
		float: right;
		display: none;
		margin-top: 3px;
	}
 
	li:before {
		content: initial;
	}
 
	li:last-child {
		border-bottom: none;
	}
</style>

src/components/MyList.vue

<template>
	<ul class="todo-main">
		<MyItem/>
		<MyItem/>
		<MyItem/>
		<MyItem/>
	</ul>
</template>
 
<script>
	import MyItem from './MyItem.vue'
	 export default {
		name:'MyList',
		components:{MyItem}
	}
</script>
 
<style scoped>
	/*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}
 
	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}
</style>

src/components/MyFooter.vue

<template>
	<div class="todo-footer">
		<label>
			<input type="checkbox"/>
		</label>
		<span>
			<span>已完成0</span> / 全部2
		</span>
		<button class="btn btn-danger">清除已完成任务</button>
	</div>
</template>
 
<script>
	export default {
		name:'MyFooter',
		
	}
</script>
 
<style scoped>
	/*footer*/
	.todo-footer {
		height: 40px;
		line-height: 40px;
		padding-left: 6px;
		margin-top: 5px;
	}
 
	.todo-footer label {
		display: inline-block;
		margin-right: 20px;
		cursor: pointer;
	}
 
	.todo-footer label input {
		position: relative;
		top: -1px;
		vertical-align: middle;
		margin-right: 5px;
	}
 
	.todo-footer button {
		float: right;
		margin-top: 5px;
	}
</style>

src/App.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader/>
				<MyList/>
				<MyFooter/>
			</div>
		</div>
	</div>
</template>
 
<script>
    // MyItem是MyList的子组件,只需用MyList就行
	// MyList里包含MyItem
	import MyHeader from './components/MyHeader.vue'
	import MyFooter from './components/MyFooter.vue'
	import MyList from './components/MyList.vue'
 
	export default {
		name:'App',
		components:{MyHeader,MyFooter,MyList}
	}
</script>
 
<style>
	/*base*/
	body {
		background: #fff;
	}
 
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
 
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
 
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
 
	.btn:focus {
		outline: none;
	}
 
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

效果如下:
在这里插入图片描述

3.7.3 展示动态数据

3.7.3.1 初始化列表

定义todos数组来保存数据,因此需要修改MyList组件和MyItem组件
src/components/MyList.vue

<template>
	<ul class="todo-main">
		<!-- 可以省略index -->
		<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
	</ul>
</template>
 
<script>
	import MyItem from './MyItem.vue'
	 export default {
		name:'MyList',
		components:{MyItem},
		data() {
			return {
				todos:[
					{id:'001',title:'抽烟',done:true},
					{id:'002',title:'喝酒',done:false},
					{id:'003',title:'开车',done:true}
				]
			}
		},
	}
</script>
 
<style scoped>
	/*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}
 
	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}
</style>

src/components/MyItem.vue

<template>
	<li>
		<label>
			<!-- checked要用数据绑定,这样双引号里面的才是表达式 -->
			<input type="checkbox" :checked="todo.done"/>
			<span>{{todo.title}}</span>
		</label>
		<button class="btn btn-danger" style="display:none">删除</button>
	</li>
</template>
 
<script>
	export default {
		name:'MyItem',
		// 接受todo对象
		props:['todo']
	}
</script>
 
<style scoped>
	/*item*/
	li {
		list-style: none;
		height: 36px;
		line-height: 36px;
		padding: 0 5px;
		border-bottom: 1px solid #ddd;
	}
 
	li label {
		float: left;
		cursor: pointer;
	}
 
	li label li input {
		vertical-align: middle;
		margin-right: 6px;
		position: relative;
		top: -1px;
	}
 
	li button {
		float: right;
		display: none;
		margin-top: 3px;
	}
 
	li:before {
		content: initial;
	}
 
	li:last-child {
		border-bottom: none;
	}
</style>

效果如下:
在这里插入图片描述

3.7.3.2 添加功能

先下载一个库nanoid(uuid太大,不使用),用于随机生成id,如图所示,终端下载:
在这里插入图片描述实现添加功能时,我们目前还无法实现如图所示的通信方式
在这里插入图片描述
因此先实现最初级的方式:
在这里插入图片描述
子组件 ==> 父组件通信的关键:

  1. 父组件先给子组件一个函数
  2. 子组件通过函数把数据传给父组件

在这里注意一个问题:vc身上的data,props,methods,computed的数据均不能重名
本功能的实现需要改变App,MyList,MyHeader组件

src/App.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<!-- 要写冒号,数据绑定 -->
				<MyList :todos="todos"/>
				<MyFooter/>
			</div>
		</div>
	</div>
</template>
 
<script>
    // MyItem是MyList的子组件,只需用MyList就行
	// MyList里包含MyItem
	import MyHeader from './components/MyHeader.vue'
	import MyFooter from './components/MyFooter.vue'
	import MyList from './components/MyList.vue'
 
	export default {
		name:'App',
		components:{MyHeader,MyFooter,MyList},
		data() {
			return {
				todos:[
					{id:'001',title:'抽烟',done:true},
					{id:'002',title:'喝酒',done:false},
					{id:'003',title:'开车',done:true}
				]
			}
		},
		methods:{
			// 添加一个todo
			addTodo(todoObj){
				this.todos.unshift(todoObj);
			}
		},
	}
</script>
 
<style>
	/*base*/
	body {
		background: #fff;
	}
 
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
 
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
 
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
 
	.btn:focus {
		outline: none;
	}
 
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

src/components/MyList.vue

<template>
	<ul class="todo-main">
		<!-- 可以省略index -->
		<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
	</ul>
</template>
 
<script>
	import MyItem from './MyItem.vue'
	 export default {
		name:'MyList',
		components:{MyItem},
		// 接收对象todos
		props:['todos'],
	}
</script>
 
<style scoped>
	/*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}
 
	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}
</style>

src/components/MyHeader.vue

<template>
	<div class="todo-header">
		<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
	</div>
</template>
 
<script>
	import {nanoid} from 'nanoid'
	export default {
		name:'MyHeader',
		props:['addTodo'],
		data() {
			return {
				title:''
			}
		},
		methods:{
			add(e){
				// 校验数据
				if(!this.title.trim()) return alert('输入不能为空')
				// 将用户输入包装成对象
				// Vue的原则是不操作Dom,因此不使用e.target.value,当然使用也可以
				// 这里使用双向数据绑定,用this.title捕获输入值
				const todoObj = {id:nanoid(),title:this.title,done:false}
				// 通知App组件添加一个todo对象
				this.addTodo(todoObj)
				// 清空输入
				this.title = ''
			}
		},
	}
</script>
 
<style scoped>
	/*header*/
	.todo-header input {
		width: 560px;
		height: 28px;
		font-size: 14px;
		border: 1px solid #ccc;
		border-radius: 4px;
		padding: 4px 7px;
	}
 
	.todo-header input:focus {
		outline: none;
		border-color: rgba(82, 168, 236, 0.8);
		box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
	}
</style>

效果如下:
在这里插入图片描述

3.7.3.3 勾选功能

思路如下:组件之间逐层通信
在这里插入图片描述
本功能要修改的组件有:App,MyList,MyItem

问题一:为什么要实现这个功能
当勾选的时候,应该将todo.done对应的数据进行更新,因此需要实现这个功能

问题二:为什么不在MyItem组件使用v-model来监视
原因在于:props传进来的数据原则上是不能修改的,但由于Vue监测props时类似浅层次的监视,并没有分析todo对象里面的属性是否修改,因此Vue没有提醒,但不建议这么做
在这里插入图片描述
src/App.vue

<template>
	<div id="root">
		<div class="todo-container">
			<div class="todo-wrap">
				<MyHeader :addTodo="addTodo"/>
				<!-- 要写冒号,数据绑定 -->
				<MyList :todos="todos" :checkTodo="checkTodo"/>
				<MyFooter/>
			</div>
		</div>
	</div>
</template>
 
<script>
    // MyItem是MyList的子组件,只需用MyList就行
	// MyList里包含MyItem
	import MyHeader from './components/MyHeader.vue'
	import MyFooter from './components/MyFooter.vue'
	import MyList from './components/MyList.vue'
 
	export default {
		name:'App',
		components:{MyHeader,MyFooter,MyList},
		data() {
			return {
				todos:[
					{id:'001',title:'抽烟',done:true},
					{id:'002',title:'喝酒',done:false},
					{id:'003',title:'开车',done:true}
				]
			}
		},
		methods:{
			// 添加一个todo
			addTodo(todoObj){
				this.todos.unshift(todoObj);
			},
			// 勾选or取消勾选一个todo
			checkTodo(id){
				this.todos.forEach((todo)=>{
					if(todo.id === id) todo.done = !todo.done
				})
			}
		},
	}
</script>
 
<style>
	/*base*/
	body {
		background: #fff;
	}
 
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
 
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
 
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
 
	.btn:focus {
		outline: none;
	}
 
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

src/components/MyList.vue

<template>
	<ul class="todo-main">
		<!-- 可以省略index -->
		<MyItem 
			v-for="todoObj in todos" 
			:key="todoObj.id" 
			:todo="todoObj"
			:checkTodo="checkTodo"
		/>
	</ul>
</template>
 
<script>
	import MyItem from './MyItem.vue'
	 export default {
		name:'MyList',
		components:{MyItem},
		// 接收对象todos
		props:['todos','checkTodo'],
	}
</script>
 
<style scoped>
	/*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}
 
	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}
</style>

src/components/MyItem.vue

<template>
	<li>
		<label>
			<!-- checked要用数据绑定,这样双引号里面的才是表达式 -->
			<!-- @click替换change事件也可以 -->
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<span>{{todo.title}}</span>
		</label>
		<button class="btn btn-danger" style="display:none">删除</button>
	</li>
</template>
 
<script>
	export default {
		name:'MyItem',
		// 接受todo对象
		props:['todo','checkTodo'],
		methods: {
			handleCheck(id){
				// 通知App组件将对应的todo对象的done值取反
				this.checkTodo(id)
			}
		},
	}
</script>
 
<style scoped>
	/*item*/
	li {
		list-style: none;
		height: 36px;
		line-height: 36px;
		padding: 0 5px;
		border-bottom: 1px solid #ddd;
	}
 
	li label {
		float: left;
		cursor: pointer;
	}
 
	li label li input {
		vertical-align: middle;
		margin-right: 6px;
		position: relative;
		top: -1px;
	}
 
	li button {
		float: right;
		display: none;
		margin-top: 3px;
	}
 
	li:before {
		content: initial;
	}
 
	li:last-child {
		border-bottom: none;
	}
</style>

3.7.3.4 删除功能

功能:实现删除功能,对于每个todo项,鼠标悬浮在其身上时,有高亮效果
由于仍然使用组件逐层通信,因此要修改的组件有:App,MyList,MyItem
在这里插入图片描述
src/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/>
			</div>
		</div>
	</div>
</template>
 
<script>
    // MyItem是MyList的子组件,只需用MyList就行
	// MyList里包含MyItem
	import MyHeader from './components/MyHeader.vue'
	import MyFooter from './components/MyFooter.vue'
	import MyList from './components/MyList.vue'
 
	export default {
		name:'App',
		components:{MyHeader,MyFooter,MyList},
		data() {
			return {
				todos:[
					{id:'001',title:'抽烟',done:true},
					{id:'002',title:'喝酒',done:false},
					{id:'003',title:'开车',done:true}
				]
			}
		},
		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)=>{
					return todo.id !== id
				})
			}
		},
	}
</script>
 
<style>
	/*base*/
	body {
		background: #fff;
	}
 
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
 
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
 
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
 
	.btn:focus {
		outline: none;
	}
 
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

src/components/MyList.vue

<template>
	<ul class="todo-main">
		<!-- 可以省略index -->
		<MyItem 
			v-for="todoObj in todos" 
			:key="todoObj.id" 
			:todo="todoObj"
			:checkTodo="checkTodo"
			:deleteTodo="deleteTodo"
		/>
	</ul>
</template>
 
<script>
	import MyItem from './MyItem.vue'
	 export default {
		name:'MyList',
		components:{MyItem},
		// 接收对象todos
		props:['todos','checkTodo','deleteTodo'],
	}
</script>
 
<style scoped>
	/*main*/
	.todo-main {
		margin-left: 0px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding: 0px;
	}
 
	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}
</style>

src/components/MyItem.vue

<template>
	<li>
		<label>
			<!-- checked要用数据绑定,这样双引号里面的才是表达式 -->
			<!-- @click替换change事件也可以 -->
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<span>{{todo.title}}</span>
		</label>
		<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
	</li>
</template>
 
<script>
	export default {
		name:'MyItem',
		// 接受todo对象
		props:['todo','checkTodo','deleteTodo'],
		methods: {
			// 勾选
			handleCheck(id){
				// 通知App组件将对应的todo对象的done值取反
				this.checkTodo(id)
			},
			// 删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					this.deleteTodo(id)
				}
			}
		},
	}
</script>
 
<style scoped>
	/*item*/
	li {
		list-style: none;
		height: 36px;
		line-height: 36px;
		padding: 0 5px;
		border-bottom: 1px solid #ddd;
	}
 
	li label {
		float: left;
		cursor: pointer;
	}
 
	li label li input {
		vertical-align: middle;
		margin-right: 6px;
		position: relative;
		top: -1px;
	}
 
	li button {
		float: right;
		display: none;
		margin-top: 3px;
	}
 
	li:before {
		content: initial;
	}
 
	li:last-child {
		border-bottom: none;
	}
	li:hover{
		background-color: #ddd;
	}
	li:hover button{
		display: block;
	}
</style>

最终的效果:选中一个item,然后点击删除即可。

3.7.3.5 底部统计

思路如下:

  1. 全部数量:todos的长度
  2. 已完成:遍历统计todo.done为真的数量

因此,todos要传给MyFooter,所以要改变的组件有:APP组件和MyFooter组件
ES6之数组reduce()方法

arr.reduce(callback(previousValue, currentValue, index, array),[initialValue])

参数如下:

  1. previousValue:必需参数。初始值(initialValue) 或者上一次调用回调时返回的累积值
  2. currentValue:必需参数。数组中正在处理的元素。
  3. Index(可选):当前正在处理的元素的索引。
  4. array(可选):调用 reduce() 的数组。
  5. initialValue(可选):初始值,会被作为第一次调用 callback 时 previousValue 的值。如果没有提供 initialValue,previousValue 将是数组中的第一个元素,currentValue 将是数组中的第二个元素。

返回值:

  1. arr.reduce 的返回值是一个累计结果,它是最后一次调用回调返回的累计值。

reduce专门用于条件统计,因此非常适合底部统计。
举个例子,如下:

const numbers = [1, 2, 3, 4, 5];

const sum = numbers.reduce((previousValue, currentValue, index, array) => {
  // 打印当前索引和数组内容
  console.log(`当前索引: ${index}, 当前元素: ${currentValue}, 当前数组: [${array}]`);

  // 累加操作
  return previousValue + currentValue;
}, 0);

console.log(`总和: ${sum}`); // 输出 15

代码结果如下:
在这里插入图片描述
接下来完成底部统计功能
src/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"/>
			</div>
		</div>
	</div>
</template>
 
<script>
    // MyItem是MyList的子组件,只需用MyList就行
	// MyList里包含MyItem
	import MyHeader from './components/MyHeader.vue'
	import MyFooter from './components/MyFooter.vue'
	import MyList from './components/MyList.vue'
 
	export default {
		name:'App',
		components:{MyHeader,MyFooter,MyList},
		data() {
			return {
				todos:[
					{id:'001',title:'抽烟',done:true},
					{id:'002',title:'喝酒',done:false},
					{id:'003',title:'开车',done:true}
				]
			}
		},
		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)=>{
					return todo.id !== id
				})
			}
		},
	}
</script>
 
<style>
	/*base*/
	body {
		background: #fff;
	}
 
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
 
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
 
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
 
	.btn:focus {
		outline: none;
	}
 
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

src/components/MyFooter.vue

<template>
	<div class="todo-footer">
		<label>
			<input type="checkbox"/>
		</label>
		<span>
			<span>已完成{{doneTotal}}</span> / 全部{{todos.length}}
		</span>
		<button class="btn btn-danger">清除已完成任务</button>
	</div>
</template>
 
<script>
	export default {
		name:'MyFooter',
		props:['todos'],
		computed:{
			// 常规写法
			// doneTotal(){
			// 	let i = 0;
			// 	this.todos.forEach((todo) =>{
			// 		if(todo.done) i++
			// 	})
			// 	return i
			// }
			// 高级写法
			doneTotal(){
				return this.todos.reduce((pre,current) => pre + (current.done ? 1 : 0) , 0)
			}
		}
	}
</script>
 
<style scoped>
	/*footer*/
	.todo-footer {
		height: 40px;
		line-height: 40px;
		padding-left: 6px;
		margin-top: 5px;
	}
 
	.todo-footer label {
		display: inline-block;
		margin-right: 20px;
		cursor: pointer;
	}
 
	.todo-footer label input {
		position: relative;
		top: -1px;
		vertical-align: middle;
		margin-right: 5px;
	}
 
	.todo-footer button {
		float: right;
		margin-top: 5px;
	}
</style>

代码结果如下:
在这里插入图片描述

3.7.3.5 底部交互

底部交互要实现的功能有:

  1. 当所有任务都勾上时,底部也得勾上
  2. 当所有任务都被删除时,底部不显示
  3. 当底部勾上时,所有任务都被勾上;当底部取消勾选时,所有任务都取消勾选

全选和取消全选功能需要操作App组件的数据,因此在App组件定义函数
src/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" />
      </div>
    </div>
  </div>
</template>
 
<script>
// MyItem是MyList的子组件,只需用MyList就行
// MyList里包含MyItem
import MyHeader from "./components/MyHeader.vue";
import MyFooter from "./components/MyFooter.vue";
import MyList from "./components/MyList.vue";

export default {
  name: "App",
  components: { MyHeader, MyFooter, MyList },
  data() {
    return {
      todos: [
        { id: "001", title: "抽烟", done: true },
        { id: "002", title: "喝酒", done: false },
        { id: "003", title: "开车", done: true },
      ],
    };
  },
  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) => {
        return todo.id !== id;
      });
    },
    //全选or取消全选
    checkAllTodo(done) {
      this.todos.forEach((todo) => {
        todo.done = done;
      });
    },
  },
};
</script>
 
<style>
/*base*/
body {
  background: #fff;
}

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

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

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

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

Footer组件需要监视框的值,最快速的方法是:使用v-model监视计算属性
src/components/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">清除已完成任务</button>
  </div>
</template>
 
<script>
export default {
  name: "MyFooter",
  props: ["todos", "checkAllTodo"],
  computed: {
    total() {
      return this.todos.length;
    },
    doneTotal() {
      return this.todos.reduce(
        (pre, current) => pre + (current.done ? 1 : 0),
        0
      );
    },
    // 涉及到数据的修改,不能简写
    isAll: {
      //全选框是否勾选
      get() {
        return this.doneTotal === this.total && this.total > 0;
      },
      //isAll被修改时set被调用
      set(value) {
        this.checkAllTodo(value);
      },
    },
  },
};
</script>
 
<style scoped>
/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

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

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

细节问题:
第一,为了方便操作,todos.length使用计算属性来获取
第二,实际上绑定是将checked和isAll进行了双向数据绑定,其流程如下:

  1. 初次读取时会执行一次get函数,由于双向数据绑定,引起checked变化
  2. 当点击input框时,checked被修改,由于双向数据绑定,从而引起isAll被修改,因此set被调用(set获得修改数据,即修改后的checked),进而调用this.checkAllTodo函数将所有任务勾选或取消勾选;set调用引起所依赖的数据发生改变,从而再次引起get被调用,isAll值再次被更新,然后再次引起checked改变(其实后面这步不影响checked结果,例如点击勾选,checked为true,此时set调用this.checkAllTodo使得所有任务勾选,那么get再次被调用时,返回的还是ture,并不影响结果)。
  3. 使用v-show控制,当所有任务都被删除时,底部不显示

第二种写法

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <!-- <input type="checkbox" v-model="isAll" /> -->
      <input type="checkbox" :checked="isAll" @change="checkAll" />
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span> / 全部{{ total }}
    </span>
    <button class="btn btn-danger">清除已完成任务</button>
  </div>
</template>
 
<script>
export default {
  name: "MyFooter",
  props: ["todos", "checkAllTodo"],
  computed: {
    total() {
      return this.todos.length;
    },
    doneTotal() {
      return this.todos.reduce(
        (pre, current) => pre + (current.done ? 1 : 0),
        0
      );
    },
    isAll() {
      return this.doneTotal === this.total && this.total > 0;
    },
  },
  methods: {
    checkAll(e) {
      this.checkAllTodo(e.target.checked);
    },
  },
};
</script>
 
<style scoped>
/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

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

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

细节:当事件"checkAll"触发,引起数据改变,则isAll的get被调用,从而引起checked改变,原理是一样的

3.7.3.6 案例总结

1.组件化编码流程:

  • 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
  • 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
    • 一个组件在用:放在组件自身即可
    • 一些组件在用:放在他们共同的父组件上(状态提升)
  • 实现交互:从绑定事件开始.

2.props适用于:

  • 父组件 ==>子组件 通信
  • 子组件 ==>父组件 通信(要求父先给子一个函数)

3.使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
4.props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

3.8 浏览器本地存储

3.8.1 什么是本地存储

打开https://www.vip.com/,搜索“皮鞋”,之后发现皮鞋有本地存储信息,如下图所示:

  • Local Storage有两个网站的原因:两个网站都是唯品会的域名

在这里插入图片描述
在如下箭头所示位置编辑,加入“电脑”,单击页面,发现浏览历史多了个"电脑"
在这里插入图片描述
如下所示,选择searchHistoryData,单击下图中的×,则会删除所有历史记录,单击页面,浏览历史会为空
在这里插入图片描述

3.8.2 WebStorage

总结

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

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

4.备注:

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

LocalStorage

存储字符串

localStorage.setItem('msg','hello!')

结果如下所示:
在这里插入图片描述
存储数字

localStorage.setItem('msg2',666)

结果如下所示:666在存储的时候是一个字符串,而不是数字,如果是数字,它显示的颜色应该是蓝色的。
在这里插入图片描述
存储对象
如果只是直接输入对象,如下所示:
在这里插入图片描述
但是结果如下:这时候存储的对象有问题,自动转成字符串了,相当于调用了toString()。
在这里插入图片描述
因此我们需要使用JSON.stringify,如下:
在这里插入图片描述
它能把对象转成字符串形式,并且能保留里面的内容,如下:
在这里插入图片描述
读取对象操作
在这里插入图片描述
代码结果如下:
在这里插入图片描述
但这并不是我们希望的样子(里面的内容都是灰色的,说明是字符串),所以需要使用JSON.parse,用于将 JSON 格式的字符串转换为 JavaScript 对象,如下:
在这里插入图片描述
最终结果如下:里面的内容都是正确的格式了
在这里插入图片描述
移除操作并不难,直接看代码即可。
源代码如下:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>localStorage</title>
	</head>
	<body>
		<h2>localStorage</h2>
		<button onclick="saveData()">点我保存一个数据</button>
		<button onclick="readData()">点我读取一个数据</button>
		<button onclick="deleteData()">点我删除一个数据</button>
		<button onclick="deleteAllData()">点我清空一个数据</button>

		<script type="text/javascript" >
			let p = {name:'张三',age:18}

			function saveData(){
				localStorage.setItem('msg','hello!!!')
				localStorage.setItem('msg2',666)
				localStorage.setItem('person',JSON.stringify(p))
			}
			function readData(){
				console.log(localStorage.getItem('msg'))
				console.log(localStorage.getItem('msg2'))

				const result = localStorage.getItem('person')
				console.log(JSON.parse(result))

				// console.log(localStorage.getItem('msg3'))
			}
			function deleteData(){
				localStorage.removeItem('msg2')
			}
			function deleteAllData(){
				localStorage.clear()
			}
		</script>
	</body>
</html>

sessionStorage

SessionStorage和LocalStorage的区别:

  • SessionStorage存储的内容会随着浏览器窗口关闭而消失。
  • LocalStorage存储的内容,需要手动清除才会消失!

直接看代码学习即可:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>sessionStorage</title>
	</head>
	<body>
		<h2>sessionStorage</h2>
		<button onclick="saveData()">点我保存一个数据</button>
		<button onclick="readData()">点我读取一个数据</button>
		<button onclick="deleteData()">点我删除一个数据</button>
		<button onclick="deleteAllData()">点我清空一个数据</button>

		<script type="text/javascript" >
			let p = {name:'张三',age:18}

			function saveData(){
				sessionStorage.setItem('msg','hello!!!')
				sessionStorage.setItem('msg2',666)
				sessionStorage.setItem('person',JSON.stringify(p))
			}
			function readData(){
				console.log(sessionStorage.getItem('msg'))
				console.log(sessionStorage.getItem('msg2'))

				const result = sessionStorage.getItem('person')
				console.log(JSON.parse(result))

				// console.log(sessionStorage.getItem('msg3'))
			}
			function deleteData(){
				sessionStorage.removeItem('msg2')
			}
			function deleteAllData(){
				sessionStorage.clear()
			}
		</script>
	</body>
</html>

3.8.3 Todolist案例本地存储

思路如下:用watch来监视todos,由于增删改查都是用的原生对应的方法,因此可以监视数组todos。仅需要修改App.vue组件

不难,因此直接展示代码,如下:
src/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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter.vue'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
        //当todos为空时,JSON.parse(localStorage.getItem('todos'))为null,这样会出错,因此加入|| []
				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)=>{
					return !todo.done
				})
			}
		},
		watch: {
			todos:{
        // 之所以要深度监视,是因为要监视todos里面的属性,例如todos.done
        // 勾选后todos.done为true,需要保存
				deep:true,
				handler(value){
          // 监视todos,value就是更新后的todos
          // 由于todos里面是对象,所以要用JSON.stringify 
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
	}
</script>

<style>
	/*base*/
	body {
		background: #fff;
	}
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
	.btn:focus {
		outline: none;
	}
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

3.9 组件自定义事件

3.9.1 总结

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.xxx.$on('atguigu',this.test)
  1. 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法

4.触发自定义事件:this.$emit('atguigu',数据)
5.解绑自定义事件 this.$off('atguigu')
6.组件上也可以绑定原生DOM事件,需要使用native修饰符
7.注意:通过 this.$refs.xxx.$on('atguig',回调) 绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!

3.9.2 绑定和销毁自定义事件

1 源代码

src/App.vue

<template>
	<div class="app">
		<h1>{{msg}},学生姓名是:{{studentName}}</h1>

		<!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
		<School :getSchoolName="getSchoolName"/>

		<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
		<!-- <Student @atguigu="getStudentName" @demo="m1"/> -->

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

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

	export default {
		name:'App',
		components:{School,Student},
		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(123)
			}
		},
		mounted() {
			this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
			// this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
		},
	}
</script>

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

src/components/Student.vue

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

<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
				number:0
			}
		},
		methods: {
			add(){
				console.log('add回调被调用了')
				this.number++
			},
			sendStudentlName(){
				//触发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 lang="less" scoped>
	.student{
		background-color: pink;
		padding: 5px;
		margin-top: 30px;
	}
</style>

src/components/School.vue

<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
		<button @click="sendSchoolName">把学校名给App</button>
	</div>
</template>

<script>
	export default {
		name:'School',
		props:['getSchoolName'],
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		methods: {
			sendSchoolName(){
				this.getSchoolName(this.name)
			}
		},
	}
</script>

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

2 绑定自定义事件

上述源代码的绑定自定义事件的一些细节问题:

  • 传递函数类型的props实现和自定义事件区别:绑定自定义事件不需要使用props实现,直接绑定一个事件即可

  • 这里的…params代表可以接收多个参数
    在这里插入图片描述

  • 还可以使用箭头函数,它会往外找,找到mounted函数,这样this就是当前的组件实例对象,即为App组件,如果不使用箭头函数,显然this就是Student组件实例对象了。
    在这里插入图片描述

  • 绑定原生事件时还要用native,如下,click就是原生事件
    在这里插入图片描述

3 解绑自定义事件

上述源代码的解绑自定义事件的一些细节问题,主要说说销毁实例的问题,如下:
在这里插入图片描述
销毁实例时也会销毁所有自定义事件,详细方法看上述Student.vue的代码。

3.9.3 Todo-list自定义事件

修改MyFooter组件,绑定自定义事件addTodo。
修改MyHeader组件,绑定自定义事件checkAllTodo和clearAllTodo。
不修改MyItem和MyList组件,涉及到了孙组件了。

src/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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter.vue'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				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)=>{
					return !todo.done
				})
			}
		},
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
	}
</script>

<style>
	/*base*/
	body {
		background: #fff;
	}
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 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>

src/components/MyHeader.vue

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

<script>
	import {nanoid} from 'nanoid'
	export default {
		name:'MyHeader',
		data() {
			return {
				//收集用户输入的title
				title:''
			}
		},
		methods: {
			add(){
				//校验数据
				if(!this.title.trim()) return alert('输入不能为空')
				//将用户的输入包装成一个todo对象
				const todoObj = {id:nanoid(),title:this.title,done:false}
				//通知App组件去添加一个todo对象
				this.$emit('addTodo',todoObj,1,2,3)
				//清空输入
				this.title = ''
			}
		},
	}
</script>

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

	.todo-header input:focus {
		outline: none;
		border-color: rgba(82, 168, 236, 0.8);
		box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
	}
</style>

src/components/MyFooter.vue

<template>
	<div class="todo-footer" v-show="total">
		<label>
			<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
			<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'],
		computed: {
			//总数
			total(){
				return this.todos.length
			},
			//已完成数
			doneTotal(){
				//此处使用reduce方法做条件统计
				/* const x = this.todos.reduce((pre,current)=>{
					console.log('@',pre,current)
					return pre + (current.done ? 1 : 0)
				},0) */
				//简写
				return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
			},
			//控制全选框
			isAll:{
				//全选框是否勾选
				get(){
					return this.doneTotal === this.total && this.total > 0
				},
				//isAll被修改时set被调用
				set(value){
					// this.checkAllTodo(value)
					this.$emit('checkAllTodo',value)
				}
			}
		},
		methods: {
			/* checkAll(e){
				this.checkAllTodo(e.target.checked)
			} */
			//清空所有已完成
			clearAll(){
				// this.clearAllTodo()
				this.$emit('clearAllTodo')
			}
		},
	}
</script>

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

	.todo-footer label {
		display: inline-block;
		margin-right: 20px;
		cursor: pointer;
	}

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

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

3.10 全局事件总线

3.10.1 图解

考虑一个X,以A组件为例,它想收到别的组件给它的数据,因此在A组件中写代码给X绑定自定义事件demo,这样demo的回调就留在了A组件中;D组件想给A组件传数据,因此在D组件中写代码去触发X身上的demo,并且携带数据666,因此demo的回调会执行,这样666会以参数形式来到了A组件。其它同理,图解如下:
在这里插入图片描述
X有如下要求:

  1. 所有组件都能看到
  2. X能绑事件,则必须能调用$on$of$emit

3.10.2 谁适合当X

先看下图,可以知道:VueComponent.prototype.proto === Vue.prototype

在这里插入图片描述
因此可以考虑往Vue的原型对象加X,就满足上面讨论的X要求了。

问题:为什么不往VueComponent.prototype身上加?
可能是因为vm和Vue会访问不到

代码实现方式一
首先使用Vue.extend返回VueComponent,这样Demo就是VueComponent,再利用Demo去new一个d组件,最后挂在Vue的原型对象的x属性上,如下所示:
在这里插入图片描述

代码实现方式二(标准方法)
使用如下所示的生命周期钩子,此时模板未解析
在这里插入图片描述
代码如下:而且要注意,一般全局事件总线不叫X,叫$bus。
在这里插入图片描述
为什么不能像如下那样写?如果这样写,绿色框的内容表示:App组件已经解析完毕,放到页面上了,如果App组件的子组件要触发X,那么此时已经执行完了。而你在绿色框之后再挂载X,就已经晚了(这样写代码报错,未挂载X,子组件就要触发X)。
而如果写在绿色框之前也不行,vm都还没开始创建。
在这里插入图片描述

3.10.3 一个例子

通过这个例子,可以知道如何创建全局事件总线,如何使用全局事件总线,看代码就能看懂

src/main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
	el:'#app',
	render: h => h(App),
	beforeCreate() {
		Vue.prototype.$bus = this //安装全局事件总线
	},
})

src/App.vue

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

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

	export default {
		name:'App',
		components:{School,Student},
		data() {
			return {
				msg:'你好啊!',
			}
		}
	}
</script>

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

src/components/School.vue

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

<script>
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		mounted() {
			// console.log('School',this),this就是School组件,因此直接使用箭头函数
			// 绑定自定义事件在$bus上
			this.$bus.$on('hello',(data)=>{
				console.log('我是School组件,收到了数据',data)
			})
		},
		beforeDestroy() {
			// School组件销毁前,解绑hello事件
			this.$bus.$off('hello')
		},
	}
</script>

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

src/components/Student.vue

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

<script>
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
			}
		},
		mounted() {
			// console.log('Student',this.x)
		},
		methods: {
			sendStudentName(){
				// 触发hello事件
				this.$bus.$emit('hello',this.name)
			}
		},
	}
</script>

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

问题:为什么School组件销毁前,解绑hello事件,要用beforeDestroy()?
回顾绑定自定义事件章节,组件是直接销毁即可,这是因为销毁了之后,组件自己的自定义事件自动销毁。
在这里插入图片描述
而这里不行,是因为自定时间绑定在了bus身上,而不是在School组件身上,所以需要利用beforeDestroy()来解绑。

3.10.4 Todo-list案例

父子组件之间的通信使用props即可,没必要使用全局事件总线。在Todo-list案例中,比较适合用全局事件总线的是App组件和MyItem组件。

因此需要修改代码的有App组件和MyItem组件和MyList组件(不再需要props逐层传递了,因此MyList组件的props要修改)。

直接看下述代码就能看懂。
src/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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter.vue'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				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)=>{
					return !todo.done
				})
			}
		},
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
		mounted() {
			this.$bus.$on('checkTodo',this.checkTodo)
			this.$bus.$on('deleteTodo',this.deleteTodo)
		},
		beforeDestroy() {
			this.$bus.$off('checkTodo')
			this.$bus.$off('deleteTodo')
		},
	}
</script>

<style>
	/*base*/
	body {
		background: #fff;
	}
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 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>

src/components/MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
			<span>{{todo.title}}</span>
		</label>
		<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
	</li>
</template>

<script>
	export default {
		name:'MyItem',
		//声明接收todo
		props:['todo'],
		methods: {
			//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					this.$bus.$emit('deleteTodo',id)
				}
			}
		},
	}
</script>

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

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

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

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

	li:before {
		content: initial;
	}

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

	li:hover{
		background-color: #ddd;
	}
	
	li:hover button{
		display: block;
	}
</style>

src/components/MyList.vue

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

<script>
	import MyItem from './MyItem'

	export default {
		name:'MyList',
		components:{MyItem},
		//声明接收App传递过来的数据
		props:['todos']
	}
</script>

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

	.todo-empty {
		height: 40px;
		line-height: 40px;
		border: 1px solid #ddd;
		border-radius: 2px;
		padding-left: 5px;
		margin-top: 10px;
	}
</style>

3.11 消息订阅与发布

3.11.1 总结

1.一种组件间通信的方式,适用于任意组件间通信
2.使用步骤:

  1. 安装pubsub:npm i pubsub-js
  2. 引入:import pubsub from 'pubsub-js'
  3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
methods(){
	demo(data){.....}
}
.....
mounted(){
	this.pid = pubsub.subscribe('xxx',this.demo)//订阅消息
}
  1. 提供数据:pusub.publish('xxx',数据)
  2. 最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)去取消订阅

3.11.2 具体说明

如下图所示:

A组件订阅一个消息,名为demo,它指定了一个回调函数test,若C组件要发布消息,消息名为demo,并且携带消息666,这样A组件的test函数就会触发调用,666就会被test接收

  • 订阅消息理解成手机号,发布消息就发到手机号上。
    在这里插入图片描述

我们下载pubsub-js库,在当前项目打开终端,输入下述命令:

在这里插入图片描述
直接看代码例子,认真看就能看懂
src/App.vue

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

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

	export default {
		name:'App',
		components:{School,Student},
		data() {
			return {
				msg:'你好啊!',
			}
		}
	}
</script>

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

src/components/School.vue

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

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		mounted() {
			// console.log('School',this)
			/* this.$bus.$on('hello',(data)=>{
				console.log('我是School组件,收到了数据',data)
			}) */
			this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
				// 使用箭头函数后,this往外找就是vc,即School组件
				console.log(this)
				// console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data)
			})
		},
		beforeDestroy() {
			// this.$bus.$off('hello')
			pubsub.unsubscribe(this.pubId)
		},
	}
</script>

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

src/components/Student.vue

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

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
			}
		},
		mounted() {
			// console.log('Student',this.x)
		},
		methods: {
			sendStudentName(){
				// this.$bus.$emit('hello',this.name)
				pubsub.publish('hello',666)
			}
		},
	}
</script>

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

细节问题1
回调函数的(msgName,data)分别是消息名和消息(数据)
细节问题2
这里的this是undefined
在这里插入图片描述
如果要使this是vc即组件本身,可以用箭头函数,如下:
在这里插入图片描述
或者写在methods里,如下:
在这里插入图片描述

3.11.3 Todo-list案例

MyItem组件的删除功能用消息订阅写一下,需要修改的组件是App组件和MyItem组件

src/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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				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)=>{
					return !todo.done
				})
			}
		},
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
		mounted() {
			this.$bus.$on('checkTodo',this.checkTodo)
			this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
		},
		beforeDestroy() {
			this.$bus.$off('checkTodo')
			pubsub.unsubscribe(this.pubId)
		},
	}
</script>

<style>
	/*base*/
	body {
		background: #fff;
	}
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
	.btn-danger:hover {
		color: #fff;
		background-color: #bd362f;
	}
	.btn:focus {
		outline: none;
	}
	.todo-container {
		width: 600px;
		margin: 0 auto;
	}
	.todo-container .todo-wrap {
		padding: 10px;
		border: 1px solid #ddd;
		border-radius: 5px;
	}
</style>

细节问题
deleteTodo函数需要占个位置,因为有msgName参数,如下:
在这里插入图片描述
src/components/MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
			<span>{{todo.title}}</span>
		</label>
		<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
	</li>
</template>

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'MyItem',
		//声明接收todo
		props:['todo'],
		methods: {
			//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id)
				}
			}
		},
	}
</script>

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

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

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

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

	li:before {
		content: initial;
	}

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

	li:hover{
		background-color: #ddd;
	}
	
	li:hover button{
		display: block;
	}
</style>

3.12 Todo-list案例和nextTick

3.12.1 源代码

在上述 Todo-list案例代码基础上,需要修改i的代码如下:
src/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'
	import MyList from './components/MyList'
	import MyFooter from './components/MyFooter'

	export default {
		name:'App',
		components:{MyHeader,MyList,MyFooter},
		data() {
			return {
				//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
				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
			updateTodo(id,title){
				this.todos.forEach((todo)=>{
					if(todo.id === id) todo.title = title
				})
			},
			//删除一个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)=>{
					return !todo.done
				})
			}
		},
		watch: {
			todos:{
				deep:true,
				handler(value){
					localStorage.setItem('todos',JSON.stringify(value))
				}
			}
		},
		mounted() {
			this.$bus.$on('checkTodo',this.checkTodo)
			this.$bus.$on('updateTodo',this.updateTodo)
			this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
		},
		beforeDestroy() {
			this.$bus.$off('checkTodo')
			this.$bus.$off('updateTodo')
			pubsub.unsubscribe(this.pubId)
		},
	}
</script>

<style>
	/*base*/
	body {
		background: #fff;
	}
	.btn {
		display: inline-block;
		padding: 4px 12px;
		margin-bottom: 0;
		font-size: 14px;
		line-height: 20px;
		text-align: center;
		vertical-align: middle;
		cursor: pointer;
		box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
		border-radius: 4px;
	}
	.btn-danger {
		color: #fff;
		background-color: #da4f49;
		border: 1px solid #bd362f;
	}
	.btn-edit {
		color: #fff;
		background-color: skyblue;
		border: 1px solid rgb(103, 159, 180);
		margin-right: 5px;
	}
	.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>

src/components/MyItem.vue

<template>
	<li>
		<label>
			<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
			<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
			<!-- <input type="checkbox" v-model="todo.done"/> -->
			<span v-show="!todo.isEdit">{{todo.title}}</span>
			<input 
				type="text" 
				v-show="todo.isEdit" 
				:value="todo.title" 
				// 这是失去焦点事件
				@blur="handleBlur(todo,$event)"
				ref="inputTitle"
			>
		</label>
		<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
		<button v-show="!todo.isEdit" class="btn btn-edit" @click="handleEdit(todo)">编辑</button>
	</li>
</template>

<script>
	import pubsub from 'pubsub-js'
	export default {
		name:'MyItem',
		//声明接收todo
		props:['todo'],
		methods: {
			//勾选or取消勾选
			handleCheck(id){
				//通知App组件将对应的todo对象的done值取反
				// this.checkTodo(id)
				this.$bus.$emit('checkTodo',id)
			},
			//删除
			handleDelete(id){
				if(confirm('确定删除吗?')){
					//通知App组件将对应的todo对象删除
					// this.deleteTodo(id)
					// this.$bus.$emit('deleteTodo',id)
					pubsub.publish('deleteTodo',id)
				}
			},
			//编辑
			handleEdit(todo){
				if(todo.hasOwnProperty('isEdit')){
					todo.isEdit = true
				}else{
					// console.log('@')
					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>
	/*item*/
	li {
		list-style: none;
		height: 36px;
		line-height: 36px;
		padding: 0 5px;
		border-bottom: 1px solid #ddd;
	}

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

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

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

	li:before {
		content: initial;
	}

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

	li:hover{
		background-color: #ddd;
	}
	
	li:hover button{
		display: block;
	}
</style>

3.12.1 代码细节问题

细节问题1
由于右浮动的影响,删除按钮在最右边,编辑按钮在左边
在这里插入图片描述
细节问题2
输入框和文字使用v-show,因为可能切换比较频繁,v-if会涉及到元素的销毁和重新创建,而v-show只是使用display属性

  • 初始时,isEdit不存在,为undefined,取反为真,所以输入框不显示,文字显示;
    isEdit存在后,通过true和false控制 。

在这里插入图片描述
细节问题3
其细节如下:

  • 没有isEdit时,必须使用vm.$set去设置isEdit,这样Vue才能监测isEdit的值。
  • isEdit存在后,直接修改即可,使用todo.hasOwnProperty(‘isEdit’)来控制。

在这里插入图片描述
细节问题4
代码的作用是,失去焦点时:

  • todo.isEdit = false,文字显示,输入框隐藏
  • 触发全局事件总线,更新数据
    在这里插入图片描述
    这是存在焦点,可以看到光标闪烁,如下:
    在这里插入图片描述
    在空白处点一下,失去焦点,光标消失,如下:
    在这里插入图片描述

3.12.2 nextTick

我们希望点击编辑按钮时,就可以自动获取焦点。

假设像下面这么改,是无效的,原因如下:走完todo.isEdit = true这句代码后,Vue并没有重新解析模板,而是继续执行this.$refs.inputTitle.focus(),由于使用v-show控制输入框,所以执行this.$refs.inputTitle.focus()时,输入框还没出现。

  • 这是一个效率问题,如果每句代码都改点东西,例如第一句改todo.isEdit,第二句又改其它的…那每一句代码都重新解析模板,效率太低了。

在这里插入图片描述
解决方法是在外面包一层nextTick,如下:
它的作用是:包了一层nextTick,就代表执行到它的时候,等到它重新解析模板之后,再去调用this.$refs.inputTitle.focus()。

在这里插入图片描述
当然也可以用定时器解决,如下:

  • 就算是定时器立即到点,也会推向队列执行,相当于异步,也能实现。
    在这里插入图片描述

解决方法是在外面包一层nextTick,如下:

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

3.13 动画与过度

3.13.1 源代码

src/App.vue

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

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

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

src/components/Test.vue(动画)

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

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

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

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

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

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

src/components/Test2.vue(过渡)

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

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

<style scoped>
	h1{
		background-color: orange;
	}
	/* 进入的起点、离开的终点 */
	.hello-enter,.hello-leave-to{
		transform: translateX(-100%);
	}
	.hello-enter-active,.hello-leave-active{
		transition: 0.5s linear;
	}
	/* 进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{
		transform: translateX(0);
	}

</style>

src/components/Test3.vue(第三方动画)

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

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

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

</style>

3.13.2 动画代码解析

需求是切换要有动画效果,不显示时向左滑动离开,显示时向右滑动出现。

我们写向右滑动出现,即从左边远方而来,定义关键帧,如下:

  • 初始帧为负代表在X轴原点的左边,而最后一帧为0,代表恰好在X轴原点,即h1标签最左边边框位于原点。

在这里插入图片描述

要用Vue控制动画,首先要用transition包裹h1(h1要展示动画效果),如下:

在这里插入图片描述
回顾一下是常用的动画属性:

  • animation-name
  • animation-duration
  • animation-delay
  • animation-timing-function
  • animation-iteration-count
  • animation-direction
  • animatiom-fill-mode
  • animation-play-state

在使用动画的复合属性时,除了**animation-duration(动画持续时间)animation-delay(动画延迟时间)**要注意顺序,其它都不需要注意顺序

然后要使用Vue规定好的class名字,来的时候,即从左边远方而来为v-enter-active,离开则用v-leave-active,如下:
在这里插入图片描述
当然这个名字也可以自定义,如下:
在这里插入图片描述
在这里插入图片描述
但是还有个细节,最好是页面刚打开时,h1(你好啊!)就是从向右滑动出现,即从左边远方而来,这时候就要使用appear,如下:

在这里插入图片描述
当然也可以写:appear="true”,这样写就代表transition标签有appear但,没必要这么麻烦,:appear="false”就代表transition标签没有appear。

平时添加属性也是,如果像下述这么写,就代表没有x
在这里插入图片描述
从下图可知,不存在x
在这里插入图片描述

3.13.3 过渡代码解析

使用过渡效果时,要定义进入的起点、进入的终点以及离开的起点、离开的终点 。

首先transition标签的名字自定义为hello

以进入的起点、进入的终点为例,如下图理解,橙色的方框就是进入的起点,绿色的方框就是进入的终点,而离开的起点、离开的终点刚好反过来,如下:
在这里插入图片描述
具体的代码如下:
在这里插入图片描述
回顾一下过渡常用的属性:

  • transition-duration
  • transition-property
  • transition-delay
  • transition-timing-function

在使用过渡的复合属性时,除了**transition-duration(过渡持续时间)transition-delay(过渡延迟时间)**要注意顺序,其它都不需要注意顺序。

然后就是写transition复合属性,一种方法是写在h1里,如下:
在这里插入图片描述
但最好还是别破坏结构,因此换一种方式,如下红框:
在这里插入图片描述
可以发现动画只是不需要进入的起点、进入的终点以及离开的起点、离开的终点 ,这是因为定义了关键帧,其余Vue操作都和跟过渡差别不大。

还有一个细节问题,当点击离开时,可以发现,当h1要离开的时候,加上了内联样式:hello-leave-active、hello-leave-to。但唯独没有hello-leave,即离开的起点,这是因为速度极快,离开的起点是瞬间加上,下一帧瞬间就移除了,进入的起点也同理。
在这里插入图片描述
当要过渡多个元素时,一定要有key,写法如下:

  • 可以看到,这个代码实现了两个h1的互斥效果,当点击按钮时,一个离开,一个出现(通过v-show不同赋值实现)

在这里插入图片描述
最好不要包在盒子里,这样有一些功能是无法实现的,例如上述所说的两个h1互斥效果。
在这里插入图片描述

3.13.4 第三方动画代码解析

使用下述这个库
在这里插入图片描述)

在这里插入图片描述

根据要求,要使用animate__animated animate__bounce这两个类,如下:
在这里插入图片描述
放到如下所示的地方:
在这里插入图片描述
例如要用到backoutUp,直接复制,如下所示:

在这里插入图片描述
然后放到如下所示地方(animate__swing同理):
在这里插入图片描述

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

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

相关文章

去除install4j学习版生成的安装程序和主程序的neg弹窗的解决思路

文章目录 去除install4j学习版生成的安装程序和主程序的neg弹窗的解决思路概述笔记打补丁之前打补丁之后 效果备注 END 去除install4j学习版生成的安装程序和主程序的neg弹窗的解决思路 概述 最近可能有修改openpnp源码并打包的需求。 openpnp2.2 用 install4j 10.0.5 来打包…

前后端服务配置

1、安装虚拟机&#xff08;VirtualBox或者vmware&#xff09;&#xff0c;在虚拟机上配置centos(选择你需要的Linux版本)&#xff0c;配置如nginx服务器等 1.1 VMware 下载路径Sign In注册下载 1.2 VirtualBox 下载路径https://www.virtualbox.org/wiki/Downloads 2、配置服…

基于javaweb的SpringBoot+MyBatis毕业设计选题答辩管理系统(源码+文档+部署讲解)

&#x1f3ac; 秋野酱&#xff1a;《个人主页》 &#x1f525; 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 运行环境开发工具技术框架适用功能说明eclipse/MyEclipse运行&#xff1a; ![在这里插入图片描述](https://i-blog.csdnimg.cn/dir…

【Elasticsearch】nested聚合

在 Elasticsearch 中&#xff0c;嵌套聚合&#xff08;nestedaggregation&#xff09;的语法形式用于对嵌套字段&#xff08;nestedfields&#xff09;进行聚合操作。嵌套字段是 Elasticsearch 中的一种特殊字段类型&#xff0c;用于存储数组中的对象&#xff0c;这些对象需要独…

Linux第106步_Linux内核RTC驱动实验

1、了解rtc_device结构体 1)、打开“include/linux/rtc.h” rtc_class_ops是需要用户根据所使用的RTC设备编写的,其结构体如下: struct rtc_class_ops { int (*ioctl)(struct device *, unsigned int, unsigned long);/*函数指针ioctl*/ int (*read_time)(struct device *,…

微信小程序案例2——天气微信小程序(学会绑定数据)

文章目录 一、项目步骤1 创建一个weather项目2 进入index.wxml、index.js、index.wxss文件,清空所有内容,进入App.json,修改导航栏标题为“中国天气网”。3进入index.wxml,进行当天天气情况的界面布局,包括温度、最低温、最高温、天气情况、城市、星期、风行情况,代码如下…

Linux系统-centos防火墙firewalld详解

Linux系统-centos7.6 防火墙firewalld详解 1 firewalld了解 CentOS 7.6默认的防火墙管理工具是firewalld&#xff0c;它取代了之前的iptables防火墙。firewalld属于典型的包过滤防火墙或称之为网络层防火墙&#xff0c;与iptables一样&#xff0c;都是用来管理防火墙的工具&a…

Sealos的k8s高可用集群搭建

Sealos 介绍](https://sealos.io/zh-Hans/docs/Intro) Sealos 是一个 Go 语言开发的简单干净且轻量的 Kubernetes 集群部署工具&#xff0c;能很好的支持在生产环境中部署高可用的 Kubernetes 集群。 Sealos 特性与优势 支持离线安装&#xff0c;工具与部署资源包分离&#…

算法篇——动态规划

核心思想&#xff1a; 将问题分解为重叠的子问题&#xff0c;并储存子问题的解&#xff08;使用字典、数组或哈希表&#xff09;&#xff0c;避免重复计算&#xff0c;从而提高效率。 题目特点&#xff1a;重叠子问题&#xff08;特殊地&#xff0c;是最优子结构&#xff09; …

一个基于ESP32S3和INMP441麦克风实现音频强度控制RGB灯带律动的代码及效果展示

一个基于ESP32S3和INMP441麦克风实现音频强度控制RGB灯带律动的代码示例&#xff0c;使用Arduino语言&#xff1a; 硬件连接 INMP441 VCC → ESP32的3.3VINMP441 GND → ESP32的GNDINMP441 SCK → ESP32的GPIO 17INMP441 WS → ESP32的GPIO 18INMP441 SD → ESP32的GPIO 16RG…

零基础学习书生.浦语大模型--基础岛

第二关:玩转书生[多模态对话]和[AI搜索]产品 任务一&#xff1a;使用MindSearch 任务二&#xff1a;尝试使用书生.浦语 尝试让其写一段Self-Attention网络模块代码 import torch import torch.nn as nn import torch.nn.functional as Fclass SelfAttention(nn.Module):def _…

AWS Fargate

AWS Fargate 是一个由 Amazon Web Services (AWS) 提供的无服务器容器计算引擎。它使开发者能够运行容器化应用程序&#xff0c;而无需管理底层的服务器或虚拟机。简而言之&#xff0c;AWS Fargate 让你只需关注应用的容器本身&#xff0c;而不需要管理运行容器的基础设施&…

启明星辰发布MAF大模型应用防火墙产品,提升DeepSeek类企业用户安全

2月7日&#xff0c;启明星辰面向DeepSeek等企业级大模型业务服务者提供的安全防护产品——天清MAF&#xff08;Model Application Firewall&#xff09;大模型应用防火墙产品正式发布。 一个新赛道将被开启…… DeepSeek的低成本引爆赛道规模 随着DeepSeek成为当前最热的现象级…

Excel大数据量导入导出

github源码 地址&#xff08;更详细&#xff09; : https://github.com/alibaba/easyexcel 文档&#xff1a;读Excel&#xff08;文档已经迁移&#xff09; B 站视频 : https://www.bilibili.com/video/BV1Ff4y1U7Qc 一、JAVA解析EXCEL工具EasyExcel Java解析、生成Excel比较…

Coze(扣子)+ Deepseek:多Agents智能体协作开发新范式

前言 在当今数字化浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;技术的迅猛发展正深刻改变着我们的生活和工作方式。从智能语音助手到自动化流程机器人&#xff0c;AI 的应用无处不在&#xff0c;为我们提供了更加便捷、高效的服务。然而&#xff0c;对于非专业人士来…

Spring AI -使用Spring快速开发ChatGPT应用

前言 Spring在Java生态中一直占据大半江山。最近我发现Spring社区推出了一个Spring AI项目&#xff0c;目前该项目还属于Spring实验性项目&#xff0c;但是我们可以通过该项目&#xff0c;可以非常快速的开发出GPT对话应用。 本篇文章将会对SpringAI进行简单的介绍和使用&#…

Unity项目接入xLua的一种流程

1. 导入xlua 首先导入xlua&#xff0c;这个不用多说 2. 编写C#和Lua交互脚本 基础版本&#xff0c;即xlua自带的版本 using System.Collections; using System.Collections.Generic; using UnityEngine; using XLua; using System; using System.IO;[Serializable] public…

LM Studio 部署本地大语言模型

一、下载安装 1.搜索&#xff1a;lm studio LM Studio - Discover, download, and run local LLMs 2.下载 3.安装 4.更改成中文 二、下载模型(软件内下载) 1.选择使用代理&#xff0c;否则无法下载 2.更改模型下载目录 默认下载位置 C:\Users\用户名\.lmstudio\models 3.搜…

route 与 router 之间的差别

简述&#xff1a; router&#xff1a;主要用于处理一些动作&#xff0c; route&#xff1a;主要获得或处理一些数据&#xff0c;比如地址、参数等 例&#xff1a; videoInfo1.vue&#xff1a; <template><div class"video-info"><h3>二级组件…

DeepSeek-V2 论文解读:混合专家架构的新突破

论文链接&#xff1a;DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model 目录 一、引言二、模型架构&#xff08;一&#xff09;多头部潜在注意力&#xff08;MLA&#xff09;&#xff1a;重塑推理效率&#xff08;二&#xff09;DeepSeekM…