我的 Vue.js 之旅
写在 Vue.js 2.0 版本发布之后, 关于 Vue.js 2.x, 在实例的生命周期上我们就可以看出关键的不同点了: Virtual DOM
在底层的实现上, Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,在应用状态改变时, Vue 能够智能地计算出重新渲染组件的最小代价并应用到 DOM 操作上
从 Vue 1.x 迁移
- Vue 2.0 几乎 90% 的 API 和核心概念都没有变
- 单向数据流(父 -> 子): props 现在只能单向传递. 父组件是使用 props 传递数据给子组件, 但如果子组件要把数据传递回去, 为了对父组件产生反向影响, 子组件需要显式地传递一个自定义事件而不是依赖于隐式地双向绑定.
- 组件内修改 prop 是反模式(不推荐的)的, 因为根据渲染机制, 当父组件重新渲染时, 子组件的内部 prop 值也将被覆盖. 所以改变 prop 值可以用以下选项替代: 通过 data 属性, 用prop去设置一个data属性的默认值; 通过 computed 属性
- 现在在组件上使用 v-on 只会监听自定义事件(组件用 $emit 触发的事件). 如果要监听根元素的原生事件, 可以使用 .native 修饰符
- $dispatch 和 $broadcast 已经被弃用, 替代手段是通过使用事件中心, 允许组件自由交流, 无论组件处于组件树的哪一层. 由于 Vue 实例实现了一个事件分发接口, 你可以通过实例化一个空的 Vue 实例来实现这个目的
- 移除了插入文本之外的过滤器: 现在过滤器只能用在插入文本中 (
{{ }}
tags)
- 现在组件总是会替换掉他们被绑定的元素, 为了模仿 replace: false 的行为, 可以用一个和将要替换元素类似的元素将根组件包裹起来
- 最佳实践: 使用 Vue.js 开发版, 多看控制台的警告信息
[Vue warn]
数据驱动的组件,为现代化的 Web 界面而生
推荐先看下官网首页的 10 秒钟看懂 Vue.js, 是不是有点小兴奋了?
<div id="demo">
<p>{{message}}</p>
<input v-model="message">
</div>
var demo = new Vue({
el: '#demo',
data: {
message: 'Hello Vue.js!'
}
})
俗话说好记性不如烂笔头
, 以下记录为我浏览了一遍 Vue.js 教程 1.0 后学习的笔记.
需要重点理解的章节有: 概述、Vue 实例、指令、组件、深入响应式原理, 过渡动画这一章非常实用.
概述
Vue.js(读音 /vjuː/, 类似于 view)是一个构建数据驱动的 web 界面的库。需要 IE9+ 及其他支持 ECMAScript 5 的浏览器。
Vue.js 不是一个全能框架——它只聚焦于视图层。目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。
Vue.js 拥抱数据驱动的视图概念。
通俗地讲,它意味着我们在普通 HTML 模板中使用特殊的语法将 DOM “绑定”到底层数据。
一旦创建了绑定,DOM 将与数据保持同步。每当修改了数据,DOM 便相应地更新。
这样我们应用中的逻辑就几乎都是直接修改数据了,不必与 DOM 更新搅在一起。
这让我们的代码更容易撰写、理解与维护。
组件系统是 Vue.js 构建大型应用的基础,因为它提供了一种抽象,让我们可以用独立可复用的小组件来构建大型应用。
如果我们考虑到这点,几乎任意类型的应用的界面都可以抽象为一个组件树:
<div id="app">
<app-nav></app-nav>
<app-view>
<app-sidebar></app-sidebar>
<app-content></app-content>
</app-view>
</div>
使用 vue-devtools Chrome 开发者工具扩展,可以更方便地调试 Vue.js 应用。
Vue 实例
每个 Vue.js 应用的起步都是通过构造函数 Vue 创建一个 Vue 的根实例
一个 Vue 实例其实正是一个 MVVM 模式中所描述的 ViewModel - 因此在文档中经常会使用 vm 这个变量名
// vue 根实例
var vm = new Vue({
// 选项
})
// 在实例化 Vue 时,需要传入一个选项对象,它可以包含数据、模板、挂载元素、方法、生命周期钩子等选项
// 可以扩展 Vue 构造器,从而用预定义选项创建可复用的组件构造器:
var MyComponent = Vue.extend({
// 扩展选项
})
// 所有的 `MyComponent` 实例都将以预定义的扩展选项被创建
var myComponentInstance = new MyComponent()
更多的选项/数据
var data = { a: 1 }
var vm = new Vue({
data: data
})
// 每个 Vue 实例都会代理其 data 对象里所有的属性
vm.a === data.a // -> true
// 设置属性也会影响到原始数据
vm.a = 2
data.a // -> 2
// ... 反之亦然
data.a = 3
vm.a // -> 3
注意只有这些被代理的属性是响应的。如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。
受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。
在实例创建之后添加属性可以通过 $set(key, value) 实例方法让它是响应的。对于普通数据对象,可以使用全局方法 Vue.set(object, key, value)
var data = { a: 1 }
var vm = new Vue({
el: '#example',
data: data
})
// Vue 实例暴露了一些有用的实例属性与方法。这些属性与方法都有前缀 $,以便与代理的数据属性区分。
vm.$data === data // -> true
vm.$el === document.getElementById('example') // -> true
// $watch 是一个实例方法
vm.$watch('a', function (newVal, oldVal) {
// 这个回调将在 `vm.a` 改变后调用
})
更多 Vue 实例属性
实例的生命周期 created/beforeCompile/compiled/ready/beforeDestroy/destroyed
更多 Vue 全局 API
数据绑定语法
Vue.js 的模板是基于 DOM 实现的。这意味着所有的 Vue.js 模板都是可解析的有效的 HTML,且通过一些特殊的特性做了增强。
数据绑定最基础的形式是文本插值,使用 “Mustache” 语法(双大括号)。放在 Mustache 标签内的文本称为绑定表达式,由一个简单的 JavaScript 表达式和可选的一个或多个过滤器(以“管道符”指示)构成。
<!-- 插值 -->
<span>Message: {{ msg }}</span>
<!-- 过滤器 -->
<span>{{ msg | filterA 'arg1' arg2 | filterB }}</span>
<!-- 单次插值 -->
<span>Message: {{* msg }}</span>
<!-- 原始HTML -->
<span>Message: {{{ rawHtml }}}</span>
复用模板片断 partials
// 注册 partial
Vue.partial('my-partial', '<p>This is a partial! {{msg}}</p>')
<div><partial name="my-partial"></partial></div>
指令
指令的职责就是当其表达式的值改变时把某些特殊的行为应用到 DOM 上。
指令可以在其名称后面带一个“参数” (Argument),中间放一个冒号隔开。
修饰符 (Modifiers) 是以半角句号 .
开始的特殊后缀,用于表示指令应当以特殊方式绑定。
例如: v-bind:href.literal="/a/b/c"
.literal
修饰符告诉指令将它的值解析为一个字面字符串而不是一个表达式。
更多 Vue 内置指令
v-bind
用于响应地更新 HTML 特性
例如: <a v-bind:href="url">link</a>
缩写: <a :href="url">link</a>
v-bind:class="{ 'class-a': isA, 'class-b': isB }"
v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"
当 v-bind:style 使用需要厂商前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。
v-on
用于监听 DOM 事件
例如: <a v-on:click="doSomething">link</a>
缩写: <a @click="doSomething($event)">link</a>
<!-- 阻止单击事件冒泡 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 添加事件侦听器时使用 capture 模式 -->
<div v-on:click.capture="doThis"></div>
<!-- 只当事件在该元素本身(而不是子元素)触发时触发回调 -->
<div v-on:click.self="doThat"></div>
<!-- 在监听键盘事件时,我们经常需要检测 keyCode。Vue.js 允许为 v-on 添加按键修饰符: -->
<!-- 只有在 keyCode 是 13 时调用 vm.submit() -->
<input v-on:keyup.13="submit">
<!-- 记住所有的 keyCode 比较困难,Vue.js 为最常用的按键提供别名: -->
<input v-on:keyup.enter="submit">
<!-- 全部的按键别名 enter/tab/delete/esc/space/up/down/left/right/单字母按键/自定义按键别名 -->
// 自定义按键别名, 可以使用 @keyup.f1
Vue.directive('on').keyCodes.f1 = 112
为什么在 HTML 中监听事件?
你可能注意到这种事件监听的方式违背了传统理念 “separation of concern”。
不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护困难。
实际上,使用 v-on 有几个好处:
- 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
- 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
- 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何自己清理它们。
v-if/v-for
v-if/v-show/v-else/<template v-if="ok">
<!-- 默认可以 $index 变量来获取索引 -->
v-for="item in items"
<!-- 自定义索引变量名 -->
v-for="(index, item) in items"
<!-- of 语法更接近 JavaScript 遍历器语法 -->
v-for="item of items"
<!-- 多个元素 -->
<template v-for="item in items">
<!-- 使用 track-by 特性给 Vue.js 一个提示,Vue.js 因而能尽可能地复用已有实例 -->
v-for="item in items" track-by="_uid"
<!-- 也可以使用 v-for 遍历对象。除了 `$index` 之外,作用域内还可以访问另外一个特殊变量 `$key` -->
v-for="(key, value) in object"
<!-- v-for 也可以接收一个整数,此时它将重复模板数次 -->
v-for="n in 10"
如果没有唯一的键供追踪,可以使用 track-by="$index",它强制让 v-for 进入原位更新模式:片断不会被移动,而是简单地以对应索引的新值刷新。这种模式也能处理数据数组中重复的值。
这让数据替换非常高效,但是也会付出一定的代价。因为这时 DOM 节点不再映射数组元素顺序的改变,不能同步临时状态(比如 <input>
元素的值)以及组件的私有状态。
因此,如果 v-for 块包含 <input>
元素或子组件,要小心使用 track-by="$index"
v-model
在表单控件元素上创建双向数据绑定。根据控件类型它自动选取正确的方法更新元素。
尽管有点神奇,v-model 不过是语法糖,在用户输入事件中更新数据。
<!-- 表单控件绑定 -->
<input type="text" v-model="message" placeholder="edit me">
<!-- 在默认情况下,v-model 在 input 事件中同步输入框值与数据,可以添加一个特性 lazy,从而改到在 change 事件中同步 -->
<!-- 在 "change" 而不是 "input" 事件中更新 -->
<input v-model="msg" lazy>
<!-- 如果想自动将用户的输入保持为数字,可以添加一个特性 number -->
<!-- checkbox/radio/select 同样适用 -->
<input v-model="age" number>
<!-- debounce 设置一个最小的延时,在每次敲击之后延时同步输入框的值与数据。如果每次更新都要进行高耗操作(例如在输入提示中 Ajax 请求),它较为有用。 -->
<input v-model="msg" debounce="500">
<!-- 注意 debounce 参数不会延迟 input 事件:它延迟“写入”底层数据。因此在使用 debounce 时应当用 vm.$watch() 响应数据的变化。 -->
<!-- 若想延迟 DOM 事件,应当使用 debounce 过滤器。 -->
<input v-model="msg" v-on:keydown="doSomething | debounce 500">
<!-- 单个勾选框,逻辑值: -->
<input type="checkbox" id="checkbox" v-model="checked">
<!-- true/false -->
<label for="checkbox">{{ checked }}</label>
<!-- 多个勾选框,绑定到同一个数组: -->
<input type="checkbox" id="jack" value="1" v-model="checkedNames"><label for="jack">Jack</label>
<input type="checkbox" id="john" value="2" v-model="checkedNames"><label for="john">John</label>
<br>
<!-- 默认多个 checkbox 绑定到数组时, 数据类型为字符串(checkedNames: ['1', '2']), 如果需要数字类型, 应该添加 number 特性(checkedNames: [1, 2]) -->
<span>Checked names: {{ checkedNames | json }}</span>
<input type="radio" id="one" value="1" v-model="picked"><label for="one">One</label>
<input type="radio" id="two" value="2" v-model="picked"><label for="two">Two</label>
<br>
<!-- picked: 1 或者 picked: '1' 都可以 -->
<span>Picked: {{ picked }}</span>
<!-- select 单选 -->
<!-- answer: 2 或者 answer: '2' 都可以 -->
<select v-model="answer">
<option value="1">A</option>
<option value="2">B</option>
</select>
<!-- 多选(绑定到一个数组) -->
<select v-model="answer" multiple>
<option value="1">A</option>
<option value="2">B</option>
</select>
<br>
<!-- answer: [1, 2] 或者 answer: ['1', '2'] 都可以 -->
<span>Selected: {{ selected | json }}</span>
<!-- 值绑定 -->
<!-- 有时我们想绑定值到 Vue 实例一个动态属性上。可以用 v-bind 做到, 而且允许绑定输入框的值到非字符串值 -->
<input type="checkbox"
v-model="toggle"
v-bind:true-value="a"
v-bind:false-value="b">
<!-- 当选中时,picked 为动态属性 a 的值, 而非字符串 "a" -->
<input type="radio" v-model="pick" v-bind:value="a">
<select v-model="selected">
<!-- 对象字面量 -->
<option v-bind:value="{ number: 123 }">123</option>
</select>
数组变动检测
Vue.js 包装了被观察数组的变异方法(修改了原始数组),故它们能触发视图更新
push/pop/unshift/shift/splice/sort/reverse
在使用非变异方法(filter/concat/slice)时, 可以直接用新数组替换旧数组
因为 JavaScript 的限制,Vue.js 不能检测到下面数组变化:
- 直接用索引设置元素,如 vm.items[0] = {}
// 为了解决问题 (1),Vue.js 扩展了观察数组,为它添加了一个 $set() 方法
// 与 `example1.items[0] = ...` 相同,但是能触发视图更新
example1.items.$set(0, { childMsg: 'Changed!'})
-
修改数据的长度,如 vm.items.length = 0。
至于问题 (2),只需用一个空数组替换 items。
除了 $set(), Vue.js 也为观察数组添加了 $remove() 方法,用于从目标数组中查找并删除元素,在内部它调用 splice()
有时我们想显示过滤/排序过的数组,同时不实际修改或重置原始数据。有两个办法:
- 创建一个计算属性,返回过滤/排序过的数组;
- 使用内置的过滤器 filterBy 和 orderBy。
计算属性有更好的控制力,也更灵活,因为它是全功能 JavaScript,但是通常过滤器更方便。
更多 Vue 内置过滤器
计算属性
模板是为了描述视图的结构。在模板中放入太多的逻辑会让模板过重且难以维护。这就是为什么 Vue.js 将绑定表达式限制为一个表达式。如果需要多于一个表达式的逻辑,应当使用计算属性。
computed: {
b: function() {
return this.a + 1
},
fullName: { // getter/setter
get: function () {
return this.firstName + ' ' + this.lastName
},
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
计算属性不是简单的 getter。计算属性持续追踪它的响应依赖。
在计算一个计算属性时,Vue.js 更新它的依赖列表并缓存结果,只有当其中一个依赖发生了变化,缓存的结果才无效。
因此,只要依赖不发生变化,访问计算属性会直接返回缓存的结果,而不是调用 getter。
computed: {
example: {
// 由于计算属性被缓存了,在访问它时 getter 不总是被调用
// 有时希望 getter 不改变原有的行为,每次访问 vm.example 时都调用 getter
// 这时可以为指定的计算属性关闭缓存
cache: false,
get: function () {
return Date.now() + this.msg
}
}
}
过渡
通过 Vue.js 的过渡系统,可以在元素从 DOM 中插入或移除时自动应用过渡效果。
Vue.js 会在适当的时机为你触发 CSS 过渡或动画,你也可以提供相应的 JavaScript 钩子函数在过渡过程中执行自定义的 DOM 操作。
<div v-if="show" transition="my-transition"></div>
transition 特性可以与下面资源一起用:
- v-if
- v-show
- v-for (只为插入和删除触发)
- 动态组件 (介绍见组件)
- 在组件的根节点上,并且被 Vue 实例 DOM 方法(如 vm.$appendTo(el))触发
当插入或删除带有过渡的元素时,Vue 将:
- 尝试以 ID "my-transition" 查找 JavaScript 过渡钩子对象——通过 Vue.transition(id, hooks) 或 transitions 选项注册。如果找到了,将在过渡的不同阶段调用相应的钩子。
- 自动嗅探目标元素是否有 CSS 过渡或动画,并在合适时添加/删除 CSS 类名。
- 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作(插入/删除)在下一帧中立即执行。
CSS 过渡
<div v-if="show" transition="expand">hello</div>
<!-- 可以通过动态绑定过渡 -->
<div v-if="show" :transition="transitionName">hello</div>
类名的添加和切换取决于 transition 特性的值。比如 transition="expand",会有三个 CSS 类名:
.expand-transition, .expand-enter, .expand-leave。如果 transition 特性没有值,类名默认是 .v-transition, .v-enter 和 .v-leave。
- .expand-transition 始终保留在元素上。
- .expand-enter 定义进入过渡的开始状态。只应用一帧然后立即删除。
- .expand-leave 定义离开过渡的结束状态。在离开过渡开始时生效,在它结束后删除。
我们可以在过渡的 JavaScript 定义中声明自定义的 CSS 过渡类名。
这些自定义类名会覆盖默认的类名。当需要和第三方的 CSS 动画库,比如 Animate.css 配合时会非常有用:
<div v-show="ok" class="animated" transition="bounce">Watch me bounce</div>
Vue.transition('bounce', {
enterClass: 'bounceInLeft',
leaveClass: 'bounceOutRight'
})
Vue.js 需要给过渡元素添加事件侦听器来侦听过渡何时结束。
基于所使用的 CSS,该事件要么是 transitionend,要么是 animationend。
如果你只使用了两者中的一种,那么 Vue.js 将能够根据生效的 CSS 规则自动推测出对应的事件类型。
但是,有些情况下一个元素可能需要同时带有两种类型的动画。
比如你可能希望让 Vue 来触发一个 CSS animation,同时该元素在鼠标悬浮时又有 CSS transition 效果。
这样的情况下,你需要显式地声明你希望 Vue 处理的动画类型 (animation 或是 transition):
Vue.transition('bounce', {
// 该过渡效果将只侦听 `animationend` 事件
type: 'animation'
})
JavaScript 过渡
可以只使用 JavaScript 钩子,不用定义任何 CSS 规则。
当只使用 JavaScript 过渡时,enter 和 leave 钩子需要调用 done 回调,否则它们将被同步调用,过渡将立即结束。
Vue.transition('expand', {
// 为 JavaScript 过渡显式声明 css: false 是个好主意,Vue.js 将跳过 CSS 检测。这样也会阻止无意间让 CSS 规则干扰过渡
css: false,
beforeEnter: function (el) {},
enter: function (el) {},
afterEnter: function (el) {},
enterCancelled: function (el) {},
beforeLeave: function (el) {},
leave: function (el) {},
afterLeave: function (el) {},
leaveCancelled: function (el) {}
})
渐近过渡
transition 与 v-for 一起用时可以创建近渐过渡。给过渡元素添加一个特性 stagger, enter-stagger 或 leave-stagger。
或者,提供一个钩子 stagger, enter-stagger 或 leave-stagger,以更好的控制:
<div v-for="item in list" transition="stagger" stagger="100"></div>
Vue.transition('stagger', {
stagger: function (index) {
// 每个过渡项目增加 50ms 延时
// 但是最大延时限制为 300ms
return Math.min(300, index * 50)
}
})
组件
组件(Component)是 Vue.js 最强大的功能之一。
组件可以扩展 HTML 元素,封装可重用的代码。
在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能
var MyComponent = Vue.extend({
// 在使用 template 选项时,模板的内容将替换实例的挂载元素。因而推荐模板的顶级元素始终是单个元素
// 片断实例: 有未知数量的顶级元素,它将把它的 DOM 内容当作片断
// 片断实例仍然会正确地渲染内容。不过,它没有一个根节点,它的 $el 指向一个锚节点,即一个空的文本节点
// 片断实例组件元素上的非流程控制指令,非 prop 特性和过渡将被忽略,因为没有根元素供绑定
template: '<div>A custom component!</div>',
// 传入 Vue 构造器的多数选项也可以用在 Vue.extend() 中,不过有两个特例: data, el
data: function() {
return {a: 1};
}
})
// 要把这个构造器用作组件,需要用 Vue.component(tag, constructor) 注册
// 全局注册组件,tag 为 my-component
Vue.component('my-component', MyComponent)
注意组件的模板替换了自定义元素,自定义元素的作用只是作为一个挂载点。这可以用实例选项 replace 改变。
不需要全局注册每个组件。可以让组件只能用在其它组件内,用实例选项 components 注册。这种封装也适用于其它资源,如指令(directives)、过滤器(filters)、过渡(transitions)和模板片断(partials)。
var Child = Vue.extend({ /* ... */ })
var Parent = Vue.extend({
template: '...',
components: {
// <my-component> 只能用在父组件模板内
'my-component': Child
}
})
为了让事件更简单,可以直接传入选项对象而不是构造器给 Vue.component() 和 component 选项。Vue.js 在背后自动调用 Vue.extend():
// 在一个步骤中扩展与注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 局部注册也可以这么做
var Parent = Vue.extend({
components: {
'my-component': {
template: '<div>A custom component!</div>'
}
}
})
一些 HTML 元素对什么元素可以放在它里面有限制。常见的限制:
- a 不能包含其它的交互元素(如按钮,链接)
- ul 和 ol 只能直接包含 li
- select 只能包含 option 和 optgroup
- table 只能直接包含 thead, tbody, tfoot, tr, caption, col, colgroup
- tr 只能直接包含 th 和 td
另一个结果是,自定义标签(包括自定义元素和特殊标签,如 <component>、<template>、 <partial>
)不能用在 ul, select, table 等对内部元素有限制的标签内。
放在这些元素内部的自定义标签将被提到元素的外面,因而渲染不正确。对于自定义元素,应当使用 is 特性:
<table>
<tr is="my-component"></tr>
</table>
使用 props 传递数据
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。
应该使用 props 把数据传给子组件。“prop” 是组件数据的一个字段,期望从父组件传下来。子组件需要显式地用 props 选项声明 props。
HTML 特性不区分大小写。名字形式为 camelCase 的 prop 用作特性时,需要转为 kebab-case(短横线隔开)
Vue.component('child', {
// 声明 props
props: ['msg'],
// prop 可以用在模板内
// 代码中可以用 `this.msg` 来获取
template: '<span>{{ msg }}</span>'
})
<!-- 普通字符串, 传递了一个字符串 "1" -->
<child msg="1"></child>
<!-- 传递实际的数字 -->
<child :msg="1"></child>
<!-- 动态 props -->
<child v-bind:msg="msg"></child>
<child v-bind:msg="'hello!'"></child>
prop 默认是单向绑定:当父组件的属性变化时,将传导给子组件,但是反过来不会。
这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
不过,也可以使用 .sync 或 .once 绑定修饰符显式地强制双向或单次绑定。
双向绑定会把子组件的 msg 属性同步回父组件的 parentMsg 属性。
单次绑定在建立之后不会同步之后的变化。
注意如果 prop 是一个对象或数组,是按引用传递。在子组件内修改它会影响父组件的状态,不管是使用哪种绑定类型。
<!-- 双向绑定 -->
<child :msg.sync="parentMsg"></child>
<!-- 单次绑定 -->
<child :msg.once="parentMsg"></child>
组件可以为 props 指定验证要求。当组件给其他人使用时这很有用,因为这些验证要求构成了组件的 API,确保其他人正确地使用组件。
此时 props 的值是一个对象,包含验证要求:
Vue.component('example', {
props: {
// 基础类型检测 (`null` 意思是任何类型都可以)
propA: Number,
// 必需且是字符串
propB: {
type: String,
required: true
}
}
}
父子组件通信
子组件可以用 this.$parent
访问它的父组件。
根实例的后代可以用 this.$root
访问它。
父组件有一个数组 this.$children
,包含它所有的子元素。
尽管可以访问父链上任意的实例,不过子组件应当避免直接依赖父组件的数据,尽量显式地使用 props 传递数据。
另外,在子组件中修改父组件的状态是非常糟糕的做法,因为:
- 这让父组件与子组件紧密地耦合;
- 只看父组件,很难理解父组件的状态。因为它可能被任意子组件修改!理想情况下,只有组件自己能修改它的状态。
Vue 实例实现了一个自定义事件接口,用于在组件树中通信
每个 Vue 实例都是一个事件触发器:
- 使用 $on() 监听事件;
- 使用 $emit() 在它上面触发事件;
- 使用 $dispatch() 派发事件,事件沿着父链冒泡;
- 使用 $broadcast() 广播事件,事件向下传导给所有的后代。
不同于 DOM 事件,Vue 事件在冒泡过程中第一次触发回调之后自动停止冒泡,除非回调明确返回 true
我们在使用子组件的地方使用 v-on 绑定自定义事件会更好
<child v-on:child-msg="handleIt"></child>
这让事情非常清晰:当子组件触发了 "child-msg" 事件,父组件的 handleIt 方法将被调用。
所有影响父组件状态的代码放到父组件的 handleIt 方法中;子组件只关注触发事件。
有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 v-ref 为子组件指定一个索引 ID(子组件索引)
<div id="parent">
<user-profile v-ref:profile></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile
使用 slot 分发内容
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板,这个处理称为内容分发(或 “transclusion”,如果你熟悉 Angular)。
Vue.js 实现了一个内容分发 API,参照了当前 Web 组件规范草稿,使用特殊的 <slot>
元素作为原始内容的插槽。
我们先明确内容的编译作用域。假定模板为:
msg 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译
一个常见错误是试图在父组件模板内将一个指令绑定到子组件属性/方法:
<!-- 无效 -->
<child v-show="someChildProperty"></child>
假定 someChildProperty 是子组件的属性,上例不能如预期工作。父组件模板不知道子组件的状态。
如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:
Vue.component('child-component', {
// 有效,因为是在正确的作用域内
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return {
someChildProperty: true
}
}
})
类似地,分发内容是在父组件作用域内编译。
<slot>
元素有一个特殊特性 name,用于配置如何分发内容。
多个 slot 可以有不同的名字。
命名 slot 将匹配有对应 slot 特性的内容片断。
也可以有一个未命名 slot,它是默认 slot,作为找不到匹配内容的回退插槽。
如果没有默认的 slot,不匹配内容将被抛弃。
假定我们有一个 multi-insertion 组件,它的模板为
<div>
<slot name="one"></slot>
<slot></slot>
<slot name="two"></slot>
</div>
父组件模板:
<multi-insertion>
<p slot="one">One</p>
<p slot="two">Two</p>
<p>Default A</p>
</multi-insertion>
渲染结果为:
<div>
<p slot="one">One</p>
<p>Default A</p>
<p slot="two">Two</p>
</div>
动态组件
多个组件可以使用同一个挂载点,然后动态地在它们之间切换。
使用保留的 <component>
元素,动态地绑定到它的 is 特性。
如果把切换出去的组件保留在内存中(例如保留 input 输入框中输入的值),可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:
<component :is="currentView" keep-alive></component>
在切换组件时,切入组件在切入前可能需要进行一些异步操作。为了控制组件切换时长,给切入组件添加 activate 钩子。
注意 activate 钩子只作用于动态组件切换或静态组件初始化渲染的过程中,不作用于使用实例方法手工插入的过程中。
transition-mode 特性用于指定两个动态组件之间如何过渡。
在默认情况下,进入与离开平滑地过渡。这个特性可以指定另外两种模式:
- in-out:新组件先过渡进入,等它的过渡完成之后当前组件过渡出去。
- out-in:当前组件先过渡出去,等它的过渡完成之后新组件过渡进入。
<component :is="view" transition="fade" transition-mode="out-in"></component>
编写可复用组件
一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue.js 组件 API 来自三部分——prop
,事件
和 slot
:
- prop 允许外部环境传递数据给组件;
- 事件 允许组件触发外部环境的 action;
- slot 允许外部环境插入内容到组件的视图结构内。
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat">
<!-- content -->
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>
在大型应用中,我们可能需要将应用拆分为小块,只在需要时才从服务器下载。
为了让事情更简单,Vue.js 允许将组件定义为一个工厂函数,动态地解析组件的定义。
Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 异步组件
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
如果子组件有 inline-template 特性,组件将把它的内容当作它的模板,而不是把它当作分发内容。这让模板更灵活。
<my-component inline-template>
<p>These are compiled as the component's own template</p>
<p>Not parent's transclusion content.</p>
</my-component>
但是 inline-template 让模板的作用域难以理解,并且不能缓存模板编译结果。最佳实践是使用 template 选项在组件内定义模板。
深入响应式原理
把一个普通对象传给 Vue 实例作为它的 data 选项,Vue.js 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。这是 ES5 特性。
用户看不到 getter/setters,但是在内部它们让 Vue.js 追踪依赖,在属性被访问和修改时通知变化。一个问题是在浏览器控制台打印数据对象时 getter/setter 的格式化不同,使用 vm.$log() 实例方法可以得到更友好的输出。
模板中每个指令/数据绑定都有一个对应的 watcher 对象,在计算过程中它把属性记录为依赖。之后当依赖的 setter 被调用时,会触发 watcher 重新计算 ,也就会导致它的关联指令更新 DOM。
尽管 Vue.js 提供了 API 动态地添加响应属性,还是推荐在 data 对象上声明所有的响应属性
- data 对象就像组件状态的模式(schema)。在它上面声明所有的属性让组件代码更易于理解。
- 添加一个顶级响应属性会强制所有的 watcher 重新计算,因为它之前不存在,没有 watcher 追踪它。这么做性能通常是可以接受的(特别是对比 Angular 的脏检查),但是可以在初始化时避免。
Vue.js 默认异步更新 DOM。每当观察到数据变化时,Vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来。
如果一个 watcher 被多次触发,只会推入一次到队列中。等到下一次事件循环,Vue 将清空队列,只进行必要的 DOM 更新。
在内部异步队列优先使用 MutationObserver,如果不支持则使用 setTimeout(fn, 0) 。
例如,设置了 vm.someData = 'new value',DOM 不会立即更新,而是在下一次事件循环清空队列时更新。
为了在数据变化之后等待 Vue.js 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback) ,回调会在 DOM 更新完成后调用。
var vm = new Vue({
el: '#example',
data: {
msg: '123'
}
})
vm.msg = 'new message' // 修改数据
// 异步更新 DOM
vm.$el.textContent === 'new message' // false
// vm.$nextTick() 这个实例方法比较方便,因为它不需要全局 Vue,它的回调的 this 自动绑定到当前 Vue 实例。
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
自定义指令
自定义指令提供一种机制将数据的变化映射为 DOM 行为。
可以用 Vue.directive(id, definition) 方法注册一个全局自定义指令,它接收两个参数指令 ID 与定义对象。
也可以用组件的 directives 选项注册一个局部自定义指令。
定义对象可以提供几个钩子函数(都是可选的):
- bind:只调用一次,在指令第一次绑定到元素上时调用。
- update: 在 bind 之后立即以初始值为参数第一次调用,之后每当绑定值变化时调用,参数为新值与旧值。
- unbind:只调用一次,在指令从元素上解绑时调用。
Vue.directive('my-directive', {
// 所有的钩子函数将被复制到实际的指令对象中,钩子内 this 指向这个指令对象。
// 这个对象暴露了一些有用的属性:
// el: 指令绑定的元素。
// vm: 拥有该指令的上下文 ViewModel。
// expression: 指令的表达式,不包括参数和过滤器。
// arg: 指令的参数。
// name: 指令的名字,不包含前缀。
// modifiers: 一个对象,包含指令的修饰符。
// descriptor: 一个对象,包含指令的解析结果。
//
// 你应当将这些属性视为只读的,不要修改它们。
// 你也可以给指令对象添加自定义属性,但是注意不要覆盖已有的内部属性。
bind: function () {
// 准备工作
// 例如,添加事件处理器或只需要运行一次的高耗任务
},
// 当指令使用了字面修饰符,它的值将按普通字符串处理并传递给 update 方法。
// update 方法将只调用一次,因为普通字符串不能响应数据变化。
update: function (newValue, oldValue) {
// 值更新时的工作
// 也会以初始值为参数调用一次
},
unbind: function () {
// 清理工作
// 例如,删除 bind() 添加的事件监听器
}
})
在注册之后,便可以在 Vue.js 模板中这样用(记着添加前缀 v-):
<div v-my-directive="someValue"></div>
当只需要 update 函数时,可以传入一个函数替代定义对象:
Vue.directive('my-directive', function (value) {
// 这个函数用作 update()
})
有时我们想以自定义元素的形式使用指令,而不是以特性的形式。这与 Angular 的 “E” 指令非常相似。
元素指令可以看做是一个轻量组件。
元素指令不能接受参数或表达式,但是它可以读取元素的特性从而决定它的行为。
迥异于普通指令,元素指令是终结性的,这意味着,一旦 Vue 遇到一个元素指令,它将跳过该元素及其子元素——只有该元素指令本身可以操作该元素及其子元素。
Vue.elementDirective('my-directive', {
// API 同普通指令
bind: function () {
// 操作 this.el...
}
})
<my-directive></my-directive>
高级选项
-
params
指定一个特性列表,Vue 编译器将自动提取绑定元素的这些特性
-
deep
如果指令用在一个对象上,当对象内部属性变化时要触发 update
-
twoWay
如果指令想向 Vue 实例写回数据
-
acceptStatement
如果指令想接受内联语句
-
terminal
Vue 通过递归遍历 DOM 树来编译模块。但是当它遇到 terminal 指令时会停止遍历这个元素的后代元素。这个指令将接管编译这个元素及其后代元素的任务。v-if 和 v-for 都是 terminal 指令。
-
priority
可以给指令指定一个优先级。如果没有指定,普通指令默认是 1000, terminal 指令默认是 2000。同一个元素上优先级高的指令会比其它指令处理得早一些。优先级一样的指令按照它在元素特性列表中出现的顺序依次处理,但是不能保证这个顺序在不同的浏览器中是一致的。
自定义过滤器
可以用全局方法 Vue.filter() 注册一个自定义过滤器,它接收两个参数:过滤器 ID 和过滤器函数。
// 过滤器函数以值为参数, 之后可接收任意数量的参数
Vue.filter('wrap', function (value, begin, end) {
// 过滤器函数的 this 始终指向调用它的 vm
// 返回转换后的值
return begin + value + end + this.a
})
一般我们使用过滤器都是在把来自模型的值显示在视图之前转换它。
不过也可以定义一个过滤器,在把来自视图(<input>
元素)的值写回模型之前转化它:
Vue.filter('currencyDisplay', {
// model -> view
// 在更新 `<input>` 元素之前格式化值
read: function(val) {
return '$'+val.toFixed(2)
},
// view -> model
// 在写回数据之前格式化值
write: function(val, oldVal) {
var number = +val.replace(/[^\d.]/g, '')
return isNaN(number) ? 0 : parseFloat(number.toFixed(2))
}
})
混合
混合以一种灵活的方式为组件提供分布复用功能。
混合对象可以包含任意的组件选项。当组件使用了混合对象时,混合对象的所有选项将被“混入”组件自己的选项中。
// 定义一个混合对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个组件,使用这个混合对象
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // -> "hello from mixin!"
当混合对象与组件包含同名选项时,这些选项将以适当的策略合并。
例如,同名钩子函数被并入一个数组,因而都会被调用。
另外,混合的钩子将在组件自己的钩子之前调用。
值为对象的选项,如 methods, components 和 directives 将合并到同一个对象内。如果键冲突则组件的选项优先。
可以全局注册混合。但要小心使用!因为一旦全局注册混合,它会影响所有之后创建的 Vue 实例。
如果使用恰当,可以为自定义选项注入处理逻辑。
慎用全局混合,因为它影响到每个创建的 Vue 实例,包括第三方组件。
在大多数情况下,它应当只用于自定义选项。
// 自定义选项也可以指定合并策略
// 如果想用自定义逻辑合并自定义选项,则向 Vue.config.optionMergeStrategies 添加一个函数
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回 mergedVal
}
// 对于多数值为对象的选项,可以简单地使用 methods 所用的合并策略
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
插件
插件通常会为 Vue 添加全局功能。
插件的范围没有限制——通常是下面几种:
- 添加全局方法或属性,如 vue-element
- 添加全局资源:指令/过滤器/过渡等,如 vue-touch
- 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如 vue-router
// Vue.js 的插件应当有一个公开方法 install
// 第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = ...
// 2. 添加全局资源
Vue.directive('my-directive', {})
// 3. 添加实例方法
Vue.prototype.$myMethod = ...
}
// 通过 Vue.use() 全局方法使用插件, 即调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin, { someOption: true })
构建大型应用
Vue.js 生态系统提供了一系列的工具与库,用于构建大型单页应用。
这些部分会感觉开始更像一个『框架』,但是它们本质上只是一套推荐的技术栈而已 - 你依然可以对各个部分进行选择和替换。
对于大型项目,为了更好地管理代码使用模块构建系统非常必要。
推荐代码使用 CommonJS 或 ES6 模块,然后使用 Webpack 或 Browserify 打包。
单文件组件
在典型的 Vue.js 项目中,我们会把界面拆分为多个小组件,每个组件在同一地方封装它的 CSS 样式,模板和 JavaScript 定义。你可以在 Webpackbin.com 上在线尝试!
my-component.vue
对于单页应用,推荐使用官方库 vue-router。
如果你只需要非常简单的路由逻辑,可以这么做,监听 hashchange
事件并使用动态组件。
利用这种机制也可以非常容易地配合其它路由库,如 Page.js 或 Director。
<div id="app">
<component :is="currentView"></component>
</div>
Vue.component('home', { /* ... */ })
Vue.component('page1', { /* ... */ })
var app = new Vue({
el: '#app',
data: {
currentView: 'home'
}
})
// 在路由处理器中切换页面
app.currentView = 'page1'
状态管理
在大型应用中,状态管理常常变得复杂,因为状态分散在许多组件内。如果一个状态要被多个实例共享,应避免复制它,而是共享引用:
var sourceOfTruth = {}
var vmA = new Vue({
data: sourceOfTruth
})
var vmB = new Vue({
data: sourceOfTruth
})
// 现在每当 sourceOfTruth 被修改后,vmA 与 vmB 将自动更新它们的视图。
// 扩展这个思路,我们可以实现 store 模式:
// 如果我们约定,组件不可以直接修改 store 的状态,而应当派发事件,通知 store 执行 action,那么我们基本上实现了 Flux 架构。
// 此约定的好处是,我们能记录 store 所有的状态变化,并且在此之上实现高级的调试帮助函数,如修改日志,快照,历史回滚等。
var store = {
state: {
message: 'Hello!'
},
actionA: function () {
// 有一点要注意,不要在 action 中替换原始的状态对象——为了观察到变化,组件和 store 需要共享这个对象
this.state.message = 'action A triggered'
},
actionB: function () {
this.state.message = 'action B triggered'
}
}
// 我们把所有的 action 放在 store 内,action 修改 store 的状态。
// 集中管理状态更易于理解状态将怎样变化。组件仍然可以拥有和管理它的私有状态。
var vmA = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
var vmB = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
生产发布
为了更小的文件体积,Vue.js 的压缩版本删除所有的警告,但是在使用 Browserify 或 Webpack 等工具构建 Vue.js 应用时,压缩需要一些配置。
对比其它框架
-
Angular
比 Angular 简单得多,Angular 使用双向绑定,Vue 也支持双向绑定,不过默认为单向绑定,数据从父组件单向传给子组件。在大型应用中使用单向绑定让数据流易于理解。
-
React
React 的渲染建立在 Virtual DOM 上——一种在内存中描述 DOM 树状态的数据结构。当状态发生变化时,React 重新渲染 Virtual DOM,比较计算之后给真实 DOM 打补丁。
因为它不使用数据观察机制,每次更新都会重新渲染整个应用,因此从定义上保证了视图与数据的同步。它也开辟了 JavaScript 同构应用的可能性。
React 团队雄心勃勃,计划让 React 成为通用平台的 UI 开发工具。
React 的开发趋势是将所有东西都放在 JavaScript 中,包括 CSS。
-
Polymer
Vue.js 的组件可以类比为 Polymer 中的自定义元素,它们提供类似的开发体验。最大的不同在于,Polymer 依赖最新的 Web 组件特性,在不支持的浏览器中,需要加载笨重的 polyfill,性能也会受到影响。
-
Riot
尽管比 Riot 重一点,Vue 提供了一些显著优处。
-
Ember