Vue
是一款高度封装的 、开箱即用的 、一栈式的前端框架 ,既可以结合
webpack 进行编译式前端开发,也适用基于 gulp、grunt
等自动化工具直接挂载至全局window
使用。本文成文于 Vue2.4.x
版本发布之初,笔者生产环境当前使用的最新版本为
2.6.12
。在经历多个前端重度交互项目的开发实践之后,笔者结合官方文档对
Vue 技术栈进行了全面的梳理、归纳和注解,因此本文可以作为 Vue2 官方《Get Started》
的补充性读物。
Vue 2
框架体系及技术栈日趋完善,相较于React+Reflux/Redux/MobX
技术栈,Vue
更加贴近 W3C 技术规范(例如实现仍处于 W3C
草案阶段的<template>
、<slot>
、is
等新特性,提供了良好易用的模板书写环境 ),并且开源生态更加完整且易于配置,将
React 手动编码才能处理的细节,整合成为最佳实践并抽象为语法糖(比如 Vuex
中提供的store
的模块化特性),使得开发人员得以将注意力聚焦于业务逻辑本身。
Vue2 的 API 结构相比 Angular 更加简洁,可以自由结合 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
上的对象属性,属于真正意义上的视图与模型的双向绑定 。
1 2 3 4 5 6 7 var vm = new Vue ({ data : { a : 1 } }); vm.a = 1 ; vm.b = 2 ;
因此,Vue
不允许在已经实例化的组件上添加新的动态根级响应属性(即直接挂载在
data
下的属性 ),但是可以使用Vue.set(object, key, value)
方法添加响应式属性。
1 2 3 4 5 6 7 Vue .set (vm.someObject , "b" , 2 );this .$set(this .someObject , "b" , 2 );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
操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <div id ="example" > {{message}}</div > <script > var vue = new Vue ({ el : "#example" , data : { message : "123" } }); vue.message = "new message" ; vue.$el .textContent === "new message" ; vue.nextTick (function ( ) { vm.$el .textContent === "new message" ; }); </script > <script > Vue .component ("example" , { template : "<span>{{ message }}</span>" , data : function ( ) { return { message : "没有更新" }; }, methods : { updateMessage : function ( ) { this .message = "更新完成" ; console .log (this .$el .textContent ); this .$nextTick(function ( ) { console .log (this .$el .textContent ); }); } } }); </script >
Object.defineProperty()
Vue2 的响应式绑定依靠 JavaScript
原生提供的Object.defineProperty()
实现,下面通过手动完成了一个简单的响应式绑定,来展示其内在的工作机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <label > 输入:</label > <input id ="input" type ="text" > <br /> <label > 输出:</label > <input id ="output" type ="text" > </input > <script > const store = {};const output = [];Object .defineProperty (store, "element" , { set : value => { output["element" ] = value; }, get : () => { return output["element" ]; } }); let inputDOM = document .querySelector ("#input" );let outputDOM = document .querySelector ("#output" );inputDOM.onkeyup = () => { store.element = inputDOM.value ; outputDOM.value = output["element" ]; }; </script >
上面的示例中,通过操作 DOM
的方式完成数据绑定显然不够优雅,目前使用较为广泛的是 Vue2 所使用的
Mustache
胡须语法{{``}}
,因此如何更加语义化的完成数据绑定,成为下一步需要着手解决的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <label > 输入:</label > <input id ="input" type ="text" /> <p id ="output" > Handlebar编译后输出:{{element}}</p > <script > const store = {}; const output = []; Object .defineProperty (store, "element" , { set : value => { output["element" ] = value || "" ; }, get : () => { return output["element" ]; } }); let inputDOM = document .querySelector ("#input" ); let outputHTML = document .querySelector ("#output" ).innerHTML ; compile (store, outputHTML); inputDOM.onkeyup = () => { store.element = inputDOM.value ; compile (store, outputHTML); }; function compile (store, html ) { const template = Handlebars .compile (html); document .querySelector ("#output" ).innerHTML = template (store); } </script >
看起来工作得学不错,不过生产环境下需要完成的功能更多,比如对用户输入的内容进行清洗,以防止
XSS;通过虚拟 DOM 和 diff 算法,更有效率的插入编译后的 DOM
等等,当然这些工作 Vue2
已经为你完成,开发人员只需要专注于业务代码本身。
虚拟 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
的操作次数来提升性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ➜ 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 实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 var vm = new Vue ({ data : "声明需要响应式绑定的数据对象" , props : "接收来自父组件的数据" , propsData : "创建实例时手动传递props,方便测试props" , computed : "计算属性" , methods : "定义可以通过vm对象访问的方法" , watch : "Vue实例化时会调用$watch()方法遍历watch对象的每个属性" , 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 属性当中进行计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <div id ="example" > {{ message.split("").reverse().join("") }} </div > <div id ="example" > <p > Original message: "{{ message }}"</p > <p > Computed reversed message: "{{ reversedMessage }}"</p > </div > <script > var vm = new Vue ({ el : "#example" , data : { message : "Hello" }, computed : { reversedMessage : function ( ) { return this .message .split ("" ) .reverse () .join ("" ); } } }); </script >
计算属性只在相关依赖发生改变时才会重新求值,这意味只要上面例子中的
message 没有发生改变,多次访问 reversedMessage
计算属性总会返回之前的计算结果,而不必再次执行函数,这是 computed 和
method 的一个重要区别。
计算属性默认只拥有getter 方法,但是可以自定义一个setter 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script > ... ... ... computed : { fullName : { get : function ( ) { return this .firstName + " " + this .lastName }, set : function (newValue ) { var names = newValue.split (" " ) this .firstName = names[0 ] this .lastName = names[names.length - 1 ] } } } ... ... ... vm.fullName = "John Doe" </script >
观察者属性 watch
通过 watch 属性可以手动观察 Vue
实例上的数据变动,当然也可以调用实例上的vm.$watch
达到相同的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <div id ="watch-example" > <p > Ask a yes/no question: <input v-model ="question" /> </p > <p > {{ answer }}</p > </div > <script > var watchExampleVM = new Vue ({ el : "#watch-example" , data : { question : "" , answer : "I cannot give you an answer until you ask a question!" }, watch : { question : function (newQuestion ) { this .answer = "Waiting for you to stop typing..." ; this .getAnswer (); } }, methods : { getAnswer : _.debounce ( function ( ) { if (this .question .indexOf ("?" ) === -1 ) { this .answer = "Questions usually contain a question mark. ;-)" ; return ; } this .answer = "Thinking..." ; var vm = this ; axios .get ("https://yesno.wtf/api" ) .then (function (response ) { vm.answer = _.capitalize (response.data .answer ); }) .catch (function (error ) { vm.answer = "Error! Could not reach the API. " + error; }); }, 500 ) } }); </script >
使用 watch
属性的灵活性在于,当监测到数据变化的时候,可以做一些设置中间状态之类的过渡处理。
混合属性 mixins
用来将指定的 mixin 对象复用到 Vue 组件当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 var mixin = { created : function ( ) { console .log ("混合对象的钩子被调用" ); }, methods : { foo : function ( ) { console .log ("foo" ); }, conflicting : function ( ) { console .log ("from mixin" ); } } }; var vm = new Vue ({ mixins : [mixin], created : function ( ) { console .log ("组件钩子被调用" ); }, methods : { bar : function ( ) { console .log ("bar" ); }, conflicting : function ( ) { console .log ("from self" ); } } }); vm.foo (); vm.bar (); vm.conflicting ();
同名组件 option 对象的属性会被合并为数组依次进行调用,其中 mixin
对象里的属性会被首先调用。如果组件 option 对象的属性值是一个对象,则
mixin 中的属性会被忽略掉。
渲染函数 render()
用来创建 VNode,该函数接收createElement()
方法作为第 1
个参数,该方法调用后会返回一个虚拟 DOM(即 VNode )。
直接使用表达式,或者在render()
函数内通过createElement()
进行手动渲染,Vue
都会自动保持blogTitle
属性的响应式更新。
1 2 3 4 5 6 7 <h1 > {{ blogTitle }}</h1 > <script > render : function (createElement ) { return createElement ("h1" , this .blogTitle ) } </script >
如果组件是一个函数组件,render()还会接收一个 context
参数,以便为没有实例的函数组件提供上下文信息。
通过 render()函数实现虚拟 DOM 比较麻烦,因此可以使用 Babel
插件babel-plugin-transform-vue-jsx
在 render()函数中应用 JSX
语法。
1 2 3 4 5 6 7 8 9 10 11 12 import AnchoredHeading from "./AnchoredHeading.vue" ;new Vue ({ el : "#demo" , render (h ) { return ( <AnchoredHeading level ={1} > <span > Hello</span > world! </AnchoredHeading > ); } });
Vue 对象全局 API
1 2 3 4 5 6 7 8 9 10 11 Vue .extend (options); Vue .nextTick ([callback, context]); 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 .mixin (mixin); Vue .compile (template); Vue .version ;
Vue.mixin(mixin)
使用全局 mixins 将会影响到所有之后创建 的 Vue
实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Vue .mixin ({ created : function ( ) { var myOption = this .$options .myOption ; if (myOption) { console .log (myOption); } } }); new Vue ({ myOption : "hello!" });
Vue.directive(id, [definition])
Vue 允许注册自定义指令,用于对底层 DOM 进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Vue .directive ("focus" , { bind : function ( ) { }, inserted : function (el ) { }, update : function ( ) { }, componentUpdated : function ( ) { }, 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
表达式当中,使用时通过管道符|
添加在表达式尾部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {{ message | capitalize }} <div v-bind:id ="rawId | formatId" > </div > <script > new Vue ({ filters : { capitalize : function (value ) { if (!value) return "" ; value = value.toString (); return value.charAt (0 ).toUpperCase () + value.slice (1 ); } } }); </script >
过滤器可以串联使用,也可以传入参数。
1 2 <span > {{ message | filterA | filterB }}</span > <span > {{ message | filterA("arg1", arg2) }}</span >
Vue.use(plugin)
Vue 通过插件来添加一些全局功能,Vue
插件都会覆写其install()
方法,该方法第 1
个参数是Vue构造器
, 第 2
个参数是可选的option对象
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 MyPlugin .install = function (Vue, options ) { Vue .myGlobalMethod = function ( ) {}; Vue .directive ("my-directive" , { bind (el, binding, vnode, oldVnode ) {} }); Vue .mixin ({ created : function ( ) {} }); Vue .prototype .$myMethod = function (methodOptions ) {}; };
通过全局方法Vue.use()
使用指定插件,使用的时候也可以传入一个
option 对象。
1 Vue .use (MyPlugin , { someOption : true });
vue-router 等插件检测到 Vue
是全局对象时会自动调用Vue.use()
,如果在 CommonJS
模块环境中,则需要显式调用Vue.use()
。
实例属性和方法
Vue 实例暴露了一系列带有前缀$ 的实例属性与方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 let vm = new Vue ();vm = { $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
实例属性对其进行访问
。
1 2 3 4 5 6 7 8 <div id ="parent" > <user-profile ref ="profile" > </user-profile > </div > <script > var parent = new Vue ({ el : "#parent" }); var child = parent.$refs .profile ; </script >
\(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 攻击。
1 2 3 <span > Message: {{ result }}</span > <span v-once > 一次性绑定: {{ msg }}</span > <div v-html ="rawHtml" > </div >
Mustache 不能用于 HTML
属性,此时需要借助于v-bind
指令。
1 2 <div v-bind:id ="dynamicId" > </div > <button v-bind:disabled ="isButtonDisabled" > Button</button >
绑定 HTML 的 class 和 style
直接操作class
与style
属性是前端开发当中的常见需求,Vue
通过v-bind:class
和v-bind:style
指令有针对性的对这两种操作进行了增强。
v-bind:class
绑定 HTML 的class
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script > ... ... data : { isActive : true , hasError : false , classObject : { active : true , "text-danger" : false } } ... ... </script > <div v-bind:class ="classObject" > </div > <div class ="static" v-bind:class ="{ active: isActive, " text-danger ": hasError }"> </div > <div class ="static active" > </div >
可以传递一个数组给v-bind:class
从而同时设置多个 class
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script > ... ... data : { activeClass : "active" , errorClass : "text-danger" } ... ... </script > <div v-bind:class ="[activeClass, errorClass]" > </div > <div class ="active text-danger" > </div > <div v-bind:class ="[isActive ? activeClass : " ", errorClass ]"> </div > <div v-bind:class ="[{ active: isActive }, errorClass]" > </div >
当在自定义组件上使用class
属性时,这些属性将会被添加到该组件的根元素上面,这一特性同样适用于v-bind:class
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script > Vue .component ("my-component" , { template : "<p class=" foo bar">Hi</p>" , data : { isActive : true }, }) </script > <my-component class ="baz boo" > </my-component > <p class ="foo bar baz boo" > Hi</p > <my-component v-bind:class ="{ active: isActive }" > </my-component > <p class ="foo bar active" > Hi</p >
v-bind:style
绑定 HTML 的style
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script > ... ... data : { styleObject : { color : "red" , fontSize : "13px" }, styleHeight : { height : 10rem; } styleWidth : { width : 20rem; } } ... ... </script > <div v-bind:style ="styleObject" > </div > <div v-bind:style ="[styleHeight, styleWidth]" > </div >
使用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 个表达式。
1 2 3 4 5 6 7 8 9 10 <span > {{ number + 1 }}</span > <button > {{ ok ? "YES" : "NO" }}</button > <p > {{ message.split("").reverse().join("") }}</p > <div v-bind:id ="" list- " + id "> </div > {{ var a = 1 }} {{ if (ok) { return message } }}
v-model 双向数据绑定
v-model
指令实质上是v-on
和v-bind
的糖衣语法,该指令会接收一个value属性
,存在新值时则触发一个input事件
。
1 2 3 4 5 6 7 <input v-model ="something" /> <input v-bind:value ="something" v-on:input ="something = $event.target.value" /> <custom-input v-bind:value ="something" v-on:input ="something = arguments[0]" > </custom-input >
单选框、复选框一类的输入域将 value
属性作为了其它用途,因此可以通过组件的model
选项来避免冲突:
内置指令
带有v-
前缀,当表达式值发生变化时,会响应式的将影响作用于
DOM。指令可以接收后面以:
表示的参数 (被指令内部的
arg
属性接收 ),或者以.
开头的修饰符 (指定该指令以特殊方式绑定 )。
1 2 3 4 5 6 7 8 9 10 <p v-if ="seen" > Hello world!</p > <a v-bind:href ="url" > </a > <a v-on:click ="doSomething" > <form v-on:submit.prevent ="onSubmit" > </form ></a >
Vue
为v-bind
和v-on
这两个常用的指令提供了简写形式:
和@
。
1 2 3 4 5 6 7 <a v-bind:href ="url" > </a > <a :href ="url" > </a > <a v-on:click ="doSomething" > </a > <a @click ="doSomething" > </a >
目前,Vue 在2.4.2 版本当中提供了如下的内置指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <html v-text = "更新元素的textContent" v-html = "更新元素的innerHTML" v-show = "根据表达式的true/false,切换HTML元素的display属性" v-for = "遍历内部的HTML元素" v-pre = "跳过表达式渲染过程,可以显示原始的Mustache标签" v-cloak = "保持在HTML元素上直到关联实例结束编译,可以隐藏未编译的Mustache" v-once = "只渲染元素和组件一次" > </html > <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 > <p v-bind:attrOrProp .prop = "被用于绑定DOM属性" .camel = "将kebab-case特性名转换为camelCase" .sync = "语法糖,会扩展成一个更新父组件绑定值的v-on监听器" > </p > <button v-on:eventName .stop = "调用event.stopPropagation()" .prevent = "调用event.preventDefault()" .capture = "添加事件监听器时使用capture模式" .self = "当事件是从监听器绑定的元素本身触发时才触发回调" .native = "监听组件根元素的原生事件" - .once = "只触发一次回调" .left = "点击鼠标左键触发" .right = "点击鼠标右键触发" .middle = "点击鼠标中键触发" .passive = "以{passive: true}模式添加监听器" . {keyCode | keyAlias } = "触发特定键触事件" > </button > <input v-model .lazy = "取代input监听change事件" .number = "输入字符串转为数字" .trim = "过滤输入的首尾空格" />
组件
组件可以扩展 HTML
元素功能,并且封装可重用代码。可以通过Vue.component( id, [definition] )
注册或者获取全局组件。
1 2 3 4 5 6 7 8 Vue .component ("my-component" , Vue .extend ({ ... }))Vue .component ("my-component" , { ... })var MyComponent = Vue .component ("my-component" )
下面代码创建了一个 Vue
实例,并将自定义组件my-component
挂载至 HTML 当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script > Vue .component ("my-component" , { template : "<div>A custom component!</div>" }); new Vue ({ el : "#example" }); </script > <div id ="example" > <my-component > </my-component > </div > <div id ="example" > <div > A custom component!</div > </div >
浏览器解析完 HTML 之后才会渲染 Vue
表达式,但是诸如<ul> <ol> <table> <select>
限制了可以被包裹的
HTML 元素,而<option>
只能出现在某些 HTML
元素内部,造成 Vue 表达式可能不会被正确的渲染。因此,Vue
提供is
作为属性别名来解决该问题。
1 2 3 4 5 6 7 8 9 <table > <my-row > ...</my-row > </table > <table > <tr is ="my-row" > </tr > </table >
Vue.component()
传入的 data
属性不能是对象,而必须是函数。这样做的目的是避免组件在相同模板的多个位置被复用时,仅仅返回对象会造成组件间的数据被相互污染,而通过函数每次都返回全新的
data 对象能完美的规避这个问题。
1 2 3 4 5 6 7 8 9 Vue .component ("simple-counter" , { template : "<button v-on:click=" counter += 1 ">{{ counter }}</button>" , data : function ( ) { return { a : "" , b : "" } } });
父组件通过props
向下传递数据给子组件,子组件通过events
给父组件发送消息,即props
down, events up 。
props
虽然每个组件的作用域都是独立的,但是可以通过props属性
向子组件传递数据,这是一种单向数据流 的体现形式。
1 2 3 4 5 6 Vue .component ("child" , { props : ["message" ], template : "<span>{{ message }}</span>" });
不要在子组件内部修改 props,这样会导致后台报错。
命名方式转换
因为 HTML 并不区分大小写,所以 kebab-case(驼峰 )风格命名的
props,在组件中会以 camelCased(短横线隔开 )风格被接收。
1 2 3 4 5 6 7 8 9 10 <script > Vue.component("child", { props: ["myMessage"], template: "<span > {{ myMessage }} </span > "}) <script > <child my-message ="hello!" > </child >
动态 props
可以通过v-bind
指令,响应式的绑定父组件数据到子组件的
props。当父组件数据变化时,该变化也会传导至子组件。
1 2 3 4 5 <div > <input v-model ="parentMsg" /> <br /> <child v-bind:my-message ="parentMsg" > </child > </div >
使用v-bind
可以让其参数值能够以 JavaScript
表达式的方式被解析,否则所有传入的 props
都会被子组件认为是字符串类型。
1 2 3 4 <comp some-prop ="1" > </comp > <comp v-bind:some-prop ="1" > </comp >
验证 props
可以为组件的 props 指定验证规则,如果传入数据不符合要求,Vue
会发出相应警告,这样可以有效提高组件的健壮性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Vue .component ("example" , { props : { propA : Number , propB : [String , Number ], propC : { type : String , required : true }, propD : { type : Number , default : 100 }, propE : { type : Object , default : function ( ) { return { message : "hello" }; } }, propF : { validator : function (value ) { return value > 10 ; } } } });
props
会在组件实例创建之前进行校验。
组件的非 props 属性
组件可以接收任意传入的属性,这些属性都会被添加到组件 HTML
模板的根元素上(无论有没有在 props 中定义 )。
1 2 3 4 5 6 7 8 9 10 <bs-date-input data-3d-date-picker ="true" class ="date-picker-theme-dark" > </bs-date-input > <input type ="date" data-3d-date-picker ="true" class ="form-control date-picker-theme-dark" />
父组件传递给子组件的属性可能会覆盖子组件本身的属性,因而会对子组件造成破坏和污染。
事件
子组件可以通过 Vue 的自定义事件与父组件进行通信。
每个 Vue 实例都实现了如下 API,但是并不能直接通过$on
监听子组件冒泡的事件,而必须使用 v-on 指令。
$on(eventName)
监听事件
$emit(eventName)
触发事件
$on
和$emit
并不是addEventListener
和dispatchEvent
的别名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <div id ="counter-event-example" > <p > {{ total }}</p > <button-counter v-on:increment ="incrementTotal" > </button-counter > <button-counter v-on:increment ="incrementTotal" > </button-counter > </div > <script > Vue .component ("button-counter" , { template : "<button v-on:click=" incrementCounter">{{ counter }}</button>" , data : function ( ) { return { counter : 0 } }, methods : { incrementCounter : function ( ) { this .counter += 1 this .$emit("increment" ) } }, }) new Vue ({ el : "#counter-event-example" , data : { total : 0 }, methods : { incrementTotal : function ( ) { this .total += 1 } } }) </script >
开发人员也可以在组件的根元素上监听原生事件,这个时候需要借助到.native
修饰符。
1 <my-component v-on:click.native ="doTheThing" > </my-component >
Vue
中的props
本质是不能进行响应式绑定的,以防止破坏单向数据流,造成多个子组件对父组件状态形成污染。但是生产环境下,props
响应式绑定的需求是切实存在的。因此,Vue
将.sync
修饰符封装为糖衣语法,父组件在子组件的 props
使用该修饰符后,父组件会为 props
自动绑定v-on
事件,子组件则在监听到 props
变化时向父组件$emit
更新事件,从而让父组件的props
能够与子组件进行同步。
1 2 3 4 5 <comp :foo.sync ="bar" > </comp > <comp :foo ="bar" @update:foo ="val => bar = val" > </comp >
非父子关系的组件进行通信时,可以使用一个空 的 Vue
实例作为中央事件总线 。
1 2 3 4 5 6 7 var bus = new Vue ()bus.$emit("id-selected" , 1 ) bus.$on("id-selected" , function (id ) { ... ... ... })
更好的方式是借助 VueX 或者 Redux 之类的 flux 状态管理库。
slot
可以将父组件的内容混入到子组件的模板当中,此时可以在子组件中使用<slot>
作为父组件内容的插槽。
父组件模板的内容在父组件作用域内编译,子组件模板的内容在子组件作用域内编译。
匿名插槽
当子组件只有一个没有属性的<slot>
时,父组件全部内容片段将插入到插槽所在的
DOM 位置,并替换插槽标签本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <div > <h2 > Child</h2 > <slot > 父组件没有需要插入的内容时显示 </slot > </div > <div > <h1 > Parent</h1 > <child > <p > Content 1</p > <p > Content 2</p > </child > </div > <div > <h1 > Parent</h1 > <div > <h2 > Child</h2 > <p > Content 1</p > <p > Content 2</p > </div > </div >
<slot>
标签中的内容会在子组件作用域内编译,并在父组件没有需要插入的内容时才会显示。
具名插槽
可以通过<slot>
元素的name
属性来配置如何分发内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <div id ="app" > <header > <slot name ="header" > </slot > </header > <main > <slot > </slot > </main > <footer > <slot name ="footer" > </slot > </footer > </div > <app > <div slot ="header" > Header</div > <p > Content 1</p > <p > Content 2</p > <div slot ="footer" > Footer</div > </app > <div id ="app" > <header > <div > Header</div > </header > <main > <p > Content 1</p > <p > Content 2</p > </main > <footer > <p > Footer</p > </footer > </div >
匿名 slot 会作为没有匹配内容的父组件片段的插槽。
作用域插槽
子组件通过props
传递数据给<slot>
插槽,父组件使用带有scope
属性的<template>
来表示表示当前作用域插槽的模板,scope
值对应的变量会接收子组件传递来的
props 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <div class ="child" > <slot text ="hello from child" > </slot > </div > <div class ="parent" > <child > <template scope ="props" > <span > hello from parent</span > <span > {{ props.text }}</span > </template > </child > </div > <div class ="parent" > <div class ="child" > <span > hello from parent</span > <span > hello from child</span > </div > </div >
函数化组件
即无状态(没有 data )无实例(没有 this
上下文 )的组件,渲染开销较小,且不会出现在Vue devtools
当中。
1 2 3 4 5 6 7 Vue .component ("my-component" , { functional : true , render : function (createElement, context ) {}, props : {} });
动态组件
使用<component>
元素并动态绑定其is
属性,可以让多个组件使用相同的
Vue 对象挂载点,并实现动态切换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script > var vm = new Vue ({ el : "#example" , data : { currentView : "home" }, components : { home : { }, posts : { }, archive : { } } }); </script > <component v-bind:is ="currentView" > </component >
如果需要将切换的组件保持在内存,保留其状态并且避免重新渲染,可以使用
Vue 内置的keep-alive
指令。
1 2 3 4 5 <keep-alive > <component :is ="currentView" > </component > </keep-alive >
组件异步加载
Vue 允许将组件定义为工厂函数,从而异步的解析组件定义。Vue
只会在组件渲染时才触发工厂函数,并将结果缓存起来用于后续渲染。定义组件的工厂函数将会接收
resolve(接收到从服务器下载的 Vue 组件 options 时被调用 )和
reject(当远程 Vue 组件 options
加载失败时调用 )回调函数作为参数。
1 2 3 4 5 6 7 8 Vue .component ("async-example" , function (resolve, reject ) { setTimeout (function ( ) { resolve ({ template : "<div>I am async!</div>" }); }, 1000 ); });
可以结合 Webpack 提供的代码切割 功能,将 Vue 组件的
options 对象提取到单独 JavaScript 文件,从而实现异步的按需加载。
1 2 3 4 5 6 7 Vue .component ("async-webpack-example" , function (resolve ) { require (["./my-async-component" ], resolve); }); Vue .component ("async-webpack-example" , () => import ("./my-async-component" ));
从 Vue 2.3.0 版本开始,可以通过下面的方式来定义一个异步组件。
1 2 3 4 5 6 7 const AsyncWebpackExample = ( ) => ({ component : import ("./MyComp.vue" ), loading : LoadingComp , error : ErrorComp , delay : 200 , timeout : 3000 });
在路由组件上使用这种写法,需要使用 vue-router 的 2.4.0 以上版本。
组件的循环引用
循环引用,即两个组件互相引用对方,例如下面代码中tree-folder
、tree-folder-contents
两个组件同时成为了对方的父或子节点,如果使用
Webpack
模块化管理工具requiring
/importing
组件的时候,会报出Failed to mount component: template or render function not defined.
错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > <p > <span > {{ folder.name }}</span > <tree-folder-contents :children ="folder.children" /> </p > </template > <template > <ul > <li v-for ="child in children" > <tree-folder v-if ="child.children" :folder ="child" /> <span v-else > {{ child.name }}</span > </li > </ul > </template >
因为tree-folder
、tree-folder-contents
相互引用对方之后,无法确定组件加载的先后顺序陷入死循环,所以需要事先指明
webpack 组件加载的优先级。解决上面例子中 Vue
组件循环引用的问题,可以在tree-folder
组件的beforeCreate()
生命周期函数内注册引发问题的tree-folder-contents
组件。
1 2 3 beforeCreate : function ( ) { this .$options .components .TreeFolderContents = require ("./tree-folder-contents.vue" ).default }
组件命名约定
JavaScript
中命名组件组件时可以使用kebab-case
、camelCase
、PascalCase
,但
HTML 模板中只能使用kebab-case
格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 <kebab-cased-component > </kebab-cased-component > <camel-cased-component > </camel-cased-component > <pascal-cased-component > </pascal-cased-component > <kebab-cased-component /> <script > components : { "kebab-cased-component" : {}, "camelCasedComponent" : {}, "PascalCasedComponent" : {} } </script >
推荐 JavaScript 中通过PascalCase
方式声明组件, HTML
中则通过kebab-case
方式使用组件。
组件递归
当局部注册的 Vue
组件递归调用自身时,需要在创建组件时添加name
选项,全局注册的组件则可以省略该属性,因为
Vue 会自动进行添加。
1 2 3 4 5 6 7 8 9 10 11 12 new Vue ({ el : "#my-component" , name : "my-component" , template : "<div><my-component></my-component></div>" }); Vue .component ("my-component" , { template : "<div><my-component></my-component></div>" });
组件递归出现死循环时,会提示max stack size exceeded
错误,所以需要确保递归操作都拥有一个终止条件(比如使用
v-if 并返回 false )。
组件模板
可以在 Vue
组件上使用inline-template
属性,组件会将内嵌的 HTML
内容作为组件本身的模板进行渲染,而非将其作为slot
分发的内容。
1 2 3 4 5 6 <my-component inline-template > <div > <p > These are compiled as the component"s own template.</p > <p > Not parent"s transclusion content.</p > </div > </my-component >
也可以通过在<script>
标签内使用type="text/x-template"
和id
属性来定义一个内嵌模板。
1 2 3 4 5 6 7 8 9 <script type ="text/x-template" id ="hello-world-template" > <p > Hello hello hello</p > </script > <script > Vue .component ("hello-world" , { template : "#hello-world-template" }); </script >
Vuex 状态管理
Vuex 是专门为 Vue 应用程序提供的状态管理模式,每个 Vuex
应用的核心是store
(仓库 ),即装载应用程序state
(状态 )的容器,每个应用通常只拥有一个store
实例。
Vuex
的state
是响应式的,即store
中的state
发生变化时,相应组件也会进行更新,修改store
当中state
的唯一途径是提交mutations
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const store = new Vuex .Store ({ state : { count : 0 }, mutations : { increment (state ) { state.count ++; } } }); store.commit ("increment" ); console .log (store.state .count );
State
从store
当中获取state
的最简单办法是在计算属性中返回指定的state
,每当state
发生改变的时候都会重新执行计算属性,并且更新关联的
DOM。
1 2 3 4 5 6 7 8 const Counter = { template : `<div>{{ count }}</div>` , computed : { count ( ) { return store.state .count ; } } };
Vuex
提供store
选项,将state
从根组件注入 到每个子组件中,从而避免频繁import store
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const app = new Vue ({ el : "#app" , store : store, components : { Counter }, template : ` <div class="app"> <counter></counter> </div>` }); const Counter = { template : `<div>{{ count }}</div>` , computed : { count ( ) { return this .$store .state .count ; } } };
Vuex
提供mapState()
辅助函数,避免使用多个state
的场景下,多次去声明计算属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { mapState } from "vuex" ;export default { computed : mapState ({ count : state => state.count , countAlias : "count" , countPlusLocalState (state ) { return state.count + this .localCount ; } }) }; computed : mapState ([ "count" ]);
mapState()
函数返回一个包含有state
相关计算属性的对象,这里可以通过
ES6 的对象展开运算符...
将该对象与 Vue
组件本身的computed
属性进行合并。
1 2 3 4 computed : { localComputed () {}, ...mapState ({}) }
Vuex 允许在store
中定义getters
(可视为
store
的计算属性 ),getters
的返回值会根据其依赖被缓存,只有当依赖值发生了改变才会被重新计算。该方法接收state
作为第
1 个参数,其它getters
作为第 2
个参数。可以直接在store
上调用getters
来获取指定的计算值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const store = new Vuex .Store ({ state : { todos : [ { id : 1 , text : "..." , done : true }, { id : 2 , text : "..." , done : false } ] }, getters : { doneTodos : (state, getters ) => { return state.todos .filter (todo => todo.done ); } } }); store.getters .doneTodos ;
这样就可以方便的根据store
中现有的state
派生出新的state
,从而避免在多个组件中复用时造成代码冗余。
1 2 3 4 5 computed : { doneTodosCount () { return this .$store .getters .doneTodos } }
Vuex
提供的mapGetters()
辅助函数将store
中的getters
映射到局部计算属性。
1 2 3 4 5 6 7 8 9 10 11 import { mapGetters } from "vuex" ;export default { computed : { ...mapGetters ([ "doneTodosCount" , (doneCount : "doneTodosCount" ) ]) } };
Mutations
修改 store 中的 state 的唯一方法是提交 mutation([mjuː"teɪʃ(ə)n]
n.变化 ),mutations
类似于自定义事件,拥有一个字符串事件类型和一个回调函数(接收 state
作为参数,是对 state 进行修改的位置 )。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const store = new Vuex .Store ({ state : { count : 1 }, mutations : { increment (state ) { state.count ++; } } }); store.commit ("increment" );
可以通过 store 的commit()
方法触发指定的
mutations,也可以通过store.commit()
向 mutation
传递参数。
1 2 3 4 5 6 7 8 9 10 11 12 store.commit ({ type : "increment" , amount : 10 }) mutations : { increment (state, payload) { state.count += payload.amount } }
mutation
事件类型建议使用常量,并且将这些常量放置在单独文件,便于管理和防止重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export const SOME_MUTATION = "SOME_MUTATION" import Vuex from "vuex" import { SOME_MUTATION } from "./mutation-types" const store = new Vuex .Store ({ state : { ... }, mutations : { [SOME_MUTATION ] (state) { } } })
mutation()
必须是同步函数,因为 devtool
无法追踪回调函数中对state
进行的异步修改。
Vue 组件可以使用this.$store.commit("xxx")
提交
mutation,或者使用mapMutations()
将 Vue
组件中的methods
映射为store.commit
调用(需要在根节点注入store
)。
1 2 3 4 5 6 7 8 9 10 11 12 import { mapMutations } from "vuex" ;export default { methods : { ...mapMutations ([ "increment" ]), ...mapMutations ({ add : "increment" }) } };
Actions
Action 用来提交 mutation,且 Action 中可以包含异步操作。Action
函数接受一个与 store
实例具有相同方法和属性的context
对象,因此可以通过调用context.commit
提交一个
mutation,或者通过context.state
和context.getters
来获取
state、getters。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const store = new Vuex .Store ({ state : { count : 0 }, mutations : { increment (state ) { state.count ++; } }, actions : { increment (context ) { context.commit ("increment" ); } } });
生产环境下,可以通过 ES6 的解构参数来简化代码。
1 2 3 4 5 6 actions : { increment ({ commit }) { commit ("increment" ) } }
Action
通过store.dispatch()
方法进行分发,mutation 当中只能进行同步 操作,而action 内部可以进行异步 的操作。下面是一个购物车的例子,代码中分发了多个
mutations,并进行了异步 API 操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 actions : { checkout ({ commit, state }, products) { const savedCartItems = [...state.cart .added ] commit (types.CHECKOUT_REQUEST ) shop.buyProducts ( products, () => commit (types.CHECKOUT_SUCCESS ), () => commit (types.CHECKOUT_FAILURE , savedCartItems) ) } }
组件中可以使用this.$store.dispatch("xxx")
分发
action,或者使用mapActions()
将组件的methods
映射为store.dispatch
(需要在根节点注入store
)。
1 2 3 4 5 6 7 8 9 10 11 12 import { mapActions } from "vuex" ;export default { methods : { ...mapActions ([ "increment" ]), ...mapActions ({ add : "increment" }) } };
store.dispatch
可以处理action
回调函数当中返回的Promise
,并且store.dispatch
本身仍然返回一个Promise
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 actions : { actionA ({ commit }) { return new Promise ((resolve, reject ) => { setTimeout (() => { commit ("someMutation" ) resolve () }, 1000 ) }) }, actionB ({ dispatch, commit }) { return dispatch ("actionA" ).then (() => { commit ("someOtherMutation" ) }) } } store.dispatch ("actionA" ).then (() => { ... ... ... })
可以体验通过 ES7
的异步处理特性async
/await
来组合 action。
1 2 3 4 5 6 7 8 9 actions : { async actionA ({ commit }) { commit ("gotData" , await getData ()) }, async actionB ({ dispatch, commit }) { await dispatch ("actionA" ) commit ("gotOtherData" , await getOtherData ()) } }
Module
整个应用使用单一状态树的情况下,所有 state 都会集中到一个 store
对象,因此 store 可能变得非常臃肿。因此,Vuex 允许将 store
切割成模块(module ),每个模块拥有自己的state
、mutation
、action
、getter
、甚至是嵌套的子模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const moduleA = { state : {}, mutations : {}, actions : {}, getters : {} }; const moduleB = { state : {}, mutations : {}, actions : {} }; const store = new Vuex .Store ({ modules : { a : moduleA, b : moduleB } }); store.state .a ; store.state .b ;
module 内部的mutations()
和getters()
接收的第
1 个参数是模块的局部状态对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 const moduleA = { state : { count : 0 }, mutations : { increment (state ) { state.count ++; } }, getters : { doubleCount (state ) { return state.count * 2 ; } } };
模块内部 action
当中,可以通过context.state
获取局部状态,以及context.rootState
获取全局状态。
1 2 3 4 5 6 7 8 9 10 const moduleA = { actions : { incrementIfOddOnRootSum ({ state, commit, rootState } ) { if ((state.count + rootState.count ) % 2 === 1 ) { commit ("increment" ); } } } };
模块内部的getters()
方法,可以通过其第 3
个参数接收到全局状态。
1 2 3 4 5 6 7 const moduleA = { getters : { sumWithRootCount (state, getters, rootState ) { return state.count + rootState.count ; } } };
严格模式
严格模式下,如果 state
变化不是由mutation()
函数引起,将会抛出错误。只需要在创建store
的时候传入strict: true
即可开启严格模式。
1 2 3 const store = new Vuex .Store ({ strict : true });
不要在生产环境下启用严格模式,因为它会深度检测不合法的 state
变化,从而造成不必要的性能损失,我们可以通过在构建工具中增加如下判断避免这种情况。
1 2 3 const store = new Vuex .Store ({ strict : process.env .NODE_ENV !== "production" });
严格模式下,在属于 Vuex 的 state 上使用 v-model
指令会抛出错误,此时需要手动绑定 value 并监听 input、change
事件,并在事件回调中手动提交
action。另外一种实现方式是直接重写计算属性的 get 和 set 方法。
总结
应用层级的状态应该集中到单个 store 对象 中。
提交mutation
是更改状态的唯一方法,并且这个过程是同步 的。
异步 逻辑都应该封装到action
里面。
Webpack Vue Loader
vue-loader 是由 Vue
开源社区提供的 Webpack
加载器,用来将.vue
后缀的单文件组件转换为 JavaScript
模块,每个.vue
单文件组件可以包括以下部分:
一个<template>
。
一个<script>
。
多个<style>
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > 只能有1个</template > <script > 只能有1个; </script > <style > 可以有多个 </style > <style > 可以有多个 </style > <style > 可以有多个 </style >
CSS 作用域
向.vue
单文件组件的<style>
标签上添加scoped
属性,可以让该<style>
标签中的样式只作用于当前组件。使用
scoped 时,样式选择器尽量使用 class 或者 id,以提升页面渲染性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <style scoped > .example { color : red; } </style > <template > <div class ="example" > Hank</div > </template > <style > .example [data-v-f3f3eg9] { color : blue; } </style > <template > <div class ="example" data-v-f3f3eg9 > Hank</div > </template >
可以在一个组件中同时使用带scoped
属性和不带该属性的<style/>
,分别用来定义组件私有样式和全局样式。
CSS 模块化
在单文件组件.vue
的<style>
标签上添加module
属性即可打开
CSS 模块化特性。CSS
Modules 用于模块化组合 CSS,vue-loader 已经集成了
CSS 模块化特性。
1 2 3 4 5 6 7 8 <style module > .red { color : red; } .bold { font-weight : bold; } </style >
CSS 模块会向 Vue
组件中注入名为$style
计算属性,从而实现在组件的<template/>
中使用动态的class
属性进行绑定。
1 2 3 4 5 <template > <p :class ="$style.red" > This should be red </p > </template >
动画
Vue
在插入、更新、移除DOM 的时候,提供了如下几种方式去展现进入(entering )和离开(leaving )的过渡效果。
在 CSS 过渡和动画中应用 class。
钩子过渡函数中直接操作 DOM。
使用 CSS、JavaScript 动画库,如Animate.css 、Velocity.js 。
transition 组件
Vue 提供了内置组件<transition/>
来为 HTML 元素、Vue
组件添加过渡动画效果,可以在条件展示 (使用v-if
或v-show
)、动态组件 、展示组件根节点 的情况下进行渲染。<transition/>
主要用来处理单个节点,或者同时渲染多个节点当中的一个。
自动切换的 class 类名
在组件或 HTML
进入(entering )和离开(leaving )的过渡效果当中,Vue
将会自动切换并应用下图中的六种 class 类名。
可以使用<transition/>
的name
属性来自动生成过渡
class
类名,例如下面例子中的name: "fade"
将自动拓展为.fade-enter
,.fade-enter-active
等,name
属性缺省的情况下默认类名为v
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <div id ="demo" > <button v-on:click ="show = !show" > Toggle</button > <transition name ="fade" > <p v-if ="show" > hello</p > </transition > </div > <script > new Vue ({ el : "#demo" , data : { show : true } }); </script > <style > .fade-enter-active , .fade-leave-active { transition : opacity 0.5s ; } .fade-enter , .fade-leave-to { opacity : 0 ; } </style >
自定义 CSS 类名
结合Animate.css
使用时,可以在<transition/>
当中通过以下属性自定义
class 类名。
1 2 3 4 5 6 7 8 9 <transition enter-class ="animated" enter-active-class ="animated" enter-to-class ="animated" leave-class ="animated" leave-active-class ="animated" leave-to-class ="animated" > </transition >
自定义 JavaScript 钩子
结合Velocity.js
使用时,通过 v-on
在属性中设置钩子函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <transition v-on:before-enter ="beforeEnter" v-on:enter ="enter" v-on:after-enter ="afterEnter" v-on:enter-cancelled ="enterCancelled" v-on:before-leave ="beforeLeave" v-on:leave ="leave" v-on:after-leave ="afterLeave" v-on:leave-cancelled ="leaveCancelled" > </transition > <script > methods : { beforeEnter : function (el ) {}, enter : function (el, done ) { done () }, afterEnter : function (el ) {}, enterCancelled : function (el ) {}, beforeLeave : function (el ) {}, leave : function (el, done ) { done () }, afterLeave : function (el ) {}, leaveCancelled : function (el ) {} } </script >
显式设置过渡持续时间
可以使用<transition>
上的duration属性
设置一个以毫秒为单位的显式过渡持续时间。
1 2 3 4 <transition :duration ="1000" > Hank </transition > <transition :duration ="{ enter: 500, leave: 800 }" > Hank </transition >
组件首次渲染时的过渡
通过<transition>
上的appear属性
设置组件节点首次被渲染时的过渡动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <transition appear appear-class ="custom-appear-class" appear-to-class ="custom-appear-to-class" appear-active-class ="custom-appear-active-class" > </transition > <transition appear v-on:before-appear ="customBeforeAppearHook" v-on:appear ="customAppearHook" v-on:after-appear ="customAfterAppearHook" v-on:appear-cancelled ="customAppearCancelledHook" > </transition >
HTML 元素的过渡效果
Vue 组件的 key 属性
key 属性主要用在 Vue 虚拟 DOM 算法中去区分新旧
VNodes,不显式使用key
的时候,Vue
会使用性能最优的自动比较算法。显式的使用key
,则会基于key
的变化重新排列元素顺序,并移除不存在key
的元素。具有相同父元素的子元素必须有独特的key
,因为重复的key
会造成渲染错误。
1 2 3 4 <ul > <li v-for ="item in items" :key ="item.id" > ...</li > </ul >
元素的的交替过渡
可以通过 Vue
提供的v-if
和v-else
属性来实现多组件的交替过渡,最常见的过渡效果是一个列表以及描述列表为空时的消息。
1 2 3 4 5 6 <transition > <table v-if ="items.length > 0" > </table > <p v-else > Sorry, no items found.</p > </transition >
Vue
中具有相同名称的元素切换时,需要通过关键字key
作为标记进行区分,否则
Vue
出于效率的考虑只会替换相同标签内的内容,因此为<transition>
组件中的同名元素设置key
是一个最佳实践 。
1 2 3 4 <transition > <button v-if ="isEditing" key ="save" > Save</button > <button v-else key ="edit" > Edit</button > </transition >
一些场景中,可以通过给相同 HTML
元素的key
属性设置不同的状态来代替冗长的v-if
和v-else
。
1 2 3 4 5 6 7 8 9 10 <transition > <button v-if ="isEditing" key ="save" > Save</button > <button v-else key ="edit" > Edit</button > </transition > <transition > <button v-bind:key ="isEditing" > {{ isEditing ? "Save" : "Edit" }}</button > </transition >
而对于使用了多个v-if
的多元素过渡,也可以通过动态的key
属性进行大幅度的简化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <transition > <button v-if ="docState === " saved "" key ="saved" > Edit </button > <button v-if ="docState === " edited "" key ="edited" > Save </button > <button v-if ="docState === " editing "" key ="editing" > Cancel </button > </transition > <transition > <button v-bind:key ="docState" > {{ buttonMessage }} </button > </transition > <script > ... computed : { buttonMessage : function ( ) { switch (this .docState ) { case "saved" : return "Edit" case "edited" : return "Save" case "editing" : return "Cancel" } } } </script >
Vue 组件的过渡效果
多个 Vue
组件之间的过渡不需要使用key
属性,只需要使用动态组件 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <transition name ="component-fade" mode ="out-in" > <component v-bind:is ="view" > </component > </transition > <script > new Vue({ el: "#transition-components-demo", data: { view: "v-a" }, components: { "v-a": { template: "<div > Component A</div > " }, "v-b": { template: "<div > Component B</div > " } } }) <script > <style > .component-fade-enter-active , .component-fade-leave-active { transition : opacity .3s ease; } .component-fade-enter , .component-fade-leave-to { opacity : 0 ; } <style>
选择 HTML 元素或 Vue
组件的过渡模式
<transition>
的默认进入(enter )和离开(leave )行为同时发生,所以当多个需要切换显示的
HTML 元素或 Vue
组件处于相同位置的时候,这种同时生效的进入和离开过渡不能满足所有需求,Vue
可以通过<transition-gruop>
组件的mode
属性来选择如下过渡模式。
in-out
:新元素先进行过渡,完成之后当前显示的元素再过渡离开。
out-in
:当前显示的元素先进行过渡,完成之后新元素再过渡进入。
1 2 3 4 5 <transition name ="fade" mode ="out-in" > <button v-if ="docState === " saved "" key ="saved" > Edit </button > <button v-if ="docState === " edited "" key ="edited" > Save </button > <button v-if ="docState === " editing "" key ="editing" > Cancel </button > </transition >
transition-group 组件
<transition-group>
用来设置多个 HTML 元素或 Vue
组件的过渡效果,不同于<transition>
,该组件默认会被渲染为一个真实的<span>
元素,但是开发人员也可以通过<transition-group>
组件的tag
属性更换为其它合法的
HTML
元素。<transition-group>
组件内部的元素必须要提供唯一的key
属性值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <div id ="list-demo" class ="demo" > <button v-on:click ="add" > Add</button > <button v-on:click ="remove" > Remove</button > <transition-group name ="list" tag ="p" > <span v-for ="item in items" v-bind:key ="item" class ="list-item" > {{ item }} </span > </transition-group > </div > <script > new Vue ({ el : "#list-demo" , data : { items : [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ], nextNum : 10 }, methods : { randomIndex : function ( ) { return Math .floor (Math .random () * this .items .length ); }, add : function ( ) { this .items .splice (this .randomIndex (), 0 , this .nextNum ++); }, remove : function ( ) { this .items .splice (this .randomIndex (), 1 ); } } }); </script > <style > .list-item { display : inline-block; margin-right : 10px ; } .list-enter-active , .list-leave-active { transition : all 1s ; } .list-enter , .list-leave-to { opacity : 0 ; transform : translateY (30px ); } </style >
<transition-group>
实现的列表过渡效果在添加、移除某个
HTML 元素时,相临的其它 HTML
元素会瞬间移动至新位置,这个过程并非平滑的过渡。为解决这个问题,<transition-group>
提供v-move 特性去覆盖移动过渡期间 所使用的
CSS
类名。开启该特性,即可以通过name
属性手动设置(下面例子中的name="flip-list"
与.flip-list-move
),也可以直接使用move-class
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <div id ="flip-list-demo" class ="demo" > <button v-on:click ="shuffle" > Shuffle</button > <transition-group name ="flip-list" tag ="ul" > <li v-for ="item in items" v-bind:key ="item" > {{ item }} </li > </transition-group > </div > <script > new Vue ({ el : "#flip-list-demo" , data : { items : [1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ] }, methods : { shuffle : function ( ) { this .items = _.shuffle (this .items ) } } }) </script > <style > .flip-list-move { transition : transform 1s ; } <style>
可以通过响应式 的绑定<transition>
和<transition-gruop>
上的
name 属性,从而能够根据组件自身的状态实现具有动态性的过渡效果。
1 <transition v-bind:name ="transitionName" > </transition >