Vue现代化使用方法(二)

vue概述

在官方文档中,有一句话对Vue的定位说的很明确:
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统

Vue在我的理解下,其实很像mvvm架构中,对vm的实行。在mvvm架构中,viewModel是负责把view和model关联起来,把model的数据同步到view显示出来,把view的修改同步回model。在Vue的data属性中可设定相关变量,并把这些变量和Dom进行关联,达到修改data中属性即可修改Dom的功能 ( model(data) --> Vue --> view(dom) )。在Dom上,通过触发绑定方法,对相关数据进行修改 ( view(dom) --> Vue --> model(data) )。按这个思路来理解Vue,可以大概猜想出Vue有些功能为什么要被设计出来。

  • 插值:快速实现data和view的快速绑定,
  • 指令:对插值的补充,作用于模板上,用来实现模板的重复生成(v-for),动态显示(v-show, v-if),属性绑定(v-bind),事件绑定(v-on),对表单的增强(v-model)
  • 计算属性和观察属性:对插值和指令的补充,动态响应更新数据达到动态更新Dom的功效
  • 过滤:对插值和v-bind指令的补充,对要绑定到Dom上的数据进行过滤
  • 组件:Vue最强大的功能,对viewModel的实现,template就是view层,script就是model层

这些内容组成了Vue的骨架,掌握以上内容基本就能上手开发。下面我就利用上一章的例子把这些内容详细的介绍下(我按照我的理解打乱了官方的介绍顺序)

插值

<template>
    <div class="wrap">
        <p>{{info}}</p>
    </div>
</template>
<script>
    export default {
        data () {
            return {
                info: 'Hello world!'
            }
        }
    }
</script>

文本插值

处于data中返回值的info就是一个文本插值,这时如果修改info的值,对应DOM(p标签)的就会发生改变

上面data必须是函数的写法,是因为这是在一个组件内部,如果使用对象形式的写法,这个组件在经过多个实例化后,实际上是在共享一个data对象,更改任一组件data中的值,就会影响所有组件,所以组件内的data必须是函数,这样每个组件就形成独立的函数作用域,彼此不冲突。

属性插值

如果想把data中的值绑定成为html元素的属性,需要使用v-bind指令(简写方式是 : ),代码改造如下:

<p v-bind:title="info">{{info}}</p>

这个绑定值可以是普通的html属性,也可以是自定义属性

<p v-bind:data-cusData="info">{{info}}</p>

这时如果看最终渲染的页面,其效果是忽略cusData的大写,只显示为cusdata

<p data-cusdata="Hello vue!">Hello vue!</p>

如果这里是设置常用的class,id,title属性是没什么问题,但如果是自定义属性比如data-cusData这种类型的数据,就会被强制转为data-cusdata

如果要绑定多个属性值可以使用对象的形式

<p v-bind='{id: elemId, class: elemClass, "data-cusData": cusData}'>{{info}}</p>

Data中设置修改如下

data () {
    return {
        info: 'Hello vue!',
        elemId: 'pId',
        elemClass: 'pClass',
        cusData: 'hello world'
    }
}

因为class和style是我们常用的属性值,Vue针对这两个属性做了特殊处理

:class绑定对象语法

改造代码如下

<p class=‘static’ :class='{red: redFlag, font: fontFlag }'>{{info}}</p>

:class是v-bind:class的简写

对应data修改如下:

{
    ...
    redFlag: true,
    fontFlag: false
}

页面渲染结果如下:

<p class="static red">Hello vue!</p>

如例子显示,绑定对象的形式可以动态切换多个class,同时也支持与普通class共存

当绑定对象要进行多个class值的设定,沿用上面的写法会显得繁琐,我们可以把绑定的对象换成一个计算属性,在计算属性中动态的设定返回值。

<p class='static' :class='classObj'>{{info}}</p>

在计算属性中设定相关值

computed: {
    classObj () {
        let classStr = '';
        if (this.redFlag) {
            classStr += 'red';
        }
        if (this.fontFlag) {
            classStr += 'font';
        }
        return classStr;
    }
}

渲染结果如上,除了上面的写法,也可以结合下面介绍的绑定数组语法,把上面的例子再简化些

<p :class='[classObj, "static"]'>{{info}}</p>

:class绑定数组语法

代码如下:

<p :class="[basicClass, redClass]">{{info}}</p>

data下数据修改如下:

{
    ...
    basicClass: 'f20 mb20',
    redClass: 'red'
}

这样最终渲染如下:

<p class="f20 mb20 red">Hello vue!</p>

在数组语法中,可以使用三元运算符控制某个元素class的添加和隐藏
代码改造如下:

<p :class="[showInfo ? basicClass : '', redClass]">{{info}}</p>

data数据修改如下:

{
    ...
    showInfo: false,
    ...
}

这时渲染的最早结果如下:

<p class="red">Hello vue!</p>

如果把showInfo设为true,渲染结果和上例一致

官方文档这里还介绍了对组件的class绑定处理,不过我觉得这样做其实会破坏组件的封装型,就算要添加或者移除某个class,最好还是通过props传值在组件内部自行处理

:style绑定对象语法

:style的绑定对象语法和:class绑定对象语法使用方法一样,只是在写css属性名要注意下

<p :style='{color: redColor, "font-size": fontSize, backgroundColor: bgColor }'>{{info}}</p>

data数据修改如下:

{
    ...
    redColor: 'red',
    fontSize: '16px',
    bgColor: '#FF0'
    ...
}

如例css的属性名,可以改写成驼峰形式(backgroundColor),或者使用引号包裹起来("font-size")

当使用对象语法进行多个css属性设置时,可以使用计算属性进行绑定

<p :style='styleObj'>{{info}}</p>

计算属性设置如下

computed: {
    styleObj () {
        return `color: ${this.redColor}; fontSize: ${this.fontSize}; backgroundColor: ${this.bgColor}`; 
    }
}

渲染结果如下:

<p style="color: red; font-size: 16px; background-color: rgb(255, 255, 0);">Hello vue!</p>

上面设定样式的方式如果只是绑定单个对象是没问题的,但如果要使用数组的形式绑定多个对象就无法生效(解决方法参看下面)。

:style绑定数组语法

使用:class绑定数组使用方法见下例:

<p :style='[colorStyle, fontStyle, backgroundColor]'>{{info}}</p>

这个时候要保证对应数组对象是对象的形式(上例是字符串形式):

这个例子中绑定对象的值分别在data和计算属性中设定,只是做演示,表示这个绑定值,可以是不同的来源值进行混合

在data中设定colorStyle

{
...
    colorStyle: {
        color: 'red'
    }
...
}

在计算属性中设定fontStyle和backgroundColor

computed: {
    fontStyle () {
       return {fontSize: this.fontSize}
    },
    backgroundColor () {
        return {backgroundColor: this.bgColor}
    }
}

这时的渲染结果和上例是一致的,但如果你把其中一个值,改成字符串形式

fontStyle () {
   return `fontSize: ${this.fontSize};`
}

这时你会发现Vue会忽略当前值的设定,所以在使用:style形式时,最好按对象形式返还设定的style。

指令

我对指令的理解就是:指令是使模板具备了逻辑处理的能力,是对插值的一种补充,因为指令的存在才使得数据层和Dom层具备了相互绑定的能力。

按官方api,vue的指令如下:

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

v-text和v-bind

v-text实现文本绑定的能力,期望值是string

<p v-text="info"></p>
<p>{{info}}</p>

上面两种写法渲染后的结果一致,通常我们会使用第二种写法,比较简洁。这个指令期望的值是string,但如果绑定的值是一个对象,就会原样把对象输出。

<p>{{infoObj}}</p>

在data中设定一个infoObj

{
...
    infoObj: {
        msg: 'Hello vue!'
    }
...
}

这时页面渲染为

<p>{
  "msg": "Hello vue!"
}</p>

如果你进行下面的设定

infoObj: true,              // => true
或
infoObj: 3 > 2,             // => true
或
infoObj: Math.random(),     // => 渲染为一个不确定的随机数
或
infoObj: 2 + 3,             // => 5

所以这个指令会对传入的值进行一个转换,转换成string。事例中该指令的绑定值,我只使用了data下的值,但实际上这个值还可以是计算属性。

computed: {
    infoObj () {
        return 'Hello vue by computed!'
    }
}

对页面进行渲染时,页面的展示数据分为静态数据和动态数据,一般情况下静态数据存放在data属性下,动态数据通过计算属性进行返回(上例只是个样式,正常情况下计算属性会包含相关逻辑处理,相关会在计算属性那部分讲解,这里就不展开了)

