一、TodoList
<!DOCTYPE html>
<html>
<head>
<title>vue</title>
<meta charset="utf-8">
<script src="./vue.js"></script>
</head>
<body>
<div id="root">
<input type="text" v-model="task"/>
<button @click="handleClick">提交</button>
<ul>
<li v-for="(item,index) of tasks" :key="index">
{{item}}
</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
task: "",
tasks: []
},
methods: {
handleClick: function() {
this.tasks.push(this.task);
}
}
});
</script>
</body>
</html>
二、组件
vue中的组件其实就是一个vue实例,反过来说,一个vue实例其实也是一个组件,当vue实例中没有定义template
模板属性时,默认会将挂载点中的内容作为模板
1.全局组件
vue的全局组件可通过Vue.component()
来构建
Vue.component('todo-item', {
template: '<li>item<li>'
});
需要注意的是,在vue中,模板内只允许存在一个根节点,以在vue-cli
构建的项目中,使用单文件组件编写模板为例
<template>
<div>……</div>
<div>……</div>
</template>
//有两个根节点所以报错
<template>
<div>
……
<div>……</div>
……
</div>
</template>
//只有一个根节点所以没问题
2.局部组件
vue的局部组件可以通过新建js对象,之后再在vue实例中通过components
属性
注册组件,这样的组件只能在该vue实例管理的dom元素内部使用
var TodoList = {template: '<li>item</li>'}
new Vue({
el: "#root",
components: {
'todo-item': TodoList
}
});
三.传值
1.父向子传值
父组件向子组件传值可以通过在使用组件的标签中通过属性名
进行传递,在组件中通过props
属性来接收,即在vue中,父组件向子组件传值是通过属性进行的
……
<todo-item content="xiaoming"></todo-item>
……
Vue.component('todo-item', {
props: ['content'],
template: '<li>{{content}}</li>'
});
……
2.若需要用到表达式,则可以通过:属性名
或者v-bind:属性名
来使用
……
<todo-item v-for="item of list" :content="item"></todo-item>
……
Vue.component('todo-item', {
props: ['content'],
template: '<li>{{content}}</li>'
});
……
3.单向数据流
在Vue中,有单向数据流的概念,当父组件向子组件(非父子组件也一样)传值时,子组件接收到父组件传递的值后,不可以直接对该值直接进行修改,这是为了避免当传递的值为引用时,多处组件传入同一引用造成相互影响。
如果需要对传进来的值进行修改,可以在组件中使用data
属性制造一个副本,并将传值赋值给这个副本,之后再对副本进行操作
<div id="root">
<v-content :number="0"></content>
</div>
<script>
var content = {
props: ['number'],
template: '<div @click="compute">{{count}}</div>',
data: function () {
return {
count: this.number
}
},
methods: {
compute: function () {
this.count ++;
}
}
};
var vm = new Vue({
el: '#root',
components: {
'v-content': content
}
});
</script>
4.传值数据校验
如果需要对父组件中传入的值进行校验,可以在prop
属性中进行定义
(1).简单校验
props: {
age: Number,
name: String
}
//当出现多类型同时校验时候可以用数组
props: {
age: [number, String]
}
(2).复杂检验
props: {
name: {
type: String,
requied: true,
default: 'OJBK',
validator: function (value) {
return (value.length > 5);
}
}
}
5.prop特性与非prop特性
(1).prop特性是指当父组件通过属性传值,并且子组件中有使用prop属性正确接收传值时,传递的属性不会渲染到页面的元素当中
(2).非prop特性是指当父组件通过属性传值,而子组件中没有使用prop属性正确接收传值时的情况,主要有两点特性,第一是在这种情况下无法调用传递的属性,否则会报错;第二是传递属性会被渲染到组件的模板标签中,例如
<body>
<div id="root">
<v-content :number="0"></content>
</div>
<script>
var content = {
template: '<div>hello</div>'
};
var vm = new Vue({
el: '#root',
components: {
'v-content': content
}
});
</script>
</body>
此时页面中会渲染成<div number="0">hello</div>
6.子组件向父组件传值
需要在子组件中通过this.$emit()
方法进行发布,函数的第一个参数为事件名称,第二个参数为传递的内容,可以传递多个内容;然后在父组件中通过@事件名
进行监听
<!DOCTYPE html>
<html>
<head>
<title>vue</title>
<meta charset="utf-8">
<script src="./vue.js"></script>
</head>
<body>
<div id="root">
<input type="text" v-model="text">
<button @click="handleSubmit">提交</button>
<br/>
<ul>
<todo-list v-for="(item,index) of list"
:key="index" :content="item" :index="index"
@delete="handleDelete">
</todo-list>
</ul>
</div>
<script>
Vue.component('todo-list',{
props: ['content','index'],
template: '<li @click="handleClick">{{content}}</li>',
methods: {
handleClick: function() {
this.$emit('delete',this.index);
}
}
});
new Vue({
el: '#root',
data: {
list: [],
text: ''
},
methods: {
handleSubmit: function() {
this.list.push(this.text);
this.text = '';
},
handleDelete: function(index) {
this.list.splice(index,1);
}
}
});
</script>
</body>
</html>
四、组件绑定原生事件
一般情况下,如果在vue组件中直接绑定事件,则该事件指的是自定义事件而非原生事件,即是通过$emit()
方法进行广播的事件,是无法直接绑定事件触发父组件中的方法的,例如
<body>
<div id="root">
<counter @click="handleClick"></counter>
</div>
<script>
Vue.component('counter', {
template: '<div>hello</div>',
});
var vm = new Vue({
el: '#root',
methods: {
handleClick: function () {
alert("hello");
}
}
})
</script>
</body>
此时无论如何点击,都不会触发handleClick方法。想要在组件中直接绑定原生事件,只要在绑定的事件属性后面加上.native
即
<counter @click.native="handleClick"></counter>
这样当点击组件时就会触发事件,调用handleClick方法
五.发布订阅模式
非父子组件之间传递数据,需要通过发布订阅模式(Bus/总线/观察者模式)来进行。
1.首先需要向Vue的原型prototype
挂载一个bus
属性并给其赋值一个新建的vue实例,这样会使得之后的每一个Vue实例都会有一个bus
属性指向同一个vue实例
2.通过this.bus.$emit()
来广播事件
3.使用生命周期钩子,通过this.bus.$on()
来监听bus所广播的事件,其中$on()
函数的第一个参数为事件名称;第二个参数为调用的函数,该函数接收广播内容
<body>
<div id="root">
<child content="Schuyler"></child>
<child content="Cai"></child>
</div>
<script>
Vue.prototype.bus = new Vue();
Vue.component('child', {
props: ['content'],
template: '<div @click="handleClick">{{selfContent}}</div>',
data: function () {
return {
selfContent: this.content
}
},
methods: {
handleClick: function () {
this.bus.$emit('change', this.selfContent);
}
},
mounted: function () {
var this_ = this;
this.bus.$on('change', function (msg) {
this_.selfContent = msg;
//需要注意的是,在这个方法体内,this的引用已经发生了变
//化,所以需要在方法体外先定义好组件实例的this引用
});
}
});
var vm = new Vue({
el: '#root'
});
</script>
</body>
六、插槽
在组件中,当父组件需要向子组件传递dom元素时,如果通过属性的方式进行传递,并在子组件中直接接收使用会将父组件传递的dom元素作为一个字符串来使用,例如
<body>
<div id="root">
<v-content content="<p>text</p>"></content>
</div>
<script>
Vue.component('v-content',{
props: ['content'],
template: '<div>{{content}}</div>'
})
var vm = new Vue({
el: '#root',
});
</script>
</body>
这样会在页面上显示为<p>text</p>
,要解决这个问题,可以在组件模板div标签(必须是div)中添加属性 v-html="this.content"
。
Vue.component('v-content',{
props: ['content'],
template: '<div v-html="this.content"></div>'
})
虽然问题解决了,但是如果父组件需要传递大量的dom元素,若通过属性的方式来传递,代码会变得十分难以阅读,此时我们可以使用Vue提供的组件的插槽
1.插槽
使用插槽的方法十分简单,首先直接在组件标签内部编写需要传递的dom元素,之后在对应的组件模板中通过slot
标签来接收即可
<body>
<div id="root">
<v-content>
<p>text</p>
</v-content>
</div>
<script>
Vue.component('v-content',{
template: '<div><slot>默认值</slot></div>'
})
var vm = new Vue({
el: '#root',
});
</script>
</body>
2.具名插槽
当需要传递多个dom元素时,有时会需要根据模板的内容去调整传递的dom元素的位置,这个时候可以使用具名插槽。只需要在父组件传递的标签中添加slot="name"
属性,在对应的组件模板的slot标签中使用name属性来接收
<body>
<div id="root">
<v-content>
<p slot="one">text-one</p>
<p slot="two">text-two</p>
</v-content>
</div>
<script>
Vue.component('v-content',{
template: '<div>'
+'<slot name="one">默认值one</slot>'
+'<p>hello</p>'
+'<slot name="two">默认值two</slot>'
+'</div>'
})
var vm = new Vue({
el: '#root',
});
</script>
</body>
3.作用域插槽
一般情况下,如果我们使用组件标签去遍历一个对象,会直接进行遍历,并在组件的模板中展示遍历的内容。但有些时候,可能会在不同场景下使用到这个组件,这个时候我们希望遍历时展示的内容会根据不同的使用场景而不同,即遍历后所展示的内容是由父组件来决定的,这时我们可以使用作用域插槽
(1).在模板中使用slot
标签进行遍历,并使用:属性名=表达式
来给父组件传值
(2).在父组件内使用template
标签(固定模式),并使用slot-scope="props"
来接收封装了子组件中传递过来的所有数据的对象(props
不是固定的,可以换个名字)
3.在template标签内编写dom元素展示相应内容
<body>
<div id="root">
<v-content>
<template slot-scope="props">
<li>{{props.item}}</li>
</template>
</v-content>
</div>
<script>
Vue.component('v-content',{
data: function () {
return {
list: [1, 2, 3, 4]
}
},
template: '<div>'
+'<slot v-for="item of list" :item=item></slot>'
+'</div>'
})
var vm = new Vue({
el: '#root',
});
</script>
</body>
七、细节
1.is
属性
在H5中规定,table标签中需要写tbody,tbody标签中需要写tr,类似的还有ul、ol、select等标签,当在这些标签中调用vue组件时会出现一些bug,例如
<body>
<div id="root">
<table>
<tbody>
<row></row>
<row></row>
<row></row>
</tbody>
</table>
</div>
<script>
Vue.component('row', {
template: '<tr><td>this is a row</td></tr>'
})
var vm = new Vue({
el: '#root'
});
</script>
</body>
在上述代码中,渲染到页面时,row组件下的模板会与table标签同级,而不是在tbody标签内,同样的问题也可能出现在select、ul、ol等标签中。解决这个问题,可以使用Vue提供的is属性
<table>
<tbody>
<tr is='row'></tr>
<tr is='row'></tr>
<tr is='row'></tr>
</tbody>
</table>
2.data
属性
在vue中,在非根组件,即子组件中定义data
属性时,data
必须是一个返回一个对象的函数,返回的对象中包含所使用的数据。这样的目的是为了在重复使用子组件数据的时候,每个子组件中的data
都是一个新返回的数据对象,避免出现多个子组件中互相影响数据的情况
<body>
<div id="root">
<table>
<tbody>
<tr is='row'></tr>
<tr is='row'></tr>
<tr is='row'></tr>
</tbody>
</table>
</div>
<script>
Vue.component('row', {
data: function () {
return {
content: 'this is content'
}
},
template: '<tr><td>{{content}}</td></tr>'
})
var vm = new Vue({
el: '#root'
});
</script>
</body>
3.ref
ref
是Vue提供的来获取引用的属性,在标签中添加ref属性之后,可以通过
this.$refs.refValue
来获取引用。
(1).当在div等常规标签中使用ref
时,获取的是这个dom对象的引用
<body>
<div id="root">
<div ref='hello'
@click='handleClick'
>
Hello Vue
</div>
</div>
<script>
var vm = new Vue({
el: '#root',
methods: {
handleClick: function () {
alert(this.$refs.hello.innerHTML);
}
}
});
</script>
</body>
(2).当在vue组件中使用ref
属性时,获取的是整个组件实例的引用
<body>
<div id="root">
<counter ref="one" @sum="handleCount"></counter>
<counter ref="two" @sum="handleCount"></counter>
<div>{{sum}}</div>
</div>
<script>
Vue.component('counter', {
template: '<div @click="handleClick">{{number}}</div>',
data: function () {
return {
number: 0
}
},
methods: {
handleClick: function () {
this.number++;
this.$emit('sum');
}
}
});
var vm = new Vue({
el: '#root',
data: {
sum: 0
},
methods: {
handleCount: function () {
this.sum = this.$refs.one.number +
this.$refs.two.number;
}
}
})
</script>
</body>