记录Vue学习和使用踩过的坑和收获的心得。
首发于2018年3月21日。
最后更新于2019年6月18日。

Vue2.6特性

v-slot新插槽语法,见下文插槽部分

[attr]="prop"动态指令参数:
例如@[eventName]="handleFunc",当eventNamefoucus时,它便被视为@foucus="handleFunc"
动态指令参数不能使用表达式。

除了v-bind可以用:来简写、v-on可以用@来简写外,Vue2.6还增加了v-slot可以用#来简写。

Vue相关Url

官网
Vue-cli
Vue-loader (Webpack插件,.vue单文件组件)
Vue-router (SPA)
Vuex
Vue-ssr
Vue-UnitTest

Vue源代码拆解
Nuxt.js框架
Weex框架

Vue-router批量import引入
Vue-cli目录介绍 (不适用Vue-cli 3)
Vue-cli的webpack配置
Vue.config.js配置 (适用Vue-cli 3)
开发、生产环境下配置不同的API
百度地图API
从零开始自行配置一个Vue-cli

数据响应式处理

只有一开始就定义在data中的属性才会被Vue跟踪检测,并对属性的变化做出响应。之后再在data中定义属性无法被跟踪;
同时,对数组成员的变更、对数组长度的修改无法被跟踪

例如,假设组件的数据最初是:

1
2
3
data(){
return {a:"XX" ,b:[1,2,3]}
}

此时,如果以this.a = "YY"这种方式将"XX"改为"YY",这个更改是可以被Vue检测到的,页面上的内容也会跟随改变;
但是如果定义一个this.c = "ZZ",那么新定义c属性这一次操作是不会被Vue检测到的;
Vue同样无法检测到数组内容的变化,例如this.b[0] = 8888这一步操作也不会被Vue检测到。

Vue给我们提供了解决方法
在对象上定义新字段用Vue.set(target, propName, propValue)或是this.$set(target, propName, propValue)
修改数组成员用Vue.set(target, index, value)或是.$set( )
也可以用arr.splice(index, 1, newValue)这种方式,Vue对修改数组的.push( ).pop( )这些方法均做了封装,使用这些方法修改数组,是可以被Vue跟踪响应的。

Vue在HTML模板中使用的变量的作用域为你在Vue中定义的数据,包括但不限于datacomputedmethods等,除此之外,还包括了少许API例如consoleDate等。
模板中的代码不能使用上面所说的这些作用域之外的变量和API,如果想要使用,在data里面再定义一遍即可。

计算属性的值会根据其依赖项进行缓存,Vue会自动分析它引用到的其他依赖值,在这些值被更改后触发计算属性的值的更新;
且它本身即具备getter的语义,还可以定义setter。
但是无法像方法一样传入参数来获取结果;这些是定义计算属性与定义方法在使用上的区别。

在模板HTML上的属性,一般默认是静态绑定,使用:attr或者v-bind:attr的是动态绑定;
需要注意的是v-开头的属性都是隐含的动态绑定,例如v-if,有些属性虽然没有v-开头但是也是隐含动态绑定,例如is

渲染逻辑

v-ifv-for可以用<template>来对多个节点使用,但是v-show不支持;
v-ifv-show都可以控制元素显示与否,后者始终会被渲染,只是通过CSS的display控制是否显示;

关于Vue.nextTick()的用法:
v-ifv-for等会导致DOM发生更新。Vue的DOM更新是异步的,因此可能会导致获取DOM和组件内容时发生失败。
此时可以使用Vue.nextTick( )或是this.$nextTick( ),它可以令代码在DOM更新后再执行。
可以直接将回调传入作为参数,也可以不传参数而是将其返回值作为一个Promise添加.then( )
.nextTick( )可以在mounted周期配合ref来实现触发元素的focus获取焦点功能。

v-cloak会令标签在渲染时有v-cloak属性,并且在渲染结束后将移除该属性。因此可以在CSS中添加:

1
2
3
[v-cloak] {
display:none;
}