v-bind用来动态的绑定一个或者多个属性

在属性插值部分已经对v-bind指令做了比较详细的介绍,有一些特殊点要特别说下:

绑定值可以是一个表达式

<p v-bind:title="info + ' 这时行内添加的信息'">{{info}}</p>

这时页面渲染结果为

<p title="Hello vue! 这时行内添加的信息">Hello vue!</p>

这是一个比较有用的特性,在开发过程中我们很容易碰到根据数据的不同展示不同的样式(比如符合某个条件时文案颜色变红)

<p :class='[this.login ? "red" : "", "static", elemClass]'>{{info}}</p>

在data中我们假定一个login的字段

{
...
    login: true
...
}

这时页面就会渲染为

<p class="red static pClass">Hello vue!</p>

如例中假设,比如我现在有个接口返回用户是否登陆,拿到接口返回数据,我们就可以根据接口返回的相关字段,动态的设定某一处的class。

通过修饰符.prop绑定DOM属性

如果在绑定属性后使用.prop修饰符,会忽略其他对该值的设定,强制使用设定的属性值

<p class="testProp" v-bind:text-content.prop="info" v-bind:className.prop="elemId">测试数据</p>

这时页面会渲染为

<p class="pId">Hello vue!</p>

如果删掉一个prop值

<p class="testProp" v-bind:text-content="info" v-bind:className.prop="elemId">测试数据</p>

对应的绑定值就会变成一个普通的属性值

<p class="pId" text-content="Hello vue!">测试数据</p>

v-html

该指令是用来直接输出HTML内容

<div v-html="html"></div>

在data中设定如下值

{
...
    html: '<p>Hello vue!</p>',
...
}

这时页面的渲染结果为

<div><p>Hello vue!</p></div>

如果使用v-text会原样把字符串内容输出

<div v-text="html"></div>

渲染为

<div>&lt;p&gt;Hello vue!&lt;/p&gt;</div>

在官方文档中强调这种写法很容易引起XSS攻击,不能把这个能力开放给普通用户,在实际开发中,我也没碰到什么地方需要使用该指令。。。

v-show

根据表达式之真假值(这个值可以是data中的值,也可以是计算属性,后续不再赘述),切换元素的 display CSS 属性。

<p v-show="showFlag">{{info}}</p>

在data中设定showFlag的值

{
...
    showFlag: true,
...
}

这时页面渲染为

<p>Hello vue!</p>

如果showFlag设为false,页面渲染为

<p style="display: none;">Hello vue!</p>

v-show指令使用时有如下内容需要注意:

  • v-show不支持<template>元素,因为template标签的元素的内容不会被渲染,所以对该标签使用v-show去改变其的display属性是没有意义的。
  • v-show不支持和v-else一起使用,v-show只管当前元素的显示与否

v-if

该指令会根据表达式的值动态渲染元素。元素及其包含的指令/组件在切换期间被销毁并重新构建。如果是作用在<template>元素上,则其内部元素内容会根据表达式的值动态建立或者销毁。(这点是与v-show的不同点,v-show只是改变display的值,v-if则会进行动态的建立或者销毁)

<template v-if="showFlag"><p>{{info}}</p></template>

渲染为

<p>Hello vue!</p>

v-else和v-else-if

对v-if的补充,必须紧跟着v-if之后,否则无法生效

<p v-if="type === 'A'">A</p>
<p v-else-if="type === 'B'">B</p>
<p v-else-if="type === 'C'">C</p>
<p v-else>D</p>

在data中设定type一个随意的值

{
...
    type: 'E',
...
}

这时页面渲染为

<p>D</p>

由于v-if会对元素进行重建或者销毁,而Vue在渲染时会尽可能复用已有元素,针对普通元素这个指令使用起来是没问题的,但针对一些表单元素就有问题了。

<template v-if="type === 'A'"><input placeholder="A"/></template>
<template v-else-if="type === 'B'"><input placeholder="B"/></template>
<template v-else-if="type === 'C'"><input placeholder="C"/></template>
<template v-else><input placeholder="D"/></template>
<button @click="changeType">切换Type的值</button>

@click是v-on:click的简写(这个是用来绑定事件),在methods中添加相关方法

methods: {
    changeType () {
        let [random, type] = [Math.random(), ''];
        if (random > 0.8) {
            type = 'A';
        } else if (random > 0.6) {
            type = 'B';
        } else if (random > 0.2) {
            type = 'C';
        } else {
            type = 'D';
        }
        this.type = type;
    }
},

页面渲染为

<input placeholder="D">
<button>切换Type的值</button>

点击button,会发现input元素的内容会发生改变,但如果你向input输入一个值,这时你再点击button,虽然input元素会变,但是已经输入的内容却不会改变,类似的还有textarea标签。
如果认为这是一个问题,可以使用Vue提供的防重的方式,使用key添加一个唯一的关键值。做以下改造

<template v-if="type === 'A'"><input key="A" placeholder="A"/></template>
<template v-else-if="type === 'B'"><input key="B" placeholder="B"/></template>
<template v-else-if="type === 'C'"><input key="C" placeholder="C"/></template>
<template v-else><input key="D" placeholder="D"/></template>
<button @click="changeType">切换Type的值</button>

这时如果输入内容,再点击切换input就会完全重新渲染,不过之前输入的内容也会被清除

v-for

v-for指令根据一组数组的选项列表进行渲染,通常我们只会在需要展示列表的部分使用该指令。

数据源是一个对象组成的数组

<ul>
    <li v-for="item in students">姓名:{{item.name}}--年龄:{{item.age}}</li>
</ul>

在data下设定students的值

{
...
    students:[
        {name:'Tom', age:24},
        {name:'Jim', age:22},
        {name:'Kate', age:21}
    ],
...
}

渲染结果为

<ul>
    <li>姓名:Tom--年龄:24</li>
    <li>姓名:Jim--年龄:22</li>
    <li>姓名:Kate--年龄:21</li>
</ul>

从这个例子,我们可以看出该指令是在当前元素进行循环渲染,根据条件展示数据,该指令还可以接受一个参数表示数组的排序(从0开始)

<ul>
    <li v-for="(item, $index) in students">{{$index}}.姓名:{{item.name}}--年龄:{{item.age}}</li>
</ul>

渲染为

<ul>
    <li>0.姓名:Tom--年龄:24</li>
    <li>1.姓名:Jim--年龄:22</li>
    <li>2.姓名:Kate--年龄:21</li>
</ul>

数据源是一个对象

<ul>
    <li v-for="value in tomInfo">{{value}}</li>
</ul>

在data中添加相关数据

{
...
    tomInfo:{
        age: 24,
        gender: 'man',
        address: '北二环前门里'
    },
...
}

渲染为

<ul>
    <li>24</li>
    <li>man</li>
    <li>北二环前门里</li>
</ul>

此时如果指令接受的第二个参数表示的是对象key值

<ul>
    <li v-for="(value, key) in tomInfo">{{key}} : {{value}}</li>
</ul>

渲染为

<ul>
    <li>age : 24</li>
    <li>gender : man</li>
    <li>address : 北二环前门里</li>
</ul>

在数据源是对象的情况下,该指令还可以接受第三个参数,表示序号

<ul>
    <li v-for="(value, key, $index) in tomInfo">{{$index}}. {{key}} : {{value}}</li>
</ul>

渲染为

<ul>
    <li>0. age : 24</li>
    <li>1. gender : man</li>
    <li>2. address : 北二环前门里</li>
</ul>

上面是我们假定的一个例子,通常我们是从后端拿到数据然后再在前端展示,而后端给到前端的数据通常是按照JSON格式给出,key值都是英文,这时前端就要对key值进行一个转为中文的操作,我们现在要对已经处理过的数据在进行简单处理(逻辑并不复杂),这时我们就需要使用过滤器(本部分只简单介绍)

在filters(这是组件内的写法)下添加相关过滤器

filters: {
    keyCheck (val) {
        let cnVal = '';
        if (val === 'age') {
            cnVal = '年龄';
        } else if (val === 'gender') {
            cnVal = '性别';
        } else if (val === 'address') {
            cnVal = '家庭住址';
        }
        return cnVal;
    }
}

页面内容进行如下改造

<ul>
    <li v-for="(value, key, $index) in tomInfo">{{$index}}. {{key | keyCheck}} : {{value}}</li>
</ul>

注意{{key | keyCheck}}就是过滤器的使用方法,页面渲染为

