本章内容:表单 与 v-model、组件、自定义指令
六、表单 与 v-model
6.1、基本用法
Vue.js 提供了 v-model 指令,同于在表单类元素上双向绑定数据
<body>
<div id="app">
<input type="text" v-model="message" placeholder="输入...">
<p>输入的内容时:{{ message }}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: ''
}
})
</script>
</body>
对于 文本域 textarea 也是同样的用法。
使用 v-model后,表单控件显示的值只依赖所绑定的数据,不再关心 初始化时的 value属性。在中文输入法下,当敲下汉字才会触发更新,如果希望总是实时更新,可以用 @input 来代替 v-model
单选按钮
单选按钮在单独使用时,直接使用 v-bind 绑定一个布尔类型的值即可。
<body>
<div id="app">
<input type="radio" :checked="isChecked">
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
isChecked: true
}
})
</script>
</body>
如果是组合使用来实现互斥选择的效果,就需要 v-model 配合 value 来使用:
<body>
<div id="app">
<input type="radio" v-model="picked" value="html" id="html">
<label for="html">HTML</label>
<input type="radio" v-model="picked" value="css" id="css">
<label for="css">css</label>
<input type="radio" v-model="picked" value="javascript" id="javascript">
<label for="javascript">JavaScript</label>
<p> 你的选择是 {{ picked }} </p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
picked: 'javascript'
}
})
</script>
</body>
复选框
复选框在单独使用时,也是有 v-bind 绑定一个布尔类型的值,或者使用 v-model 来绑定一个布尔值。
<body>
<div id="app">
<input type="checkbox" name="" :checked="isChecked" id="">
<input type="checkbox" name="" v-model="isChecked" id="">
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
isChecked: false
}
})
</script>
</body>
组合使用时,也是 v-model 与 value 一起,多个勾选框都绑定到同一个数组类型的数据。
<body>
<div id="app">
<input type="checkbox" name="hobby" v-model="checked" value="sleep" id="sleep">
<label for="sleep">睡觉</label>
<input type="checkbox" name="hobby" v-model="checked" value="eat" id="eat">
<label for="eat">吃饭</label>
<input type="checkbox" name="hobby" v-model="checked" value="fishing" id="fishing">
<label for="fishing">钓鱼</label>
<input type="checkbox" name="hobby" v-model="checked" value="coding" id="coding">
<label for="coding">编程</label>
<p> 你的选择是 {{ checked }} </p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
checked: ['eat', 'sleep'],
}
})
</script>
</body>
选项列表
同样分为单选和多选两种方式。
先看一下单选的示例代码:
<body>
<div id="app">
<select v-model="selected">
<option>html</option>
<option value="js">javascript</option>
<option>css</option>
</select>
<p>你的选择是:{{ selected }}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
selected: '',
}
})
</script>
</body>
给 <select> 添加属性 multiple 就可以多选了,此时 v-model 绑定的是一个数组。
<body>
<div id="app">
<select v-model="selected" multiple>
<option>html</option>
<option value="js">javascript</option>
<option>css</option>
</select>
<p>你的选择是:{{ selected }}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
selected: ['html', 'js']
}
})
</script>
</body>
<select>控件 在实际业务中,因为它的样式依赖平台和浏览器,无法统一,也不太美观,功能也受限,常见的解解方案是用 div 模拟一个类似的控件。
6.2、绑定值
v-model 绑定的值是一个静态字符串 或 布尔值,但在业务中,有时需要绑定一个动态的数据,这时可以用 v-bind 来实现。
<body>
<div id="app">
<input type="radio" v-model="picked" name="radio" :value="value1"> 单选按钮
<input type="radio" v-model="picked" name="radio" :value="value2"> 单选按钮
<p>{{ picked }}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
picked: true,
value1: 123,
value2: 456
}
})
</script>
</body>
在选中时,picked === 对应的value
复选框
<body>
<div id="app">
<input type="checkbox" v-model="toggle" :true-value="value1" :false-value="value2">
<p>{{ toggle }}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
toggle: false,
value1: 123,
value2: 456
}
})
</script>
</body>
勾选时,app.toggle === app.value1; 未勾选时,app.toggle === app.value2。
选择列表
<body>
<div id="app">
<select v-model="selected">
<option :value="{ number: 123}">123</option>
</select>
{{ selected.number }}
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
selected: ''
}
})
</script>
</body>
当选中时,app.selected 是一个 Object,所以 app.selected.number === 123
6.3、修饰符
与事件的修饰符类似,v-model 也有修饰符,用于控制数据同步的时机。
- .lazy: 会转变为在 change 事件中同步,在失焦 或 按回车时才更新。
- .number:可以将 输入转换为 Number 类型。
- .trim:可以自动过滤输入的首尾空格
<input type="text" v-model.lazy="message" />
<input type="number" v-model.number="message" />
<input type="text" v-model.trim="message" />
七、组件
组件(Component)是 Vue.js 最核心的功能,也是整个框架设计最精彩的部分,当然也是最难掌握的。
7.1、组件与复原
7.1.1、为什么使用组件
Vue.js 的组件化开发有以下的优势:
提高开发效率
方便重复使用
简化调试步骤
提升整个项目的可维护性
便于多人协同开发
7.1.2、组件用法
组件需要注册后才可以使用。注册有 全局注册 和 局部注册两种方式。
全局注册 后 任何 Vue 实例都可以使用,要在父实例中使用组件,必须要在实例创建前注册
<body>
<div id="app">
<my-component />
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('my-component', {
// options
template: '<div>组件 my-conponent</div>'
})
const app = new Vue({
el: '#app',
})
</script>
</body>
my-component 就是注册的组件自定义标签名称,推荐使用小写加减号分割的形式命名。 template 的 内容 必须被一个 DOM 元素包含,如果直接写一段文本,而不加上 HTML 标记,那样是不能渲染的。
局部组件,注册后的组件只有在该实例作用域下有效。组件中也可以使用 components 选项来注册组件,使组件可以嵌套。
<body>
<div id="app">
<my-component />
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const Child = {
template: '<div> 局部组件 </div>'
}
const app = new Vue({
el: '#app',
components: { // 注册组件
'my-component': Child
}
})
</script>
</body>
Vue 组件的模板在某些情况下会受到 HTML 的限制,比如 <table> 内规定只允许是 <tr>、<td>、<th>等这些表格元素。这种情况下,可以使用 特殊的 is 属性来挂载组件
<body>
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const Child = {
template: '<div> 局部组件 </div>'
}
const app = new Vue({
el: '#app',
components: { // 注册组件
'my-component': Child
}
})
</script>
</body>
渲染后的结构为
<table>
<div> 局部组件 </div>
</table>
除了 template 选项外,组件中还可以像 Vue 实例那样使用 其他的选项,比如 data、computed、methods 等。
在使用 data 时,和实例稍有区别,data必须是函数,然后将数据 return 出去。
<body>
<div id="app">
<my-component />
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const Child = {
template: '<div> 局部组件 | {{ msg }} </div>',
data() {
return {
msg: '组件内容'
}
}
}
const app = new Vue({
el: '#app',
components: { // 注册组件
'my-component': Child
}
})
</script>
</body>
这样做的原因是因为 JavaScript 对象是引用关系,所以如果 return 出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。
7.2、使用 props 传递数据
7.2.1、基本用法
组件不仅仅 是要把模板的内容进行复用,更重要的是组件要进行通信。通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接受到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过 props 来实现的。
使用 选项 props 来声明需要从腹肌接收的数据,props 的值可以是两种,一种是字符串数组,一种是对象。
数组的用法
大部分时候,传递的数据都是通过 父组件的动态数据 而来的,一般使用 v-bind 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件
<body>
<div id="app">
<input type="text" v-model="message">
<my-component :parent-msg='message'/>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const Child = {
props: ['parentMsg'],
template: '<div> 组件 {{ parentMsg }} </div>'
}
const app = new Vue({
el: '#app',
data: {
message: ''
},
components: { // 注册组件
'my-component': Child
}
})
</script>
</body>
由于 HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名(camelCase)的 props 名称要转为短横分割命名(kebab-case)
组件中 props 中声明的数据与 组件 data 中的数据主要区别是 ,props 来自父级。这两种数据都可以在模板中的 computed、methods 等地方中使用
7.2.2、单向数据流
业务中会经常遇到 2 种需要改变 prop 的情况
- 父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。
这种情况下可以在组件 data 内再声明一个数据,引用父组件的 prop。
const Child = {
props: ['initCount'],
template: '<div> 组件 {{ count }} </div>',
data() {
return {
count: this.initCount
}
}
}
他在 组件初始化时会获得来自父组件的 initCount,之后就与之无关了,只要进行维护 count,这样就可以避免直接操作 initCount
- 另一种情况就是 prop 作为需要被转变的原始值传入。这种情况下可以使用计算属性
const Child = {
props: ['width'],
template: '<div :style="styles"> 组件 </div>',
computed: {
styles() {
return { width: this.width + 'px' }
}
}
}
7.2.3、数据验证
上面介绍的 props 选项的值都是一个数组,除了数组外,还可以是对象,当 prop 需要验证是 ,就需要对象写法。
以下是几个 prop 的示例:
Vue.component('my-componet', {
props: {
// 必须是数字类型
propA: Number,
// 必须是 字符串 或 数字类型
propB: [String, Number]
// Boolean 类型,如果没有定义,默认值就是 true
propC: {
type: Boolean,
default: true
}
// 数字,而且是必传项
propD: {
type: Number,
required: true
}
// 自定义一个验证函数
propE: {
validator: function(value) {
return value > 10
}
}
// 如果是数组或对象,默认值必须是一个函数来返回
propF: {
type: Array, //Object
default() {
return [] // {}
}
}
}
})
验证的type 类型可以是
- String
- Number
- Boolean
- Object
- Array
- Function
当 prop 验证失败时,在开发版本下会在控制台抛出一条警告。
type 也可以是一个自定义的构造器,使用 instanceof 检测
7.3、组件通信
组件关系可分为父子组件、兄弟组件通信、跨级组件通信。
7.3.1、自定义事件
当子组件需要向父组件传递数据时,就要用到自定义事件。
如果你了解过去 JavaScript 的设计模式 —— 观察者模式,一定知道 dispatchEvent 和 addEventListener 这两个方法。Vue 组件也有与之类似的一套模式,子组件用 $emit() 来触发事件,父组件在自定义标签上使用 v-on 来监听子组件触发的自定义事件 或 用 $on() 来监听子组件的事件。
<body>
<div id="app">
<p>总数:{{ total }}</p>
<!-- 侦听 increase 和 reduce 事件 -->
<my-component @increase="handleGetTotal" @reduce="handleGetTotal"/>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('my-component', {
template: `
<div>
<button @click="handleIncrease"> +1 </button>
<button @click="handleReduce"> -1 </button>
</div>
`,
data() {
return { // 组件内的数据
counter: 0
}
},
methods: {
handleIncrease() { // 增加按钮
this.counter++
this.$emit('increase', this.counter) // 触发 increase 事件
},
handleReduce() { // 减少 按钮
this.counter--
this.$emit('reduce', this.counter) // 触发 reduce 事件
}
}
})
const app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal(total) {
this.total = total
}
}
})
</script>
</body>
除了用 v-on 在组件上监听自定义事件外,也可以监听 DOM 事件,这时可以用 .native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素。
<!-- 监听原生 click 事件 -->
<my-component @click.native="handleClick"></my-component>
7.3.2、使用 v-model
在 Vue 2.x 中可以在自定义组件上使用 v-model 指令。
<body>
<div id="app">
<p>总数:{{ total }}</p>
<my-component v-model="total"/> <!-- 侦听 increase 和 reduce 事件 -->
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('my-component', {
template: `<button @click="handleClick"> + 1 </button>`,
data() {
return {
counter: 0
}
},
methods: {
handleClick() {
this.counter++
this.$emit('input', this.counter)
}
}
})
const app = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
</bod
上面的代码,并没有在 <my-component> 上使用 @input="handler", 而是直接用来 v-model 绑定了一个数据 total。这也可以称作是一个语法糖,因为上面的示例可以直接地用自定义事件实现。
7.3.3、非父子组件通信
在实际业务中,处理父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种,兄弟组件和跨多级组件。
在 Vue 2x 中,使用 一个空的 Vue 实例作为中央事件总线(bus),类似于一个中介。这种方法巧妙而轻量地实现了任何组件的通信,包括父子、兄弟、跨级。如果深入使用,可以扩展 bus 实例,给它添加 data、methods、computed 等选项。
<body>
<div id="app">
{{ message}}
<component-a />
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const bus = new Vue() // 创建中央事件总线
Vue.component('component-a', {
template: `<button @click="handleEvent"> 传递事件 </button>`,
methods: {
handleEvent() { // 触发事件
bus.$emit('on-message', '来自组件 component-a 的内容')
}
}
})
const app = new Vue({
el: '#app',
data: {
message: 0
},
mounted () {
bus.$on('on-message', msg => { // 监听事件
this.message = msg
})
}
})
</script>
</body>
当项目较大的时候,可以更好的选择 状态管理解决方案 vuex
除了 中央事件总线 bus 外,还有两种方法可以实现组件间通信:父链和子组件索引。
父链
在子组件中,使用 this.$parent 可以直接访问该组件的父实例或组件,父组件也可以通过 this.$children 访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
<body>
<div id="app">
{{ message}}
<component-a />
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('component-a', {
template: `<button @click="handleEvent"> 通过父链直接修改数据 </button>`,
methods: {
handleEvent() {
// 访问到父链后,可以做任何操作,比如直接修改数据
this.$parent.message = '来自组件 component-a 的内容'
}
}
})
const app = new Vue({
el: '#app',
data: {
message: ''
}
})
</script>
</body>
尽管 Vue 中允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧密耦合
子组件索引
Vue 提供子组件索引方法,用特殊的属性 ref 来为子组件指定一个索引名称。
<body>
<div id="app">
<button @click="handleRef">通过 ref 获取子组件实例</button>
<component-a ref="comA"/>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('component-a', {
template: `<div> 这里 component-a 子组件 </div>`,
data() {
return {
message: '呼叫 0101, over!'
}
}
})
const app = new Vue({
el: '#app',
methods: {
handleRef() {
// 通过 $refs 来访问 指定的实例
var msg = this.$refs.comA.message
console.log(msg)
}
}
})
</script>
</body>
这个属性 同样也可以作用与 普通的 DOM 元素,Vue 会自动去判断是普通标签还是组件。
7.4、使用 slot 分发内容
7.4.1、什么是 slot
- 插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。
- 插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制
props 传递数据、events 触发事件、slot 内容分发就构成了 Vue 组件的 3个 API 来源,在复杂的组件也是由这 3 部分构成的。
7.4.2、作用域
了解 slot 之前,需要先知道一个 概念:编译的作用域
比如父组件中有如下模板
<child-component>
{{ message }}
</child-component>
这里的 message 就是一个 slot,但是它绑定的是父组件的数据,而不是组件 <child-compoent> 的数据。父组件模板的内容实在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。
7.4.3、slot 用法
单个 slot
在子组件内使用 特殊的 <slot> 元素就可以为这个子组件开启一个 slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的 <slot> 标签及它的内容。
<body>
<div id="app">
<component-a>
<p>谁说没有内容</p>
<p>谁说的。。。</p>
</component-a>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('component-a', {
template: `
<div>
这里 component-a 子组件
<slot>
如果没有插入内容,我将作为默认内容出现
</slot>
</div>`
})
const app = new Vue({
el: '#app'
})
</script>
</body>
上例渲染后的结果为:
<div id="app">
<div>
这里 component-a 子组件
<p>谁说没有内容</p>
<p>谁说的。。。</p>
</div>
</div>
具名 Slot
给 <slot> 元素指定一个 name 后可以分发多个内容,具名 Slot 可以与单个 Slot 共存。
<body>
<div id="app">
<component-a>
<p slot="header">标题</p>
<p>正文内容</p>
<p>更多内容</p>
<p slot="footer">底部信息</p>
</component-a>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('component-a', {
template: `
<div>
<slot name="header" />
<slot />
<slot name="footer" />
</div>`
})
const app = new Vue({
el: '#app'
})
</script>
</body>
没有使用 name 特性,它将作为默认 slot 出现,父组件没有使用 slot 特性的元素与内容都将出现在这里。如果没有指定默认的匿名 slot,父组件内多余的内容片段都将被抛弃。
7.4.4、作用域插槽
作用域插槽 是一种特殊的 slot,使用一个可以复用的模板代替以渲染的元素。
<body>
<div id="app">
<child-component>
<template scope="props">
<p>来自父组件的内容</p>
<p>{{ props.msg }}</p>
</template>
</child-component>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('child-component', {
template: `
<div>
<slot msg="来自子组件的内容" />
</div>`
})
const app = new Vue({
el: '#app'
})
</script>
</body>
作用域插槽更具代表的用例是列表组件,允许组件自定义应该如何渲染列表每一项。
<body>
<div id="app">
<my-list :books="books">
<!-- 作用域插槽也可以是具名的 Slot -->
<template slot="book" scope="props">
<li>{{ props.bookName}}</li>
</template>
</my-list>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('my-list', {
props: {
books: {
type: Array,
default() {
return []
}
}
},
template: `
<ul>
<slot name="book" v-for="book in books" :book-name="book.name" />
</ul>
`
})
const app = new Vue({
el: '#app',
data: {
books: [
{ name: '《Vue.js 实战》'},
{ name: '《JavaScript 语言精粹》'},
{ name: '《JavaScript 高级程序设计》'}
]
}
})
</script>
</body>
scope="props" 这里的 props 是一个临时变量,就是 v-for="item in items" 里面的 item 一样。template 内部可以通过临时变量 props 来访问子组件插槽的数据 msg。作用域插槽的使用场景就是 既可以 复用子组件 的 slot,又可以使 slot 内容不一致。
7.4.5、访问 slot
Vue 2.x 中提供了用来访问被 slot 分发的内容的方法 $slots
mouted() {
console.log(this.$slots.header) // 访问 slot = "header" 的插槽
console.log(this.$slots.default) // 访问没有 具名的所有插槽
}
7.5、组件高级用法
7.5.1、递归组件
组件在它的模板内可以递归地调用自己,只要给组件设置了 name 的选项就可以了。
<body>
<div id="app">
<child-component :count="1" />
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('child-component', {
name: 'child-component',
props: {
count: {
type: Number,
default: 1
}
},
template: `
<div class="child">
xxx
<child-component :count="count+1" v-if="count < 3" />
</div>
`
})
const app = new Vue({
el: '#app',
})
</script>
</body>
不过需要注意的是,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded
7.5.2、内联模板
Vue 提供了一个内联模板的功能,在使用组件时,给组件标签使用 inline-template 特性,组件就会把它的内容当做模板,而不是把它当做内容分发,这让模板更灵活。
<body>
<div id="app">
<child-component inline-template>
<div>
<h2>在父组件中定义子组件</h2>
<p>{{ message }}</p>
<p>{{ msg }}</p>
</div>
</child-component>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('child-component', {
data() {
return {
msg: '在子组件声明的数据'
}
}
})
const app = new Vue({
el: '#app',
data: {
message: '在父组件声明的数据'
}
})
</script>
</body>
父组件和子组件中的数据都可以进行渲染(如果同名,优先使用子组件的数据)。这反而是内联模板的缺点,就是作用域比较难理解,如果不是非常特殊的常见,建议不要轻易使用内联模板。
7.5.3、动态组件
Vue.js 提供了一个 特殊的元素 <component> 用来动态挂载不同的组件,使用 is 特性来选择要挂载的组件
<body>
<div id="app">
<component :is="currentView" ></component>
<button @click="toggleView"> 点击切换组件 </button>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('comA', {
template: '<p> 我是组件 A </p>'
})
Vue.component('comB', {
template: '<p> 我是组件 B </p>'
})
const app = new Vue({
el: '#app',
data: {
currentView: 'comA'
},
methods: {
toggleView() {
this.currentView = this.currentView === 'comA' ? 'comB' : 'comA'
}
}
})
</script>
</body>
7.5.4 异步组件
当工程足够大的时候,在 一开始就把所有的组件都加载是没有必要的一笔开销。Vue.js 允许将组件定义为一个工程函数,动态地解析组件。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
<body>
<div id="app">
<child-compont></child-compont>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('child-compont', function(resolve, reject) {
window.setTimeout(() => {
resolve({
template: '<p>异步组件加载</p>'
})
}, 2000)
})
const app = new Vue({
el: '#app'
})
</script>
</body>
resolve 回调中传入的参数 为 组件的 配置选项。也可以调用 reject(error) 指示加载失败。
7.6、其他
7.6.1、$nextTick
先运行如下代码
<body>
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">点击 div内容</button>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('child-compont', )
const app = new Vue({
el: '#app',
data: {
showDiv: false
},
methods: {
getText() {
this.showDiv = true // 显示元素
// 输出文本
console.log(document.getElementById('div').innerHTML)
}
}
})
</script>
</body>
当点击按钮是,控制台会抛出一个错误:Cannot read property 'innerHTML' of null
。这里就涉及 Vue 一个重要的概念:异步更新队列
Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据变化。在缓冲时会去除重复数据,从而避免不必要的计算和 DOM操作。然后,在下一个事件循环 tick 中,Vue 刷新队列并执行实际 (以去重的)工作。
所以如果你用一个 for 循环来动态改变数据 100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘 100次。
Vue会根据当前浏览器环境优先使用原生的 Promise.then 和 MutationObserver,如果都不支持就会采用 setTimeout
在执行 this.showDiv=true 时,div仍然还是没有被创建出来,直到下一个 Vue 事件循环时,才开始创建。
$nextTick 就是用来知道什么时候 DOM 更新完成的
<body>
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">点击 div内容</button>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('child-compont', )
const app = new Vue({
el: '#app',
data: {
showDiv: false
},
methods: {
getText() {
this.showDiv = true // 显示元素
this.$nextTick(function() {
// 输出文本
console.log(document.getElementById('div').innerHTML)
})
}
}
})
</script>
</body>
7.6.2、X-Templates
Vue 提供了另一种定义模板的方式,在 <script> 标签里使用 text/x-template 类型,并且指定一个 id,将这个 id 赋给 template
<body>
<div id="app">
<my-component></my-component>
<script type="text/x-template" id="my-component">
<div>这里是 X-Templates</div>
</script>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.component('my-component', {
template: '#my-component'
})
const app = new Vue({
el: '#app'
})
</script>
</body>
7.6.3、手动挂载实例
我们现在所创建的实例都是通过 new vue() 的形式创建出来的。在一些非常特殊的情况下,我们需要动态地去创建 Vue 实例,Vue 提供了 Vue.extend 和 $mount 两个方法来手动挂载一个实例。
Vue.extend 是基础 Vue 构造器,创建一个 “子类”,参数是一个包含组件选项的对象。
如果 Vue 实例在实例化阶段没有收到 el 选项,他就处于“未挂载” 状态,没有关联的 DOM 元素。可以使用 $mount() 手动地挂载一个未挂载的实例,这个方法返回实例自身,因此可以链式调用其他实例方法。
<body>
<div id="app">
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const MyComponent = Vue.extend({
template: '<div> Hello:{{ name }} </div>',
data() {
return {
name: 'sakura'
}
}
})
new MyComponent().$mount('#app')
</script>
</body>
除了这种常见的写法,还有以下几种方式进行挂载
new MyComponent().$mount('#mount-div')
new MyComponent({
el: '#mount-div'
})
// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('mount-div').appendChild(component.$el)
手动挂载实例是一种比较极端的高级用法,一般只会应用到一些复杂的独立组件开发时。
八、自定义指令
Vue.js 内置的指令能够满足我们的绝大部分也无需求,不过在需要一些特殊功能时,我们仍然希望对 DOM 进行底层的操作,这时候就要用到自定义指令
8.1、基本用法
自定义指令的注册方法和组件很小,也分全局注册 和 局部注册。
全局注册
Vue.directive(('focus', {
// options
})
局部注册
var app = new Vue({
el: '#app',
directives: {
focus: {
// options
}
}
})
自定义指令的选项是由几个钩子函数组成的,每个都是可选的
- bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作
- inserted:被绑定元素插入父节点时调用
- update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新
- componentUpdated:被绑定的元素所在模板完成一次更新周期时调用。
-
unbind:只调用一次,指令与元素解绑时调用
根据需求在不同的钩子函数内完成逻辑代码。
例如上面的 v-focus 指令,我们希望在元素插入父节点时就调用,那用到的最好是 inserted。
<body>
<div id="app">
<input type="text" v-focus>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.directive('focus', {
inserted(el) {
console.log(el)
// 聚焦元素
el.focus()
}
})
const app = new Vue({
el: '#app'
})
</script>
</body>
每个钩子函数都有几个参数可用,比如上面我们用到了 el
它们的含义如下:
- el:指令所绑定的元素,可以用来直接操作 DOM
-
binding:一个对象,包含以下属性
- name:指令名,不包括 v- 前缀。
- value:指令的绑定值,例如 v-my-directive=“1 + 1”,value 的值是2.
- oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用
- expression:绑定值的字符串形式。例如 v-my-directive=“1+1”,expression 的值是“1+1”
- arg:传给指令的参数。例如 v-my-directive.foo.bar,修饰符对象 modifiers 的值是 {foo: true, bar: true}
- vnode:Vue编译生成的虚拟节点
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可以
下面是结合以上参数的一个具体实例:
<body>
<div id="app">
<!-- 如果需要多个值,可以传入一个 JavaScript 对象字面量,只要是合法类型的表达式都可以 -->
<div type="text" v-test:msg.a.b="message"></div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
Vue.directive('test', {
bind(el, binding, vnode) {
var keys = []
var html = ''
for(let key in binding) { // 遍历 binding
if (typeof binding[key] === 'object') html += key + ': ' + JSON.stringify(binding[key]) + '<br/>'
else html += key + ': ' + binding[key].toString() + '<br/>'
}
for(let i in vnode) { // 遍历 vnode
keys.push(i)
}
el.innerHTML = html + keys.join(',')
}
})
const app = new Vue({
el: '#app',
data: {
message: 'some text'
}
})
</script>
</body>
在大多数使用场景,我们会在 bind 钩子里绑定一些事件,比如在 document 上用 addEventListener 绑定,在 unbind 里用 removeEventListener 解绑。
案例
开发 实时时间转换指令 v-time
需求:
根据当前时间计算 某一条信息的相对时间
- 1分钟之内,显示“刚刚”
- 1分钟~1小时之间,显示“XX分钟前”
- 1分钟~1天之间,显示“XX小时之前”
- 1分钟~一个月(以31天为标准)之间,显示“XX天前”
- 大于 1 个月,显示“XX年 XX月 XX日”
<body>
<div id="app">
<span v-time="timeNow"></span>
<span v-time="timeBefore"></span>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
const Timer = {
// 获取当前 时间
getCurrentUnix() {
const date = new Date()
return date.getTime()
},
// 获取 今天 00:00:00 的时间戳
getTodayUnix() {
const date = new Date()
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
},
// 获取标准时间
getFormatDate(time) {
const date = new Date(time)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year} - ${month} - ${day}`
},
// 获取 时间格式字符串
getTimeString(timestamp) {
const now = this.getCurrentUnix()
const today = this.getTodayUnix()
const timer = (now - timestamp) / 1000 // 获取相差秒数
let tip = ''
if (timer <= 0 || Math.floor(timer / 60) <= 0) tip = '刚刚'
// 60 * 60 * 一小时内
else if (timer < 3600) tip = Math.floor(timer / 60) + '分钟前'
else if (timer >= 3600 && (timestamp - today > 0)) tip = Math.floor(timer / 3600) + '小时前'
// 60 * 60 * 24
else if (timer / 86400 <= 31) tip = Math.ceil(timer / 86400) + '天前'
else tip = this.getFormatDate(timestamp)
return tip;
}
}
Vue.directive('time', { // 自定义指令
bind(el, binding) {
el.innerHTML = Timer.getTimeString(binding.value)
el._timeout = setInterval(() => { // 一分钟更新一次
el.innerHTML = Timer.getTimeString(binding.value)
}, 60000)
},
unbind(el) { // 卸载
clearInterval(el._timeout)
delete el._timeout
}
})
const app = new Vue({
el: '#app',
data: {
timeNow: new Date().getTime(), // 当前时间
timeBefore: 1566890795721
}
})
</script>
</body>