这样在渲染过程中就不会暴露出{{ }}模板字符串了。

v-once求值一次后不再响应式;
keep-alive令组件保持内部状态,即使Vue未渲染它,注意这要求组件必须有name属性;
v-html="raw"可以将raw变量的内容渲染为原始HTML内容,谨防XSS!

事件处理

例如使用Element时,给组件绑定click、focus等事件的处理器其实都是Element组件传出来的,它并不是原生的浏览器focus事件。
这里用Element源码中的<el-input>组件举例,从Github源码中可以看到focus事件实际上是由组件通过this.$emit('focus',event)来传出的:

如果想监听原生的focus事件,就需要使用v-on:focus.native="handle"这种方式,添加.native修饰符。

组件中使用this.$emit('name', ...args)可以在组件上触发事件,args这些参数将传给事件处理器;
而在使用v-on绑定的事件处理器时,可以用$event表示事件对象,例如@click="handleClick(item.id, $event)",这时传入handleClick( )方法的第二个参数便是事件对象;

事件修饰符:
.stop(阻止事件传播,e.stopPropagation())
.prevent(阻止事件默认行为,e.preventDefault())
.capture(在捕获阶段处理事件,而不是默认的冒泡阶段)
.self(只在事件是由本身触发时才处理,if(e.target === e.currentTarget))
.passive(滚动事件的默认行为立即触发)
.once(触发一次后即被移除)。

数据双向绑定:
v-model会导致初始绑定的值(例如给输入框绑定:value=xxx)失效;
checkbox可以使用true-valuefalse-value来绑定选中与否的值;
注意它的select会在iOS上出问题,所以一般设置第一个选择为disabled,并且该选项为空;

v-model的实现方式:
提供一个名为valueprops成员(你也可以将其自身带有的value绑定到该属性上):props: ['value']
通过调用input事件:this.$emit('input', value) 来将要传出的value值传出。

如果v-model不使用input事件名,或者是不使用value属性名,你可以在组件中定义如下属性:

1
2
3
4
model: {
prop: 'checked',
event: 'change'
}

这样便将input事件名改成了change,而将value属性名改成了checked,复选框通常会使用这种形式。

Vue对单选框的绑定方式:
单选框的形式一般是:<input type="radio" name="groupA" value="1001">
通常,单选框按照name的值形成互斥分组,即name属性相同的单选框是互相联动的,每次只能选择一个。

而Vue绑定单选框的时候是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="main">
<input type="radio" value="1001" v-model="radioValue">
<input type="radio" value="1002" v-model="radioValue">
<input type="radio" value="1003" v-model="radioValue">
</div>
<script>
new Vue({
el: '#main',
data() {
return { radioValue: undefined }
}
})
</script>

上面的3个单选框并没有name属性,但是却能形成互斥的联动;radioValue的值会绑定到当前选中的单选框的value值;
对此,Vue的实现方式如下:
首先,从HTML元素的角度来讲,每个单选框都有一个checked属性表示它当前是否被选中,这个值是可以被赋值操作所修改的;
Vue在绑定单选框元素时,会依据当前单选框的value是否等于v-model绑定的值来赋给checked属性,这样当v-model绑定的值等于某一个单选框的value时,其他绑定了该v-model的单选框的选中状态会自动取消,实现了联动。

事件修饰符:
.lazy(将input触发事件转化为change触发事件)
.number(将输入值转化为数值)
.trim(去除首尾空格)。

如果组件比较繁琐,可以使用Vuex来实现事件,或者使用事件总线;
以下是一个事件总线的用法,类似于发布/订阅模式:

1
2
3
let bus = new Vue();
bus.$on('event1', function(...args) { /*...*/});
bus.$emit('event1', ...args);

生命周期

Vue组件生命周期钩子:
初始化事件和生命周期:beforeCreate
初始化参数和响应式:created
编译挂载到DOM前:beforeMount
编译挂载完成后:mounted
(如果有依赖数据发送变更会触发更新:beforeUpdate、updated);
组件将被销毁:beforeDestory
组件销毁完成:destoryed