<ul>
    <li>0. 年龄 : 24</li>
    <li>1. 性别 : man</li>
    <li>2. 家庭住址 : 北二环前门里</li>
</ul>

关于v-for使用:key提升性能的解释

在Vue的官方文档中介绍v-for指令时,提到使用key值提升渲染性能,具体原因官方没有详细介绍,只说Vue在进行渲染时进行的是“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

参考如下实例,有助于理解这段话:

<div v-for="(item, $index) in listInfo" :title="item.id">
    <input :placeholder="item.value"/>
</div>
<button @click="addList">添加新的列表内容</button>

在data和methods下添加如下内容

{
...
listInfo: [
    {id: 'listA', value: 'A'},
    {id: 'listB', value: 'B'},
    {id: 'listC', value: 'C'},
    {id: 'listD', value: 'D'}
]
...
}
methods: {
    addList () {
        this.listInfo.splice(1, 0, {id: 'listE', value: 'E'})
    }
},

这时页面渲染为

<div title="listA"><input placeholder="A"></div>
<div title="listB"><input placeholder="B"></div>
<div title="listC"><input placeholder="C"></div>
<div title="listD"><input placeholder="D"></div> 
<button>添加新的列表内容</button>

如果在几个输入框中输入随意文字,然后再点击按钮,这时我们发现渲染结果为

<div title="listA"><input placeholder="A"></div>
<div title="listE"><input placeholder="E"></div>
<div title="listB"><input placeholder="B"></div>
<div title="listC"><input placeholder="C"></div>
<div title="listD"><input placeholder="D"></div> 
<button>添加新的列表内容</button>

渲染的信息如我们所预期的一致,但是我们刚刚输入的文字输入框却没有按预期的下移,比较明显的被直接复用,这也就是官方文档中提到的如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,不过在这个例子中这种Vue的默认处理方式是不符合我们的预期,所以这时要使用key值来标记每个节点,方便Vue对数据进行重新排序

<div v-for="(item, $index) in listInfo" :title="item.id" :key="item.id">
    <input :placeholder="item.value"/>
</div>
<button @click="addList">添加新的列表内容</button>

渲染结果同上没有变化,但是如果你这时再输入框输入文字,然后再点击按钮时,Dom就会按预期的排序

在这里会有个容易忽略的问题,如果你把key的绑定值换成数组序号的$index值

:key="$index"

Vue会忽略这个key的设定,还是按照未设定key时的方式进行渲染,原因(以下是个人理解,可能根本原因并不是这样)是此时key值绑定的是数组的序号值(数字是不可变值),虽然可以通过addList方法触发listInfo值的更新,但是无法触发key绑定值的更新,Vue会把新加的数据当作普通数据,采取"就地复用"的策略(也就是官方文档中提到的确保它在特定索引下显示已被渲染过的每个元素),直接更新Dom而不对Dom进行重新排序,所以我们做如下修改,来验证我们的想法

<div v-for="(item, $index) in listInfo" :title="list[$index]" :key="list[$index]">
    <input :placeholder="item.value"/>
</div>
<button @click="addList">添加新的列表内容</button>

在data中设定list值

{
...
    list: [1, 2, 3, 4],
...
}

同时在修改addList方法,触发list数组的更新

addList () {
    this.listInfo.splice(1, 0, {id: 5, value: 'E'});
    this.list.splice(1, 0, 5);
}

这时如果进行相关操作,就会发现和上面方法(:key="item.id")呈现一致,在上例中key的绑定值是一个数组对象,同时在addList方法中更新数组数据(如果不进行更新,而是在list中提前定义好,一样无法触发Dom的重新排序)

触发v-for的数据更新
v-for的数据源如果是数组时,我们可以使用触发数组更新的方法,来触发v-for的重新渲染

push()
pop()
shift()
unshift()
splice()
sort()
reverse()
copyWithin()
fill()

以上的方法都会触发原始数组的更新,数组更新自然就会触发v-for的重新渲染,数组的其他方法比如filter,concat,slice,every,map,find,findIndex等方法都不回直接修改原始数组,数组不更新,v-for也就不会重新渲染,如果需要对数组使用到非更新方法,可以把处理后的数组直接赋值给原始数组

其他特殊情况
Vue没有实现对以下内容变化的检测(按官方说法是受javascript限制,后续单独讲解):

检测数组的变化

  • 利用索引直接设置数组值:arr[0] = newValue;
  • 修改数组长度:arr.length = newLength;

用下面例子演示解决方法:

<div v-for="(item, $index) in listInfo" :title="item.id" :key="item.id">
    <input :placeholder="item.value"/>
</div>
<button @click="editList">修改list内容</button>

在methods添加editList方法:

methods: {
    editList () {
        // this.listInfo[0] = {id:1, value:'AA'};
        this.$set(this.listInfo, 0, {id:1, value:'AA'});
    }
}

this.$set是在methods方法内的使用方式,如果你不在其中使用可以使用Vue.set的形式(保证Vue的存在)

你可以尝试使用直接设置数组值(注释部分的代码),这时会发现页面并不会改变,使用Vue.set会让Vue检测到数组的变化,还可以使用数组的splice方法让数组更新

this.listInfo.splice(0, 1, {id:1, value:'AA'});

如果这时你使用

this.listInfo.length = 2;

设置数组长度,视图不会更新,解决方法一样可以调用splice方法

this.listInfo.splice(2);

检测对象的变化

  • Vue不能检测对象属性的添加或删除

事例如下:

<ul>
    <li v-for="(value, key, $index) in tomInfo">{{$index}}. {{key | keyCheck}} : {{value}}</li>
</ul>
<button @click="addSchool">添加学校信息</button>

在methods方法中添加addSchool方法

methods: {
    addSchool () {
        this.tomInfo.school = '深圳大学';
        console.log(this.tomInfo);
    }
}

点击页面按钮,我们在控制台可以看到属性是添加到指定数据上,但页面视图却没有重新渲染,解决方法同样使用Vue.set方法

addSchool () {
    this.$set(this.tomInfo, 'school', '深圳大学');
    console.log(this.tomInfo);
}

这时视图如期望变化,与数组的splice方法类似,针对对象也可以使用Object.assign方法把更新后的对象重新赋值

addSchool () {
    this.tomInfo = Object.assign({}, this.tomInfo, {school: '深圳大学'});
    console.log(this.tomInfo);
}

使用Object.assign的好处就是可以同时添加多个属性值

this.tomInfo = Object.assign({}, this.tomInfo, {school: '深圳大学', class: '软件工程'});

一个需要特别注意的地方:v-for的优先级比v-if的优先级高

<li v-for="(value, key, $index) in tomInfo" v-if="$index > 1">{{$index}}. {{key | keyCheck}} : {{value}}</li>

因为v-for优先级高于v-if,所以这个会先进行循环,然后再判断是否符合条件进行展示

v-on

该指令是要来监听DOM事件,并在触发时运行指定代码(使用方法是v-on:事件名=“事件”)

<p @click="editInfo">{{info}}</p>

相关事件是放到methods下,@click是v-on:click的简写方式

methods: {
    editInfo () {
        this.info = 'info值被修改';
    }
}

这时点击页面就会发现页面数据发生改变,事件可以接收参数来获取原生DOM事件信息

editInfo (event) {
    this.info = 'info值被修改';
    console.log(event);
}

进行点击,在控制台会输出原生DOM事件信息

<p @click="editInfo('修改info值')">{{info}}</p>

绑定事件时可以接收参数传值

editInfo (msg) {
    this.info = msg;
}

这时如果还要输出DOM事件信息,就要在传参数时传一个特殊参数$event

<p @click="editInfo('修改info值', $event)">{{info}}</p>

同一个元素可以绑定多个事件

<p @mouseover="mouseOver" @mouseout="mouseOut" @click="editInfo('修改info值', $event)">{{info}}</p>

事件修饰符

Vue官方推崇方法内只有纯粹的数据逻辑,而不是去处理 DOM 事件细节同时也建议把阻止冒泡阻止默认行为这些事件使用修饰符进行处理。

修饰符是紧跟方法名之后,使用"."加上下面的关键字:

.stop       // 类似使用stopPropagation阻止冒泡
.prevent    // 类似使用preventDefault阻止默认行为
.once       // 事件执行一次后自动移除
.capture    // 监听事件切换成捕获模式
.self       // 事件只在目标元素上触发才会执行,忽略冒泡的事件

有如下的例子验证stop、capture、once修饰符作用:

<ul @click="testModel">
    <li @click="printInfo" v-for="item in clickInfo">{{item.msg}}</li>
