Vue 学习手册
前言
Vue.js(读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。vue的英文意思就是“视图”,所以可以知道,vue是一个偏视图的js框架,根据官方的解释的理解,vue不属于MVC的范畴,因为它只有MV,而且它的M是内置于框架内核中,对于开发者而言,可能基本上都是在处理V的东西。这也是为什么现在把vue和react对比如此热门的原因之一。既然是解决视图问题,那么不可避免的就会和DOM打交道,自然就会联想到react首创的Virtual DOM理念,这在后文会详细分析虚拟DOM是怎么回事。vue2.0也使用了Virtual DOM,因此在视图层面的渲染速度也非常快。
总之,vue是一个关注视图层面的js框架。
发展历史
Vue.js正式发布于2014年2月,对于目前的Vue.js:
从脚手架、构建、插件化、组件化,到编辑器工具、浏览器插件等,基本涵盖了从开发到测试等多个环节。
Vue.js的发展里程碑如下:
2013年12月24日,发布0.7.0。
2014年1月27日,发布0.8.0。
2014年2月25日,发布0.9.0。
2014年3月24日,发布0.10.0。
2015年10月27日,正式发布1.0.0。
2016年4月27日,发布2.0的preview版本。
2016年10月1日,2.0正式版发布,整个框架重新编写,性能提升,支持服务端渲染。
2019年02月05日,Vue 发布了 2.6.0 ,这是一个承前启后的版本。
2023年9月18日,3.0正式发布,使用了组合(composition)api 代替了vue2的选项(option)api。
创建项目
可以有两种创建vue的方式,一种是 vue CLI 脚手架,一种是 vite 工具
Vue CLI
基于 webpack
命令 vue create 项目名
vue create vue-demo1
create-vue
基于 vite
命令 npm init vue@latest
npm init vue@latest
Vue 实例
首先,你必须掌握一个概念,就是“vue实例”。什么意思呢?所有的vue程序都需要实例化之后使用,实例主要有两种,一个是Vue实例,一个是组件实例。当然,如果你把router和resource加进来,它们也有实例,我们可以称之为插件实例。
构造器
每个 Vue.js 应用都是通过构造函数 Vue 创建一个 Vue 的根实例 启动的:
var vm = new Vue({\n // 选项\n})
在实例化 Vue 时,需要传入一个选项对象,它可以包含数据、模板、挂载元素、方法、生命周期钩子等选项。全部的选项可以在 https://cn.vuejs.org/v2/api中查看。
组件构造器
可以扩展 Vue 构造器,从而用预定义选项创建可复用的组件构造器。所谓组件构造器,就是创建一个组件的原型类。
var MyComponent = Vue.extend({\n // 扩展选项\n})\n// 所有的 `MyComponent` 实例都将以预定义的扩展选项被创建\nvar myComponentInstance = new MyComponent()
尽管可以命令式地创建扩展实例,不过在多数情况下建议将组件构造器注册为一个自定义元素,然后声明式地用在模板中。我们将在后面详细说明组件系统。现在你只需知道所有的 Vue.js 组件其实都是被扩展的 Vue 实例。
实例属性(数据代理和响应)
每个 Vue 实例都会代理其 data 对象里所有的属性:
var data = { a: 1 }var app = new Vue({\n data: data\n}) \n\napp.a === data.a // -> true // 设置属性也会影响到原始数据\napp.a = 2data.a // -> 2 // ... 反之亦然\ndata.a = 3app.a // -> 3
注意只有这些被代理的属性是响应的。如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。
编者按:也就是说,你得在new之前,就把所有的data都传进去。实例创建之后,其实可以使用$set来加入属性,也可以实现响应功能。
除了 data 属性, Vue 实例暴露了一些有用的实例属性与方法。这些属性与方法都有前缀 $,以便与代理的 data 属性区分。例如:
var data = { a: 1 }\nvar app = new Vue({\n el: '#example',\n data: data\n}) \napp.$data === data // -> true\napp.$el === document.getElementById('example') // -> true
实例方法
在实例里面可以自己传进去一些方法进行调用:
var app = new Vue({\n methods: {\n myMethod() {},\n otherMethod() {\n // 这里就可以使用this.myMethod()了\n },\n },\n})\napp.otherMethod() // 可以在外面调用
和this.$data一样,vue也有一些以$开头的方法,比如app.$watch等,这些都是vue内置的方法。
实例属性和方法的完整列表中查阅 API 参考。
模板和数据绑定
Vue的模板语法
Vue.js 使用了基于 HTML 的模版语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。
在底层的实现上, Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,在应用状态改变时, Vue 能够智能地计算出重新渲染组件的最小代价并应用到 DOM 操作上。
如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染(render)函数,使用可选的 JSX 语法。
文本
数据绑定最常见的形式就是使用 “Mustache” 语法(双大括号)的文本插值:
<span>Message: {{ msg }}</span>
Mustache 标签将会被替代为对应数据对象上 msg 属性的值。无论何时,绑定的数据对象上 msg 属性发生了改变,插值处的内容都会更新。
msg 和你传入的 data.msg 是绑定的,当你在操作实例的时候,把实例的 data.msg 改变了,那么视图上的这个msg 也会改变。
export default {\n data() {\n return {\n msg: 'Welcome!',\n }\n },\n mounted() {\n this.msg = 'Hello!'\n }\n}
纯 HTML
双大括号会将数据解释为纯文本,而非 HTML 。为了输出真正的 HTML ,你需要使用 v-html 指令:
<div v-html=\"htmlContent\"></div>\n\nexport default {\n data() {\n return {\n htmlContent: '<h1>Welcome!</h1>'\n }\n }\n}
被插入的内容都会被当做 HTML —— 数据绑定会被忽略。注意,你不能使用 v-html 来复合局部模板,因为 Vue 不是基于字符串的模板引擎。组件更适合担任 UI 重用与复合的基本单元。
你的站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容插值。
属性
Mustache 不能在 HTML 属性中使用,应使用 v-bind 指令(下文指令一章详细说):
<div v-bind:id=\"dynamicId\"></div>
这对布尔值的属性也有效 —— 如果条件被求值为 false 的话该属性会被移除:
<button v-bind:disabled=\"btnDisabled\">Button</button>
使用 JavaScript 表达式
迄今为止,在我们的模板中,我们一直都只绑定简单的属性键值。但实际上,对于所有的数据绑定, Vue.js 都提供了完全的 JavaScript 表达式支持
{{ number + 1 }}\n{{ ok ? 'YES' : 'NO' }}\n{{ message.split('').reverse().join('') }}\n<div v-bind:id=\"'list-' + id\"></div>
这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。
<!-- 这是语句,不是表达式 -->{{ var a = 1 }}\n<!-- 流控制也不会生效,请使用三元表达式 -->{{ if (ok) { return message } }}
模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。
计算属性(computed)
什么是计算属性
模板内的表达式是非常便利的,但是它们实际上只用于简单的运算。在模板中放入太多的逻辑会让模板过重且难以维护。例如:
<div id=\"example\">{{ message.split('').reverse().join('') }}</div>
在这种情况下,模板不再简单和清晰。在意识到这是反向显示 message 之前,你不得不再次确认第二遍。当你想要在模板中多次反向显示 message 的时候,问题会变得更糟糕。
这就是对于任何复杂逻辑,你都应当使用计算属性的原因。
所谓计算属性,就是跟 ES5 的 getter 一样的,用 function 来定义个属性,当你获取这个属性的时候,实际上是要执行这个 function,执行 function 的过程就是计算过程,所以也就叫计算属性。所有的计算属性被放在 computed 里面,computed 和 data, methods 同级,如下:
<template>\n\t<div><p>{{ reversedMessage }}</p></div>\n</template>\n\n<script>\nexport default {\n data() {\n return {\n message.split: 'Welcome!'\n }\n },\n computed: {\n reversedMessage: function () {\n return this.message.split.split('').reverse().join('')\n }\n }\n}\n</script>
你可以像绑定普通属性一样在模板中绑定计算属性。 Vue 知道 vm.reversedMessage 依赖于 vm.message ,因此当 vm.message 发生改变时,所有依赖于 vm.reversedMessage 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 是没有副作用,这使得它易于测试和推理。
计算缓存 vs Methods
你可能已经注意到我们可以通过调用表达式中的 method 来达到同样的效果:
<p>{{ reversedMessage() }}</p>\n\nmethods: {\n reversedMessage: function () {return this.message.split('').reverse().join('')\n }\n}
我们可以将同一函数定义为一个 method 而不是一个计算属性。对于最终的结果,两种方式确实是相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:
computed: {\n now: function () {\n return Date.now()\n }\n}
相比而言,只要发生重新渲染,method 调用总会执行该函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的的计算属性 A ,它需要遍历一个极大的数组和做大量的计算。然后我们可能有其他的计算属性依赖于 A 。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用 method 替代。
计算 setter
计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter :
// ...\ncomputed: {\n fullName: {\n // getterget: function () {\n return this.firstName + ' ' + this.lastName\n },\n // setterset: function (newValue) {\n var names = newValue.split(' ')\n this.firstName = names[0]\n this.lastName = names[names.length - 1]\n }\n }\n}\n// ...
现在在运行 vm.fullName = 'John Doe' 时, setter 会被调用, vm.firstName 和 vm.lastName 也相应地会被更新。
侦听器(watch)
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
什么是watch
watch 是和 data, methods, computed 同级的一个参数,你可以在 watch 里面规定你要 watch 的属性,当这些属性发生变化的时候,它会执行对应的那个函数。
export default {\n data() {\n return {\n message: 'Welcome!'\n }\n },\n watch: {\n // 当 msg 发生变化的时候,执行这个函数message: function () {\n this.message = 'Changed!'\n },\n },\n mounted() {\n this.message = 'Hello'\n }\n}
在上面这个例子中我们在 mounted 生命周期钩子里面改变了 message 的值,这个时候会触发 watch 里面的 message 函数
计算属性(Computed) vs 侦听器(Watch)
Vue 确实提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:watch 属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch,通常更好的想法是使用 computed 属性而不是命令式的 watch 回调。细想一下这个例子:
<div id=\"demo\">{{ fullName }}</div>\n\ndata: {\n firstName: 'Foo',\n lastName: 'Bar',\n fullName: 'Foo Bar'\n},\nwatch: {\n firstName: function (val) {\n this.fullName = val + ' ' + this.lastName\n },\n lastName: function (val) {\n this.fullName = this.firstName + ' ' + val\n }\n}
上面代码是命令式的和重复的。将它与 computed 属性的版本进行比较:
data: {\n firstName: 'Foo',\n lastName: 'Bar'\n},\ncomputed: {\n fullName: function () {\n return this.firstName + ' ' + this.lastName\n }\n}
好得多了,不是吗?
指令
什么是指令?
指令(Directives)是带有 v- 前缀的特殊属性。指令属性的值预期是单一 JavaScript 表达式(除了 v-for,之后再讨论)。指令的职责就是当其表达式的值改变时相应地将某些行为应用到 DOM 上。
让我们看一个例子:
<p v-if=\"seen\">Now you see me</p>
这里, v-if 指令将根据表达式 seen 的值的真假来移除/插入 <p> 元素。而v-if就是指令。vue里面有很多内置的指令,你还可以自定义指令。
Vue2 提供的所有指令可以在这里看到,主要有
- v-text
- v-html
- v-show
- v-if
- v-else
- v-else-if
- v-for
- v-on
- v-bind
- v-model
- v-pre
- v-cloak
- v-once
因为指令还挺重要的,所以本书会全部解释一遍。
指令怎么用?
只需要直接将指令作为html元素的属性来使用就可以了,例如<div v-if=\"hasChild\"></div>。指令只对当前所在的这个元素起作用,它在哪个元素上,就哪个元素做出对应的动作。
该属性的值被称为指令的“表达式”,例如v-if=\"isExists\",isExists对应this.isExists,既然是表达式,就有返回值,它的值被传给指令,指令根据表达式的返回结果来决定所在的元素进行怎样的操作。表达式里面的变量,都是来自vue实例的属性。
指令也可以有参数,主要是指v-on和v-bind这两个指令,它们后面跟上一个冒号,冒号和等号之间的内容就是参数,例如v-bind:src=\"src\",红色的src就是参数。
一些指令还有修饰符,用.符号连接,主要是绑定型指令会有。
内容型指令
v-text
<span v-text=\"msg\"></span><!-- 和下面的一样 --><span>{{msg}}</span>
v-html
<div v-html=\"html\"></div>
和v-text不一样,v-html真的是完完全全innerHTML。为了安全起见,不要用。
条件判断型指令
v-if
<div v-if=\"is\"></div>
如果this.is值为真,则当前元素会被插入到dom中,如果is为假,当前元素会被从dom中移除。因此,v-if是有dom消耗的,使用时不应该反复更改is值。
v-show
<div v-show=\"is\"></div>
v-show和v-if是一样的用法,但是相对于v-if不一样。v-if是会把这个元素从dom中移走或者插入的,但是v-show是不会的,is为false的时候,会给这个元素加一个display:none,仅仅是隐藏这个元素,所以不会有dom的消耗。
v-else
必须跟v-if一起用。并且不需要表达式,还记得上面说的“表达式”是什么意思吗?
<div v-if=\"Math.random() > 0.5\"> Now you see me</div><div v-else> Now you don't</div>
v-else-if
2.1.0版本新增的指令,所以2.0版本是没有的,使用的时候注意。前一兄弟元素必须有 v-if 或 v-else-if。表示 v-if 的 “else if 块”。可以链式调用。
<div v-if=\"type === 'A'\">A</div><div v-else-if=\"type === 'B'\">B</div><div v-else-if=\"type === 'C'\">C</div><div v-else>Not A/B/C</div>
循环调用型指令
v-for
基于源数据多次渲染元素或模板块。也就是说,跟前面的指令不一样,上面的指令当前元素只会出现0次或1次,而v-for可以让当前元素重复渲染。此指令之值,必须使用特定语法 alias in expression ,为当前遍历的元素提供别名:
<div v-for=\"item in items\">{{ item.text }}</div>
另外也可以为数组索引指定别名(或者用于对象的键):
<div v-for=\"(item, index) in items\"></div><div v-for=\"(val, key) in object\"></div><div v-for=\"(val, key, index) in object\"></div>
v-for 默认行为试着不改变整体,而是替换元素。迫使其重新排序的元素,您需要提供一个 key 的特殊属性:
<div v-for=\"item in items\" :key=\"item.id\">{{ item.text }}</div>
关于这个key,Vue为了确保在virtual dom里面,更新的时候用来做对比用的。如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用key,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素。具体可以阅读这里。下文“特殊属性”一节会讲到。
绑定型指令
绑定型指令会有参数,也就是有冒号,参数是绑定的属性或事件。绑定型指令还有修饰符。
v-bind
动态地绑定一个或多个attr或prop属性。简单的说就是,通过v-bind绑定一个属性,这个属性可以是html原本有的,也可以是某些特殊的,绑定的表达式的值赋值给这个属性。例如:<img v-bind:src=\"imgSrc\">,imgSrc是实例的imgSrc属性this.imgSrc,它的值将会作为字符串作为属性src的值。绑定之后,如果this.imgSrc更改,那么src的值也随着更改,图片也就会换成另外一幅。
在绑定 class 或 style 特性时,支持其它类型的值,如数组或对象。下文会详细讲class和style属性的问题。
在绑定 prop 时,prop 必须在子组件中声明,这你得读到组件那一章才会了解什么是自组件,以及自组件的props。
没有参数时,可以绑定到一个包含键值对的对象。注意此时 class 和 style 绑定不支持数组和对象。
v-bind:可以缩写成单独的一个冒号:。
修饰符:
- .prop - 被用于绑定 DOM 属性。(what’s the difference?)。编者按:prop和attr是不同的,熟悉jquery的同学应该知道,比如checkbox的checked属性。
- .camel - transform the kebab-case attribute name into camelCase. (supported since 2.1.0)
示例:
<!-- 绑定一个属性 -->\n<img v-bind:src=\"imageSrc\">\n<!-- 缩写 -->\n<img :src=\"imageSrc\">\n<!-- with inline string concatenation -->\n<img :src=\"'/path/to/images/' + fileName\">\n<!-- class 绑定 -->\n<div :class=\"{ red: isRed }\"></div>\n<div :class=\"[classA, classB]\"></div>\n<div :class=\"[classA, { classB: isB, classC: isC }]\">\n<!-- style 绑定 -->\n<div :style=\"{ fontSize: size + 'px' }\"></div>\n<div :style=\"[styleObjectA, styleObjectB]\"></div>\n<!-- 绑定一个有属性的对象 --><div v-bind=\"{ id: someProp, 'other-attr': otherProp }\"></div>\n<!-- 通过 prop 修饰符绑定 DOM 属性 --><div v-bind:text-content.prop=\"text\"></div>\n<!-- prop 绑定. “prop” 必须在 my-component 中声明。 -->\n<my-component :prop=\"someThing\"></my-component>\n<!-- XLink -->\n<svg><a :xlink:special=\"foo\"></a></svg>
camel 修饰符允许在使用 DOM 模板时将 v-bind 属性名称驼峰化,例如 SVG 的 viewBox 属性:
<svg :view-box.camel=\"viewBox\"></svg>
v-on
绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。
用在普通元素上时,只能监听 原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。关于这个“子组件的自定义事件”也可以在下文的“组件”一章学到。
在监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event 属性: v-on:click=\"handle('ok', $event)\"。
v-on:可以缩写成@。
修饰符:
- .stop - 调用 event.stopPropagation()。
- .prevent - 调用 event.preventDefault()。
- .capture - 添加事件侦听器时使用 capture 模式。
- .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
- .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
- .native - 监听组件根元素的原生事件。
- .once - 只触发一次回调。
- .left - (2.2.0) 只当点击鼠标左键时触发。
- .right - (2.2.0) 只当点击鼠标右键时触发。
- .middle - (2.2.0) 只当点击鼠标中键时触发。
示例:
<!-- 方法处理器 -->\n<button v-on:click=\"doThis\"></button>\n<!-- 内联语句 -->\n<button v-on:click=\"doThat('hello', $event)\"></button>\n<!-- 缩写 -->\n<button @click=\"doThis\"></button>\n<!-- 停止冒泡 -->\n<button @click.stop=\"doThis\"></button>\n<!-- 阻止默认行为 -->\n<button @click.prevent=\"doThis\"></button>\n<!-- 阻止默认行为,没有表达式 -->\n<form @submit.prevent></form>\n<!-- 串联修饰符 -->\n<button @click.stop.prevent=\"doThis\"></button>\n<!-- 键修饰符,键别名 -->\n<input @keyup.enter=\"onEnter\">\n<!-- 键修饰符,键代码 -->\n<input @keyup.13=\"onEnter\">\n<!-- 点击回调只会触发一次 -->\n<button v-on:click.once=\"doThis\"></button>
还有更多的修饰符:
- .enter
- .tab
- .delete (捕获 “删除” 和 “退格” 键)
- .esc
- .space
- .up
- .down
- .left
- .right
- .ctrl
- .alt
- .shift
- .meta
这些修饰符是跟键盘相关的,主要是在一些输入相关的元素上可以使用。
v-model
在表单控件或者组件上创建双向绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖,它负责监听用户的输入事件以更新数据,并特别处理一些极端的例子。
v-model 并不关心表单控件初始化所生成的值。因为它会选择 Vue 实例数据来作为具体的值。
对于要求 IME (如中文、 日语、 韩语等) 的语言,你会发现那v-model不会在 ime 构成中得到更新。如果你也想实现更新,请使用 input事件。
编者按:这个指令超级复杂,它一定会跟表单控件联系在一起。所以要事先认真解释一下。首先,要理解双向绑定的概念。非常简单的说,就是包含了两种绑定。第一种就是v-bind的绑定,v-bind绑定之后,如果vue实例对应的属性变化了,视图也会跟着改变。第二种是反过来的,是视图上的改变会导致vue实例对应的属性的变化。视图怎么变呢?比如在input, textarea中输入内容,通过点击,切换radio, checkbox, select的选项。这就是双向绑定。
v-model因为只会用在表单控件上,所以有关内容都在下面的《表单控件绑定》一章中详解。
为了区别v-bind和v-model,我还专门写了一篇文章,你可以在看完这里之后再读一下。
补充:<input v-model=\"something\">其实是<input v-bind:value=\"something\" v-on:input=\"something = $event.target.value\">的语法糖,也就是说v-model本身就已经包含了v-bind,所以当v-bind:value, v-on:input和v-model同时出现在一个input上时,这个v-bind, v-on会失效。这在下文会反复出现,如果你此刻没有理解,可以等到下面阅读到相关内容之后再反过来思考。
控制型指令
还有内置指令可以实现在渲染过程中暂时挂起当前指令所在的元素,或者改变vue默认的渲染机制。
v-pre
不进行模板编译的部分。
<span v-pre>{{ this will not be compiled }}</span>
里面的{{}}不会被认为是js表达式编译成html,而是原模原样的展示出来。
v-once
只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
<!-- 单个元素 -->\n<span v-once>This will never change: {{msg}}</span>\n<!-- 有子元素 -->\n<div v-once><h1>comment</h1><p>{{msg}}</p></div>\n<!-- 组件 -->\n<my-component v-once :comment=\"msg\"></my-component>\n<!-- v-for 指令-->\n<ul><li v-for=\"i in list\" v-once>{{i}}</li></ul>
也就是说,once让渲染失去了数据绑定机制,只有在第一次渲染的时候,会获取当时的变量对应的数据进行渲染,渲染结束之后,将不再跟着数据变化而变化。
这个指令有的时候非常有用。比如一些组件并不需要跟着数据变化而变化的时候。
v-cloak
这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。
[v-cloak] { display: none;}\n\n<div v-cloak>{{ message }}\n</div>
其实也就是一个普通的属性,当编译结束的时候,就把这个属性移除掉。所以上面那个css,使得这些有v-cloak属性的元素全部先隐藏起来,最后当v-cloak被移除的时候,这些元素就显示出来了。为什么要有这么一个奇怪的属性呢?因为你要知道,{{message}}在原始的html中是字符串,是会显示在界面中的。如果你的浏览器编译模板速度慢,或者用户网速慢,vue代码半天没加载完,那么用户就会看到这些奇怪的{{}},而如果使用一个v-cloak属性,就可以把这些内容事先隐藏起来,不让用户看到。
自定义指令
vue里你还可以自定义指令,它们和内置指令一样,以v-开头。开发自己的指令之前,你需要理解指令到底是拿来干什么的,而不要把所有的功能都开发成指令来用。
创建指令
让我们来创建一个指令:当页面加载时,元素将获得焦点。事实上,你访问后还没点击任何内容,input 就获得了焦点。我们希望使用的时候用v-focus来作为标识符。
new Vue({\n directives: {\n focus: {\n inserted(el) {\n el.focus()\n },\n },\n },\n})
这样我们我们就创建了v-focus指令,在模板中,就可以这样使用:
<input v-focus>
这样当页面打开的时候,这个input就会自动获取焦点。
钩子函数
你可以看到上面的定义一个指令的结构:在实力参数中传入directives,在directives里面就是所有的自定义指令,focus就是指令的名称,focus里面会加入多个方法函数,这些函数就是钩子函数,每一个钩子函数都会在不同的时刻执行,从而实现你想要的功能。
指令定义函数提供了几个钩子函数(可选):
- bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。
- inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
- update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新(详细的钩子函数参数见下)。
- componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
- unbind: 只调用一次, 指令与元素解绑时调用。
钩子函数的参数
钩子函数被赋予了以下参数:
- 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, arg 的值是 \"foo\"。
- modifiers: 一个包含修饰符的对象。 例如: v-my-directive.foo.bar, 修饰符对象 modifiers 的值是 { foo: true, bar: true }。
- vnode: Vue 编译生成的虚拟节点,查阅 VNode API 了解更多详情。
- oldVnode: 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
除了 el 之外,其它参数都应该是只读的,尽量不要修改他们。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
<div id=\"hook-arguments-example\" v-demo:hello.a.b=\"message\"></div>\n\nVue.directive('demo', { bind: function (el, binding, vnode) { var s = JSON.stringify el.innerHTML = 'name: ' + s(binding.name) + '<br>' + 'value: ' + s(binding.value) + '<br>' + 'expression: ' + s(binding.expression) + '<br>' + 'argument: ' + s(binding.arg) + '<br>' + 'modifiers: ' + s(binding.modifiers) + '<br>' + 'vnode keys: ' + Object.keys(vnode).join(', ') }})new Vue({ el: '#hook-arguments-example', data: { message: 'hello!' }})
函数简写
大多数情况下,我们可能想在 bind 和 update 钩子上做重复动作,并且不想关心其它的钩子函数。可以这样写:
Vue.directive('color-swatch', function (el, binding) { el.style.backgroundColor = binding.value})
对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法类型的 JavaScript 表达式。
<div v-demo=\"{ color: 'white', text: 'hello!' }\"></div>Vue.directive('demo', function (el, binding) { console.log(binding.value.color) // => \"white\" console.log(binding.value.text) // => \"hello!\"})
到目前为止,所有的方法都是在局部进行定义,而没有进行全局定义,在vue里面,指令可以通过全局方法Vue.directives()来定义,也可以在实例参数、组件参数中加入directives来进行局部定义。局部定义就是说只有在当前这个实例的模板内可以使用指令,而全局定义则可以让所有模板都可以使用这个指令。除了指令,还有下文要提的过滤器也有这两种定义方式。但为了增加学习曲线的平滑度,在前面几章都是主要提到局部定义。后面会有专门的章节来阐述全局和局部问题。
过滤器
什么是过滤器?
什么是过滤器呢?你用过一些模板引擎的话可能知道,比如 {{name | toUpperCase}} ,通过一个管道符号,后面跟上一个函数,对前面的变量进行输出前的处理。
Vue 2.x 中,过滤器只能在 mustache 绑定和 v-bind 表达式(从 2.1.0 开始支持)中使用,因为过滤器设计目的就是用于文本转换。为了在其他指令中实现更复杂的数据变换,你应该使用计算属性。
<!-- in mustaches -->\n{{ message | capitalize }}\n<!-- in v-bind -->\n<div v-bind:id=\"rawId | formatId\"></div>
过滤器可以串联:
{{ message | filterA | filterB }}
过滤器是 JavaScript 函数,因此可以接受参数:
{{ message | filterA('arg1', arg2) }}
这里,字符串 'arg1' 将传给过滤器作为第二个参数, arg2 表达式的值将被求值然后传给过滤器作为第三个参数。
自定义过滤器
从vue2.0开始,vue不再内置过滤器,以前很多过滤器可以直接使用,比如uppercase等。现在不能直接用了,但是我们可以自己自定义。过滤器函数总接受表达式的值作为第一个参数。
new Vue({\n // ...\n filters: {\n capitalize: function (value) { // 注意,这里的参数上面说过了,可以新增其他参数的if (!value) return ''value = value.toString()\n return value.charAt(0).toUpperCase() + value.slice(1)\n }\n }\n})
使用很简单,和上面一样。
{{userName | capitalize}}
总之,把你自己的过滤器放在filters变量中。
Vue2.0 把过滤器这块移除是有道理的,他们希望你更多使用 computed,而不是过滤器。
表单控件绑定
在前面的v-model一节中已经说了,这一章,其实就是对v-model的展开说明。当然,也不完全就是v-model,这章会全面的讲解跟表单相关的所有开发相关内容。
数据的双向绑定
前面说了,v-model是双向绑定的秘诀。那么到底怎么实现呢?
文本 input text
<input v-model=\"message\"> \n<p>Message is: {{ message }}</p>\n\nnew Vue({ \n data: { \n message: 'placeholder'\n }, \n})
这里的双向绑定包括两个绑定,一个是data.message被绑定到input控件上,被作为input的value值。当然,{{message}}也是被绑定好的。当data.message发生变化的时候,视图里面会跟着变。
现在,在input里面输入自己的字符串。由于v-model的作用,data.message也随之发生变化,data.message这次反过来等于输入的值。于此同时,由于data.message变了,{{message}}也跟着变了。
这个过程就是v-model的双向绑定的结果。
编者按:关于v-model是v-bind和v-on的语法糖的问题,你此刻可能不是很懂,因为你还没有阅读事件绑定这一章,下一章会专门讲事件绑定,阅读完下一章你再回头阅读这一章,就会有不一样的理解。
多行文本 textarea
<span>Multiline message is:</span>\n<p style=\"white-space: pre\">{{ message }}</p>\n<br>\n<textarea v-model=\"message\" placeholder=\"add multiple lines\"></textarea>
在文本区域插值( <textarea>{{message}}</textarea> ) 并不会生效,应用 v-model 来代替。
textarea和上面的input text是一样的,效果也和我们想象的一样。
复选框 input checkbox
单个勾选框
<input type=\"checkbox\" v-model=\"checked\">\n<label for=\"checkbox\">{{ checked }}</label>
这里的v-model绑定了checked,但是checked这个变量应该是一个boolean值,表示这个checkbox是否被选中,是绑定到了prop属性。
这个时候,你会问,当复选框选中了,那么我怎么知道要用什么值呢?这里有两种解释,一种是你不需要知道它要用什么值,你只需要知道它的结果是选中还是不选中即可,至于选中了怎么办,不选中又怎么办,请自己来决定。
另一种是给两个绑定值,如下:
<input type=\"checkbox\" v-model=\"checked\" :true-value=\"a\" :false-value=\"b\">
这里的a和b都是变量,选中的时候checked值等于a,即app.checked = app.a,否则app.checked = app.b。这样checked就不是boolean值了。
注意:checkbox单个复选框的时候,不能使用:value=\"xxx\"绑定值,即使绑定了也没用,会被舍弃,读过我前面那篇文章的就知道。
多个勾选框
<input type=\"checkbox\" id=\"jack\" value=\"Jack\" v-model=\"checkedNames\">\n<label for=\"jack\">Jack</label>\n<input type=\"checkbox\" id=\"john\" value=\"John\" v-model=\"checkedNames\">\n<label for=\"john\">John</label>\n<input type=\"checkbox\" id=\"mike\" value=\"Mike\" v-model=\"checkedNames\">\n<label for=\"mike\">Mike</label>\n<span>Checked names: {{ checkedNames }}</span>// 这可以checkedNames直接显示成JSON.stringify()的结果\nnew Vue({\n el: '...',\n data: {\n checkedNames: [],\n }\n})
这里,要求type=\"checkbox\"是一定要有的,如果没有type=\"checkbox\",会报错,type值必须是字符串,不能通过v-bind进行绑定使用动态值,否则vue会报错。
data.checkedNames一定要是数组,当用户勾选了上面checkbox的其中一个时,它的value值会被丢到data.checkedNames这个数组中来。而它的value值则可以通过v-bind绑定一个动态数据。
在HTML中,通过相同的一个name值来确定这组checkbox是一个组的,而在这里,则是通过v-model的值是同一个值来确定是一个组的。
这个时候就可以使用:value来动态绑定value值了,这和单个复选框不一样。
单选按钮 input radio
<div id=\"example-4\" class=\"demo\"><input type=\"radio\" id=\"one\" value=\"One\" v-model=\"picked\"><label for=\"one\">One</label><br><input type=\"radio\" id=\"two\" value=\"Two\" v-model=\"picked\"><label for=\"two\">Two</label><br><span>Picked: {{ picked }}</span>\n</div>\nnew Vue({\n el: '#example-4',\n data: {\n picked: '',\n }\n})
单选框组跟多个checkbox复选框组是一样的。包括:value绑定。唯一不同的是picked是一个单选值,所以是一个值,而不是数组。
选择列表 select
单选列表
<div id=\"example-5\" class=\"demo\"><select v-model=\"selected\"><option>A</option><option>B</option><option>C</option></select><span>Selected: {{ selected }}</span>\n</div>\nnew Vue({\n el: '#example-5',\n data: {\n selected: null\n }\n})
多选列表(绑定到一个数组)
<div id=\"example-6\" class=\"demo\"><select v-model=\"selected\" multiple style=\"width: 50px\"><option>A</option><option>B</option><option>C</option></select><br><span>Selected: {{ selected }}</span>\n</div>\nnew Vue({\n el: '#example-6',\n data: {\n selected: []\n }\n})
因为 multiple 要选择的值是一组,因此 selected 是一个数组。
动态选项,用 v-for 渲染:
<select v-model=\"selected\">\n <option v-for=\"option in options\" v-bind:value=\"option.value\">\n {{ option.text }}\n </option>\n</select>\n<span>Selected: {{ selected }}</span>\nnew Vue({\n el: '...',\n data: {\n selected: 'A',\n options: [\n { text: 'One', value: 'A' },\n { text: 'Two', value: 'B' },\n { text: 'Three', value: 'C' }\n ]\n }\n})
绑定 value
对于单选按钮,勾选框及选择列表选项, v-model 绑定的 value 通常是静态字符串(对于勾选框是逻辑值):
<!-- 当选中时,`picked` 为字符串 \"a\" -->\n<input type=\"radio\" v-model=\"picked\" value=\"a\">\n<!-- `toggle` 为 true 或 false -->\n<input type=\"checkbox\" v-model=\"toggle\">\n<!-- 当选中时,`selected` 为字符串 \"abc\" -->\n<select v-model=\"selected\"><option value=\"abc\">ABC</option>\n</select>
但是有时我们想绑定 value 到 Vue 实例的一个动态属性上,这时可以用 v-bind 实现,并且这个属性的值可以不是字符串。
当我们通过v-bind:value对value进行绑定时,应该要注意,v-bind就是一个单向绑定操作,你有没有必要对这个value进行单向绑定?你应该怎么绑定呢?准备写表单之前,先想好你的表单逻辑再开始写代码。在这之前,一定要先读一下我的这篇文章。
补充:<input v-model=\"something\">其实是<input v-bind:value=\"something\" v-on:input=\"something = $event.target.value\">的语法糖,也就是说v-model本身就已经包含了v-bind,所以当v-bind和v-model同时出现在一个input上时,这个v-bind会失效。
修饰符
.lazy
在默认情况下, v-model 在 input 事件中同步输入框的值与数据 (除了 上述 IME 部分),但你可以添加一个修饰符 lazy ,从而转变为在 change 事件中同步:
<!-- 在 \"change\" 而不是 \"input\" 事件中更新 --> \n<input v-model.lazy=\"msg\" >
.number
如果想自动将用户的输入值转为 Number 类型(如果原值的转换结果为 NaN 则返回原值),可以添加一个修饰符 number 给 v-model 来处理输入值:
<input v-model.number=\"age\" type=\"number\">
这通常很有用,因为在 type=\"number\" 时 HTML 中输入的值也总是会返回字符串类型。
.trim
如果要自动过滤用户输入的首尾空格,可以添加 trim 修饰符到 v-model 上过滤输入:
<input v-model.trim=\"msg\">
表单验证:vue-validator
这一章是插入的一章,在实际开发中,你不一定需要vue-validator来验证你的表单选项。但是本章可以给你一些启示,帮助你设计自己的验证思路。vue-validator也不是vue官方的插件,而是一个个人项目。不过插件作者比较早的开发了完善的验证机制,所以插件也得到广泛使用。vue-validator目前对vue2.2+是不支持的,v3版本支持vue2.0。它也有自己的完整中文文档,你可以点击这里进入阅读。但是这个中文文档是v2.x的,v2.x版本不支持vue2.0,但是大部分接口都是一样的,所以也可以看。但是因为vue-validator对vue2.0的支持还不稳定,所以,本书也更多的只介绍一些常用功能,以及验证思路。
安装和开始
vue 的插件都使用use方法来安装。我们使用 npm 来安装 vue-validator:
npm install --save vue-validator
安装好之后,在你的项目中使用它:
import Vue from 'vue'import VueValidator form 'vue-validator'\nVue.use(VueValidator)
在这之后,你就可以使用vue-validator进行表单验证了。
<div id=\"app\"><validator name=\"validation1\"><form novalidate><div class=\"username-field\"><label for=\"username\">username:</label><input id=\"username\" type=\"text\" v-validate:username=\"['required']\"></div><div class=\"comment-field\"><label for=\"comment\">comment:</label><input id=\"comment\" type=\"text\" v-validate:comment=\"{ maxlength: 256 }\"></div><div class=\"errors\"><p v-if=\"$validation1.username.required\">Required your name.</p><p v-if=\"$validation1.comment.maxlength\">Your comment is too long.</p></div><input type=\"submit\" value=\"send\" v-if=\"$validation1.valid\"></form></validator>\n</div>
验证结果会关联到验证器元素上。在上例中,验证结果保存在 $validation1 下,$validation1 是由 validator 元素的 name 属性值加 $ 前缀组成。
验证结果结构
验证结果(也就是上面的$validation1)有如下结构:
{\n // top-level validation propertiesvalid: true,\n invalid: false,\n touched: false,\n undefined: true,\n dirty: false,\n pristine: true,\n modified: false,\n errors: [{\n field: 'field1', validator: 'required', message: 'required field1'\n },\n ...\n {\n field: 'fieldX', validator: 'customValidator', message: 'invalid fieldX'\n }],\n // field1 validationfield1: {\n required: false, // build-in validator, return `false` or `true`email: true, // custom validatorurl: 'invalid url format', // custom validator, if specify the error message in validation rule, set it...customValidator1: false, // custom validator// field validation propertiesvalid: false,\n invalid: true,\n touched: false,\n undefined: true,\n dirty: false,\n pristine: true,\n modified: false,\n errors: [{\n validator: 'required', message: 'required field1'\n }]\n },\n ...// fieldX validationfieldX: {\n min: false, // validator...customValidator: true,\n // fieldX validation propertiesvalid: false,\n invalid: true,\n touched: true,\n undefined: false,\n dirty: true,\n pristine: false,\n modified: true,\n errors: [{\n validator: 'customValidator', message: 'invalid fieldX'\n }]\n },\n}
全局结果可以直接从验证结果中获取到,字段验证结果保存在以字段名命名的键下。
字段验证结果
- valid: 字段有效时返回 true,否则返回 false。
- invalid: valid 的逆.
- touched: 字段获得过焦点时返回 true,否则返回 false。
- untouched: touched 的逆.
- modified: 字段值与初始值不同时返回 true,否则返回 false。
- dirty: 字段值改变过至少一次时返回 true,否则返回 false。
- pristine: dirty 的逆.
- errors: 字段无效时返回存有错误信息的数据,否则返回 undefined。
全局结果
- valid: 所有字段都有效时返回 true,否则返回 false。
- invalid: 只要存在无效字段就返回 true,否则返回 false。
- touched: 只要存在获得过焦点的字段就返回 true,否则返回 false。
- untouched: touched 的逆。
- modified: 只要存在与初始值不同的字段就返回 true,否则返回 false。
- dirty: 只要存在值改变过至少一次的字段就返回 true,否则返回 false。
- pristine: 所有字段都没有发生过变化时返回 true,否则返回 false。
- errors: 有无效字段时返回所有无效字段的错误信息,否则返回 undefined。
验证器语法
v-validate 指令用法如下:
v-validate[:field]=\"array literal | object literal | binding\"
字段
2.0-alpha以前的版本中,验证器是依赖于 v-model 的。从2.0-alpha版本开始,v-model 是可选的。
~v1.4.4:
<form novalidate>\n <input type=\"text\" v-model=\"comment\" v-validate=\"minLength: 16, maxLength: 128\">\n <div>\n <span v-show=\"validation.comment.minLength\">Your comment is too short.</span>\n <span v-show=\"validation.comment.maxLength\">Your comment is too long.</span>\n </div>\n <input type=\"submit\" value=\"send\" v-if=\"valid\">\n</form>
v2.0-alpha后:
<validator name=\"validation\">\n <form novalidate>\n <input type=\"text\" v-validate:comment=\"{ minlength: 16, maxlength: 128 }\">\n <div>\n <span v-show=\"$validation.comment.minlength\">Your comment is too short.</span>\n <span v-show=\"$validation.comment.maxlength\">Your comment is too long.</span>\n </div>\n <input type=\"submit\" value=\"send\" v-if=\"valid\">\n </form>\n</validator>
上面的蓝色comment使得$validation多了一个comment属性,也就是红色的comment。蓝色的大括号里面是规则,其中minlength的规则是16,而当你输入内容之后,验证结果怎么样呢?$validation.comment.minlength就可以得到验证的结果。
事件绑定
在前端领域,事件绑定你应该不陌生,比如click, hover,已经熟的不能再熟悉了。本章就来介绍在vue里面怎么快速实现事件绑定。
<button @click.prevent=\"say('A word!', $event)\">ok</button>
上面这短短一段代码,有4个东西要介绍,下面一一介绍:
使用v-on:绑定事件
前面已经对v-on指令讲过了,不讲了。
方法函数
事件的回调函数可以直接写在methods里面:
new Vue({\n methods: {\n say(word, e) { alert(word) },\n },\n})
$event变量
它是直接被绑定到vue的实例上的属性。相当于this.$event,也是事件信息,比如$event.target等信息。它和DOM原生事件的event是一样的。
修饰符
事件修饰符比较多,前文已经提到了,也不讲了。
自定义事件
vue提供了四个和事件相关的实例方法,分别是$on, $once, $off, $emit,用过jquery的同学应该非常熟悉了。我们来看下具体怎么用:
var app = new Vue({...})\napp.$on('myEvent', (e, value) => console.log(value))\n....\napp.$emit('myEvent', 'changed')
上面的$on和$emit都是vue实例的方法,所以说,对于事件绑定而言,vue内部直接提供了非指令的事件系统,跟jquery的用法几乎一样,v-on主要是在模板中绑定DOM原生的一些事件,而$on, $once可以绑定自定义的事件,两者互不干扰,$emit能否触发原生的DOM事件呢?请注意阅读下文。
$on
监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
vm.$on('test', function (msg) {\n console.log(msg)\n})\nvm.$emit('test', 'hi')// -> \"hi\"
$once
监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
vm.$on('test', function (msg) {\n console.log(msg)\n})\nvm.$emit('test', 'hi')// -> \"hi\",同时移除test事件\nvm.$emit('test', 'again') // test事件被移除了,所以这个触发不会有任何结果
$on和$once绑定的事件是在实例上,而非DOM元素上,所以它们跟DOM原生的事件是两回事。DOM原生事件是在触发DOM元素特定事件时被触发的,比如click。但是对于这里的实例vm而言,click没有来源,实例根本不存在被click之说,所以$on和$once跟DOM原生事件扯不上任何关系。同理,$emit也是作用于实例之上,既然实例跟原生的DOM事件扯不上关系,那么$emit也就跟原生DOM事件扯不上关系了。这就回答了上文提出的那个问题。
所以说,$on和$once绑定的是一个自定义事件,这些事件是存储在vue内部的事件管理器中,跟DOM事件是两码事,既然如此,跟v-on事件绑定也就是两回事。
$off
移除事件监听器。
- 如果没有提供参数,则移除所有的事件监听器;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器。
$emit
触发当前实例上的事件。附加参数都会传给监听器回调。参数怎么传前面的代码已经演示过了。
Class和Style的绑定
数据绑定一个常见需求是操作元素的 class 列表和它的内联样式。因为它们都是属性 ,我们可以用v-bind 处理它们:只需要计算出表达式最终的字符串。不过,字符串拼接麻烦又易错。因此,在 v-bind 用于 class 和 style 时, Vue.js 专门增强了它。表达式的结果类型除了字符串之外,还可以是对象或数组。
绑定 HTML Class
对象语法
我们可以传给 v-bind:class 一个对象,以动态地切换 class 。
<div v-bind:class=\"{ active: isActive }\"></div>
上面的语法表示 classactive 的更新将取决于数据属性 isActive 是否为真值 。
我们也可以在对象中传入更多属性用来动态切换多个 class 。此外, v-bind:class 指令可以与普通的 class 属性共存。如下模板:
<div class=\"static\" v-bind:class=\"{ active: isActive, 'text-danger': hasError }\"></div>
如下 data:
渲染为:
<div class=\"static active\"></div>
当 isActive 或者 hasError 变化时,class 列表将相应地更新。例如,如果 hasError 的值为 true , class列表将变为 \"static active text-danger\"。
你也可以直接绑定数据里的一个对象:
<div v-bind:class=\"classObject\"></div>\ndata: {\n classObject: {\n active: true,\n 'text-danger': false\n }\n}
渲染的结果和上面一样。我们也可以在这里绑定返回对象的计算属性。这是一个常用且强大的模式:
<div v-bind:class=\"classObject\"></div>\n\ndata: {\n isActive: true,\n error: null\n},\ncomputed: {\n classObject: function () {\n return {\n active: this.isActive && !this.error,\n 'text-danger': this.error && this.error.type === 'fatal',\n }\n }\n}
数组语法
我们可以把一个数组传给 v-bind:class ,以应用一个 class 列表:
<div v-bind:class=\"[activeClass, errorClass]\">\n\ndata: {\n activeClass: 'active',\n errorClass: 'text-danger'\n}
渲染为:
<div class=\"active text-danger\"></div>
如果你也想根据条件切换列表中的 class ,可以用三元表达式:
<div v-bind:class=\"[isActive ? activeClass : '', errorClass]\">
此例始终添加 errorClass ,但是只有在 isActive 是 true 时添加 activeClass 。
不过,当有多个条件 class 时这样写有些繁琐。可以在数组语法中使用对象语法:
<div v-bind:class=\"[{ active: isActive }, errorClass]\">
用在组件上
这个章节假设你已经对 Vue 组件 有一定的了解。当然你也可以跳过这里,稍后再回过头来看。当你在一个定制的组件上用到 class 属性的时候,这些类将被添加到根元素上面,这个元素上已经存在的类不会被覆盖。
例如,如果你声明了这个组件:
Vue.component('my-component', { template: '<p class=\"foo bar\">Hi</p>'})
然后在使用它的时候添加一些 class:
<my-component class=\"baz boo\"></my-component>
HTML 最终将被渲染成为:
<p class=\"foo bar baz boo\">Hi</p>
同样的适用于绑定 HTML class :
<my-component v-bind:class=\"{ active: isActive }\"></my-component>
当 isActive 为 true 的时候,HTML 将被渲染成为:
<p class=\"foo bar active\">Hi</p>
绑定内联样式
对象语法
v-bind:style 的对象语法十分直观——看着非常像 CSS ,其实它是一个 JavaScript 对象。 CSS 属性名可以用驼峰式(camelCase)或短横分隔命名(kebab-case):
<div v-bind:style=\"{ color: activeColor, fontSize: fontSize + 'px' }\"></div>\n\ndata: {\n activeColor: 'red',\n fontSize: 30\n}
直接绑定到一个样式对象通常更好,让模板更清晰:
<div v-bind:style=\"styleObject\"></div>\n\ndata: {\n styleObject: {\n color: 'red',\n fontSize: '13px'\n }\n}
同样的,对象语法常常结合返回对象的计算属性使用。
数组语法
v-bind:style 的数组语法可以将多个样式对象应用到一个元素上:
<div v-bind:style=\"[baseStyles, overridingStyles]\">
自动添加前缀
当 v-bind:style 使用需要特定前缀的 CSS 属性时,如 transform ,Vue.js 会自动侦测并添加相应的前缀。
修饰符
前面两处提到了修饰符,这一章其实主要是要总结一下,内容是跟前面一样的,如果你已经理解了,就不用看。
事件修饰符
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在 methods 中轻松实现这点,但更好的方式是:methods 只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题, Vue.js 为 v-on 提供了 事件修饰符。通过由点(.)表示的指令后缀来调用修饰符。
- .stop
- .prevent
- .capture
- .self
- .once
按键修饰符
在监听键盘事件时,我们经常需要监测常见的键值。 Vue 允许为 v-on 在监听键盘事件时添加按键修饰符。记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名。
全部的按键别名:
- .enter
- .tab
- .delete (捕获 “删除” 和 “退格” 键)
- .esc
- .space
- .up
- .down
- .left
- .right
可以通过全局 config.keyCodes 对象自定义按键修饰符别名。
Vue.config.keyCodes.f1 = 112\n// 这样用\n<input v-on:keyup.112=\"submit\">
可以用如下修饰符开启鼠标或键盘事件监听,使在按键按下时发生响应。
- .ctrl
- .alt
- .shift
- .meta
组件
终于进入组件这一章。前文多次提到组件是 vue 里面非常核心的概念。但是实际上,组件在本书中,更多的是提倡作为一种思想,具体使用的时候跟本书最前面的“vue实例”差别不会太大,除了data()的地方需要着重强调,如果不涉及其他,组件实例和app级别实例差距不会有太大理解上的鸿沟。
既然是一种思想,那么组件涉及的问题就不单单是编程问题。比如组件之间如何通信,父子组件之间的问题,数据流等问题。这些只有当我们探讨组件才会提出来的问题,需要你更多的进行思考,想通之后,这些问题在实践中怎么处理就迎刃而解了。
这一章也是所有入门级别知识的最后一章,从这一章之后,就要进入到更加深入的学习,这些深入学习是必须的,不然无法认识 vue 的原貌。但是深入学习之前,前面几章的基础学习可以帮助你快速理解 vue 的基本使用方法,掌握之后,算是入门了。
什么是组件?
组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
简单的说,vue组件就是一个js类,使用的时候new一下,得到一个组件的实例,组件实例有很多接口,app层面就可以调用这些接口来把组件的功能集成到app中。
在开发层面上,你只需要使用Vue.extend({...})就可以得到一个组件。而开发的大部分工作,就是写好大括号里面的内容。
创建一个组件
上面说过了,开发层面上,创建一个组件只需要使用Vue.extend,如下:
上面说过了,开发层面上,创建一个组件只需要使用Vue.extend,如下:
var MyComponent = Vue.extend({\n template: '<div>OK</div>',\n})
这样就创建了一个组件构造器MyComponent。实际上,Vue.extend的结果是Vue的一个子类。既然是一个类,就可以被实例化,通过实例化得到一个组件实例,“组件实例”这个称呼在前面已经提到过了。组件构造器是一个类,可以被实例化为组件实例。
var component1 = new MyComponent()
关于Vue.extend,我们会在后文继续深入,这里就不赘述了。组件实例有一些方法可以用,例如$mount方法,这也在后面的Vue.extend部分去解释。本章主要是要让你学会组件的思想和主要开发方式。
注册一个组件
创建好了组件,接下来要在vue中注册它。注册组件也有全局和局部两种。注册是什么概念呢?其实注册是创建一个组件构造器的引用,并可以形式化的进行实例化。所谓形式化,简单的说,就是把组件注册成一个html元素,这样可以直接在模板中使用这个跟组件等价的元素来实例化组件。
// 局部注册\nnew Vue({\n el: '#app',\n template: '<div><my-component></my-component></div>',\n components: {\n 'my-component': MyComponent,\n },\n})\n\n// 全局注册Vue.component('my-component', MyComponent)
// 全局注册Vue.component('my-component', MyComponent)
全局注册有一个好处,就是可以在任何一个vue实例中去使用。
使用一个注册好的组件
组件注册好之后,就可以在模板中使用注册的组件名称,像一个html元素一样调用它,而且这个元素还支持指令,比如v-for之类的。基本的使用方法是在app的模板中使用它:
<div><my-component></my-component>\n</div>
因为在创建组件的时候,传入了template值,所以<my-component>的地方就会替换为组件的模板编译后的html,所以最终看到的结果是:
<div>\n <div>OK</div>\n</div>
这是最简单的情况,就是模板替换一下而已。如果再把指令加进来,把数据绑定加进来,组件和app的互动就非常复杂了。
使用一个组件(比如在模板中插入<my-component>元素)的本质,是创建一个组件实例。也就是说,一个<my-component>就是一个组件实例,它们共享一个组件构造器(一个js类)。
简易注册
上面我的做法是先创建一个组件构造器,然后把组件构造器传入实例构造器。实际上,在实践开发中,我们基本不会这样干,我们大部分都会使用简易方式直接注册,跳过创建步骤:
Vue.component('my-component', {\n template: '<div>OK</div>',\n})
包括在创建vue实例的时候也是这样,直接传对象字面量进去即可。相当于注册过程会自动创建一个匿名的组件构造器。
在开发中,如果不涉及组件实例复用问题,这个方法非常好。也是最推荐的做法。
data必须是函数
在创建或注册模板的时候,传入一个data属性作为用来绑定的数据。但是在组件中,data必须是一个函数,而不能直接把一个对象赋值给它。
Vue.component('my-component', {\n template: '<div>OK</div>',\n data() {\n return {} // 返回一个唯一的对象,不要和其他组件共用一个对象进行返回\n },\n})
你在前面看到,在new Vue()的时候,是可以给data直接赋值为一个对象的。这是怎么回事,为什么到了组件这里就不行了。
你要理解,上面这个操作是一个简易操作,实际上,它首先需要创建一个组件构造器,然后注册组件。注册组件的本质其实就是建立一个组件构造器的引用。使用组件才是真正创建一个组件实例。所以,注册组件其实并不产生新的组件类,但会产生一个可以用来实例化的新方式。
理解这点之后,再理解js的原型链:
var MyComponent = function() {}MyComponent.prototype.data = {\n a: 1,\n b: 2,\n}// 上面是一个虚拟的组件构造器,真实的组件构造器方法很多\nvar component1 = new MyComponent()\nvar component2 = new MyComponent()// 上面实例化出来两个组件实例,也就是通过<my-component>调用,创建的两个实例\ncomponent1.data.a === component2.data.a // true\ncomponent1.data.b = 5component2.data.b // 5
可以看到上面代码中最后三句,这就比较坑爹了,如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改。这怎么可以,两个实例应该有自己各自的域才对。所以,需要通过下面方法来进行处理:
var MyComponent = function() {\n this.data = this.data()}MyComponent.prototype.data = function() {\n return {\n a: 1,\n b: 2,\n }\n}
这样每一个实例的data属性都是独立的,不会相互影响了。所以,你现在知道为什么vue组件的data必须是函数了吧。这都是因为js本身的特性带来的,跟vue本身设计无关。其实vue不应该把这个方法名取为data(),应该叫setData或其他更容易立即的方法名。
prop
什么是prop?
首先,什么是prop?prop也是属性,但是它和attribution不一样,attribution往往是固定的值属性,而prop更多的是动态的状态值属性,最简单的例子就是input checkbox的checked属性,checked属性的attr值是它的初始值,而prop值是它的当前值,这对于熟悉jquery的同学而已应该比较好理解。
为组件声明props属性
vue里面,组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的props选项。
子组件要显式地用 props 选项声明它期待获得的数据:
Vue.component('child', {\n // 声明 props\n props: ['message'],\n // 就像 data 一样,prop 可以用在模板内\n // 同样也可以在 vm 实例中像 “this.message” 这样使用\n template: '<span>{{ message }}</span>'\n})
使用prop属性
这样之后,message就变成了一个prop属性,在模板中,你使用child这个元素时,就可以给这个元素传一个message属性进去:
<child message=\"hello!\"></child>
如果在创建组件的时候没有声明props,那么<child>的message就没用。
prop属性命名注意点
如果你在声明props的时候,属性名是多个单词构成的怎么办?在注册组件的时候使用驼峰命名方式:
Vue.component('child', {\n // camelCase in JavaScriptprops: ['myMessage'],\n template: '<span>{{ myMessage }}</span>'\n})
但是在使用组件的时候,传入的属性名得是短横线隔开的:
<child my-message=\"hello!\"></child>
这是因为html不区分大小写,你写成<child myMessage=\"hello!\"></child>的话,假如有一个属性名是mymessage怎么办?所以,一定要注意这一点。
动态绑定prop属性值
既然是当做html元素的属性,那么就跟前面讲的模板语法想通,你可以在prop属性上尝试使用一些指令,比如v-bind,例如:
<child :my-message=\"msg\"></child>
注意,这里的<child>是在父组件,或者app层面的vue实例的模板中使用的,所以这里的msg这个变量也是来自父组件或app。
这里有一个注意点,如果是普通的传值,不使用v-bind,那么值的内容是一个字符串,即使你给了一个数字,它传进去还是个字符串。想要传数字或其他类型的数据,应该用v-bind,比如:
<child :my-message=\"1\"></child>
这个时候组件内获取的才是number,而不是string。
单向数据绑定
prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
这一点非常好理解:JavaScript的对象是引用类型数据,它的每一个属性都指向同一个内存空间,所以不同的变量引用同一个对象的话,其中一个的属性一改,另一个也会跟着改。父组件和子组件之间的prop也是一样,你修改子组件里面的prop,那么父组件里面的对应的属性也该了,这会让父组件或app层面产生混乱,一定会出bug。
正确的处理方法是,在创建组件时,保证组件只是接收prop数据,接收到以后马上放在自己的私有属性中去:
props: ['initialCounter'],data: function () {\n return { counter: this.initialCounter }\n}\n\nprops: ['size'],computed: {\n normalizedSize: function () {\n return this.size.trim().toLowerCase()\n }\n}
上面一个是通过data直接挂在一个属性上,一个是通过计算属性,把prop只当做计算的一个依赖。总之,这两种方式都可以解决上面说的不允许子组件修改prop原始值的问题。
如果你使用data,你在组件内修改了这个属性值,那就跟prop没关系了,后期父组件的prop值改变,也不会影响data了。
Prop 验证
我们可以为组件的 props 指定验证规格。如果传入的数据不符合规格,Vue 会发出警告。当组件给其他人使用时,这很有用。
要指定验证规格,需要用对象的形式,而不能用字符串数组:
Vue.component('example', {\n props: {\n // 基础类型检测 (`null` 意思是任何类型都可以)propA: Number,\n // 多种类型propB: [String, Number],\n // 必传且是字符串propC: {\n type: String,\n required: true\n },\n // 数字,有默认值propD: {\n type: Number,\n default: 100\n },\n // 数组/对象的默认值应当由一个工厂函数返回propE: {\n type: Object,\n default: function () {\n return { message: 'hello' }\n }\n },\n // 自定义验证函数propF: {\n validator: function (value) {\n return value > 10\n }\n }\n }\n})
type 可以是下面原生构造器:
- String
- Number
- Boolean
- Function
- Object
- Array
type 也可以是一个自定义构造器函数,使用 instanceof 检测。
当 prop 验证失败,Vue会在抛出警告 (如果使用的是开发版本)。
事件反馈
前面一节指出,子组件不能自己主动修改prop,以防改变了父组件或app的值导致bug。那么如果子组件确实需要上级应用修改这些变量,让自己更好的适应新情况怎么办?那就让父组件或app自己来改。可是父组件怎么知道自己要改?通过子组件的事件就可以做到了。
前文我们已经学过事件绑定的知识了,组件的实例也具有事件绑定的能力。所以我们可以在子组件里面触发事件,在父组件里面监听这个事件,当事件被触发时,父组件通过绑定的回调函数来执行prop的更改。当然除了更改prop,还可以做其他的事情。
只能使用v-on绑定自定义事件
前文我们指出,直接用实例的$on方法来绑定自定义事件。但是在这里不行,不能直接在父组件里面直接用$on,因为我们使用<my-component>实例化组件时,没有得到一个变量用来存储实例化对象,它相当于是匿名的,所以我们找不到它,当然也找不到它的$on。
那怎么办?只能使用v-on来进行事件绑定。
<child @clicked=\"showSomething\"></child>
这里的clicked是事件名,在子组件里面,通过this.$emit('clicked')触发事件。父组件里面,通过上面代码中的@clicked=\"showSomething\"监听这个事件,而showSomething就是事件回调函数,是父组件methods方法之一。
使用v-model实现input双向绑定
上面不是说只能使用v-on绑定事件吗?是的,但是你是否还记得前文提到过v-model其实是v-bind和v-on的语法糖?上一节我们说过了,<child>可以使用v-bind,而这里又说可以使用v-on,所以只要情况允许,就可以使用v-model。
所谓情况允许,是指符合下面条件:
- 接受一个 value 属性
- 在有新的 value 时触发 input 事件
所以,只有下面这种<child>才可以使用v-model实现双向绑定:
Vue.component('child', {\n\ttemplate: '<input :value=\"value\" @keyup=\"update($event.target.value)\">', //\n props: ['value'],\n methods: {\n update(value) {\n this.$emit('input', value)\n },\n\t},\n})
这样,在父组件中才可以这样使用:
<child v-model=\"someData\"></child>\n// 等价于:\n<child :value=\"someData\" @input=\"someData = $event.target.value\"></child>
:value表示的是v-bind部分,@表示v-on部分。组件内部,keyup是input元素的DOM原生事件,udpate是回调函数,当keyup的时候执行update(),而update()的时候就$emit('input'),触发了父组件的v-on:input。
基于这种原理,不一定要使用在input输入框上,实际上,任何元素都可以模拟这种方式实现数据双向绑定。当然,如果没有输入,双向绑定的说法就很奇怪。
使用 Slot 分发内容
在使用组件时,我们常常要像这样组合它们:
<app><app-header></app-header><app-footer></app-footer>\n</app>
注意两点:
- <app> 组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
- <app> 组件很可能有它自己的模版。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API ,参照了当前 Web 组件规范草案,使用特殊的 <slot> 元素作为原始内容的插槽。
编译作用域
在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为:
<child-component>{{ message }}</child-component>
message应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
<!-- 无效 --><child-component v-show=\"someChildProperty\"></child-component>
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
如果要绑定作用域内的指令到一个组件的根节点,你应当在组件自己的模板上做:
Vue.component('child-component', { // 有效,因为是在正确的作用域内 template: '<div v-show=\"someChildProperty\">Child</div>', data: function () { return { someChildProperty: true } }})
类似地,分发内容是在父作用域内编译。
单个 Slot
除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身。
最初在 <slot> 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。
假定 my-component 组件有下面模板:
<div><h2>我是子组件的标题</h2><slot>\n 只有在没有要分发的内容时才会显示。\n </slot>\n</div>
父组件模版:
<div><h1>我是父组件的标题</h1><my-component><p>这是一些初始内容</p><p>这是更多的初始内容</p></my-component>\n</div>
渲染结果:
<div><h1>我是父组件的标题</h1><div><h2>我是子组件的标题</h2><p>这是一些初始内容</p><p>这是更多的初始内容</p></div>\n</div>
具名 Slot
<slot> 元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。
仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。
例如,假定我们有一个 app-layout 组件,它的模板为:
<div class=\"container\">\n <header>\n <slot name=\"header\"></slot>\n </header>\n <main>\n <slot></slot>\n </main>\n <footer>\n <slot name=\"footer\"></slot>\n </footer>\n</div>
父组件模版:
<app-layout><h1 slot=\"header\">这里可能是一个页面标题</h1><p>主要内容的一个段落。</p><p>另一个主要段落。</p><p slot=\"footer\">这里有一些联系信息</p>\n</app-layout>
渲染结果为:
<div class=\"container\">\n <header>\n <h1>这里可能是一个页面标题</h1>\n </header>\n <main>\n <p>主要内容的一个段落。</p>\n <p>另一个主要段落。</p>\n </main>\n <footer>\n <p>这里有一些联系信息</p>\n </footer>\n</div>
在组合组件时,内容分发 API 是非常有用的机制。
作用域插槽
作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。2.1.0才新增的,因此,如果你只是使用2.0版本,还是使用不了。
在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样:
<div class=\"child\">\n <slot text=\"hello from child\"></slot>\n</div>
在父级中,具有特殊属性 scope 的 <template> 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象:
<div class=\"parent\"><child><template scope=\"props\"><span>hello from parent</span><span>{{ props.text }}</span></template></child>\n</div>
如果我们渲染以上结果,得到的输出会是:
<div class=\"parent\">\n <div class=\"child\">\n <span>hello from parent</span>\n <span>hello from child</span>\n </div>\n</div>
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项:
<my-awesome-list :items=\"items\"><!-- 作用域插槽也可以是具名的 --><template slot=\"item\" scope=\"props\"><li class=\"my-fancy-item\">{{ props.text }}</li></template>\n</my-awesome-list>
列表组件的模板:
<ul><slot name=\"item\"v-for=\"item in items\":text=\"item.text\"\n ><!-- 这里写入备用内容 --></slot>\n</ul>
内联模板
如果子组件有 inline-template 特性,组件将把它的内容当作它的模板,而不是把它当作分发内容。这让模板更灵活。
<my-component inline-template><div><p>These are compiled as the component's own template.</p><p>Not parent's transclusion content.</p></div>\n</my-component>
但是 inline-template 让模板的作用域难以理解。最佳实践是使用 template 选项在组件内定义模板或者在 .vue 文件中使用 template 元素。
编者按:关于.vue文件,会在后文详细讲解。
动态组件
通过使用保留的 <component> 元素,动态地绑定到它的 is 特性,我们让多个组件可以使用同一个挂载点,并动态切换:
var vm = new Vue({\n el: '#example',\n data: {\n currentView: 'home'\n },\n components: {\n home: { /* ... */ },\n posts: { /* ... */ },\n archive: { /* ... */ }\n }\n})\n\n<component v-bind:is=\"currentView\"><!-- 组件在 vm.currentview 变化时改变! -->\n</component>
也可以直接绑定到组件对象上:
var Home = {\n template: '<p>Welcome home!</p>'\n}\nvar vm = new Vue({\n el: '#example',\n data: {\n currentView: Home\n }\n})
在动态组件模式下,你可以使用keep-alive指令实现一个缓存效果:
<keep-alive><component :is=\"currentView\"><!-- 非活动组件将被缓存! --></component>\n</keep-alive>
在API 参考查看更多 <keep-alive> 的细节。
异步创建组件
在大型应用中,我们可能需要将应用拆分为多个小模块,按需从服务器下载。为了让事情更简单, Vue.js 允许将组件定义为一个工厂函数,动态地解析组件的定义。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
这里的工厂函数和promise的工厂函数是一样的,接受resolve, reject两个参数。resolve的时候,将创建数组所要用到的对象返回即可。
Vue.component('async-example', function (resolve, reject) {\n setTimeout(function () {\n // Pass the component definition to the resolve callback\n resolve({\n template: '<div>I am async!</div>'\n })\n }, 1000)\n})
工厂函数接收一个 resolve 回调,在收到从服务器下载的组件定义时调用。也可以调用 reject(reason) 指示加载失败。
递归和循环引用
递归组件
组件在它的模板内可以递归地调用自己,不过,只有当它有 name 选项时才可以:
name: 'unique-name-of-my-component'
当你利用Vue.component全局注册了一个组件, 全局的ID作为组件的 name 选项,被自动设置.
Vue.component('unique-name-of-my-component', {\n // ...\n})
如果你不谨慎, 递归组件可能导致死循环:
name: 'stack-overflow',\ntemplate: '<div><stack-overflow></stack-overflow></div>'
上面组件会导致一个错误 “max stack size exceeded” ,所以要确保递归调用有终止条件 (比如递归调用时使用 v-if 并让他最终返回 false )。
组件间的循环引用
假设你正在构建一个文件目录树,像在Finder或文件资源管理器中。你可能有一个 tree-folder组件:
<p><span>{{ folder.name }}</span><tree-folder-contents :children=\"folder.children\"/>\n</p>
然后 一个tree-folder-contents组件:
<ul><li v-for=\"child in children\"><tree-folder v-if=\"child.children\" :folder=\"child\"/><span v-else>{{ child.name }}</span></li>\n</ul>
当你仔细看时,会发现在渲染树上这两个组件同时为对方的父节点和子节点–这点是矛盾的。当使用Vue.component将这两个组件注册为全局组件的时候,框架会自动为你解决这个矛盾,如果你是这样做的,就不用继续往下看了。
然而,如果你使用诸如Webpack或者Browserify之类的模块化管理工具来requiring/importing组件的话,就会报错了:
Failed to mount component: template or render function not defined.
为了解释为什么会报错,简单的将上面两个组件称为 A 和 B ,模块系统看到它需要 A ,但是首先 A 需要 B ,但是 B 需要 A, 而 A 需要 B,陷入了一个无限循环,因此不知道到底应该先解决哪个。要解决这个问题,我们需要在其中一个组件中(比如 A )告诉模块化管理系统,“A 虽然需要 B ,但是不需要优先导入 B”。
在我们的例子中,我们选择在tree-folder 组件中来告诉模块化管理系统循环引用的组件间的处理优先级,我们知道引起矛盾的子组件是tree-folder-contents,所以我们在beforeCreate 生命周期钩子中去注册它:
beforeCreate: function () {\n this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue')\n}
问题解决了。
X-Templates
另一种定义模版的方式是在 JavaScript 标签里使用 text/x-template 类型,并且指定一个id。例如:
<script type=\"text/x-template\" id=\"hello-world-template\"><p>Hello hello hello</p>\n</script>\nVue.component('hello-world', {\n template: '#hello-world-template'\n})
这在有很多模版或者小的应用中有用,否则应该避免使用,因为它将模版和组件的其他定义隔离了。
对低开销的静态组件使用v-once
尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:
Vue.component('terms-of-service', {\n template: `<div v-once><h1>Terms of Service</h1>\n ... a lot of static content ... \n </div>`\n})
小结
这一章介绍了vue的核心理念之一——组件。组件的概念并没有想象的那么复杂,简单的说就是一个相对隔离的vue子类的实例化对象。怎么来理解这里的“隔离”呢?主要是两个方面:
- 组件通过props属性获取父组件或app层面传来的数据,这些数据不应该被直接修改,也就是说这些数据仅属于组件的上一层,而不属于当前组件,组件不能通过修改这些数据来影响上一层;
- 组件不能直接对父组件或app层产生影响,但是可以通过事件绑定对上一层进行事件通知,上一层接收到这些通知时,自己决定是否要进行变化。
组件的另一个话题就是复用性。基于上面两个方面的特性,组件应该是具备高可复用性的,当一个地方需要使用这个组件时,只需要实例化,并给适合的props即可。只要props给的符合要求,组件就可以根据自己的逻辑运行,既不受外界影响,也不影响外界。
全局和局部
在前文我多次提到了全局和局部的问题,可以看到,好几个操作,其实都存在全局和局部的相同操作。但前文我们大部分时间更专注局部,本章其实更多从全局角度出发,把之前从来没提过的全局方法,都提一遍。这样可以帮助你更好的全面了解vue的api。
全局配置
Vue.config 是一个对象,包含 Vue 的全局配置。可以在启动应用之前修改下列属性:
silent
- 类型: boolean
- 默认值: false
- 作用:取消 Vue 所有的日志与警告。
Vue.config.silent = true
optionMergeStrategies
- 类型: { [key: string]: Function }
- 默认值: {}
- 作用:自定义合并策略的选项。合并策略选项分别接受第一个参数作为父实例,第二个参数为子实例,Vue实例上下文被作为第三个参数传入。
- 参考 自定义选项的混合策略
devtools
- 类型: boolean
- 默认值: true (生产版为 false)
- 作用:配置是否允许 vue-devtools 检查代码。开发版本默认为 true,生产版本默认为 false。生产版本设为 true 可以启用检查。
// 务必在加载 Vue 之后,立即同步设置以下内容 \nVue.config.devtools = true
errorHandler
- 类型: Function
- 默认值: 默认抛出错误
- 作用:指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
Vue.config.errorHandler = function (err, vm) {\n // handle error\n}
warnHandler
2.4.0 新增
- 类型:Function
- 默认值:undefined
- 作用:为 Vue 的运行时警告赋予一个自定义处理函数。注意这只会在开发者环境下生效,在生产环境下它会被忽略。
Vue.config.warnHandler = function (msg, vm, trace) {\n // `trace` 是组件的继承关系追踪\n}
ignoredElements
- 类型: Array<string>
- 默认值: []
- 作用: 须使 Vue 忽略在 Vue 之外的自定义元素 (e.g., 使用了 Web Components APIs)。否则,它会假设你忘记注册全局组件或者拼错了组件名称,从而抛出一个关于 Unknown custom element 的警告。
Vue.config.ignoredElements = [\n 'my-custom-web-component',\n 'another-web-component'\n]
keyCodes
- 类型: { [key: string]: number | Array<number> }
- 默认值: {}
- 作用:给 v-on 自定义键位别名。
Vue.config.keyCodes = {\n v: 86,\n f1: 112,\n mediaPlayPause: 179,\n up: [38, 87]\n}
performance
2.2.0 新增
- 类型:boolean
- 默认值:false (自 2.2.3 起)
- 用法:设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上。
productionTip
2.2.0 新增
- 类型:boolean
- 默认值:true
- 用法:设置为 false 以阻止 vue 在启动时生成生产提示。
全局 API
Vue.extend
- 参数:{Object} options
- 作用:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数。
<div id=\"mount-point\"></div>\n // 创建构造器var Profile = Vue.extend({\n template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',\n data: function () {\n return {\n firstName: 'Walter',\n lastName: 'White',\n alias: 'Heisenberg'\n }\n }\n })\n // 创建 Profile 实例,并挂载到一个元素上。new Profile().$mount('#mount-point')
结果如下:
<p>Walter White aka Heisenberg</p>
Vue.nextTick
- 参数:
- {Function} [callback]
- {Object} [context]
- 作用:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
- 2.1.0新增:如果没有提供回调且支持 promise 的环境中返回 promise。
// 修改数据\nvm.msg = 'Hello'\n// DOM 还没有更新\nVue.nextTick(function () {\n // DOM 更新了\n})
Vue.set( object, key, value )
- 参数:
- {Object} object
- {string} key
- {any} value
- 返回值: 设置的值.
- 作用:设置对象的属性。如果对象是响应式的,确保属性被创建后也是响应式的,同时触发视图更新。这个方法主要用于避开 Vue 不能检测属性被添加的限制。注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
- 参考: 深入响应式原理
Vue.delete( object, key )
- 参数:
- {Object} object
- {string} key
- 作用:删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
- 参考: 深入响应式原理
Vue.directive( id, [definition] )
- 参数:
- {string} id
- {Function | Object} [definition]
- 作用:注册或获取全局指令。
- 参考: 自定义指令
// 注册\nVue.directive('my-directive', {\n bind: function () {},\n inserted: function () {},\n update: function () {},\n componentUpdated: function () {},\n unbind: function () {}\n})\n// 注册(传入一个简单的指令函数)\nVue.directive('my-directive', function () {\n // 这里将会被 `bind` 和 `update` 调用\n})\n// getter,返回已注册的指令\nvar myDirective = Vue.directive('my-directive')
Vue.filter
- 参数:
- {string} id
- {Function} [definition]
- 作用:注册或获取全局过滤器。
// 注册\nVue.filter('my-filter', function (value) {\n // 返回处理后的值\n})\n// getter,返回已注册的过滤器\nvar myFilter = Vue.filter('my-filter')
Vue.component
- 参数:
- {string} id
- {Function | Object} [definition]
- 作用:注册或获取全局组件。注册还会自动使用给定的id设置组件的名称。
- 参考: 组件
// 注册组件,传入一个扩展过的构造器\nVue.component('my-component', Vue.extend({ /* ... */ }))\n// 注册组件,传入一个选项对象(自动调用 Vue.extend)\nVue.component('my-component', { /* ... */ })\n// 获取注册的组件(始终返回构造器)\nvar MyComponent = Vue.component('my-component')
Vue.use
- 参数:{Object | Function} plugin
- 作用:安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法将被作为 Vue 的参数调用。当 install 方法被同一个插件多次调用,插件将只会被安装一次。
- 参考: 插件
编者按:关于如何开发一个插件,读者应该要学习一下,因为有的时候你确实需要全局实现某些功能。通过插件,可以给Vue这个全局变量加入一些全局方法,也可以给每一个实例加入原型链方法,在组件内使用this.$yourmethod这种方式来执行某些功能。点击上面的参考链接去学习如何写插件。
Vue.mixin
- 参数:{Object} mixin
- 作用:全局注册一个混合,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混合,向组件注入自定义的行为。不推荐在应用代码中使用。
- 参考: 全局混合
编者按:关于混入,如果你学过react应该知道。简单的说就是把几个类合并为一个类,这样,继承这个混合的类的子类就拥有了多个父类的方法。在vue里面,组件也可以使用混入来继承组件,在创建组件的时候就可以使用一个mixin参数,这在前面的组件一章没有介绍过:
// 定义一个混合对象\nvar myMixin = {\n created: function () {\n this.hello()\n },\n methods: {\n hello: function () {\n console.log('hello from mixin!')\n }\n }\n}\n// 定义一个使用混合对象的组件\nvar Component = Vue.extend({\n mixins: [myMixin]\n})\nvar component = new Component() // -> \"hello from mixin!\"
Vue.compile
- 参数:{string} template
- 作用:在render函数中编译模板字符串。只在独立构建时有效。
- 参考: Render 函数
var res = Vue.compile('<div><span>{{ msg }}</span></div>')\nnew Vue({\n data: {\n msg: 'hello'\n },\n render: res.render,\n staticRenderFns: res.staticRenderFns\n})
全局注册
全局注册函数直接挂在Vue这个全局对象上。注意下面的方法,都是复数形式。
Vue.directives
全局注册一个指令。这样你就可以在任何的Vue实例上使用这个指令。
// 注册一个全局自定义指令 v-focus\nVue.directive('focus', {\n // 当绑定元素插入到 DOM 中。\n inserted: function (el) {\n // 聚焦元素\n el.focus()\n }\n})
Vue.filters
注册或获取全局过滤器。
// 注册\nVue.filter('my-filter', function (value) {\n // 返回处理后的值\n})\n// getter,返回已注册的过滤器\nvar myFilter = Vue.filter('my-filter')
Vue.components
全局注册一个组件。
Vue.component('my-component', {\n template: '<div>A custom component!</div>'\n})
局部配置
所谓局部配置,就是在实例化的时候的配置。
parent
指定实例的父实例,例如:
var parent = new Vue({...})\nvar child = new Vue({\n parent,\n})
这样,就指定了child的父实例是parent。当child被实例化出来之后,child.$parent就引用parent,而parent.$children是一个数组,里面就包含了child,可以用parent.$children.indexOf(child) > -1来判断是否是否包含了某个实例。
mixin
前面已经讲过全局的mixin,其实它也可以在实例化配置时传入:
var mixin = {\n created: function () {\n console.log(1)\n }\n}\nvar vm = new Vue({\n created: function () {\n console.log(2)\n },\n mixins: [mixin]\n})\n// -> 1\n// -> 2
mixins 选项接受一个混合对象的数组。这些混合实例对象可以像正常的实例对象一样包含选项,他们将在 Vue.extend() 里最终选择使用相同的选项合并逻辑合并。举例:如果你混合包含一个钩子而创建组件本身也有一个,两个函数将被调用。Mixin钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
name
注意,这个选项只对组件的创建起作用。
var MyComponent = Vue.extend({\n name: 'my-component',\n})
允许组件模板递归地调用自身。注意,组件在全局用 Vue.component() 注册时,全局 ID 自动作为组件的 name。
指定 name 选项的另一个好处是便于调试。有名字的组件有更友好的警告信息。另外,当在有 vue-devtools, 未命名组件将显示成 <AnonymousComponent>, 这很没有语义。通过提供 name 选项,可以获得更有语义信息的组件树。
extend
允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件。
这和 mixins 类似,区别在于,组件自身的选项会比要扩展的源组件具有更高的优先级。
var CompA = { ... }\n// 在没有调用 Vue.extend 时候继承 CompA\nvar CompB = { extends: CompA, ...}
delimiters
改变纯文本插入分隔符。 这个选择只有在独立构建时才有用。
默认值: [\"{{\", \"}}\"]
new Vue({ delimiters: ['${', '}']})\n// 分隔符变成了 ES6 模板字符串的风格
functional
使组件无状态(没有 data )和无实例(没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
参考: 函数式组件
实例属性
vm.$data
- 类型: Object
- 详细: Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象属性的访问。
- 另见: 选项 - data
vm.$el
- 类型: HTMLElement
- 只读
- 详细: Vue 实例使用的根 DOM 元素。
vm.$options
- 类型: Object
- 只读
- 详细: 用于当前 Vue 实例的初始化选项。需要在选项中包含自定义属性时会有用处:
new Vue({\n customOption: 'foo',\n created: function () {\n console.log(this.$options.customOption) // -> 'foo'\n }\n})
vm.$parent
- 类型: Vue instance
- 只读
- 详细: 父实例,如果当前实例有的话。
vm.$root
- 类型: Vue instance
- 只读
- 详细: 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自已。
vm.$children
- 类型: Array<Vue instance>
- 只读
- 详细: 当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。
vm.$slots
- 类型: { [name: string]: ?Array<VNode> }
- 只读
- 详细: 用来访问被 slot 分发的内容。每个具名 slot 有其相应的属性(例如:slot=\"foo\" 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了所有没有被包含在具名 slot 中的节点。在使用 render 函数书写一个组件时,访问 vm.$slots 最有帮助。
- 参考:
- <slot> 组件
- 使用 Slots 进行内容分发
- Render 函数
vm.$scopedSlots
2.1.0 新增
- 类型:{ [name: string]: props => Array<VNode> | undefined }
- 只读
- 详细:用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。vm.$scopedSlots 在使用渲染函数开发一个组件时特别有用。注意:从 2.6.0 开始,这个 property 有两个变化:
- 作用域插槽函数现在保证返回一个 VNode 数组,除非在返回值无效的情况下返回 undefined。
- 所有的 $slots 现在都会作为函数暴露在 $scopedSlots 中。如果你在使用渲染函数,不论当前插槽是否带有作用域,我们都推荐始终通过 $scopedSlots 访问它们。这不仅仅使得在未来添加作用域变得简单,也可以让你最终轻松迁移到所有插槽都是函数的 Vue 3。
vm.$refs
- 类型:Object
- 只读
- 作用:一个对象,持有注册过 refattribute 的所有 DOM 元素和组件实例。
- 参考:
- 子组件 ref
- 特殊 attribute - ref
vm.$isServer
- 类型:boolean
- 只读
- 详细:当前 Vue 实例是否运行于服务器。
- 参考:服务端渲染
vm.$attrs
2.4.0 新增
- 类型:{ [key: string]: string }
- 只读
- 作用:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=\"$attrs\" 传入内部组件——在创建高级别的组件时非常有用。
vm.$listeners
2.4.0 新增
- 类型:{ [key: string]: Function | Array<Function> }
- 只读
- 作用:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=\"$listeners\" 传入内部组件——在创建更高层次的组件时非常有用。
实例方法 / 数据
vm.$watch
- 参数:
- {string | Function} expOrFn
- {Function | Object} callback
- {Object} [options]
- {boolean} deep
- {boolean} immediate
- 返回值:{Function} unwatch
- 用法:观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。注意:在变更 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。
// 键路径\nvm.$watch('a.b.c', function (newVal, oldVal) {\n // 做点什么\n})\n\n// 函数\nvm.$watch(\n function () {\n // 表达式 `this.a + this.b` 每次得出一个不同的结果时// 处理函数都会被调用。// 这就像监听一个未被定义的计算属性return this.a + this.b\n },\n function (newVal, oldVal) {\n // 做点什么\n }\n)
vm.$watch 返回一个取消观察函数,用来停止触发回调:
var unwatch = vm.$watch('a', cb)\n// 之后取消观察\nunwatch()
选项:deep
为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。
vm.$watch('someObject', callback, {\n deep: true\n})\nvm.someObject.nestedValue = 123\n// callback is fired
选项:immediate
在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:
vm.$watch('a', callback, {\n immediate: true\n})\n// 立即以 `a` 的当前值触发回调
注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。
// 这会导致报错\nvar unwatch = vm.$watch(\n 'value',\n function () {\n doSomething()\n unwatch()\n },\n { immediate: true }\n)
如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:
var unwatch = vm.$watch(\n 'value',\n function () {\n doSomething()\n if (unwatch) {\n unwatch()\n }\n },\n { immediate: true }\n)
vm.$set
- 参数:
- {Object | Array} target
- {string | number} propertyName/index
- {any} value
- 返回值:设置的值。
- 用法:这是全局 Vue.set 的别名。
- 参考:Vue.set
vm.$delete
- 参数:
- {Object | Array} target
- {string | number} propertyName/index
- 用法:这是全局 Vue.delete 的别名。
- 参考:Vue.delete
特殊属性
Vue里面使用了DOM模板,也就是说,所有元素的属性(attr或prop)首先可以被HTML解析识别。而如果不是HTML本身内置的属性的话,vue可以自己在编译模板的时候对这些属性进行解析,主要包括下列属性:
- vue的指令,内置指令或自定义指令
- 子组件的props属性
- 本章将要讲到的特殊属性
除了上述这些属性之外,比如,我们给一个元素添加了go属性<div go=\"-1\"></div>,这个属性不会起到任何作用,当然,你可以用css来改变它的样式,但是在元素本身层面上,真没用。回到主题,本章要讲vue的内置的特殊属性。
key
key 的特殊属性主要用在 Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用key,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素。
有相同父元素的子元素必须有独特的key。重复的key会造成渲染错误。
最常见的用例是结合 v-for:
<ul><li v-for=\"item in items\" :key=\"item.id\">...</li>\n</ul>
它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:
- 完整地触发组件的生命周期钩子
- 触发过渡
例如:
<transition><span :key=\"text\">{{ text }}</span>\n</transition>
当 text 发生改变时,<span> 会随时被更新,因此会触发过渡。
ref
ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素; 如果用在子组件上,引用就指向组件实例:
<!-- vm.$refs.p will be the DOM node -->\n<p ref=\"p\">hello</p>\n<!-- vm.$refs.child will be the child comp instance -->\n<child-comp ref=\"child\"></child-comp>
当 v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。
关于ref注册时间的重要说明: 因为ref本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs 也不是响应式的,因此你不应该试图用它在模版中做数据绑定。
- 参考: 子组件 Refs
slot废弃
推荐 2.6.0 新增的 v-slot。
- 预期:string用于标记往哪个具名插槽中插入子组件内容。
- 参考:具名插槽
内置组件
我们新注册一个组件之后,就可以用这个组件的名称作为一个html元素插入到你的模板中去。但是,vue保留了几个组件名,而这几个组件可以直接调用,是vue的内置组件,实现对应的功能。
component
- Props:
- is - string | ComponentDefinition | ComponentConstructor
- inline-template - boolean
- 用法:渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。
- 参考: 动态组件
<!-- 动态组件由 vm 实例的属性值 `componentId` 控制 -->\n<component :is=\"componentId\"></component>\n<!-- 也能够渲染注册过的组件或 prop 传入的组件 -->\n<component :is=\"$options.components.child\"></component>
transition
- Props:
- name - string, 用于自动生成 CSS 过渡类名。例如:name: 'fade' 将自动拓展为.fade-enter,.fade-enter-active等。默认类名为 \"v\"
- appear - boolean, 是否在初始渲染时使用过渡。默认为 false。
- css - boolean, 是否使用 CSS 过渡类。默认为 true。如果设置为 false,将只通过组件事件触发注册的 JavaScript 钩子。
- type - string, 指定过渡事件类型,侦听过渡何时结束。有效值为 \"transition\" 和 \"animation\"。默认 Vue.js 将自动检测出持续时间长的为过渡事件类型。
- mode - string, 控制离开/进入的过渡时间序列。有效的模式有 \"out-in\" 和 \"in-out\";默认同时生效。
- enter-class - string
- leave-class - string
- enter-active-class - string
- leave-active-class - string
- appear-class - string
- appear-active-class - string
- 事件:
- before-enter
- enter
- after-enter
- before-leave
- leave
- after-leave
- before-appear
- appear
- after-appear
- 作用:<transition> 元素作为单个元素/组件的过渡效果。<transition> 不会渲染额外的 DOM 元素,也不会出现在检测过的组件层级中。它只是将内容包裹在其中,简单的运用过渡行为。
- 参考: 过渡:进入,离开和列表
<!-- 简单元素 -->\n<transition><div v-if=\"ok\">toggled content</div>\n</transition>\n<!-- 动态组件 -->\n<transition name=\"fade\" mode=\"out-in\" appear><component :is=\"view\"></component>\n</transition>\n<!-- 事件钩子 -->\n<div id=\"transition-demo\"><transition @after-enter=\"transitionComplete\"><div v-show=\"ok\">toggled content</div></transition>\n</div>\n\nnew Vue({\n ...\n methods: {\n transitionComplete: function (el) {\n // 传入 'el' 这个 DOM 元素作为参数。\n }\n }\n ...\n}).$mount('#transition-demo')
transition-group
- Props:
- tag - string, 默认为 span
- move-class - 覆盖移动过渡期间应用的 CSS 类。
- 除了 mode,其他特性和 <transition> 相同。
- 事件:
- 事件和 <transition> 相同.
- 作用:<transition-group> 元素作为多个元素/组件的过渡效果。<transition-group> 渲染一个真实的 DOM 元素。默认渲染 <span>,可以通过 tag 属性配置哪个元素应该被渲染。注意,每个 <transition-group> 的子节点必须有 独立的key ,动画才能正常工作<transition-group> 支持通过 CSS transform 过渡移动。当一个子节点被更新,从屏幕上的位置发生变化,它将会获取应用 CSS 移动类(通过 name 属性或配置 move-class 属性自动生成)。如果 CSS transform 属性是“可过渡”属性,当应用移动类时,将会使用 FLIP 技术 使元素流畅地到达动画终点。
- 参考: 过渡:进入,离开和列表
<transition-group tag=\"ul\" name=\"slide\"><li v-for=\"item in items\" :key=\"item.id\">{{ item.text }}</li>\n</transition-group>
keep-alive
- Props:
- include - 字符串或正则表达式。只有匹配的组件会被缓存。
- exclude - 字符串或正则表达式。任何匹配的组件都不会被缓存。
- 作用:<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。主要用于保留组件状态或避免重新渲染。
- include and exclude (2.1.0 新增)include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串或正则表达式来表示。匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值)。匿名组件不能被匹配。
- <keep-alive> 不会在函数式组件中正常工作,因为它们没有缓存实例。
- 参考: 动态组件 - keep-alive
<!-- 基本 -->\n<keep-alive><component :is=\"view\"></component>\n</keep-alive>\n<!-- 多个条件判断的子组件 -->\n<keep-alive><comp-a v-if=\"a > 1\"></comp-a><comp-b v-else></comp-b>\n</keep-alive>\n<!-- 和 <transition> 一起使用 -->\n<transition><keep-alive><component :is=\"view\"></component></keep-alive>\n</transition>
--
<!-- 逗号分隔字符串 -->\n<keep-alive include=\"a,b\"><component :is=\"view\"></component>\n</keep-alive>\n<!-- 正则表达式 (使用 v-bind) -->\n<keep-alive :include=\"/a|b/\"><component :is=\"view\"></component>\n</keep-alive>
slot
- Props:name - string, 用于命名插槽。
- Usage:<slot> 元素作为组件模板之中的内容分发插槽。 <slot> 元素自身将被替换。详细用法,请参考下面教程的链接。
- 参考: 使用Slots分发内容
生命周期
生命周期,是组件思想中非常重要的一个环节。简单的说就是一个组件从一个类,被实例化之后执行的一系列操作,到最后这个实例被销毁的整个过程。
什么是生命周期?
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如,实例需要配置数据观测(data observer)、编译模版、挂载实例到 DOM ,然后在数据变化时更新 DOM 。在这个过程中,实例也会调用一些 生命周期钩子 ,这就给我们提供了执行自定义逻辑的机会。
我们只需要在实例中使用这些钩子函数,那么当生命周期进行到特定位置时,就会调用这些函数,从而进行函数中规定的操作,这样就可以在一个实例的不同生命阶段执行一些你想要执行的操作。
钩子的 this 指向调用它的 Vue 实例。一些用户可能会问 Vue.js 是否有“控制器”的概念?答案是,没有。组件的自定义逻辑可以分布在这些钩子中。
生命周期示意图
生命周期钩子函数
所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对属性和方法进行运算。这意味着 你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同, this.fetchTodos 的行为未定义。
beforeCreate
在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
created
实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。
beforeMount
在挂载开始之前被调用:相关的 render 函数首次被调用。
该钩子在服务器端渲染期间不被调用。
mounted
el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。
该钩子在服务器端渲染期间不被调用。
beforeUpdate
数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
该钩子在服务器端渲染期间不被调用。
updated
由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。
该钩子在服务器端渲染期间不被调用。
activated
keep-alive 组件激活时调用。
该钩子在服务器端渲染期间不被调用。
参考:
deactivated
keep-alive 组件停用时调用。
该钩子在服务器端渲染期间不被调用。
beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。
该钩子在服务器端渲染期间不被调用。
destroyed
Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
该钩子在服务器端渲染期间不被调用。
Virtual DOM 虚拟DOM
终于写(copy)了那么多章之后,要来谈一谈Virtual DOM。相信很多人对这项技术早都垂涎三尺,想要对它的原理进行窥探。本章将结合vue简单梳理Virtual DOM的原理。
vue2.0之后才支持Virtual DOM,它介于编译template和渲染界面之间。
Virtual DOM的灵感来源
DOM是一个很昂贵的对象,之所以昂贵,是因为它内部的方法太多,而且相互联系。DOM本质也是一个对象,但是这个对象首先有非常多的属性和方法,一个DOM节点对象就会占用非常多的内存,其次,当你操作DOM节点的时候,DOM对象会不断操作自己,同时还会操作和自己相关的其他DOM节点对象,所以整个DOM树是牵一发而动全身,操作DOM就会消耗很多资源。
既然DOM是一个对象,跟javascript的其他对象并没有本质上的不同,那么,可以不可以在另一个空间复制一个和DOM结构相同的对象,但是,这个新的对象会删除很多方法或属性,只保留几个必要的,而操作这些对象的时候,不会有那么高消耗的连带操作,这样操作这个对象和操作DOM就完全是两回事,性能上肯定快很多。最后,就是当操作完之后,怎么把这个对象跟真实的DOM映射起来?你可能还记得前面提到过一个key属性,通过一个唯一标记来确定哪些位置改变了,针对这些改变的对象,找到对应的DOM节点,进行重新渲染。
vue2.0中的Virtual DOM
在前面的阅读中,你已经见过VNode了,它是vue2里面加入的一种新对象,用来实现Virtual DOM。在vue渲染真实的DOM之前,内部的响应式系统改变的都是VNode。响应式系统在下一章讲。
VNode模拟DOM树
在vue中Virtual DOM是通过VNode类来表达的,每个DOM元素或vue组件都对应一个VNode对象。VNode结构如下(图来自《vue.js权威指南》):
VNode
它包含了tag, text, elm, data, parent, children等属性。它可以由真实的DOM生成,也可以由组件生成。如果由组件生成的话,VNode的componentOptions有值,而如果由DOM生成,则该值空。
那么有什么方法获取一个元素的VNode呢?没有。
VNodeComponentOptions
如果VNode是由组件生成的,所有的组件相关信息都在这个对象里面。
VNodeData
VNode中的节点数据data属性的详细描述,包括slot, ref, staticClass, style, class, props, attrs, transition, directives等信息。
VNodeDirective
VNodeData中的directives属性的详细信息,包括name, value, oldValue, arg, modifiers等。
如何生成VNode?
前面我们提到过render函数的参数createElement,其实你再回头去看createElement这个函数,就大概清楚是怎么回事,它实际上就生成了VNode(一个对象)。但是如果我们传入了template而没有传入render函数呢?vue会通过一个ast语法优化,对我们传入的template经过HTML解析器之后的对象转化为给createElement的参数。
总之,你会发现,vue的render函数实际上是要生成VNode,它到真实的DOM,还有一个过程。
VNode patch生成DOM
Virtual DOM之所以快,是因为在生成真实的DOM之前,通过内部的一个简单的多的对象的对比,判断是否有变化,具体的变化在哪里,这个对比的过程比直接操作DOM要快非常多。
vue还有一个特点,VNode还具有队列,当VNode发生变化时,会放在一个队列里,并不会马上去更新DOM,而是在遍历完整个队列之后才更新DOM。所以性能上又好了一些。
vue里对比新旧DOM的方法是patchVnode这个方法,当它决定是否要更新DOM之前,会比较DOM节点对应的新旧VNode,只有不同时,才进行更新,这个对比是在VNode内部,因此比对比DOM快很多。patchValue这个方法是vue里面非常出色,可以说是vue里面使得Virtual DOM可行的核心部分。它的实现比较复杂,本书也说不清楚,你要是有兴趣,可以阅读源码,细心研究。
vue生成真正的DOM靠createElm方法,它把一个VNode真正转化为真实的DOM。
响应式原理
我们已经涵盖了大部分的基础知识 - 现在是时候深入底层原理了!Vue 最显著的特性之一便是不太引人注意的响应式系统(reactivity system)。模型层(model)只是普通 JavaScript 对象,修改它则更新视图(view)。这会让状态管理变得非常简单且直观,不过理解它的工作原理以避免一些常见的问题也是很重要的。在本章中,我们将开始深入挖掘 Vue 响应式系统的底层细节。
如何追踪变化
把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
变化检测问题
受现代 JavaScript 的限制(以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。例如:
var vm = new Vue({\n data:{ \n a:1\n }\n})\n// `vm.a` 是响应的vm.b = 2\n// `vm.b` 是非响应的
Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上:
Vue.set(vm.someObject, 'b', 2)
您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.someObject,'b',2)
有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })` this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
声明响应式属性
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明根级响应式属性,哪怕只是一个空值:
var vm = new Vue({\n data: {\n // 声明 message 为一个空值字符串\n message: ''\n },\n template: '<div>{{ message }}</div>'\n})\n// 之后设置 `message` \nvm.message = 'Hello!'
如果你在 data 选项中未声明 message,Vue 将警告你渲染函数在试图访问的属性不存在。
这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例在类型检查系统的帮助下运行的更高效。而且在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的概要,提前声明所有的响应式属性,可以让组件代码在以后重新阅读或其他开发人员阅读时更易于被理解。
异步更新队列
可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际(已去重的)工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationObserver,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。例如:
<div id=\"example\">{{message}}</div>\nvar vm = new Vue({\n el: '#example',\n data: {\n message: '123'\n }\n})\nvm.message = 'new message' // 更改数据\nvm.$el.textContent === 'new message' // false\nVue.nextTick(function () {\n vm.$el.textContent === 'new message' // true\n})
在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component('example', {\n template: '<span>{{ message }}</span>',\n data: function () {\n return {\n message: 'not updated'\n }\n },\n methods: {\n updateMessage: function () {\n this.message = 'updated'\n console.log(this.$el.textContent) // => '没有更新'\n this.$nextTick(function () {\n console.log(this.$el.textContent) // => '更新完成'\n })\n }\n }\n})