注意这里的钩子都可以写成async方法,并在里面使用await,如此做则Vue会等到该方法执行完毕再进行下一步,在诸如HttpAPI获取数据等场合就会方便很多。

Vue-router的导航守卫):
全局前置:router.beforeEach((to, from, next) => {});(也可用于单个路由项)
全局解析:router.beforeResolve((to, from) => {});(进入组件并解析完成后调用)
全局后置:router.afterEach((to, from) => {})

进入组件:beforeRouteEnter(to, from, next) => {};(无法获取this实例)
组件复用:beforeRouteUpdate(to, from, next) => {};(在/user/1/user/2这种复用组件情况下,很重要,下面会讲)
离开组件:beforeRouteUpdate(to, from, next) => {}

钩子调用顺序大致是全局>路由项>组件。
这里的next()无参调用表示继续路由,带有一个路径则表示跳转到该路由,传入false则取消路由。

有一种情况需要使用组件复用:
例如用Vue做一个论坛,每个帖子的url格式为:/post/:pid,这种情况下如果从一个帖子跳转到另一个帖子,例如从/post/000001进入到/post/000002,此时是组件复用,页面视图的Vue组件根本没有销毁和重建,因此页面上的这些组件的生命周期钩子都不会触发。
此时如果你有类似如下的代码:

1
2
3
async beforeMount() {
await this.init();
}

这种在组件生命周期内初始化页面的代码均不会生效,如果从一个帖子进入另一个帖子页,除了url变了,内容还是原来的内容。
想要避免这种错误,就必须再添加一个导航钩子:

1
2
3
4
async beforeRouteUpdate(to, from, next) {
next();
await this.init();
}

注意这里next()要写在init()前面,不然执行了init()也是对未跳转的页面执行,没有用的。

动画效果

Vue组件初次加载到框架也可以定义动画类,使用appear标签或是appear-classappear-leave等类名;
动画类可用@leave@enter-to等注册渲染动画触发事件,回调(el、callback)el是触发的节点;
给动画类设置mode属性,值可选in-out或是out-in控制先渲染enter还是先渲染leave。

组件的实现

获取组件的模板,按照以下顺序:
1、在.vue单文件组件中,如果有<template>标签内容,那么这里面的内容会被用做模板;
2、render( )渲染函数;
3、template模板,它的值可以是模板的HTML内容,也可以是一个以#开头的字符串,这种情况下将使用x-template。例如该属性设置为template: '#mycomponent'即可使用下面的模板:

1
2
3
<script type="x-template" id="mycomponent">
//...
</script>

也可以使用内联模板,例如:

1
<my-component inline-template> </my-component>

这种带有inline-template的HTML其内部的内容即为组件的模板,此时可以这样注册组件:

1
Vue.component('my-component', {/*...*/});

4、el挂载点,它的值可以是选择器字符串或者是一个Element,这种情况下Vue会忽略其中的内容将之替换为Vue组件内容,除非定义了插槽。
注意因为HTML要求必须有body元素,所以如果用<body>作为挂载点,它的内容被替换成你自己组件的HTML了,这是不符合规范的行为,也无法正常显示,因此el切勿设为body

注册组件的方式:
1、Vue.component('myComponent', { … }) 可以注册一个全局组件(注意驼峰式命名);
2、在单个Vue实例中定义components:[]属性可以注册局部组件;
以上两种方式,组件的定义选项也可以是一个回调:(resolve, reject) => {},这样即是异步组件。

如果使用Vue-router,可以使用.vue格式的单文件组件,需要在<script>export default出Vue实例。.vue单文件组件需要配合Vue-loader。
Vue-router的异步组件可以这样实现:

1
2
3
4
const routers=[{
path: '/index',
component: (res) => require(['./views/index.vue'], res)
}];

注意这里的require(arr, func(...))的用法是Wepback专属的,用于实现异步加载,参数arr有几个数组成员,后面的回调func就有几个参数,类似require.js的API。