</ul>

data中添加如下数据:

clickInfo: [
    {msg: '标题'},
    {msg: '内容'},
    {msg: '落款'}
]

methods下添加如下事件:

methods: {
    printInfo () {
        console.log('li标签的点击事件');
    },
    testModel () {
        console.log('ul标签的点击事件');
    }
}

点击页面渲染后的任意元素,控制台会输出

li标签的点击事件
ul标签的点击事件

说明事件监听按冒泡模式触发,进行如下改动,添加capture修饰符:

<ul @click.capture="testModel">
...

再点击页面元素,此时控制台输出为

ul标签的点击事件
li标签的点击事件

事件监听换成了捕获模式,清除刚刚添加的capture修饰符,添加stop修饰符

<li @click.stop="printInfo" v-for="item in clickInfo">{{item.msg}}</li>

这时添加页面任意元素,控制台输出:

li标签的点击事件

并不会执行ul的绑定事件

<li @click.stop.once="printInfo" v-for="item in clickInfo">{{item.msg}}</li>

使用once修饰符,可以使该方法只执行一次,这里这么操作会有一个有意思的现象,你对任意一个li进行多次点击,我们会发现li标签上的事件只执行一次,但是ul标签上的事件除了第一次点击不执行,其后的点击都会执行,如果要避免这种情况可以使用self修饰符,完整代码如下:

<ul @click.self="testModel">
    <li @click.stop.once="printInfo" v-for="item in clickInfo">{{item.msg}}</li>
</ul>

这时页面点击后的表现行为就和我们预期的一致,点击li标签只会执行一次li标签上的事件,再次点击没有任何事件执行,点击li标签之外ul之内的区域,则会触发ul上的绑定事件

<a href="http://www.baidu.com" @click.prevent>百度</a>

.prevent修饰符是阻止标签的默认行为,比如按上面的调用方式,就会阻止a标签的跳转行为

.left .right .middle修饰符

<p @click.right="editInfo('修改info值', $event)">{{info}}</p>

这三个修饰符分别代表事件只在鼠标的点击左键,右键,中键时触发,如上代码只会在鼠标点击右键时触发

使用.passive修饰符提升移动端滚动体验

该修饰符设置后事件就不会等待事件执行完成再进行默认的行为,下面是一个很夸张的例子:

<p @touchmove="scrollEvt" style="height: 3000px">{{info}}</p>

在methods中建立方法scrollEvt

methods: {
    scrollEvt () {
        let j = 0;
        for (let i = 0; i < 1000000000; i++) {
            j = i;
        }
        console.log('页面发生了滚动');
    }
}

此时如果你在浏览器中使用移动版模式,点击拉动页面,你会感觉有明显的卡顿之感,因为这时会等待scrollEvt事件执行完成后才会继续进行滚动,要解决这个问题可以使用.passive修饰符

<p @touchmove.passive="scrollEvt" style="height: 3000px">{{info}}</p>

这时就会发现页面可以流畅拖动,控制台会在事件完成后输出

事件修饰符可以串起来使用,但要注意顺序,不同的顺序下,事件执行不同
@click.prevent.self 会阻止所有的点击,而 @click.self.prevent 只会阻止对元素自身的点击

<ul @click.self.prevent="testModel">
    <li v-for="item in clickInfo"><a href="http://www.baidu.com">{{item.msg}}</a></li>
</ul>

这种情况下,并不会阻止a标签的跳转行为

<ul @click.prevent.self="testModel">

如果修改成这样,则会阻止所有a标签的跳转行为

按键修饰符

事件修饰符主要是针对事件进行的一些限制条件,按键修饰符是点击特定按键时触发

<input placeholder="输入内容" @keyup.enter="printMsg"/>

执行上述代码,输入框在选中状态,点击回车键就会触发printMsg事件,以下时Vue支持的键盘修饰符

.enter              // 捕获回车键
.tab                // 捕获tab键
.delete             // 捕获“删除”和“退格”键
.esc                // 捕获esc键
.space              // 捕获空格 
.up                 // 捕获键盘向上按钮
.down               // 捕获键盘向下按钮
.left               // 捕获键盘向左按钮
.right              // 捕获键盘向右按钮

系统修饰键

系统修饰符是实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。

<p @click.shift="printMsg">{{info}}</p>

这个点击事件只会在按住shift按钮时,进行点击才会触发,类似的键盘事件还有

.ctrl
.alt
.shift
.meta

.exact精确控制系统修饰符的组合事件

上例,除了按住shift外,如果你同时按住shift和其他任意键,进行点击是亦然可以触发绑定事件,为了修正这个错误,可以添加exact修饰符达到精确控制,只在shift被按住才会触发

<p @click.shift.exact="printMsg">{{info}}</p>

v-model

表单相关的元素是页面进行交互时比较重要的元素,很多操作都是这些元素进行操作的。Vue对此进行了增强,专门添加了该指令

文本

<input v-model="msg" placeholder="edit me">
<p>Message is: {{ msg }}</p>

在data中建立msg属性

msg: '',

在页面,输入框中输入任意数据,会发现p标签内容会同步更新

多行文本

<p style="white-space: pre-line;">多行文本信息:{{msg}}</p>
<textarea v-model="msg" placeholder="edit me"></textarea>

同文本,输入信息会同步到绑定属性中

单选框

<input type="radio" name="gender" value="man" id="manRadio" v-model="gender"/>
<label for="manRadio">男</label><br/>
<input type="radio" name="gender" value="woman" id="womanRadio" v-model="gender"/>
<label for="womanRadio">女</label><br/>
<p>你选择的性别为:{{gender}}</p>

在data中建立gender属性

gender: '',

绑定属性gender会同步为选中单选的value值

复选框

复选框会因为设定的绑定值类型不同,展示的不同

<input type="checkbox" name="vehicle" id="vehicle" value="bike" v-model="vehicle"/>
<label for="vehicle">自行车</label>
<p>你的车辆为:{{vehicle}}</p>

在data中建立属性vehicle

vehicle: '',

此时使用的是单个复选框,按照单选框的例子,很容易就认为我们选中复选框时p标签显示的应该是复选框的value值,但实际上这里显示的true值,不勾选后显示的是false值,原因是此时Vue期望绑定值是数组,如果把绑定值换成数组

vehicle: [],

选中后数据显示正常

<input type="checkbox" name="vehicle" id="bike" value="bike" v-model="vehicle"/>
<label for="bike">自行车</label>
<input type="checkbox" name="vehicle" id="car" value="car" v-model="vehicle"/>
<label for="car">汽车</label>
<p>你的车辆为:{{vehicle}}</p>

多个条目进行选择或者移除选择,数组会同步删除指定数据

下拉列表

<select v-model="selValue">
    <option disabled value="">你出行的方式</option>
    <option>步行</option>
    <option>自行车</option>
    <option>汽车</option>
</select>
<p>你的出行方式:{{selValue}}</p>

在data中设置selValue值

selValue: '',

与复选框类似,如果要多选时,绑定的属性要是数组类型

<select v-model="selValue" multiple>
    <option disabled value="">你出行的方式</option>
    <option>步行</option>
    <option>自行车</option>
    <option>汽车</option>
</select>
<p>你的出行方式:{{selValue}}</p>

如果此时selValue设定值还是字符串,Vue会在控制台抛出警告,提醒你此时应该使用数组数据

v-model修饰符

修饰符是对v-model的适当补充,修饰符有:

.lazy       // 取代 input 监听 change 事件
.number     // 输入字符串转为数字
.trim       // 输入首尾空格过滤

上文文本的事例:

<input v-model.lazy="msg" placeholder="edit me">
<p>Message is: {{ msg }}</p>

v-model对input元素默认进行的input监听,添加.lazy修饰符后,进行的change监听

v-model.number

换成number修饰符,就会把输入的字符串转为数字的形式,比如你输入:1.34e+5; 1.34e-5,Vue会进行string-to-number的操作

v-model.trim

使用trim修饰符会对输入框进行头尾去除空格的操作

v-pre

该指令是用来跳过这个元素和它的子元素的编译过程,跳过大量没有指令的节点会加快编译。按指令设定的意义来看,是想当模版元素是纯静态内容时,使用这个指令,指定某个区域不行编译,提升Vue的编译速度。不过可能就只有纯背景的DOM区域会需求这个指令吧

v-cloak

很少用,按现在构建代码的形式,只要在进入页面加上一个全局loading,load完再展示页面

自定义指令

