Vue是一款高度封装的、开箱即用的、一栈式的前端框架,既可以结合webpack进行编译式前端开发,也适用基于gulp、grunt等自动化工具直接挂载至全局window
使用。本文成文于Vue2.4.x版本发布之初,笔者生产环境当前使用的最新版本为2.5.2。在经历多个前端重度交互项目的开发实践之后,笔者结合官方文档对Vue技术栈进行了全面的梳理、归纳和注解,因此本文可以作为Vue2官方tutorial的补充性读物。建议暂不具备Vue2开发经验的同学,完成官方tutorial的学习之后再行阅读本文。
Vue2.2.x之后的版本,Vue框架及其技术栈功能日趋完善,相比React+Reflux/Redux/MobX的组合,Vue更加贴近W3C技术规范(例如实现仍处于W3C草案阶段的
、
、is
等新特性,提供了良好易用的模板书写环境),并且技术栈和开源生态更加完整和易于配置,将React中大量需要手动编码处理的位置,整合成最佳实践并抽象为简单的语法糖(比如Vuex中提供的store
的模块化特性),让开发人员始终将精力聚焦于业务逻辑本身。
Vue2的API结构相比Angular2更加简洁,可以自由的结合TypeScript或是ECMAScript6使用,并不特定于具体的预处理语言去获得最佳使用体验,框架本身的特性也并不强制依赖于各类炫酷的语法糖。Vue2总体是一款非常轻量的技术栈,设计实现上紧随W3C技术规范,着力于处理HTML模板组件化、事件和数据的作用域分离、多层级组件通信三个单页面前端开发当中的重点问题。本文在行文过程中,穿插描述了Angular、React等前端框架的异同与比较,供徘徊于各类前端技术选型的开发人员参考。
Vue与Angular的比较
组件化
Angular的设计思想照搬了Java Web开发当中MVC分层的概念,通过Controller
切割并控制页面作用域,然后通过Service
来实现复用,是一种对页面进行纵向分层的解耦思想。而Vue允许开发人员将页面抽象为若干独立的组件,即将页面DOM结构进行横向切割,通过组件的拼装来完成功能的复用、作用域控制。每个组件只提供props
作为单一接口,并采用Vuex进行state tree
的管理,从而便捷的实现组件间状态的通信与同步。
Angular在1.6.x版本开始提供component()
方法和Component Router
来提供组件化开发的体验,但是依然需要依赖于controller
和service
的划分,实质上依然没有摆脱MVC纵向分层思想的桎梏。
双向绑定与响应式绑定
Vue遍历data对象上的所有属性,并通过原生Object.defineProperty()
方法将这些属性转换为getter/setter
(只支持IE9及以上浏览器)。Vue内部通过这些getter/setter追踪依赖,在属性被修改时触发相应变化,从而完成模型到视图的双向绑定。每个Vue组件实例化时,都会自动调用$watch()
遍历自身的data属性,并将其记录为依赖项,当这些依赖项的setter被触发时会通知watcher重新计算新值,然后触发Vue组件的render()
函数重新渲染组件。
与Aangular双向数据绑定不同,Vue组件不能检测到实例化后data属性的添加、删除,因为Vue组件在实例化时才会对属性执行getter/setter处理,所以data对象上的属性必须在实例化之前存在,Vue才能够正确的进行转换。因而,Vue提供的并非真正意义上的双向绑定,更准确的描述应该是单向绑定,响应式更新,而Angular即可以通过$scope
影响view上的数据绑定,也可以通过视图层操作$scope
上的对象属性,属于真正意义上的视图与模型的双向绑定。
var vm = new Vue({
data:{
a:1
}
})
vm.a = 1 // 响应的
vm.b = 2 // 非响应的
因此,Vue不允许在已经实例化的组件上添加新的动态根级响应属性(即直接挂载在data下的属性),但是可以使用Vue.set(object, key, value)
方法添加响应式属性。
Vue.set(vm.someObject, "b", 2)
// vm.$set()实例方法是Vue.set()全局方法的别名
this.$set(this.someObject, "b",2)
// 使用Object.assign()或_.extend()也可以添加响应式属性,但是需要创建同时包含原属性、新属性的对象,从而有效触发watch()方法
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Vue对DOM的更新是异步的,观察到数据变化后Vue将开启一个队列,缓冲在同一事件循环(Vue的event loop被称为tick* [tɪk] n.标记,记号*)中发生的所有数据变化。如果同一个watcher被多次触发,只会向这个队列中推入一次。
Vue内部会通过原生JavaScript的Promise.then
、MutationObserver
、setTimeout(fn, 0)
来执行异步队列当中的watcher。
在需要人为操作DOM的场景下,为了在Vue响应数据变化之后再更新DOM,可以手动调用Vue.nextTick(callback)
,并将DOM操作逻辑放置在callback回调函数中,从而确保响应式更新完成之后再进行DOM操作。
{{message}}
虚拟DOM
Vritual DOM这个概念最先由React引入,是一种DOM对象差异化比较方案,即将DOM对象抽象成为Vritual DOM对象(即render()函数渲染的结果),然后通过差异算法对Vritual DOM进行对比并返回差异,最后通过一个补丁算法将返回的差异对象应用在真实DOM结点。
Vue当中的Virtual DOM对象被称为VNode(template
当中的内容会被编译为render()函数,而render()函数接收一个createElement()函数,并最终返回一个VNode对象),补丁算法来自于另外一个开源项目snabbdom,即将真实的DOM操作映射成对虚拟DOM的操作,通过减少对真实DOM的操作次数来提升性能。
➜ vdom git:(dev) tree
├── create-component.js
├── create-element.js
├── create-functional-component.js
├── helpers
│ ├── extract-props.js
│ ├── get-first-component-child.js
│ ├── index.js
│ ├── is-async-placeholder.js
│ ├── merge-hook.js
│ ├── normalize-children.js
│ ├── resolve-async-component.js
│ └── update-listeners.js
├── modules
│ ├── directives.js
│ ├── index.js
│ └── ref.js
├── patch.js
└── vnode.js
VNode的设计出发点与Angular的$digest
循环类似,都是通过减少对真实DOM的操作次数来提升性能,但是Vue的实现更加轻量化,摒弃了Angular为了实现双向绑定而提供的$apply()
、$eval()
封装函数,有选择性的实现Angular中$compile()
、$watch()
类似的功能。
Vue对象的选项
通过向构造函数new Vue()
传入一个option
对象去创建一个Vue实例。
var vm = new Vue({
// 数据
data: "声明需要响应式绑定的数据对象",
props: "接收来自父组件的数据",
propsData: "创建实例时手动传递props,方便测试props",
computed: "计算属性",
methods: "定义可以通过vm对象访问的方法",
watch: "Vue实例化时会调用$watch()方法遍历watch对象的每个属性",
// DOM
el: "将页面上已存在的DOM元素作为Vue实例的挂载目标",
template: "可以替换挂载元素的字符串模板",
render: "渲染函数,字符串模板的替代方案",
renderError: "仅用于开发环境,在render()出现错误时,提供另外的渲染输出",
// 生命周期钩子
beforeCreate: "发生在Vue实例初始化之后,data observer和event/watcher事件被配置之前",
created: "发生在Vue实例初始化以及data observer和event/watcher事件被配置之后",
beforeMount: "挂载开始之前被调用,此时render()首次被调用",
mounted: "el被新建的vm.$el替换,并挂载到实例上之后调用",
beforeUpdate: "数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前",
updated: "数据更改导致虚拟DOM重新渲染和打补丁之后被调用",
activated: "keep-alive组件激活时调用",
deactivated: "keep-alive组件停用时调用",
beforeDestroy: "实例销毁之前调用,Vue实例依然可用",
destroyed: "Vue实例销毁后调用,事件监听和子实例全部被移除,释放系统资源",
// 资源
directives: "包含Vue实例可用指令的哈希表",
filters: "包含Vue实例可用过滤器的哈希表",
components: "包含Vue实例可用组件的哈希表",
// 组合
parent: "指定当前实例的父实例,子实例用this.$parent访问父实例,父实例通过$children数组访问子实例",
mixins: "将属性混入Vue实例对象,并在Vue自身实例对象的属性被调用之前得到执行",
extends: "用于声明继承另一个组件,从而无需使用Vue.extend,便于扩展单文件组件",
provide&inject: "2个属性需要一起使用,用来向所有子组件注入依赖,类似于React的Context",
// 其它
name: "允许组件递归调用自身,便于调试时显示更加友好的警告信息",
delimiters: "改变模板字符串的风格,默认为{{}}",
functional: "让组件无状态(没有data)和无实例(没有this上下文)",
model: "允许自定义组件使用v-model时定制prop和event",
inheritAttrs: "默认情况下,父作用域的非props属性绑定会应用在子组件的根元素上。当编写嵌套有其它组件或元素的组件时,可以将该属性设置为false关闭这些默认行为",
comments: "设为true时会保留并且渲染模板中的HTML注释"
});
Vue实例通常使用vm
(View Model)变量来命名。
属性计算computed
在HTML模板表达式中放置太多业务逻辑,会让模板过重且难以维护。因此,可以考虑将模板中比较复杂的表达式拆分到computed属性当中进行计算。
{{ message.split("").reverse().join("") }}
Original message: "{{ message }}"
Computed reversed message: "{{ reversedMessage }}"
计算属性只在相关依赖发生改变时才会重新求值,这意味只要上面例子中的message没有发生改变,多次访问reversedMessage计算属性总会返回之前的计算结果,而不必再次执行函数,这是computed和method的一个重要区别。
计算属性默认只拥有getter方法,但是可以自定义一个setter方法。
观察者属性watch
通过watch属性可以手动观察Vue实例上的数据变动,当然也可以调用实例上的vm.$watch
达到相同的目的。
使用watch属性的灵活性在于,当监测到数据变化的时候,可以做一些设置中间状态之类的过渡处理。
混合属性mixins
用来将指定的mixin对象复用到Vue组件当中。
// mixin对象
var mixin = {
created: function () {
console.log("混合对象的钩子被调用")
},
methods: {
foo: function () {
console.log("foo")
},
conflicting: function () {
console.log("from mixin")
}
}
}
// vue属性
var vm = new Vue({
mixins: [mixin],
created: function () {
console.log("组件钩子被调用")
},
methods: {
bar: function () {
console.log("bar")
},
conflicting: function () {
console.log("from self")
}
}
})
// => "混合对象的钩子被调用"
// => "组件钩子被调用"
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
同名组件option对象的属性会被合并为数组依次进行调用,其中mixin对象里的属性会被首先调用。如果组件option对象的属性值是一个对象,则mixin中的属性会被忽略掉。
渲染函数render()
用来创建VNode,该函数接收createElement()
方法作为第1个参数,该方法调用后会返回一个虚拟DOM(即VNode)。
直接使用表达式,或者在render()
函数内通过createElement()
进行手动渲染,Vue都会自动保持blogTitle
属性的响应式更新。
{{ blogTitle }}
如果组件是一个函数组件,render()还会接收一个context参数,以便为没有实例的函数组件提供上下文信息。
通过render()函数实现虚拟DOM比较麻烦,因此可以使用Babel插件babel-plugin-transform-vue-jsx
在render()函数中应用JSX语法。
import AnchoredHeading from "./AnchoredHeading.vue"
new Vue({
el: "#demo",
render (h) {
return (
Hello world!
)
}
})
Vue对象全局API
Vue.extend(options) // 通过继承一个option对象来创建一个Vue实例。
Vue.nextTick([callback, context]) // 在下次DOM更新循环结束之后执行延迟回调。
Vue.set(target, key, value) // 设置对象的属性,如果是响应式对象,将会触发视图更新。
Vue.delete(target, key) // 删除对象的属性,如果是响应式对象,将会触发视图更新。
Vue.directive(id, [definition]) // 注册或获取全局指令。
Vue.filter(id, [definition]) // 注册或获取全局过滤器。
Vue.component(id, [definition]) // 注册或获取全局组件。
Vue.use(plugin) // 安装Vue插件。
Vue.mixin(mixin) // 全局注册一个mixin对象。
Vue.compile(template) // 在render函数中编译模板字符串。
Vue.version // 提供当前使用Vue的版本号。
Vue.mixin(mixin)
使用全局mixins将会影响到所有之后创建的Vue实例。
// 为自定义选项myOption注入一个处理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: "hello!"
})
// => "hello!"
Vue.directive(id, [definition])
Vue允许注册自定义指令,用于对底层DOM进行操作。
Vue.directive("focus", {
bind: function() {
// 指令第一次绑定到元素时调用,只会调用一次,可以用来执行一些初始化操作。
},
inserted: function (el) {
// 被绑定元素插入父节点时调用。
},
update: function() {
// 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。
},
componentUpdated: function() {
// 所在组件VNode及其子VNode全部更新时调用。
},
unbind: function() {
// 指令与元素解绑时调用,只会被调用一次。
}
})
钩子之间共享数据可以通过HTMLElement
的dataset
属性来进行(即HTML标签上通过data-
格式定义的属性)。
上面的钩子函数拥有如下参数:
- el: 指令绑定的HTML元素,可以用来直接操作DOM。
- vnode: Vue编译生成的虚拟节点。
- oldVnode: 之前的虚拟节点,仅在
update
、componentUpdated
钩子中可用。
- 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}
。
上面参数除el
之外,其它参数都应该是只读的,尽量不要对其进行修改操作。
Vue.filter(id, [definition])
Vue可以通过定义过滤器,进行一些常见的文本格式化,可以用于mustache插值和v-bind表达式当中,使用时通过管道符|
添加在表达式尾部。
{{ message | capitalize }}
过滤器可以串联使用,也可以传入参数。
{{ message | filterA | filterB }}
{{ message | filterA("arg1", arg2) }}
Vue.use(plugin)
Vue通过插件来添加一些全局功能,Vue插件都会覆写其install()
方法,该方法第1个参数是Vue构造器
, 第2个参数是可选的option对象
:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function () {}
// 2. 添加全局资源
Vue.directive("my-directive", {
bind (el, binding, vnode, oldVnode) {}
})
// 3. 注入组件
Vue.mixin({
created: function () {}
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {}
}
通过全局方法Vue.use()
使用指定插件,使用的时候也可以传入一个option对象。
Vue.use(MyPlugin, {someOption: true})
vue-router等插件检测到Vue是全局对象时会自动调用Vue.use()
,如果在CommonJS模块环境中,则需要显式调用Vue.use()
。
实例属性和方法
Vue实例暴露了一系列带有前缀$的实例属性与方法。
let vm = new Vue();
vm = {
// Vue实例属性的代理
$data: "被watch的data对象",
$props: "当前组件收到的props",
$el: "Vue实例使用的根DOM元素",
$options: "当前Vue实例的初始化选项",
$parent: "父组件Vue对象的实例",
$root: "根组件Vue对象的实例",
$children: "当前实例的直接子组件",
$slots: "访问被slot分发的内容",
$scopedSlots: "访问scoped slots",
$refs: "包含所有拥有ref注册的子组件",
$isServer: "判断Vue实例是否运行于服务器",
$attrs: "包含父作用域中非props的属性绑定",
$listeners: "包含了父作用域中的v-on事件监听器",
// 数据
$watch: "观察Vue实例变化的表达式、计算属性函数",
$set: "全局Vue.set的别名",
$delete: "全局Vue.delete的别名",
// 事件
$on: "监听当前实例上的自定义事件,事件可以由vm.$emit触发",
$once: "监听一个自定义事件,触发一次之后就移除监听器",
$off: "移除自定义事件监听器",
$emit: "触发当前实例上的事件",
// 生命周期
$mount: "手动地挂载一个没有挂载的Vue实例",
$forceUpdate: "强制Vue实例重新渲染,仅影响实例本身和插入插槽内容的子组件",
$nextTick: "将回调延迟到下次DOM更新循环之后执行",
$destroy: "完全销毁一个实例",
}
$refs属性
子组件指定ref
属性之后,可以通过父组件的$refs
实例属性对其进行访问 。
$refs会在组件渲染完毕后填充,是非响应式的,仅作为需要直接访问子组件的应急方案,因此要避免在模板或计算属性中使用$refs。
生命周期
每个Vue实例在创建时,都需要经过一系列初始化过程(设置数据监听、编译模板、挂载实例到DOM、在数据变化时更新DOM),并在同时运行一些钩子函数,让开发人员能够在特定生命周期内执行自己的代码。
不要在Vue实例的属性和回调上使用箭头函数,比如created: () => console.log(this.a)
或vm.$watch("a", newValue => this.myMethod())
。因为箭头函数的this与父级上下文绑定,并不指向Vue实例本身,所以前面代码中的this.a
或this.myMethod
将会是undefined
。
通过jQuery对DOM进行的操作可以放置在Mounted
属性上进行,即当Vue组件已经完成在DOM上挂载的时候。
数据绑定
Vue视图层通过Mustache["mʌstæʃ]
语法与Vue实例中的data属性进行响应式绑定,但是也可以通过内置指令v-once
完成一个单向的绑定,再或者通过v-html
指令将绑定的字符串输出为HTML,虽然这样很容易招受XSS攻击。
Message: {{ result }}
一次性绑定: {{ msg }}
Mustache不能用于HTML属性,此时需要借助于v-bind
指令。
绑定HTML的class和style
直接操作class
与style
属性是前端开发当中的常见需求,Vue通过v-bind:class
和v-bind:style
指令有针对性的对这两种操作进行了增强。
v-bind:class
绑定HTML的class
属性。
可以传递一个数组给v-bind:class
从而同时设置多个class属性。
当在自定义组件上使用class
属性时,这些属性将会被添加到该组件的根元素上面,这一特性同样适用于v-bind:class
。
Hi
Hi
v-bind:style
绑定HTML的style
属性。
使用v-bind:style
时Vue会自动添加prefix前缀,常见的prefix前缀如下:
-
-webkit-
Chrome、Safari、新版Opera、所有iOS浏览器(包括iOS版Firefox),几乎所有WebKit内核浏览器。
-
-moz-
针对Firefox浏览器。
-
-o-
未使用WebKit内核的老版本Opera。
-
-ms-
微软的IE以及Edge浏览器。
使用JavaScript表达式
Vue对于所有数据绑定都提供了JavaScript表达式支持,但是每个绑定只能使用1个表达式。
{{ number + 1 }}
{{ message.split("").reverse().join("") }}
{{ var a = 1 }}
{{ if (ok) { return message } }}
v-model双向数据绑定
v-model
指令实质上是v-on
和v-bind
的糖衣语法,该指令会接收一个value属性
,存在新值时则触发一个input事件
。
单选框、复选框一类的输入域将value属性作为了其它用途,因此可以通过组件的model
选项来避免冲突:
内置指令
带有v-
前缀,当表达式值发生变化时,会响应式的将影响作用于DOM。指令可以接收后面以:
表示的参数(被指令内部的arg属性接收),或者以.
开头的修饰符(指定该指令以特殊方式绑定)。
Hello world!
Vue为v-bind
和v-on
这两个常用的指令提供了简写形式:
和@
。
目前,Vue在2.4.2版本当中提供了如下的内置指令:
A
B
C
Not A/B/C
组件
组件可以扩展HTML元素功能,并且封装可重用代码。可以通过Vue.component( id, [definition] )
注册或者获取全局组件。
// 注册组件,传入一个扩展过的构造器
Vue.component("my-component", Vue.extend({ ... }))
// 注册组件,传入一个option对象(会自动调用Vue.extend)
Vue.component("my-component", { ... })
// 获取注册的组件(始终返回构造器)
var MyComponent = Vue.component("my-component")
下面代码创建了一个Vue实例,并将自定义组件my-component
挂载至HTML当中。
浏览器解析完HTML之后才会渲染Vue表达式,但是诸如