组件 (Component) 是 Vue.js 最强大的功能之一,组件可以扩展 HTML 元素,封装可重用的代码。
1 - 组件化规范:Web Components
我们自定义组件的问题:
- 我们希望尽可能多的重用代码
- 自定义组件的方式不太容易(需要写html、css和js代码)
- 多次使用组件可能导致冲突
Web Components 通过创建封装好功能的定制元素解决上述问题,官网:https://developer.mozilla.org/zh-CN/docs/Web/Web_Components,虽然这个规范还没有被浏览器广泛支持,但是Vue部分实现了上述规范。
下面我们介绍的组件是传统组件,以后我们会学到单文件组件,以后我们基本上都使用单文件组件。
2 - 全局组件
Vue.component('组件名称', {
data: function(){
return {
msg: '组件数据'
}
},
template: '<div>{{组件模板内容}}</div>'
});
全局组件注册后,任何vue实例都可以用。
组件注意事项:
-
组件参数data值必须是函数,同时这个函数要求返回一个对象。
因为只有是函数,才能保证函数中的数据是一个独立的封闭环境,这样各个组件的数据才互不影响。
其实vue实例也是一个组件,只不过它的data是个对象,而我们自定义的组件的data是个函数,函数返回一个对象。 -
组件模板template必须是单个根元素。
就是模板必须是一个根标签,至于根标签里面什么内容就随你了。其实vue实例也是一个组件,比如下面代码它也有一个根标签:<div id="app">。 -
组件模板的内容可以使用模板字符串。
因为如果模板内容比较多,都写在一行肯定看不懂,所以可以使用模板字符串,模板字符串需要浏览器的支持,因为是ES6语法,模板字符串使用反引号 ``。 - 组件命名方式:
短横线方式 Vue.component('my-component', { /* ... */ }),使用这个组件的时候也要是短横线。
驼峰方式 Vue.component('MyComponent', { /* ... */ }),使用这个组件的时候可以是短横线也可以是驼峰。
推荐使用驼峰方式。