除了Vue自带的指令,还可以自定义一些自己需要的指令,自定义指令可以放在组件内部使用directives属性,也可以把公用的指令,放到外部文件然后引用扩展到内部属性directives上

<input placeholder="edit name" v-focus/>

...
directives: {
    focus: {
        inserted: function (el) {
            el.focus()
        }
    }
}

此时渲染页面进到页面就会自动获取输入框焦点,如果是要把公共指令放到外部文件可以这么操作

  • 在src根目录下,建立directives文件夹,然后建立directives.js,在其内部输入
let focus = {
    inserted: function (el) {
        el.focus()
    }
};

export { focus }
  • 在组件的目录下引入该文件
<script>
    import * as directives from './../../directives/directives'; // 引入公共指令
    ...
  • 在组件内部的相关属性上扩展相关属性
directives: {
    ...directives
},  

自定义指令的相关钩子函数

自定义指令的钩子一共有如下几个

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind:只调用一次,指令与元素解绑时调用。

在directives.js中做如下改造

let focus = {
    bind () {
        console.log('bind focus');
    },
    inserted (el) {
        console.log('inserted focus');
        el.focus()
    },
    update () {
        console.log('update focus');
    },
    componentUpdated () {
        console.log('componentUpdated focus');
    },
    unbind () {
        console.log('unbind focus');
    }
};

export { focus }

模板中做如下调整

<input placeholder="edit name" v-focus="focusValue" v-model="focusValue"/>

...
// data中设定focusValue属性值
focusValue: ''

刷新页面,在未进行任何操作前控制台输出

console.log('bind focus');
console.log('inserted focus');

在输入框输入任意字符触发focus指令更新,控制台输出

console.log('update focus');
console.log('componentUpdated focus');

多次输入,控制也同样多次输出,证明update、componentUpdated钩子函数会多次执行,而bind、inserted钩子函数只会在初始化时执行一次

自定义指令的钩子函数,可以接收如下几个参数:

el:指令所绑定的元素,可以用来直接操作 DOM
binding:一个对象,包含以下属性:
  - arg:指令的参数(可选值),修饰符和参数的顺序会影响这个值的取值,比如:v-focus:gool.foo.rel:bar,arg的取值为gool,如果为v-focus.foo.rel:bar:gool,arg的取值就为空不展示,此时指令会把gool当作修饰符rel的值
  - def:指令的方法,显示的是添加的钩子函数(添加了几个钩子函数就显示几个)
  - expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
  - modifiers:一个包含修饰符的对象。比如:v-focus:gool.foo.rel:bar中,修饰符对象为 {foo: true, rel:bar: true}
  - name:指令名,不包括 v- 前缀,不包括修饰符和参数。
  - rawName:指令名全称,包括v-前缀,包括修饰符和参数
  - value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
  - oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
vnode:Vue 编译生成的虚拟节点
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用

这四个参数,在钩子函数中都是通用的,做如下改造

inserted (el, binding, vnode, oldVnode) {
    console.log('inserted focus');
    console.log(binding);
    console.log(vnode);
    console.log(oldVnode);
    el.focus();
},
update (el, binding, vnode, oldVnode) {
    console.log('update focus');
    console.log(binding);
    console.log(vnode);
    console.log(oldVnode);
},

这时再控制台就输出相关信息binding,vnode和oldVnode的值了,这三个对象的值,官方建议是只进行只读操作,从中读取需要的信息,并不要进行相关修改

focusValue: {color: 'red', size: '14px'}

如果传入的指令的值是对象,那么binding对象中的value也是对象,方便调用

计算属性(computed)和观察属性(watch)

计算属性在上文已经使用了多次,只要插值展示的值包含一定程度的计算量,而不能直观看出变量信息时均可使用计算属性。

<p>你的姓名: {{name}}</p>
<p>你的性别: {{gender}}</p>
<p>你的学校: {{school}}</p>
<p>你的学院: {{college}}</p>

在data中建立相关信息

name: 'Tom',
gender: '男',
school: '深圳大学',
college: '软件学院'

如所见,这些信息都非常的直观,如果我们换一个稍微复杂些的信息

<p>你的全名是: {{fullName}}</p>
<input placeholder="edit firstName" v-model="firstName"/><br/>
<input placeholder="edit lastName" v-model="lastName"/><br/>

在data下建立如下信息:

firstName: 'Lin',
lastName: 'Ken',

在computed中建立如下信息:

computed: {
    fullName () {
        return `${this.firstName} ${this.lastName}`
    }
}

这个例子是一个不合理的例子,只为了简单说明计算属性包含了逻辑处理,在输入框修改相关信息p标签信息就会更新,在开发中还有可能碰到类似这样的需求,虽然插值fullName是依赖firstName和lastName计算出来的值,但偶尔需要直接修改fullName的情况,我们先直接在页面上添加对应方法

<button @click="changeFullName">改变姓名</button>
methods: {
    changeFullName () {
        this.fullName = '李 寻欢';
    }
}

这时如果你直接点击页面上的按钮,Vue会在控制台报错,告知fullName没有setter属性,那是因为我们直接使用计算属性时,其实是直接使用getter属性,要解决这个问题我们可以做如下改造

computed: {
    fullName: {
        get () {
            return `${this.firstName} ${this.lastName}`
        },
        set (newValue) {
            let names = newValue.split(' ');
            this.firstName = names[0];
            this.lastName = names[names.length - 1];
        }
    }
}

再次点击页面按钮,页面逻辑就会正常,我们要记得这个思路,就算我们要修改一个计算属性值,实际上是修改计算属性相关联的属性中的值(比如这里的firstName和lastName),依赖这些关联值更新触发相关计算属性的更新,不要尝试直接在set中直接返回相关值,比如这样:

set (newValue) {
    return newValue;
}

计算属性可以满足我们常见的数据关联更新需求,但还有些会是自更新触发不相关数据更新的需求,不如下面的问题:

<p>你的职业为:{{occupation}}</p>

在模板中添加上面内容,对应data中设置occupation的值

occupation: '程序员',

我们现在有这么一个需求,就是当用户姓名变成指定姓名时,occupation的值变成特定值,也就是当firstName或lastName发生更新时,触发器occupation值的更新,这个时候我们如果尝试用计算属性来处理,会感觉有些逻辑上的问题,毕竟occupation的值和firstName与lastName值,并不像它们和fullName那样有很强的逻辑关联型,这时如果使用观察属性来进行处理(watch)一切就变得很好理解

watch: {
    firstName (val) {
        if (val === '李') {
            if (this.lastName === '寻欢') {
                this.occupation = '小李飞刀的传人'
            } else {
                this.occupation = '程序员';
            }
        } else {
            this.occupation = '程序员';
        }
    },
    lastName (val) {
        if (val === '寻欢') {
            if (this.firstName === '李') {
                this.occupation = '小李飞刀的传人'
            } else {
                this.occupation = '程序员';
            }
        } else {
            this.occupation = '程序员';
        }
    }
}

这时的逻辑就是当firstName或lastName数据发生变化时,判断变化后的值是不是指定名称,然后进行相关更新

过滤(filter)

过滤是对插值和v-bind指令的补充,是对其返回值再进行一次筛选,过滤器的使用分为组件内和全局两种

<p>{{info | upperCase}}</p>

此时我们先使用组件内的过滤器,在组件内使用filters属性

filters: {
    upperCase (val) {
        return val.toUpperCase();
    }
}

过滤器默认接收的第一个参数是表达式的值,如果在使用过滤器时需求传参数,可以用下面这种形式

<p>{{info | upperCase('进行大写转换后为:')}}</p>

...
// filters下的upperCase做如下修改
upperCase (val, str) {
    return `${val} ${str} ${val.toUpperCase()}`;
},

过滤器支持多个条件过滤,按如下写法进行

<p>{{info | upperCase | reverse}}</p>

多个过滤器类型管道的写法,下一个过滤器会把上一个过滤器处理后的值当作入参处理

filters: {
    upperCase (val) {
        return `${val.toUpperCase()}`;
    },
    reverse (val) {
        return val.split("").reverse().join("");
    }
}

以上都是过滤器在组件内的使用方法,这些过滤器只适合处理没有很强复用型的过滤器,其他比如币种的转换,金额的格式化,这些都是那种很通用的过滤器,还用组件内的写法就不合适了,这时就需要把过滤器放在公共的地方,通过全局方式进行引用

  • 在src的根目录下建立filters文件夹,然后在文件夹内建立filters.js文件,里面内容如下
let upperCase = (val) => {
    return `${val.toUpperCase()}`;
};