把初始化组件的对象(Vue构造参数那个对象)作为他们的参数来调用即可创建组件。
异步组件的方法也可以返回一个对象,带有以下字段:
component(值为Promise类型,表示需加载的组件);
loading(表示加载中时渲染的组件);
error(表示加载出错时渲染的组件);
delay(表示展示加载组件的最小延迟时间);
timeout(超过超时时间后就会失败)。

基础组件可以全局化注册,例如使用require.context( )进行批量引入,全局化注册必须在main.js入口的new Vue( )实例创建之前进行。
如果使用Vue-router,需要对一个目录下所有.vue单文件组件做路由(试一试Nuxt.js?),可以这样实现

1
2
3
4
5
const viewContext = require.context('./views', true, /\.vue$/, 'lazy');
viewContext.keys.forEach(k=>{
//k表示文件名
//使用viewContext(k)即可访问该组件
})

注意require.context必须配合webpack使用,原生的Node.js是没有这个的,它的参数依次表示目录名、是否递归子目录、匹配文件名的正则,第四个是可选项,表示懒加载。

组件可以动态挂载模板,例如实例中的componentA变量:

1
<my-component :is="componentA"></my-component>

组件的数据

组件实例有props:[ ]属性,表示组件接受的外部传入的属性。例如定义了props:['title']的组件可以接受在模板上使用title="xxx"静态传入的属性值或:title="xxx"动态传入的属性值。
props是单向数据流,添加.sync修饰符可以将之变为双向绑定。

组件会继承标签属性上的class、style等,可以添加 inheritAttrs: false 来阻止

Vue2.6版已经废弃slot、slot-scope,取而代之的是v-slot插槽属性,查看详情查看旧版
定义命名插槽:<slot name="slotName">标签;
定义默认插槽:<slot>标签,默认插槽隐含了default插槽名。
使用命名插槽:<template v-slot:slotName>标签内的内容会放入名为slotName的插槽内;
使用默认插槽:所有未被<template v-slot:xxx>标签包裹的HTML均放入默认插槽。

插槽中的作用域上下文和父级组件不在同一个,传递数据需要使用作用域插槽
定义作用域插槽:使用<slot :item="myProp">这种方式可以向插槽中注入参数;
使用作用域插槽:以<template v-solt:slotName="slotProps">这种形式的DOM元素来从slotProps字段上获取组件向插槽注入的参数。slotProps你可以任意命名,甚至可以用解构取值表达式。
如果组件只使用默认插槽且定义了默认内容,则可以这些内容可以直接访问插槽注入的值,也不需要给插槽命名。

(以上的<template v-slot:slotName>均可简写为<template #slotName>,因为v-slot可以用#来作为缩写)

this.$childrenthis.$parent分别表示组件的所有子组件和它的父组件;
也可以用在组件上添加属性ref="componentA",然后在父组件上即可通过this.$ref.componentA来访问。

类组件和JSX

如果组件的逻辑较为复杂,可以使用JSX和类组件的方式,以仿照React的方式来实现Vue组件。
Vue会使用.vue组件文件或是.jsx组件文件中export default导出的类,把它当做一个类组件。
在.vue单文件组件中,可以只写一个<script>并将类组件写在其中,更好的解决方式是使用.jsx文件,将类组件代码和JSX写在其中。

类组件的内容如同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default {
class: { /* 同:class */ },
style: { /* 同:style */ },
attrs: { /* 普通HTML属性 */ },
props: { },
data: { },
domProps: {/* HTML元素的属性,例如innerHTML */},
on: {
click() { }
},
nativeOn: { /* 原生事件监听器 */ },
directives: { /* 自定义指令 */ },
scopedSlots: { /* 作用域插槽 */ },
slot: 'name' /* 同#name,使用的插槽名 */,
key: 'key',
ref: 'ref',
render() { }
}

渲染的内容,是从render( )方法中返回JSX来实现的;具体使用方式可以参考这篇小教程