<div id="app">
<!--
3. 使用组件
组件可以重复使用多次
因为data中返回的是一个对象所以每个组件中的数据是私有的
即每个实例可以维护一份被返回对象的独立的拷贝
-->
<buttonCounter></buttonCounter>
<buttonCounter></buttonCounter>
<buttonCounter></buttonCounter>
</div>
<script type="text/javascript">
// 2. 注册全局组件
Vue.component('buttonCounter', {
// 1. 定义全局组件
// 组件参数的data值必须是函数,同时这个函数要求返回一个对象
data: function(){
return {
count: 0
}
},
// 组件模板必须是单个根元素
// 组件模板的内容可以是模板字符串
// 注意:@click="handle"这里要使用双引号,因为模板使用的是单引号`
template: `
<div>
<button @click="handle">点击了{{count}}次</button>
</div>
`,
methods: {
handle: function(){
this.count += 2;
}
}
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
3 - 局部组件
局部组件只能在当前注册它的vue实例(父组件)中使用,其他地方是使用不了的,比如如果再定义一个全局组件,在这个全局组件中就不能使用下面定义的局部组件HelloWorld,因为局部组件HelloWorld只能在id="app"父组件中使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 3. 局部组件只能在注册他的父组件中使用 -->
<HelloWorld></HelloWorld>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
// 1. 定义局部组件
var HelloWorld = {
data: function(){
return {
msg: 'HelloWorld'
}
},
template: '<div>{{msg}}</div>'
};
var vm = new Vue({
el: '#app',
data: {
}
// 2. 注册局部组件
components: {
'HelloWorld': HelloWorld,
}
});
</script>
</body>
</html>
4 - Vue调试工具
组件最终也会渲染成原生的DOM元素,但是我们并不知道哪些元素属于这个组件,哪些元素属于那个组件,为了更直观的展示组件之间的层次关系,并且对组件的数据进行调试,我们可以借助Vue调试工具。
1. Vue调试工具安装

// ① 下载调试工具包
到 https://github.com/vuejs/vue-devtools.git
下载调试工具包,记住一定要下正式版,比如选择Tags/v5.1.1
// ② 安装依赖包
cd /Users/zjnx1111/Desktop/vue-devtools
sudo npm install
// ③ 构建
npm run build
// ④ 点击浏览器右上 ···-更过工具-扩展程序,打开Chrome扩展页面
// ⑤ 打开开发者模式
// ⑥ 加载已解压的扩展程序,选择shells/chrome即可
选择之后如果扩展程序中有了,那么就代表安装成功,如下图:

2. 调试工具用法

右键检查之后,点击Vue,在左边就可以看到组件之间的层次关系,并且在右边可以直接看到并且修改组件的数据,组件的数据修改之后,页面的内容也会变化。
5 - Vue组件之间传值
1. 父组件向子组件传值
① 使用props传值
父组件发送的数据是以属性绑定(v-bind:)的形式绑定值到子组件身上,然后子组件用属性props接收。
<div id="app">
<div>{{pmsg}}</div>
<!-- menuItem 在"app"中嵌套着,故menuItem为子组件 -->
<!-- 1. 如果加冒号:,就是需要动态数据的时候,需要动态属性绑定的形式设置,此时ptitle来自父组件data中的 ptitle数据,传的值可以是数字、对象、数组等等
-->
<!-- 2. 如果不加冒号:,代表给子组件传入一个写死静态的值 -->
<!-- :title='ptitle' 就是 v-bind:title='ptitle' -->
<menuItem :title='ptitle' content='hello'></menuItem>
</div>
<script type="text/javascript">
Vue.component('menuItem', {
// 3. 子组件用属性props接收父组件传递过来的数据
props: ['title', 'content'],
data: function() {
return {
msg: '子组件本身的数据'
}
},
template: '<div>{{msg + "----" + title + "-----" + content}}</div>'
// 子组件本身的数据----动态绑定属性----hello
});
var vm = new Vue({
el: '#app',
data: {
// 父组件的数据
pmsg: '父组件中内容',
ptitle: '动态绑定属性'
}
});
</script>
注意:上面我们说了组件命名推荐使用驼峰方式,props属性名也一样,也是推荐使用驼峰方式。
② props属性值类型
props属性值类型可以是String、Number、Boolean、Array、Object。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<div>{{pmsg}}</div>
<!--
注意:对于数值和布尔值,如果加上冒号就是v-bind绑定,会保留原来的类型(数值或者布尔值),如果不加冒号就是自定义属性,就是字符串。
比如下面,:pnum='12',就是数值,所以在模板中可以使用 12 + pnum 进行计算。
对于 pboo='true',就是字符串,打印类型 typeof pboo 也是字符串。
如果写成 pboo='aaa', 因为不是属性绑定, 那么就是字符串'aaa',字符串的bool值也是真,所以这样写也生效,但是是错误的。
所以切记不能忘记加冒号:。
-->
<menuItem :pstr='pstr' :pnum='12' pboo='true' :parr='parr' :pobj='pobj'></menuItem>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
// props属性值类型
Vue.component('menuItem', {
props: ['pstr','pnum','pboo','parr','pobj'],
template: `
<div>
# 传递字符串
<div>{{pstr}}</div>
# 传递数值
<div>{{12 + pnum}}</div>
# 传递Bool
<div>{{typeof pboo}}</div>
<ul>
# 传递数组
<li :key='index' v-for='(item,index) in parr'>{{item}}</li>
</ul>
# 传递对象
<span>{{pobj.name}}</span>
<span>{{pobj.age}}</span>
</div>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中内容',
pstr: 'hello',
parr: ['apple','orange','banana'],
pobj: {
name: 'lisi',
age: 12
}
}
});
</script>
</body>
</html>

注意:上面把props放到数组中的方式是以前旧的方式,虽然可以传递String、Number、Boolean、Array、Object类型的数据,但是每个props具体的类型还是没那么清晰的看到,所以现在我们都使用如下方式,当props是个对象就可以指定类型和默认值,更清晰明了,推荐下面这种方式。
props: {
inputType: {
type: String,
default: "text" //默认值
},
label: String,
value: String,
placeholder: String,
icon: String, //右边图标的名称
textarea: String, //输入域文字
tags: Array, //tag数组
sex: String //选中的性别
},
2. 子组件向父组件传值
从上面我们知道,父组件可以通过props向子组件传值,那么我们是不是可以在子组件中操作父组件传过来的props值,然后修改,这样父组件的数据也会变化,父组件的UI也会刷新,从而达到子组件向父组件传值的目的呢?其实是可以的(代码就省略了),但是这样数据的逻辑就比较复杂难以控制了,不推荐使用。所以Vue的设计者推荐我们使用:props单向数据流的传递原则,不让你改。
Vue提供了:子组件通过自定义事件向父组件传递信息的方式
- 子组件用
$emit()
触发事件,$emit()
第一个参数为自定义的事件名称,第二个参数为需要传递的数据 - 父组件用 v-on 监听子组件的事件
子组件点击按钮会控制父组件文字的大小,代码如下:
<div id="app">
<div :style='{fontSize: fontSize + "px"}'>{{pmsg}}</div>
<!-- 2. 父组件用 v-on 监听子组件的事件 -->
<menuItem :parr='parr' @enlargeText='enlargeText'></menuItem>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
// 子组件向父组件传值
Vue.component('menuItem', {
props: ['parr'],
emits: ['enlargeText']
template: `
<div>
<ul>
<li :key='index' v-for='(item,index) in parr'>{{item}}</li>
</ul>
# 1. 子组件通过自定义事件向父组件传递信息
<button @click='$emit('enlargeText', {font: 10, inputValue: $event.target.value})'>扩大父组件中字体大小</button>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中内容',
parr: ['apple','orange','banana'],
fontSize: 10
},
methods: {
// 3. 父组件的处理函数处理数据
enlargeText: function(val) {
// 扩大字体大小
this.fontSize += val.font;
// 如果是input框,这样可以拿到输入的值
console.log(val.inputValue);
}
}
});
</script>

注意:上面的methods是使用name:function() {}
的方式,这种方式是ES5的语法,ES6我们可以使用name() {}
的方式(推荐使用)如下:
methods: {
// 不加function
enlargeText(val) {
this.fontSize += val.font;
console.log(val.inputValue);
}
}
3. 兄弟组件之间的传递

- 兄弟之间传递数据需要通过事件中心,提供事件中心 var hub = new Vue()
- 传递数据方,通过一个事件触发hub.$emit('方法名',传递的数据)
- 接收数据方,通过mounted(){} 钩子中,触发hub.$on('方法名',传递的数据)方法
- 销毁事件,通过hub.$off('方法名')销毁之后无法进行传递数据
<div id="app">
<div>父组件</div>
<div>
<button @click='handleDestory'>点击销毁事件</button>
</div>
<!-- 兄弟组件Tom -->
<testTom></testTom>
<!-- 兄弟组件Jerry -->
<testJerry></testJerry>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
兄弟组件之间数据传递
*/
// 1、提供事件中心
var hub = new Vue();
// 兄弟组件Tom
Vue.component('testTom', {
data: function(){
return {
num: 0
}
},
template: `
<div>
<div>TOM:{{num}}</div>
<div>
<button @click='handleEvent'>点击传递事件</button>
</div>
</div>
`,
methods: {
handleEvent: function(){
// 2、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据) 触发兄弟组件的事件
hub.$emit('jerryEvent', 2);
}
},
mounted: function() {
// 5、接收数据方,通过mounted(){} 钩子中 触发hub.$on(方法名
hub.$on('tomEvent', (val) => {
this.num += val;
});
}
});
// 兄弟组件Jerry
Vue.component('testJerry', {
data: function(){
return {
num: 0
}
},
template: `
<div>
<div>JERRY:{{num}}</div>
<div>
<button @click='handleEvent'>点击传递事件</button>
</div>
</div>
`,
methods: {
handleEvent: function(){
// 4、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据) 触发兄弟组件的事件
hub.$emit('tomEvent', 1);
}
},
mounted: function() {
// 3、接收数据方,通过mounted(){} 钩子中 触发hub.$on()方法名
// 箭头函数中this指向父级作用域的this,在这里就是vm实例,如果使用function函数,this就指向window了
hub.$on('jerryEvent', (val) => {
this.num += val;
});
}
});
var vm = new Vue({
el: '#app',
data: {
},
methods: {
handleDestory: function(){
// 6、销毁事件 通过hub.$off('方法名')销毁之后无法进行传递数据
hub.$off('tomEvent');
hub.$off('jerryEvent');
}
}
});
</script>

兄弟组件之间的传值我们很少使用,因为我们有Vuex。
6 - 组件插槽
有时候一些子组件很相似,如果写成不同的组件,可能又会很浪费,所以我们用到了组件插槽。
组件插槽的作用:父组件向子组件传递内容,这里的内容指的是模板内容。
组件插槽就相当于一个在子组件模板中的占位符,父组件使用子组件的时候再指定占位符的内容是什么,比如下面的子组件就会把<slot></slot>替换成hi、hello、haha等。
组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力。

1. 匿名插槽
匿名插槽就是没有名字的插槽,使用匿名插槽可以设置默认内容。
<div id="app">
<!-- 2. 插槽内容 -->
<!-- 这里的所有组件标签中嵌套的内容会替换掉slot,如果没值,则使用slot中的默认值 -->
<alertBox>有bug发生</alertBox>
<alertBox>有一个警告</alertBox>
<alertBox></alertBox>
</div>
<script type="text/javascript">
/*
匿名插槽
*/
Vue.component('alertBox', {
template: `
<div>
<strong>ERROR:</strong>
# 1. 插槽定义位置:子组件中
# 当组件渲染的时候,这个 <slot> 元素将会被替换为“组件标签中嵌套的内容”。
# 插槽内可以包含任何模板代码,包括 HTML
<slot>默认内容</slot>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>

2. 具名插槽:name
具名插槽就是具有名字的插槽,使用 <slot> 中的 "name" 属性绑定元素。因为有名字了,当有多个插槽的时候就能把不同的内容插入不同的插槽。
<div id="app">
<baseLayout>
<!-- 2、插槽内容 -->
<!-- 通过slot属性来指定, 这个slot的值必须和下面slot组件得name值对应上,如果没有匹配到,则放到匿名的插槽中 -->
<p slot='header'>标题信息</p>
<p>主要内容1</p>
<p>主要内容2</p>
<p slot='footer'>底部信息信息</p>
</baseLayout>
<!-- 结果如下图:header里面有一个p,main里面有两个p,footer里面有一个p -->
<!-- 3、如果想往插槽中填充多个标签就可以使用template包裹一下 -->
<baseLayout>
<!-- 注意:template临时的包裹标签最终不会渲染到页面上,只是包裹一下 -->
<template slot='header'>
<p>标题信息1</p>
<p>标题信息2</p>
</template>
<p>主要内容1</p>
<p>主要内容2</p>
<template slot='footer'>
<p>底部信息信息1</p>
<p>底部信息信息2</p>
</template>
</baseLayout>
<!-- 结果如下图:header里面有两个p,main里面有两个p,footer里面有两个p -->
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
具名插槽
*/
Vue.component('baseLayout', {
template: `
<div>
<header>
### 1、插槽定义位置:子组件中。使用 <slot> 中的 "name" 属性绑定元素 指定当前插槽的名字
<slot name='header'></slot>
</header>
<main>
### 匿名插槽
<slot></slot>
</main>
<footer>
### 注意:具名插槽的渲染顺序,完全取决于模板,而不是取决于父组件中元素的顺序
<slot name='footer'></slot>
</footer>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>

3. 作用域插槽:slot-scope
应用场景:父组件对子组件的内容进行加工处理,既可以复用子组件的slot,又可以使slot内容不一致。
比如:当我们希望li的样式由外部使用组件的地方控制(比如希望某一个li高亮显示),因为可能有多种地方要使用该组件,而且希望样式不一样,这个时候我们需要使用作用域插槽。
<div id="app">
<fruitList :list='list'> <!-- 父组件向子组件传值 --->
<!-- 2. 插槽内容 -->
<!-- 父组件中使用了<template>元素,而且包含slot-scope='slotProps',slotProps在这里只是临时变量,用来获取子组件的item数据
--->
<template slot-scope='slotProps'>
<!-- 如果id为2,就设置class为current -->
<strong v-if='slotProps.info.id==3' class="current">
{{slotProps.info.name}}
</strong>
<!-- 否则直接显示name -->
<span v-else>{{slotProps.info.name}}</span>
</template>
</fruitList>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
作用域插槽
*/
Vue.component('fruitList', {
props: ['list'],
template: `
<div>
<li :key='item.id' v-for='item in list'>
### 1. 插槽定义位置:子组件中
### 在子组件模板中,绑定属性 :info='item' 其中info是自定义的,在父组件中使用slotProps.info可以取到,item就是上面的item。
<slot :info='item'>我是默认内容</slot>
</li>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
list: [{
id: 0,
name: 'apple'
},{
id: 1,
name: 'orange'
},{
id: 2,
name: 'banana'
}]
}
});
</script>

7 - 购物车案例
代码地址:Vue案例

1. 实现组件化布局
- 编写静态页面代码
- 把静态页面转换成组件化模式
- 把组件渲染到页面上
购物车组件是个大组件,里面有三个小组件,分别是标题组件、商品列表组件、商品结算组件。
<div id="app">
<div class="container">
<!-- 2、把组件渲染到页面上 -->
<my-cart></my-cart>
</div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
# 1、 把静态页面转换成组件化模式
# 1.1 标题组件
var CartTitle = {
template: `
<div class="title">我的商品</div>
`
}
# 1.2 商品列表组件
var CartList = {
# 注意点 :组件模板必须是单个根元素
template: `
<div>
<div class="item">
<img src="img/a.jpg"/>
<div class="name"></div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
<div class="del">×</div>
</div>
<div class="item">
<img src="img/b.jpg"/>
<div class="name"></div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
<div class="del">×</div>
</div>
<div class="item">
<img src="img/c.jpg"/>
<div class="name"></div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
<div class="del">×</div>
</div>
<div class="item">
<img src="img/d.jpg"/>
<div class="name"></div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
<div class="del">×</div>
</div>
<div class="item">
<img src="img/e.jpg"/>
<div class="name"></div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
<div class="del">×</div>
</div>
</div>
`
}
# 1.3 商品结算组件
var CartTotal = {
template: `
<div class="total">
<span>总价:123</span>
<button>结算</button>
</div>
`
}
## 1.4 定义一个全局组件 my-cart
Vue.component('my-cart',{
## 1.6 引入子组件
template: `
<div class='cart'>
<cart-title></cart-title>
<cart-list></cart-list>
<cart-total></cart-total>
</div>
`,
# 1.5 注册局部子组件
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
}
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
2. 实现标题和结算功能组件
- 从父组件把标题数据传递过来,即父向子组件传值,把传递过来的数据渲染到标题组件上
- 从父组件把商品列表list 数据传递过来,即父向子组件传值,把传递过来的数据计算最终价格渲染到结算功能组件上
使用到了父组件向子组件传值。
<div id="app">
<div class="container">
<my-cart></my-cart>
</div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
var CartTitle = {
# 3.1 标题组件 子组件通过props形式接收父组件传递过来的uname数据
props: ['uname'],
# 3.2 将父组件传过来的uname渲染到页面上
template: `
<div class="title">{{uname}}的商品</div>
`
}
var CartTotal = {
# 4.1 商品结算组件 子组件通过props形式接收父组件传递过来的list数据
props: ['list'],
template: `
<div class="total">
# 4.3 将计算属性的值渲染到页面上
<span>总价:{{total}}</span>
<button>结算</button>
</div>
`,
computed: {
# 4.2 这里使用计算属性,计算商品的总价
total: function() {
var t = 0;
this.list.forEach(item => {
t += item.price * item.num;
});
return t;
}
}
}
Vue.component('my-cart',{
data: function() {
return {
# 1. 父组件中定义数据
uname: '张三',
list: [{
id: 1,
name: 'TCL彩电',
price: 1000,
num: 1,
img: 'img/a.jpg'
},{
id: 2,
name: '机顶盒',
price: 1000,
num: 1,
img: 'img/b.jpg'
},{
id: 3,
name: '海尔冰箱',
price: 1000,
num: 1,
img: 'img/c.jpg'
},{
id: 4,
name: '小米手机',
price: 1000,
num: 1,
img: 'img/d.jpg'
},{
id: 5,
name: 'PPTV电视',
price: 1000,
num: 2,
img: 'img/e.jpg'
}]
}
},
# 2. 父组件向子组件以属性传递的形式 传递数据
# 向标题组件传递 uname 属性, 向商品结算组件传递 list 属性
template: `
<div class='cart'>
<cart-title :uname='uname'></cart-title>
<cart-list></cart-list>
<cart-total :list='list'></cart-total>
</div>
`,
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
}
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
3. 实现列表组件删除功能
- 将list数组通过父组件传递给列表组件
- 列表组件遍历数组,渲染到界面上
- 点击删除按钮删除对应的数据
- 给按钮添加点击事件,点击按钮把需要删除的id传递给父组件
- 子组件中不推荐操作父组件的数据,因为有可能多个子组件使用父组件的数据,我们需要把数据传递给父组件让父组件自己操作数据
- 父组件删除对应的数据
使用到了父组件向子组件传值和子组件向父组件传值。
<div id="app">
<div class="container">
<my-cart></my-cart>
</div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
var CartTitle = {
props: ['uname'],
template: `
<div class="title">{{uname}}的商品</div>
`
}
var CartList = {
# 2. 通过props拿到传过来的值
props: ['list'],
template: `
<div>
# 3. 把列表数据动态渲染到页面上
<div :key='item.id' v-for='item in list' class="item">
# src属性的绑定
<img :src="item.img"/>
<div class="name">{{item.name}}</div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
# 4. 给按钮添加点击事件
<div class="del" @click='del(item.id)'>×</div>
</div>
</div>
`,
methods: {
del: function(id){
# 5. 通过自定义事件把子组件的id传递给父组件,让父组件操作数据
# 子组件中不推荐操作父组件的数据,因为有可能多个子组件使用父组件的数据
this.$emit('cart-del', id);
}
}
}
var CartTotal = {
props: ['list'],
template: `
<div class="total">
<span>总价:{{total}}</span>
<button>结算</button>
</div>
`,
computed: {
total: function() {
// 计算商品的总价
var t = 0;
this.list.forEach(item => {
t += item.price * item.num;
});
return t;
}
}
}
Vue.component('my-cart',{
data: function() {
return {
uname: '张三',
list: [{
id: 1,
name: 'TCL彩电',
price: 1000,
num: 1,
img: 'img/a.jpg'
},{
id: 2,
name: '机顶盒',
price: 1000,
num: 1,
img: 'img/b.jpg'
},{
id: 3,
name: '海尔冰箱',
price: 1000,
num: 1,
img: 'img/c.jpg'
},{
id: 4,
name: '小米手机',
price: 1000,
num: 1,
img: 'img/d.jpg'
},{
id: 5,
name: 'PPTV电视',
price: 1000,
num: 2,
img: 'img/e.jpg'
}]
}
},
template: `
<div class='cart'>
<cart-title :uname='uname'></cart-title>
# 1. 从父组件把商品列表list 数据传递过来 即 父向子组件传值
# 6. 父组件通过事件绑定,接收子组件传递过来的数据
<cart-list :list='list' @cart-del='delCart($event)'></cart-list>
<cart-total :list='list'></cart-total>
</div>
`,
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
},
methods: {
# 7. 父组件根据id删除list中对应的数据
delCart: function(id) {
// ① 找到id所对应数据的索引
var index = this.list.findIndex(item=>{
return item.id == id;
});
// ② 根据索引删除对应数据
this.list.splice(index, 1);
}
}
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
4. 实现组件更新数据功能-上
- 将输入框中的默认数据动态渲染出来
- 输入框失去焦点的时候更改商品的数量,子组件中不推荐操作数据,把这些数据传递给父组件,让父组件处理这些数据,父组件中接收子组件传递过来的数据并处理
使用到了子组件向父组件传值。
<div id="app">
<div class="container">
<my-cart></my-cart>
</div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
var CartTitle = {
props: ['uname'],
template: `
<div class="title">{{uname}}的商品</div>
`
}
var CartList = {
props: ['list'],
template: `
<div>
<div :key='item.id' v-for='item in list' class="item">
<img :src="item.img"/>
<div class="name">{{item.name}}</div>
<div class="change">
<a href="">-</a>
# 1. 将输入框中的默认数据动态渲染出来,我们的数据来自父组件,原则是不修改父组件的数据,所以不用v-model,直接使用属性绑定的方式:value='item.num'填充数据
# 2. 输入框失去焦点的时候,更改商品的数量,需要将当前商品的id、$event事件对象传过去
<input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
<a href="">+</a>
</div>
<div class="del" @click='del(item.id)'>×</div>
</div>
</div>
`,
methods: {
changeNum: function(id, event){
# 2. 把这些数据传递给父组件,让父组件处理这些数据,第一个参数是函数名,第二个参数是传过去的值,分别是商品id、商品数量
# 子组件中不推荐操作数据,因为别的组件可能也引用了这些数据
this.$emit('change-num', {
id: id,
num: event.target.value
});
},
del: function(id){
// 把id传递给父组件
this.$emit('cart-del', id);
}
}
}
var CartTotal = {
props: ['list'],
template: `
<div class="total">
<span>总价:{{total}}</span>
<button>结算</button>
</div>
`,
computed: {
total: function() {
// 计算商品的总价
var t = 0;
this.list.forEach(item => {
t += item.price * item.num;
});
return t;
}
}
}
Vue.component('my-cart',{
data: function() {
return {
uname: '张三',
list: [{
id: 1,
name: 'TCL彩电',
price: 1000,
num: 1,
img: 'img/a.jpg'
}]
},
template: `
<div class='cart'>
<cart-title :uname='uname'></cart-title>
# 3. 父组件中接收子组件传递过来的数据
<cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
<cart-total :list='list'></cart-total>
</div>
`,
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
},
methods: {
changeNum: function(val) {
// 4. 父组件根据子组件传递过来的数据,更新list中对应的数据
this.list.some(item=>{
if(item.id == val.id) {
item.num = val.num;
// 终止遍历
return true;
}
});
},
delCart: function(id) {
// 根据id删除list中对应的数据
// 找到id所对应数据的索引
var index = this.list.findIndex(item=>{
return item.id == id;
});
// 根据索引删除对应数据
this.list.splice(index, 1);
}
}
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
5. 实现组件更新数据功能-下
- 子组件通过一个标识符来标记对应的用户点击 + - 或者输入框输入的内容
- 父组件拿到标识符更新对应的组件
使用到了子组件向父组件传值。
<body>
<div id="app">
<div class="container">
<my-cart></my-cart>
</div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
var CartTitle = {
props: ['uname'],
template: `
<div class="title">{{uname}}的商品</div>
`
}
var CartList = {
props: ['list'],
template: `
<div>
<div :key='item.id' v-for='item in list' class="item">
<img :src="item.img"/>
<div class="name">{{item.name}}</div>
<div class="change">
# 1. + - 按钮绑定事件 禁止默认行为
<a href="" @click.prevent='sub(item.id)'>-</a>
<input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
<a href="" @click.prevent='add(item.id)'>+</a>
</div>
<div class="del" @click='del(item.id)'>×</div>
</div>
</div>
`,
methods: {
changeNum: function(id, event){
this.$emit('change-num', {
id: id,
// 3. 添加输入的type
type: 'change',
num: event.target.value
});
},
sub: function(id){
# 2. 数量的增加和减少通过父组件来计算,每次都是加1和减1不需要传递数量,父组件需要一个类型来判断是加1还是减1,我们通过type标识符来标记不同的操作
this.$emit('change-num', {
id: id,
type: 'sub'
});
},
add: function(id){
this.$emit('change-num', {
id: id,
type: 'add'
});
},
del: function(id){
// 把id传递给父组件
this.$emit('cart-del', id);
}
}
}
var CartTotal = {
props: ['list'],
template: `
<div class="total">
<span>总价:{{total}}</span>
<button>结算</button>
</div>
`,
computed: {
total: function() {
// 计算商品的总价
var t = 0;
this.list.forEach(item => {
t += item.price * item.num;
});
return t;
}
}
}
Vue.component('my-cart',{
data: function() {
return {
uname: '张三',
list: [{
id: 1,
name: 'TCL彩电',
price: 1000,
num: 1,
img: 'img/a.jpg'
},{
id: 2,
name: '机顶盒',
price: 1000,
num: 1,
img: 'img/b.jpg'
},{
id: 3,
name: '海尔冰箱',
price: 1000,
num: 1,
img: 'img/c.jpg'
},{
id: 4,
name: '小米手机',
price: 1000,
num: 1,
img: 'img/d.jpg'
},{
id: 5,
name: 'PPTV电视',
price: 1000,
num: 2,
img: 'img/e.jpg'
}]
}
},
template: `
<div class='cart'>
<cart-title :uname='uname'></cart-title>
# 4. 父组件通过事件监听,接收子组件的数据
<cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
<cart-total :list='list'></cart-total>
</div>
`,
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
},
methods: {
changeNum: function(val) {
# 5. 更新数据,分为三种情况:输入框变更、加号变更、减号变更
if(val.type=='change') {
// 根据子组件传递过来的数据,跟新list中对应的数据
this.list.some(item=>{
if(item.id == val.id) {
item.num = val.num;
// 终止遍历
return true;
}
});
} else if(val.type=='sub'){
// 减一操作
this.list.some(item=>{
if(item.id == val.id) {
item.num -= 1;
// 终止遍历
return true;
}
});
} else if(val.type=='add'){
// 加一操作
this.list.some(item=>{
if(item.id == val.id) {
item.num += 1;
// 终止遍历
return true;
}
});
}
}
}
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
</body>
</html>
网友评论