第3章 使用 Vue 脚手架

第3章 使用 Vue 脚手架

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

这个过程可能会发生警告,可以直接忽略。如果卡住了,则直接按回车即可

如图所示,安装完毕

第二步:切换到你要创建项目的目录,然后使用命令创建项目

在桌面创建项目,使用命令: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 总结

python 复制代码
.文件目录
├── 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的代码和总结如下

javascript 复制代码
/* 
	该文件是整个项目的入口文件
*/
//引入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函数的完整形式和简写形式

javascript 复制代码
// 简写形式
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

javascript 复制代码
//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

javascript 复制代码
//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:负责汇总所有组件

javascript 复制代码
<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也无所谓,只是为了美观,不影响脚手架的使用
javascript 复制代码
<!-- 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组件

javascript 复制代码
<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组件

javascript 复制代码
<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. 第三种方式(限制类型、限制必要性、指定默认值):

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

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

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

例子

src/App.vue组件

  • 这里有个细节,age要用数据绑定,以确保18不是字符串
javascript 复制代码
<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

javascript 复制代码
<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. 使用方式:

定义混入

javascript 复制代码
const mixin = {
    data(){....},
    methods:{....}
    ....
}

使用混入

  • 全局混入:Vue.mixin(xxx)
  • 局部混入:mixins:'xxx'
  1. 备注:

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

javascript 复制代码
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" }
  	}
})

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

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

具体使用方法例子

src/mixin.js

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

src/components/School.vue

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
//引入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. 定义插件:

javascript 复制代码
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

python 复制代码
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

javascript 复制代码
//引入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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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这个名字也不会冲突

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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()方法

javascript 复制代码
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专门用于条件统计,因此非常适合底部统计。

举个例子,如下:

javascript 复制代码
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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

第一种写法

javascript 复制代码
<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控制,当所有任务都被删除时,底部不显示

第二种写法

javascript 复制代码
<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

存储字符串

javascript 复制代码
localStorage.setItem('msg','hello!')

结果如下所示:

存储数字

javascript 复制代码
localStorage.setItem('msg2',666)

结果如下所示:666在存储的时候是一个字符串,而不是数字,如果是数字,它显示的颜色应该是蓝色的。

存储对象

如果只是直接输入对象,如下所示:

但是结果如下:这时候存储的对象有问题,自动转成字符串了,相当于调用了toString()。

因此我们需要使用JSON.stringify,如下:

它能把对象转成字符串形式,并且能保留里面的内容,如下:

读取对象操作

代码结果如下:

但这并不是我们希望的样子(里面的内容都是灰色的,说明是字符串),所以需要使用JSON.parse,用于将 JSON 格式的字符串转换为 JavaScript 对象,如下:

最终结果如下:里面的内容都是正确的格式了

移除操作并不难,直接看代码即可。

源代码如下:

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存储的内容,需要手动清除才会消失!

直接看代码学习即可:

javascript 复制代码
<!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

javascript 复制代码
<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. 第二种方式,在父组件中:
javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
//引入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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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组件自身
javascript 复制代码
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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
<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(动画)

javascript 复制代码
<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(过渡)

javascript 复制代码
<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(第三方动画)

javascript 复制代码
<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同理):

相关推荐
weixin_3823952335 分钟前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__41 分钟前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.1 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~2 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华3 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子5 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅5 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni6 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学7 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端