let reverse = (val) => {
    return val.split("").reverse().join("");
};

export { upperCase, reverse }
  • 在组件内引入刚刚添加的文件
<script>
    import * as filter from './../../filters/filters'; // 引入公共过滤器
    ...
  • 在filters下把刚刚引入的文件扩展到组件内过滤器(把上一步相关过滤器删除)
filters:{
    ...filter
}

这样页面的过滤器一样正常生效,不过这种把部分过滤器提取到一个公共文件夹下做法,比较易于维护

关于data,computed,watch和filters的使用

这四个属性是我们处理数据最常用的操作属性,很多操作都是围绕这些属性展开,下面说下我对这些属性的理解:

data通常是定义基础数据,在模板中使用的插值,一般都是由这些数据演变而来,比如现在是一个产品详情页,在页面模板中,有关于产品的信息,而这些信息通常来源于接口,一般会在接口请求的回调函数中,把接口返回的产品数据赋值给某个data中代表产品的属性值(比如为prdInfo,这个值默认会是空值,在接口请求的成功回调中会把这个接口返回的产品信息赋给prdInfo),模版中会引用多个计算属性来动态的设定包括比如产品名称,产品图片之类的信息,而这些计算属性(computed)触发更新的条件就是prdInfo值的变化watch关注的是值的更新,这个值可以是data中的值,也可以是computed的值,只要值发生变化就会触发watch,至于filters是对模板插值的补充,会对模板输出的插值(以及v-bind的绑定数据)再进行一次过滤

组件

组件是Vue最强大的功能,对Html进行了很强的扩展,对相关代码进行了封装。

在整个项目中,因为我们使用了vue-loader,所以会使用.vue的形式去创建组件。

创建一个组件

上面所有例子,其实都是在index.vue这个组件中进行的,为了强化理解组件的感念,下面所进行的例子都是在组件内进行,这也是Vue官方推崇的一种思路,即把界面抽象成一个组件树,页面由一个个独立的组件拼接而成,组件也可以由多个组件拼接而成

全局组件和局部组件

通过Vue.component进行注册的组件就是全局组件,不过在这里有一个需要注意的地方,因为我们是通过npm进行安装的Vue,最新版的Vue是一个运行时的版本,不包含支持template的编译功能,我们上面可以使用template是因为使用了vue-loader并使用了vue-template-compiler这个插件,如果在我们当前的项目中使用全局组件,需要使用render而不是template(后面渲染函数会详细讲解)

在index.js中添加全局插件的注册

...
Vue.component('anchored-heading', {
    render: function (createElement) {
        return createElement(
            'h' + this.level,   // tag name 标签名称
            this.$slots.default // 子组件中的阵列
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
});

let vm = new Vue({
    el: "#app",
    render: h => h(app)
});

在index.vue的模板中添加如下代码

<anchored-heading :level="1">Hello world!</anchored-heading>

这样页面就能渲染出如下内容

<h1>Hello world!</h1>

在实例选项components中注册的组件就是局部组件,只在组件内使用,比如在hello-world的目录下建立一个新的组件header.vue,在其中输入如下内容

<template>
    <header>{{title}}</header>
</template>
<script>
    export default {
        data () {
            return {
                title: '这是头部'
            }
        }
    }
</script>

在index.vue中引入这个组件,在components中组册该组件

<script>
...
import cusHeader from './header.vue'; // 引入头部组件

...
components: {
    cusHeader
}

在模板中引用这个组件

<cusHeader></cusHeader>

最终渲染结果为

<header>这是头部</header>
由于我们使用的是.vue组件形式,所以无需考虑自定义组件无法在普通DOM中不能使用(渲染出错)的情况(比如table标签下出现了出现了自定义标签),如果碰到这种情况需要使用is关键字,告诉编译器使用的是特殊字符,这个不在我的考虑,如要了解,请参考这个特殊处理

组件组合

像上面局部组件的例子,就是在一个组件内引用另外一个组件,这个例子并没有涉及父子组件之间的通信,下面用一个例子来说明父子组件之间如何通信:

父组件向子组件传递数据

// 在模板中添加title属性,在自定义组件添加的属性就会被作为props传递给组件
<cusHeader title="头部信息"></cusHeader>

在组件内部设置接收这个属性的参数(props):

props: ['title'],

这时如果直接渲染Vue会提示你data中的title被声明为prop,要使用默认值代替在data中声明,因为上一步我们在data中声明了title,而在这里我们用props来接收一个传入的title值,从这个例子我们推测在Vue中props的优先级应该是高于data的优先级(?这个地方有点疑问,按官方文档所说,因为prop会在组件实例创建之前进行校验,此时data,computed,methods等实例属性都还不能使用,这里应该能解释为什么props的优先级为什么会比data的优先级高,但判断使用默认值代替data中声明的逻辑是在哪里进行的?),解决方法就是删除在data中的声明,如果你确实希望这个title有个默认值要像下面这样做改造:

props: {
    title: {
        default: '这是头部'
    }
}

这时如果你删除模板中cusHeader设定的title值,页面就渲染成这个指定的默认值,上面对象的写法,除了可以指定默认值以外还可以对接收参数的类型以及值进行设定

  • 接收参数,直接跟一个数据类型,这时是指定当前值只接收某类数据
props: {
    title: Number
}

这样写就表示接收的值必须是数字类型,如果传入其他类似的值,Vue会渲染出来,但是会在控制台报错,此例中,控制台会报错提示title期望的是一个Number类型,但是接收的却是一个String类型,Vue可以设置如下的参数类型:

String
Number
Boolean
Function
Object
Array
Symbol

如果要设置可以同时接收多种数据类型,可以使用数组的形式

title: [Number, String], // 接收数字或字符类型
  • 接收参数,跟一个对象,可以在对象中对接收参数设置更多参数

参数必传:required设为true

props: {
    title: {
        type: String,
        required: true
    }
}

如果做了这种设定,在cusHeader中如果不传title值,控制台报错告知缺少必须的参数

默认值:default跟相关值

如上所述,可以使用default跟一个值,表示接收参数的默认值,这个跟随的值,根据type类型的不同,其值也不同

// 如果是数字类型,默认值为数字
type: Number,
default: 100
...
// 如果是数组或值对象,默认值也必须是工厂函数的返回值(如果不用工厂函数,直接把数组值赋值给default,控制台会报错,提示props的type类型为数组/对象,必须使用工厂函数返回)
title: {
    type: Array,
    default: function () {
        return ['这是头部', '这是主体']
    }
}

自定义验证函数:validator

title: {
    type: Number,
    validator: function (val) {
        return val > 10;
    }
}

如果在index.vue中模板这么写

<cusHeader title="12"></cusHeader>

控制台会报错,提示传值的类型错误,因为props进行父子组件通信时默认传值类型是String,如果要传其他类型数据,要使用绑定属性,做如下处理

// 在data中建立一个titleNum
titleNum: 12
...
// 模板中改为
<cusHeader :title="titleNum"></cusHeader>

这时页面就能正常渲染,如果把titleNum换成1,控制台报错提示不符合验证条件

普通HTML属性添加到组件上

如果在组件内部的props没有设置对某个属性的接收,那么那个属性就会当作普通HTML属性加载到组件内最外层的DOM元素上

// index.vue模板做如下改造
<cusHeader class="header" :title="titleNum"></cusHeader>
...
// header.vue组件做如下改造
<template>
    <div>
        <header>{{title}}</header>
    </div>
</template>

这时页面会渲染为

<div class="header"><header>12</header></div>

替换/合并现有特性

如果组件内部的外层元素和要添加的属性存在相同属性,那么这个属性就会进行替换或合并,例如:

// 模板内做如下修改,添加一个自定义的info属性
<cusHeader info="header" style="color: red" class="header" :title="titleNum"></cusHeader>
...
// 组件内部也一样做调整
<template>
    <div class="basis" info="basis" style="color: yellow; font-size: 14px">
        <header>{{title}}</header>
    </div>
</template>

此时页面渲染为

<div class="basis header" info="basis" style="color: red; font-size: 14px;">
    <header>12</header>
</div>

我们发现info属性被替换,class属性进行了合并,而style属性被有选择型合并了(组件内的color被父组件的color覆盖了,而font-size被保留),Vue在处理这些组件传递的特性时大部分会被直接替换,只有在处理class和style时才会有选择性的合并

子组件向父组件传递数据

使用props我们可以很容易的从父组件向子组件传递数据,但是如果反过来,要从子组件向父组件传递数据时,就要借助自定义事件来完成:

// index.vue模板中做如下改造
<cusHeader :title="titleNum" @concat="changeNum"></cusHeader>
...
// 在index.vue的methods下新建方法changeNum
methods: {
    changeNum (val) {
        this.titleNum = val;
    }
}
...
// header.vue做如下改造
<template>
    <div @click="clickEvt">
        <header>{{title}}</header>
    </div>
</template>
...
// header.vue的methods下添加下面方法
clickEvt () {
    this.$emit('concat', 20);
}

渲染完成的页面,当你点击页面元素,就会发现,对应数值发生了改变,在这里主要关心这几个方面:

// concat是自定义事件,这里表示监听自定义事件concat,当concat被触发时,再触发changeNum
@concat="changeNum"; 
...
// 表示触发concat事件,并传值20
this.$emit('concat', 20); 
...
// 在changeNum中接收的参数val,就是上面从子组件中传递而来
changeNum (val) {
    this.titleNum = val;
}

从上面的例子可以看出,借用自定义事件,我们实现了从子组件向父组件传递数据的能力,Vue推崇的单向数据流,所以才会用父组件到子组件通过props,子组件到父组件使用自定义事件,下图是官网解释父子组件通信的图片:


父子通信

不过Vue针对子组件对父组件通信,提供了一个语法糖修饰符sync,让子组件可以通过事件直接触发父组件中值或状态的修改,形成类似双向绑定。

<cusHeader :title.sync="titleNum"></cusHeader>
...
// 在header.vue做如下改动,update:title是显示触发相关更新
clickEvt () {
    this.$emit('update:title', 30)
}

这时点击页面元素,我们发现对应数值也一样发生了变动,此时页面的数据就形成了一种类似双向绑定的交互逻辑:
子组件传值到父组件,父组件更新数据,又回传给子组件,子组件数据更新

使用native修饰符触发事件

// 比如,你想直接在组件上绑定一个click事件
<cusHeader :title="titleNum" @click="changeNum"></cusHeader>
...
methods: {
    changeNum () {
        console.log('changeNum');
    }
}
...
// 此时Vue是会把@click="changeNum"中的click识别成一个自定义事件,如果想正常触发,需要在组件内添加相应的事件进行触发
// 在header.vue中的事件做改造,这时组件上的click事件就能正常触发
clickEvt () {
    console.log('从子组件触发');
    this.$emit('click');
}

为了解决上面的问题,Vue制定了一个.native的修饰符

<cusHeader :title="titleNum" @click.native="changeNum"></cusHeader>
...
// header.vue把触发click的代码删除
clickEvt () {
    console.log('从子组件触发');
}

这时组件的click事件就能正常触发

子组件之间的通信

上面探讨了父组件和子组件,子组件和父组件通信的问题,为了解决子组件之间的通信,官方提供了Vuex,内容偏多,分开讨论

使用插槽分发内容

如果我们把新建的组件当作普通的HTML元素,在其中引用普通元素

<cusHeader>
    <p>内部元素</p>
</cusHeader>

最终渲染会忽略插在组件内部的元素,如果要解决这个问题,需要借助插槽(slot),在header.vue组件做如下改造:

<template>
    <div>
        <header>{{title}}</header>
        <slot>只有在没有要分发的内容时才会显示</slot>
    </div>
</template>

这时页面就可以正常渲染出p标签中的内容,现在如果删除p标签,那么页面就显示slot中的文字,这个地方就会有一个问题,比如slot中这段文字,你希望是按需展示出来,而不是当没有插槽内容就直接展示,这时可以借助作用域插槽来解决这个问题

// header.vue做如下修改,把插槽的默认值放到text(自定义属性,可以是任意值)属性中
<slot text="只有在没有要分发的内容时才会显示"></slot>
...
// index.vue做如下修改,在组件引用的地方调用这个插槽存储的值
// 下面slot-scope可以用在p标签是在Vue 2.5+以上的版本
// prop也可以是任意值
<cusHeader>
    <p slot-scope="prop">{{prop.text}}</p>
</cusHeader>

通过这种方式,我们就可以在组件内定义一些默认信息,同时可以按需进行加载,形成来类似父组件通过props向子组件传值的能力,此时绑在组件插槽的内容只能在组件调用的域中可用。

slot最初被设定为备用内容,但当和name属性一并使用时,其就具备了分发内容的能力

// header.vue下模板改造为
<template>
    <div>
        <div class="header">
            <slot name="header"></slot>
        </div>
        <div class="body">
            <slot></slot>
        </div>
        <div class="footer">
            <slot name="footer"></slot>
        </div>
    </div>
</template>
...
// index.vue下模板修改为
<cusHeader>
    <h1 slot="header">这是头部</h1>
    <p>这个会放到默认body中</p>
    <h1 slot="footer">这是底部</h1>
</cusHeader>
...
// 页面渲染为
<div>
    <div class="header">
        <h1>这是头部</h1>
    </div> 
    <div class="body"> 
        <p>这个会放到默认body中</p> 
    </div> 
    <div class="footer">
        <h1>这是底部</h1>
    </div>
</div>

使用具名插槽在组件设计上,会使组件更加灵活

动态组件

官网上提到的动态组件并不是动态加载的组件,而是可以动态切换的组件,是指利用保留的component标签,在结合is特性,对某一处的加挂点动态切换组件,在这里我们实现一个相对复杂点tab页切换

// 在hello-world目录下,新建三个.vue文件,分别表示三个新闻块的内容
game.vue
sport.vue
news.vue
...
// game.vue内添加如下内容
<template>
    <ul>
        <li v-for="item in gameList">{{item.title}} - {{item.time}}</li>
    </ul>
</template>
<script>
    export default {
        data () {
            return {
                gameList: [
                    {id: 'g01', title: '游戏新闻1', time: '03-07'},
                    {id: 'g02', title: '游戏新闻2', time: '03-07'},
                    {id: 'g03', title: '游戏新闻3', time: '03-07'},
                    {id: 'g04', title: '游戏新闻4', time: '03-07'},
                    {id: 'g05', title: '游戏新闻5', time: '03-07'},
                ]
            }
        },
        created () {
            console.log('game module');
        }
    }
</script>
...
// news.vue添加如下内容
<template>
    <ul>
        <li v-for="item in gameList">{{item.title}} - {{item.time}}</li>
    </ul>
</template>
<script>
    export default {
        data () {
            return {
                gameList: [
                    {id: 'n01', title: '新闻1', time: '03-07'},
                    {id: 'n02', title: '新闻2', time: '03-07'},
                    {id: 'n03', title: '新闻3', time: '03-07'},
                    {id: 'n04', title: '新闻4', time: '03-07'},
                    {id: 'n05', title: '新闻5', time: '03-07'},
                ]
            }
        },
        created () {
            console.log('news module');
        }
    }
</script>
...
// sport.vue添加如下内容
<template>
    <ul>
        <li v-for="item in gameList">{{item.title}} - {{item.time}}</li>
    </ul>
</template>
<script>
    export default {
        data () {
            return {
                gameList: [
                    {id: 's01', title: '体育新闻1', time: '03-07'},
                    {id: 's02', title: '体育新闻2', time: '03-07'},
                    {id: 's03', title: '体育新闻3', time: '03-07'},
                    {id: 's04', title: '体育新闻4', time: '03-07'},
                    {id: 's05', title: '体育新闻5', time: '03-07'},
                ]
            }
        },
        created () {
            console.log('sport module');
        }
    }
</script>
...
// index.vue引入添加的三个组件
import gameList from './game.vue'; // 引入游戏列表
import newsList from './news.vue'; // 引入新闻列表
import sportList from './sport.vue'; // 引入运动新闻列表
...
// index.vue的components注册这三个组件
components: {
    gameList,
    newsList,
    sportList
}
...
// index.vue的data中添加如下信息
tabList: ['新闻', '游戏新闻', '体育新闻'],
showTabView: 'gameList'
...
// index.vue的模板做如下修改
<ul>
    <li v-for="item in tabList" @click="changeTab(item)">{{item}}</li>
</ul>
<component :is="showTabView"></component>
...
// index.vue的methods下添加changeTab方法
changeTab (val) {
    if (val === '新闻') {
        this.showTabView = 'newsList';
    } else if (val === '游戏新闻') {
        this.showTabView = 'gameList';
    } else if (val === '体育新闻') {
        this.showTabView = 'sportList';
    }
}

当然这个例子是有点不合适的,这三个列表应该是复用一个列表组件,只不过根据不同数据源展示不同,我们在点击进行切换时,会发现控制台会一直输出我们设置在created中的log,证明在切换组件时,组件每一次都是重新加载,这个从性能上考虑是需要进行优化的,使用Vue尽可能的复用重复代码

使用keep-alive标签保留组件状态以及静态资源,避免重新渲染

// 在动态加载组件的标签外添加keep-alive标签
<keep-alive>
    <component :is="showTabView"></component>
</keep-alive>

这时如果我们再点击页面进行组件切换,会发现各个组件created方法,只会执行一次,再次切换回相关组件时,created就不会执行,不过这样虽然达到了组件的复用保留了静态资源,而且也不会进行也避免了不必要的渲染,但是要如何知道这个组件发生了切换或值停用?Vue针对keep-alive的特性,添加了两个钩子函数activated 和 deactivated

// game.vue添加下面代码
activated () {
    console.log('game activated');
},
deactivated () {
    console.log('game deactivated');
}
...
// news.vue添加下面代码
activated () {
    console.log('new activated');
},
deactivated () {
    console.log('new deactivated');
}
...
// sport.vue添加下面代码
activated () {
    console.log('sport activated');
},
deactivated () {
    console.log('sport deactivated');
}

这时我们点击页面元素,进行组件切换时,发现控制台,会按上一个组件的deactivated切换后组件的activated的顺序执行,这样我们在进行组件切换时依然可以通过这两个钩子函数,动态控制组件的加载

include/exclude控制缓存组件

默认Vue会对keep-alive包裹下的所有切换组件进行缓存,但在某些情况下,我们可能只打算对部分组件进行缓存,这时就要借助include和exclude进行细化控制

// 字符形式:这么就表示缓存gameList,newsList两个模块,注意“,”后面不能有空格,要不Vue识别有误,模块名直接使用,不需添加引号
<keep-alive include="gameList,newsList">
...
// 数组形式:这么也表示缓存gameList和newsList两个模块,模块名要用字符形式表示
<keep-alive :include="['gameList', 'newsList']">
...
// 正则表达式:模块名直接使用无需添加引号
<keep-alive :include="/gameList|newsList/">

exclude使用方式和include一致,区别在于exclude表示除此之外,与include功能相反

组件的设计

官方文档提示我们在设计组件时,要考虑下面内容:

  • Prop:接收从组件外部传递数据;

  • 事件: 从组件内触发组件外部的方法达到从组件内部向外传递数据的能力;

  • 插槽: 允许从组件外部将额外的内容组合在组件中。

使用ref获取子组件引用

ref是用来获取元素或子组件的引用信息,针对元素ref获取就是元素对应的Dom对象,可以使用原声js写法获取相关属性,针对组件获取的是一个VueComponent

// 在组件和p标签都添加ref属性
<keep-alive :include="/gameList|newsList/">
    <component ref="listChild" :is="showTabView"></component>
</keep-alive>
<p class="info" ref="info">{{info}}</p>
...
// 在mounted方法中使用this.$refs查看ref的引用,如果你把这个引用放到created中是无法生效的,因为$refs只有在元素渲染完成才会被添加到实例对象上
// 如果用在普通标签上,获取其引用可以使用原生js属性
mounted () {
    console.log(this.$refs);
    console.log(this.$refs.info.className);
}

异步组件

异步组件要结合webpack来进行使用,可以参看webpack中的介绍

递归组件

递归组件和递归调用类似,是组件反复套用组件本身,当符合某个条件时不再引用

// 在header-world目录下,建立list.vue,在其中输入如下内容
<template>
    <div class="list">
        <p>{{listInfo}}</p>
        <p v-if="closeList"><list :info="list" :flag="listFlag"></list></p>
    </div>
</template>
<script>
    export default {
        name: 'list',
        props: ['flag', 'info'],
        data () {
            return {
                list: '这时递归内部调用的数据',
                listFlag: false
            }
        },
        computed: {
            closeList () {
                return this.flag;
            },
            listInfo () {
                return this.info;
            }
        }
    }
</script>
...
// index.vue中引入list.vue,并引用
<script>
    import list from './list.vue'; // 引入递归组件
...
    components:{
        list
    }
...
// 模板中调用这个组件,info,showFlag都是之前设定的值
<list :info="info" :flag="showFlag"></list>

这时页面就会渲染为:

<div class="list">
    <p>Hello vue!</p> 
    <p><div class="list"><p>这时递归内部调用的数据</p> <!----></div></p>
</div>

我们可以看到,我们在组件内部复用了组件本身,在这里有几个点需要注意

  • 组件内部的name属性需要添加,要不在组件内使用list组件是编程出错的
  • 组件递归的终止条件一定要明确,要不组件会陷入一种死循环

递归组件非常适合去开发那种不确定有几层循环的组件,比如多级菜单,后续在实例开发部分会有案例

组件之间的循环引用

递归组件可以认为成是相同组件之间的循环调用,当是不同组件形成类似递归组件的调用,就会出现组件调用异常的情况

// 在项目目录新建两个文件moduleA.vue和moduleB.vue
// moduleA.vue内容如下
<template>
    <div>
        <p>{{info}}</p>
        <ul>
            <li v-for="item in listData">
                <module-b v-if="item.child" :listData="item.child"></module-b>
                <p v-else>{{item.title}}</p>
            </li>
        </ul>

    </div>
</template>
<script>
    import moduleB from './moduleB.vue'; // 引入moduleB

    export default {
        data () {
            return {
                info: 'from moduleA'
            }
        },
        props: ['listData'],
        components: {
            moduleB
        }
    }
</script>
...
// moduleB.vue内容如下
<template>
    <div>
        <p>{{info}}</p>
        <ul>
            <li v-for="item in listData">
                <module-a v-if="item.child" :listData="item.child"></module-a>
                <p v-else>{{item.title}}</p>
            </li>
        </ul>

    </div>
</template>
<script>
    import moduleA from './moduleA.vue';

    export default {
        data () {
            return {
                info: 'from moduleB'
            }
        },
        props: ['listData'],
        components: {
            moduleA
        }
    }
</script>
...
// index.vue引入moduleA.vue和moduleB.vue并添加相关数据
<module-a :listData="moduleList"></module-a>
...
<script>
    import moduleA from './moduleA.vue';
    ...
    data: {
        moduleList: [
            {title: '新闻',
           child: [
                {title: '本地新闻', 
                 child: [
                    {title: '本地新闻1', 
                        child: [{title:'本地新闻11'}, {title:'本地新闻12'}]}, {title: '本地新闻2'}]},
                 {title: '社会新闻'},
                 {title: '社会民生'}
               ]
            }, {title: '游戏新闻'}, {title: '体育新闻'}],
    }
    ...
    components: {
        moduleA
    }

这时控制台会报错,提示你在moduleB.vue文件中module-a没有注册,我们回看整个调用逻辑会发现,moduleA组件中引用了moduleB组件,moduleB组件又引用了moduleA组件,并且根据我们造的数据能看出,moduleA和moduleB也彼此产生了调用,这个时候两个组件形成了类似递归组件的那种调用的情况,只不过区别在于递归组件调用自己,是一定存在,但是moduleA和moduleB就无法保证在彼此调用时一定存在,而且这个问题按官方的说法,只有在使用webpack等模块管理工具才会有这问题,如果使用Vue.component进行注册,Vue会直接处理这个冲突,要解决此问题,就需要借助beforeCreate这个钩子函数,我们在moduleA(因为moduleA针对moduleB是入口组件)

// moduleA.vue中进行相关修改,保证moduleB不会在moduleA之前引用
<script>
    export default {
        data () {
            return {
                info: 'from moduleA'
            }
        },
        props: ['listData'],
        beforeCreate () {
            this.$options.components.moduleB = require('./moduleB.vue').default;
        },
        components: {
        }
    }
</script>

经过上面的改造,页面就可以正常展示,以上就是Vue的基础内容,下一章讨论Vue其他的一些混合特性,以及过渡和动画

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 1.安装 可以简单地在页面引入Vue.js作为独立版本,Vue即被注册为全局变量,可以在页面使用了。 如果希望搭建...
    Awey阅读 10,999评论 4 129
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,046评论 0 29
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,204评论 0 6
  • 从前的你,愿意跑很远的路,来看我,买我喜欢的东西给我,现在,你总是一句话对不起或让你委屈了
    薇薇安1802785阅读 250评论 0 0
  • 每个地方都有不一样的风景,身处一个地方久了,便会有一些想出去走走的心思。 赶巧~遇上了小朋友毕业后的闲暇时间,挤着...
    净染阅读 753评论 9 7