Vue現(xiàn)代化使用方法(二)

vue概述

在官方文檔中,有一句話(huà)對(duì)Vue的定位說(shuō)的很明確:
Vue.js 的核心是一個(gè)允許采用簡(jiǎn)潔的模板語(yǔ)法來(lái)聲明式地將數(shù)據(jù)渲染進(jìn) DOM 的系統(tǒng)

Vue在我的理解下,其實(shí)很像mvvm架構(gòu)中,對(duì)vm的實(shí)行。在mvvm架構(gòu)中,viewModel是負(fù)責(zé)把view和model關(guān)聯(lián)起來(lái),把model的數(shù)據(jù)同步到view顯示出來(lái),把view的修改同步回model。在Vue的data屬性中可設(shè)定相關(guān)變量,并把這些變量和Dom進(jìn)行關(guān)聯(lián),達(dá)到修改data中屬性即可修改Dom的功能 ( model(data) --> Vue --> view(dom) )。在Dom上,通過(guò)觸發(fā)綁定方法,對(duì)相關(guān)數(shù)據(jù)進(jìn)行修改 ( view(dom) --> Vue --> model(data) )。按這個(gè)思路來(lái)理解Vue,可以大概猜想出Vue有些功能為什么要被設(shè)計(jì)出來(lái)。

  • 插值:快速實(shí)現(xiàn)data和view的快速綁定,
  • 指令:對(duì)插值的補(bǔ)充,作用于模板上,用來(lái)實(shí)現(xiàn)模板的重復(fù)生成(v-for),動(dòng)態(tài)顯示(v-show, v-if),屬性綁定(v-bind),事件綁定(v-on),對(duì)表單的增強(qiáng)(v-model)
  • 計(jì)算屬性和觀(guān)察屬性:對(duì)插值和指令的補(bǔ)充,動(dòng)態(tài)響應(yīng)更新數(shù)據(jù)達(dá)到動(dòng)態(tài)更新Dom的功效
  • 過(guò)濾:對(duì)插值和v-bind指令的補(bǔ)充,對(duì)要綁定到Dom上的數(shù)據(jù)進(jìn)行過(guò)濾
  • 組件:Vue最強(qiáng)大的功能,對(duì)viewModel的實(shí)現(xiàn),template就是view層,script就是model層

這些內(nèi)容組成了Vue的骨架,掌握以上內(nèi)容基本就能上手開(kāi)發(fā)。下面我就利用上一章的例子把這些內(nèi)容詳細(xì)的介紹下(我按照我的理解打亂了官方的介紹順序)

插值

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

文本插值

處于data中返回值的info就是一個(gè)文本插值,這時(shí)如果修改info的值,對(duì)應(yīng)DOM(p標(biāo)簽)的就會(huì)發(fā)生改變

上面data必須是函數(shù)的寫(xiě)法,是因?yàn)檫@是在一個(gè)組件內(nèi)部,如果使用對(duì)象形式的寫(xiě)法,這個(gè)組件在經(jīng)過(guò)多個(gè)實(shí)例化后,實(shí)際上是在共享一個(gè)data對(duì)象,更改任一組件data中的值,就會(huì)影響所有組件,所以組件內(nèi)的data必須是函數(shù),這樣每個(gè)組件就形成獨(dú)立的函數(shù)作用域,彼此不沖突。

屬性插值

如果想把data中的值綁定成為html元素的屬性,需要使用v-bind指令(簡(jiǎn)寫(xiě)方式是 : ),代碼改造如下:

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

這個(gè)綁定值可以是普通的html屬性,也可以是自定義屬性

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

這時(shí)如果看最終渲染的頁(yè)面,其效果是忽略cusData的大寫(xiě),只顯示為cusdata

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

如果這里是設(shè)置常用的class,id,title屬性是沒(méi)什么問(wèn)題,但如果是自定義屬性比如data-cusData這種類(lèi)型的數(shù)據(jù),就會(huì)被強(qiáng)制轉(zhuǎn)為data-cusdata

如果要綁定多個(gè)屬性值可以使用對(duì)象的形式

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

Data中設(shè)置修改如下

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

因?yàn)閏lass和style是我們常用的屬性值,Vue針對(duì)這兩個(gè)屬性做了特殊處理

:class綁定對(duì)象語(yǔ)法

改造代碼如下

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

:class是v-bind:class的簡(jiǎn)寫(xiě)

對(duì)應(yīng)data修改如下:

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

頁(yè)面渲染結(jié)果如下:

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

如例子顯示,綁定對(duì)象的形式可以動(dòng)態(tài)切換多個(gè)class,同時(shí)也支持與普通class共存

當(dāng)綁定對(duì)象要進(jìn)行多個(gè)class值的設(shè)定,沿用上面的寫(xiě)法會(huì)顯得繁瑣,我們可以把綁定的對(duì)象換成一個(gè)計(jì)算屬性,在計(jì)算屬性中動(dòng)態(tài)的設(shè)定返回值。

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

在計(jì)算屬性中設(shè)定相關(guān)值

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

渲染結(jié)果如上,除了上面的寫(xiě)法,也可以結(jié)合下面介紹的綁定數(shù)組語(yǔ)法,把上面的例子再簡(jiǎn)化些

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

:class綁定數(shù)組語(yǔ)法

代碼如下:

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

data下數(shù)據(jù)修改如下:

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

這樣最終渲染如下:

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

在數(shù)組語(yǔ)法中,可以使用三元運(yùn)算符控制某個(gè)元素class的添加和隱藏
代碼改造如下:

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

data數(shù)據(jù)修改如下:

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

這時(shí)渲染的最早結(jié)果如下:

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

如果把showInfo設(shè)為true,渲染結(jié)果和上例一致

官方文檔這里還介紹了對(duì)組件的class綁定處理,不過(guò)我覺(jué)得這樣做其實(shí)會(huì)破壞組件的封裝型,就算要添加或者移除某個(gè)class,最好還是通過(guò)props傳值在組件內(nèi)部自行處理

:style綁定對(duì)象語(yǔ)法

:style的綁定對(duì)象語(yǔ)法和:class綁定對(duì)象語(yǔ)法使用方法一樣,只是在寫(xiě)css屬性名要注意下

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

data數(shù)據(jù)修改如下:

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

如例css的屬性名,可以改寫(xiě)成駝峰形式(backgroundColor),或者使用引號(hào)包裹起來(lái)("font-size")

當(dāng)使用對(duì)象語(yǔ)法進(jìn)行多個(gè)css屬性設(shè)置時(shí),可以使用計(jì)算屬性進(jìn)行綁定

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

計(jì)算屬性設(shè)置如下

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

渲染結(jié)果如下:

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

上面設(shè)定樣式的方式如果只是綁定單個(gè)對(duì)象是沒(méi)問(wèn)題的,但如果要使用數(shù)組的形式綁定多個(gè)對(duì)象就無(wú)法生效(解決方法參看下面)。

:style綁定數(shù)組語(yǔ)法

使用:class綁定數(shù)組使用方法見(jiàn)下例:

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

這個(gè)時(shí)候要保證對(duì)應(yīng)數(shù)組對(duì)象是對(duì)象的形式(上例是字符串形式):

這個(gè)例子中綁定對(duì)象的值分別在data和計(jì)算屬性中設(shè)定,只是做演示,表示這個(gè)綁定值,可以是不同的來(lái)源值進(jìn)行混合

在data中設(shè)定colorStyle

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

在計(jì)算屬性中設(shè)定fontStyle和backgroundColor

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

這時(shí)的渲染結(jié)果和上例是一致的,但如果你把其中一個(gè)值,改成字符串形式

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

這時(shí)你會(huì)發(fā)現(xiàn)Vue會(huì)忽略當(dāng)前值的設(shè)定,所以在使用:style形式時(shí),最好按對(duì)象形式返還設(shè)定的style。

指令

我對(duì)指令的理解就是:指令是使模板具備了邏輯處理的能力,是對(duì)插值的一種補(bǔ)充,因?yàn)橹噶畹拇嬖诓攀沟脭?shù)據(jù)層和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實(shí)現(xiàn)文本綁定的能力,期望值是string

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

上面兩種寫(xiě)法渲染后的結(jié)果一致,通常我們會(huì)使用第二種寫(xiě)法,比較簡(jiǎn)潔。這個(gè)指令期望的值是string,但如果綁定的值是一個(gè)對(duì)象,就會(huì)原樣把對(duì)象輸出。

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

在data中設(shè)定一個(gè)infoObj

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

這時(shí)頁(yè)面渲染為

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

如果你進(jìn)行下面的設(shè)定

infoObj: true,              // => true
或
infoObj: 3 > 2,             // => true
或
infoObj: Math.random(),     // => 渲染為一個(gè)不確定的隨機(jī)數(shù)
或
infoObj: 2 + 3,             // => 5

所以這個(gè)指令會(huì)對(duì)傳入的值進(jìn)行一個(gè)轉(zhuǎn)換,轉(zhuǎn)換成string。事例中該指令的綁定值,我只使用了data下的值,但實(shí)際上這個(gè)值還可以是計(jì)算屬性。

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

對(duì)頁(yè)面進(jìn)行渲染時(shí),頁(yè)面的展示數(shù)據(jù)分為靜態(tài)數(shù)據(jù)和動(dòng)態(tài)數(shù)據(jù),一般情況下靜態(tài)數(shù)據(jù)存放在data屬性下,動(dòng)態(tài)數(shù)據(jù)通過(guò)計(jì)算屬性進(jìn)行返回(上例只是個(gè)樣式,正常情況下計(jì)算屬性會(huì)包含相關(guān)邏輯處理,相關(guān)會(huì)在計(jì)算屬性那部分講解,這里就不展開(kāi)了)

v-bind用來(lái)動(dòng)態(tài)的綁定一個(gè)或者多個(gè)屬性

在屬性插值部分已經(jīng)對(duì)v-bind指令做了比較詳細(xì)的介紹,有一些特殊點(diǎn)要特別說(shuō)下:

綁定值可以是一個(gè)表達(dá)式

<p v-bind:title="info + ' 這時(shí)行內(nèi)添加的信息'">{{info}}</p>

這時(shí)頁(yè)面渲染結(jié)果為

<p title="Hello vue! 這時(shí)行內(nèi)添加的信息">Hello vue!</p>

這是一個(gè)比較有用的特性,在開(kāi)發(fā)過(guò)程中我們很容易碰到根據(jù)數(shù)據(jù)的不同展示不同的樣式(比如符合某個(gè)條件時(shí)文案顏色變紅)

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

在data中我們假定一個(gè)login的字段

{
...
    login: true
...
}

這時(shí)頁(yè)面就會(huì)渲染為

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

如例中假設(shè),比如我現(xiàn)在有個(gè)接口返回用戶(hù)是否登陸,拿到接口返回?cái)?shù)據(jù),我們就可以根據(jù)接口返回的相關(guān)字段,動(dòng)態(tài)的設(shè)定某一處的class。

通過(guò)修飾符.prop綁定DOM屬性

如果在綁定屬性后使用.prop修飾符,會(huì)忽略其他對(duì)該值的設(shè)定,強(qiáng)制使用設(shè)定的屬性值

<p class="testProp" v-bind:text-content.prop="info" v-bind:className.prop="elemId">測(cè)試數(shù)據(jù)</p>

這時(shí)頁(yè)面會(huì)渲染為

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

如果刪掉一個(gè)prop值

<p class="testProp" v-bind:text-content="info" v-bind:className.prop="elemId">測(cè)試數(shù)據(jù)</p>

對(duì)應(yīng)的綁定值就會(huì)變成一個(gè)普通的屬性值

<p class="pId" text-content="Hello vue!">測(cè)試數(shù)據(jù)</p>

v-html

該指令是用來(lái)直接輸出HTML內(nèi)容

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

在data中設(shè)定如下值

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

這時(shí)頁(yè)面的渲染結(jié)果為

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

如果使用v-text會(huì)原樣把字符串內(nèi)容輸出

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

渲染為

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

在官方文檔中強(qiáng)調(diào)這種寫(xiě)法很容易引起XSS攻擊,不能把這個(gè)能力開(kāi)放給普通用戶(hù),在實(shí)際開(kāi)發(fā)中,我也沒(méi)碰到什么地方需要使用該指令。。。

v-show

根據(jù)表達(dá)式之真假值(這個(gè)值可以是data中的值,也可以是計(jì)算屬性,后續(xù)不再贅述),切換元素的 display CSS 屬性。

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

在data中設(shè)定showFlag的值

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

這時(shí)頁(yè)面渲染為

<p>Hello vue!</p>

如果showFlag設(shè)為false,頁(yè)面渲染為

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

v-show指令使用時(shí)有如下內(nèi)容需要注意:

  • v-show不支持<template>元素,因?yàn)閠emplate標(biāo)簽的元素的內(nèi)容不會(huì)被渲染,所以對(duì)該標(biāo)簽使用v-show去改變其的display屬性是沒(méi)有意義的。
  • v-show不支持和v-else一起使用,v-show只管當(dāng)前元素的顯示與否

v-if

該指令會(huì)根據(jù)表達(dá)式的值動(dòng)態(tài)渲染元素。元素及其包含的指令/組件在切換期間被銷(xiāo)毀并重新構(gòu)建。如果是作用在<template>元素上,則其內(nèi)部元素內(nèi)容會(huì)根據(jù)表達(dá)式的值動(dòng)態(tài)建立或者銷(xiāo)毀。(這點(diǎn)是與v-show的不同點(diǎn),v-show只是改變display的值,v-if則會(huì)進(jìn)行動(dòng)態(tài)的建立或者銷(xiāo)毀)

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

渲染為

<p>Hello vue!</p>

v-else和v-else-if

對(duì)v-if的補(bǔ)充,必須緊跟著v-if之后,否則無(wú)法生效

<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中設(shè)定type一個(gè)隨意的值

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

這時(shí)頁(yè)面渲染為

<p>D</p>

由于v-if會(huì)對(duì)元素進(jìn)行重建或者銷(xiāo)毀,而Vue在渲染時(shí)會(huì)盡可能復(fù)用已有元素,針對(duì)普通元素這個(gè)指令使用起來(lái)是沒(méi)問(wèn)題的,但針對(duì)一些表單元素就有問(wèn)題了。

<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的簡(jiǎn)寫(xiě)(這個(gè)是用來(lái)綁定事件),在methods中添加相關(guān)方法

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;
    }
},

頁(yè)面渲染為

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

點(diǎn)擊button,會(huì)發(fā)現(xiàn)input元素的內(nèi)容會(huì)發(fā)生改變,但如果你向input輸入一個(gè)值,這時(shí)你再點(diǎn)擊button,雖然input元素會(huì)變,但是已經(jīng)輸入的內(nèi)容卻不會(huì)改變,類(lèi)似的還有textarea標(biāo)簽。
如果認(rèn)為這是一個(gè)問(wèn)題,可以使用Vue提供的防重的方式,使用key添加一個(gè)唯一的關(guān)鍵值。做以下改造

<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>

這時(shí)如果輸入內(nèi)容,再點(diǎn)擊切換input就會(huì)完全重新渲染,不過(guò)之前輸入的內(nèi)容也會(huì)被清除

v-for

v-for指令根據(jù)一組數(shù)組的選項(xiàng)列表進(jìn)行渲染,通常我們只會(huì)在需要展示列表的部分使用該指令。

數(shù)據(jù)源是一個(gè)對(duì)象組成的數(shù)組

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

在data下設(shè)定students的值

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

渲染結(jié)果為

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

從這個(gè)例子,我們可以看出該指令是在當(dāng)前元素進(jìn)行循環(huán)渲染,根據(jù)條件展示數(shù)據(jù),該指令還可以接受一個(gè)參數(shù)表示數(shù)組的排序(從0開(kāi)始)

<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>

數(shù)據(jù)源是一個(gè)對(duì)象

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

在data中添加相關(guān)數(shù)據(jù)

{
...
    tomInfo:{
        age: 24,
        gender: 'man',
        address: '北二環(huán)前門(mén)里'
    },
...
}

渲染為

<ul>
    <li>24</li>
    <li>man</li>
    <li>北二環(huán)前門(mén)里</li>
</ul>

此時(shí)如果指令接受的第二個(gè)參數(shù)表示的是對(duì)象key值

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

渲染為

<ul>
    <li>age : 24</li>
    <li>gender : man</li>
    <li>address : 北二環(huán)前門(mén)里</li>
</ul>

在數(shù)據(jù)源是對(duì)象的情況下,該指令還可以接受第三個(gè)參數(shù),表示序號(hào)

<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 : 北二環(huán)前門(mén)里</li>
</ul>

上面是我們假定的一個(gè)例子,通常我們是從后端拿到數(shù)據(jù)然后再在前端展示,而后端給到前端的數(shù)據(jù)通常是按照J(rèn)SON格式給出,key值都是英文,這時(shí)前端就要對(duì)key值進(jìn)行一個(gè)轉(zhuǎn)為中文的操作,我們現(xiàn)在要對(duì)已經(jīng)處理過(guò)的數(shù)據(jù)在進(jìn)行簡(jiǎn)單處理(邏輯并不復(fù)雜),這時(shí)我們就需要使用過(guò)濾器(本部分只簡(jiǎn)單介紹)

在filters(這是組件內(nèi)的寫(xiě)法)下添加相關(guān)過(guò)濾器

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

頁(yè)面內(nèi)容進(jìn)行如下改造

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

注意{{key | keyCheck}}就是過(guò)濾器的使用方法,頁(yè)面渲染為

<ul>
    <li>0. 年齡 : 24</li>
    <li>1. 性別 : man</li>
    <li>2. 家庭住址 : 北二環(huán)前門(mén)里</li>
</ul>

關(guān)于v-for使用:key提升性能的解釋

在Vue的官方文檔中介紹v-for指令時(shí),提到使用key值提升渲染性能,具體原因官方?jīng)]有詳細(xì)介紹,只說(shuō)Vue在進(jìn)行渲染時(shí)進(jìn)行的是“就地復(fù)用”策略。如果數(shù)據(jù)項(xiàng)的順序被改變,Vue 將不會(huì)移動(dòng) DOM 元素來(lái)匹配數(shù)據(jù)項(xiàng)的順序, 而是簡(jiǎn)單復(fù)用此處每個(gè)元素,并且確保它在特定索引下顯示已被渲染過(guò)的每個(gè)元素。

參考如下實(shí)例,有助于理解這段話(huà):

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

在data和methods下添加如下內(nèi)容

{
...
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'})
    }
},

這時(shí)頁(yè)面渲染為

<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>添加新的列表內(nèi)容</button>

如果在幾個(gè)輸入框中輸入隨意文字,然后再點(diǎn)擊按鈕,這時(shí)我們發(fā)現(xiàn)渲染結(jié)果為

<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>添加新的列表內(nèi)容</button>

渲染的信息如我們所預(yù)期的一致,但是我們剛剛輸入的文字輸入框卻沒(méi)有按預(yù)期的下移,比較明顯的被直接復(fù)用,這也就是官方文檔中提到的如果數(shù)據(jù)項(xiàng)的順序被改變,Vue 將不會(huì)移動(dòng) DOM 元素來(lái)匹配數(shù)據(jù)項(xiàng)的順序,不過(guò)在這個(gè)例子中這種Vue的默認(rèn)處理方式是不符合我們的預(yù)期,所以這時(shí)要使用key值來(lái)標(biāo)記每個(gè)節(jié)點(diǎn),方便Vue對(duì)數(shù)據(jù)進(jìn)行重新排序

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

渲染結(jié)果同上沒(méi)有變化,但是如果你這時(shí)再輸入框輸入文字,然后再點(diǎn)擊按鈕時(shí),Dom就會(huì)按預(yù)期的排序

在這里會(huì)有個(gè)容易忽略的問(wèn)題,如果你把key的綁定值換成數(shù)組序號(hào)的$index值

:key="$index"

Vue會(huì)忽略這個(gè)key的設(shè)定,還是按照未設(shè)定key時(shí)的方式進(jìn)行渲染,原因(以下是個(gè)人理解,可能根本原因并不是這樣)是此時(shí)key值綁定的是數(shù)組的序號(hào)值(數(shù)字是不可變值),雖然可以通過(guò)addList方法觸發(fā)listInfo值的更新,但是無(wú)法觸發(fā)key綁定值的更新,Vue會(huì)把新加的數(shù)據(jù)當(dāng)作普通數(shù)據(jù),采取"就地復(fù)用"的策略(也就是官方文檔中提到的確保它在特定索引下顯示已被渲染過(guò)的每個(gè)元素),直接更新Dom而不對(duì)Dom進(jìn)行重新排序,所以我們做如下修改,來(lái)驗(yàn)證我們的想法

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

在data中設(shè)定list值

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

同時(shí)在修改addList方法,觸發(fā)list數(shù)組的更新

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

這時(shí)如果進(jìn)行相關(guān)操作,就會(huì)發(fā)現(xiàn)和上面方法(:key="item.id")呈現(xiàn)一致,在上例中key的綁定值是一個(gè)數(shù)組對(duì)象,同時(shí)在addList方法中更新數(shù)組數(shù)據(jù)(如果不進(jìn)行更新,而是在list中提前定義好,一樣無(wú)法觸發(fā)Dom的重新排序)

觸發(fā)v-for的數(shù)據(jù)更新
v-for的數(shù)據(jù)源如果是數(shù)組時(shí),我們可以使用觸發(fā)數(shù)組更新的方法,來(lái)觸發(fā)v-for的重新渲染

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

以上的方法都會(huì)觸發(fā)原始數(shù)組的更新,數(shù)組更新自然就會(huì)觸發(fā)v-for的重新渲染,數(shù)組的其他方法比如filter,concat,slice,every,map,find,findIndex等方法都不回直接修改原始數(shù)組,數(shù)組不更新,v-for也就不會(huì)重新渲染,如果需要對(duì)數(shù)組使用到非更新方法,可以把處理后的數(shù)組直接賦值給原始數(shù)組

其他特殊情況
Vue沒(méi)有實(shí)現(xiàn)對(duì)以下內(nèi)容變化的檢測(cè)(按官方說(shuō)法是受javascript限制,后續(xù)單獨(dú)講解):

檢測(cè)數(shù)組的變化

  • 利用索引直接設(shè)置數(shù)組值:arr[0] = newValue;
  • 修改數(shù)組長(zhǎng)度: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內(nèi)容</button>

在methods添加editList方法:

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

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

你可以嘗試使用直接設(shè)置數(shù)組值(注釋部分的代碼),這時(shí)會(huì)發(fā)現(xiàn)頁(yè)面并不會(huì)改變,使用Vue.set會(huì)讓Vue檢測(cè)到數(shù)組的變化,還可以使用數(shù)組的splice方法讓數(shù)組更新

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

如果這時(shí)你使用

this.listInfo.length = 2;

設(shè)置數(shù)組長(zhǎng)度,視圖不會(huì)更新,解決方法一樣可以調(diào)用splice方法

this.listInfo.splice(2);

檢測(cè)對(duì)象的變化

  • Vue不能檢測(cè)對(duì)象屬性的添加或刪除

事例如下:

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

在methods方法中添加addSchool方法

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

點(diǎn)擊頁(yè)面按鈕,我們?cè)诳刂婆_(tái)可以看到屬性是添加到指定數(shù)據(jù)上,但頁(yè)面視圖卻沒(méi)有重新渲染,解決方法同樣使用Vue.set方法

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

這時(shí)視圖如期望變化,與數(shù)組的splice方法類(lèi)似,針對(duì)對(duì)象也可以使用Object.assign方法把更新后的對(duì)象重新賦值

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

使用Object.assign的好處就是可以同時(shí)添加多個(gè)屬性值

this.tomInfo = Object.assign({}, this.tomInfo, {school: '深圳大學(xué)', class: '軟件工程'});

一個(gè)需要特別注意的地方:v-for的優(yōu)先級(jí)比v-if的優(yōu)先級(jí)高

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

因?yàn)関-for優(yōu)先級(jí)高于v-if,所以這個(gè)會(huì)先進(jìn)行循環(huán),然后再判斷是否符合條件進(jìn)行展示

v-on

該指令是要來(lái)監(jiān)聽(tīng)DOM事件,并在觸發(fā)時(shí)運(yùn)行指定代碼(使用方法是v-on:事件名=“事件”)

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

相關(guān)事件是放到methods下,@click是v-on:click的簡(jiǎn)寫(xiě)方式

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

這時(shí)點(diǎn)擊頁(yè)面就會(huì)發(fā)現(xiàn)頁(yè)面數(shù)據(jù)發(fā)生改變,事件可以接收參數(shù)來(lái)獲取原生DOM事件信息

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

進(jìn)行點(diǎn)擊,在控制臺(tái)會(huì)輸出原生DOM事件信息

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

綁定事件時(shí)可以接收參數(shù)傳值

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

這時(shí)如果還要輸出DOM事件信息,就要在傳參數(shù)時(shí)傳一個(gè)特殊參數(shù)$event

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

同一個(gè)元素可以綁定多個(gè)事件

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

事件修飾符

Vue官方推崇方法內(nèi)只有純粹的數(shù)據(jù)邏輯,而不是去處理 DOM 事件細(xì)節(jié)同時(shí)也建議把阻止冒泡阻止默認(rèn)行為這些事件使用修飾符進(jìn)行處理。

修飾符是緊跟方法名之后,使用"."加上下面的關(guān)鍵字:

.stop       // 類(lèi)似使用stopPropagation阻止冒泡
.prevent    // 類(lèi)似使用preventDefault阻止默認(rèn)行為
.once       // 事件執(zhí)行一次后自動(dòng)移除
.capture    // 監(jiān)聽(tīng)事件切換成捕獲模式
.self       // 事件只在目標(biāo)元素上觸發(fā)才會(huì)執(zhí)行,忽略冒泡的事件

有如下的例子驗(yàn)證stop、capture、once修飾符作用:

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

data中添加如下數(shù)據(jù):

clickInfo: [
    {msg: '標(biāo)題'},
    {msg: '內(nèi)容'},
    {msg: '落款'}
]

methods下添加如下事件:

methods: {
    printInfo () {
        console.log('li標(biāo)簽的點(diǎn)擊事件');
    },
    testModel () {
        console.log('ul標(biāo)簽的點(diǎn)擊事件');
    }
}

點(diǎn)擊頁(yè)面渲染后的任意元素,控制臺(tái)會(huì)輸出

li標(biāo)簽的點(diǎn)擊事件
ul標(biāo)簽的點(diǎn)擊事件

說(shuō)明事件監(jiān)聽(tīng)按冒泡模式觸發(fā),進(jìn)行如下改動(dòng),添加capture修飾符:

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

再點(diǎn)擊頁(yè)面元素,此時(shí)控制臺(tái)輸出為

ul標(biāo)簽的點(diǎn)擊事件
li標(biāo)簽的點(diǎn)擊事件

事件監(jiān)聽(tīng)換成了捕獲模式,清除剛剛添加的capture修飾符,添加stop修飾符

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

這時(shí)添加頁(yè)面任意元素,控制臺(tái)輸出:

li標(biāo)簽的點(diǎn)擊事件

并不會(huì)執(zhí)行ul的綁定事件

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

使用once修飾符,可以使該方法只執(zhí)行一次,這里這么操作會(huì)有一個(gè)有意思的現(xiàn)象,你對(duì)任意一個(gè)li進(jìn)行多次點(diǎn)擊,我們會(huì)發(fā)現(xiàn)li標(biāo)簽上的事件只執(zhí)行一次,但是ul標(biāo)簽上的事件除了第一次點(diǎn)擊不執(zhí)行,其后的點(diǎn)擊都會(huì)執(zhí)行,如果要避免這種情況可以使用self修飾符,完整代碼如下:

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

這時(shí)頁(yè)面點(diǎn)擊后的表現(xiàn)行為就和我們預(yù)期的一致,點(diǎn)擊li標(biāo)簽只會(huì)執(zhí)行一次li標(biāo)簽上的事件,再次點(diǎn)擊沒(méi)有任何事件執(zhí)行,點(diǎn)擊li標(biāo)簽之外ul之內(nèi)的區(qū)域,則會(huì)觸發(fā)ul上的綁定事件

<a  @click.prevent>百度</a>

.prevent修飾符是阻止標(biāo)簽的默認(rèn)行為,比如按上面的調(diào)用方式,就會(huì)阻止a標(biāo)簽的跳轉(zhuǎn)行為

.left .right .middle修飾符

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

這三個(gè)修飾符分別代表事件只在鼠標(biāo)的點(diǎn)擊左鍵,右鍵,中鍵時(shí)觸發(fā),如上代碼只會(huì)在鼠標(biāo)點(diǎn)擊右鍵時(shí)觸發(fā)

使用.passive修飾符提升移動(dòng)端滾動(dòng)體驗(yàn)

該修飾符設(shè)置后事件就不會(huì)等待事件執(zhí)行完成再進(jìn)行默認(rèn)的行為,下面是一個(gè)很夸張的例子:

<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('頁(yè)面發(fā)生了滾動(dòng)');
    }
}

此時(shí)如果你在瀏覽器中使用移動(dòng)版模式,點(diǎn)擊拉動(dòng)頁(yè)面,你會(huì)感覺(jué)有明顯的卡頓之感,因?yàn)檫@時(shí)會(huì)等待scrollEvt事件執(zhí)行完成后才會(huì)繼續(xù)進(jìn)行滾動(dòng),要解決這個(gè)問(wèn)題可以使用.passive修飾符

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

這時(shí)就會(huì)發(fā)現(xiàn)頁(yè)面可以流暢拖動(dòng),控制臺(tái)會(huì)在事件完成后輸出

事件修飾符可以串起來(lái)使用,但要注意順序,不同的順序下,事件執(zhí)行不同
@click.prevent.self 會(huì)阻止所有的點(diǎn)擊,而 @click.self.prevent 只會(huì)阻止對(duì)元素自身的點(diǎn)擊

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

這種情況下,并不會(huì)阻止a標(biāo)簽的跳轉(zhuǎn)行為

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

如果修改成這樣,則會(huì)阻止所有a標(biāo)簽的跳轉(zhuǎn)行為

按鍵修飾符

事件修飾符主要是針對(duì)事件進(jìn)行的一些限制條件,按鍵修飾符是點(diǎn)擊特定按鍵時(shí)觸發(fā)

<input placeholder="輸入內(nèi)容" @keyup.enter="printMsg"/>

執(zhí)行上述代碼,輸入框在選中狀態(tài),點(diǎn)擊回車(chē)鍵就會(huì)觸發(fā)printMsg事件,以下時(shí)Vue支持的鍵盤(pán)修飾符

.enter              // 捕獲回車(chē)鍵
.tab                // 捕獲tab鍵
.delete             // 捕獲“刪除”和“退格”鍵
.esc                // 捕獲esc鍵
.space              // 捕獲空格 
.up                 // 捕獲鍵盤(pán)向上按鈕
.down               // 捕獲鍵盤(pán)向下按鈕
.left               // 捕獲鍵盤(pán)向左按鈕
.right              // 捕獲鍵盤(pán)向右按鈕

系統(tǒng)修飾鍵

系統(tǒng)修飾符是實(shí)現(xiàn)僅在按下相應(yīng)按鍵時(shí)才觸發(fā)鼠標(biāo)或鍵盤(pán)事件的監(jiān)聽(tīng)器。

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

這個(gè)點(diǎn)擊事件只會(huì)在按住shift按鈕時(shí),進(jìn)行點(diǎn)擊才會(huì)觸發(fā),類(lèi)似的鍵盤(pán)事件還有

.ctrl
.alt
.shift
.meta

.exact精確控制系統(tǒng)修飾符的組合事件

上例,除了按住shift外,如果你同時(shí)按住shift和其他任意鍵,進(jìn)行點(diǎn)擊是亦然可以觸發(fā)綁定事件,為了修正這個(gè)錯(cuò)誤,可以添加exact修飾符達(dá)到精確控制,只在shift被按住才會(huì)觸發(fā)

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

v-model

表單相關(guān)的元素是頁(yè)面進(jìn)行交互時(shí)比較重要的元素,很多操作都是這些元素進(jìn)行操作的。Vue對(duì)此進(jìn)行了增強(qiáng),專(zhuān)門(mén)添加了該指令

文本

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

在data中建立msg屬性

msg: '',

在頁(yè)面,輸入框中輸入任意數(shù)據(jù),會(huì)發(fā)現(xiàn)p標(biāo)簽內(nèi)容會(huì)同步更新

多行文本

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

同文本,輸入信息會(huì)同步到綁定屬性中

單選框

<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會(huì)同步為選中單選的value值

復(fù)選框

復(fù)選框會(huì)因?yàn)樵O(shè)定的綁定值類(lèi)型不同,展示的不同

<input type="checkbox" name="vehicle" id="vehicle" value="bike" v-model="vehicle"/>
<label for="vehicle">自行車(chē)</label>
<p>你的車(chē)輛為:{{vehicle}}</p>

在data中建立屬性vehicle

vehicle: '',

此時(shí)使用的是單個(gè)復(fù)選框,按照單選框的例子,很容易就認(rèn)為我們選中復(fù)選框時(shí)p標(biāo)簽顯示的應(yīng)該是復(fù)選框的value值,但實(shí)際上這里顯示的true值,不勾選后顯示的是false值,原因是此時(shí)Vue期望綁定值是數(shù)組,如果把綁定值換成數(shù)組

vehicle: [],

選中后數(shù)據(jù)顯示正常

<input type="checkbox" name="vehicle" id="bike" value="bike" v-model="vehicle"/>
<label for="bike">自行車(chē)</label>
<input type="checkbox" name="vehicle" id="car" value="car" v-model="vehicle"/>
<label for="car">汽車(chē)</label>
<p>你的車(chē)輛為:{{vehicle}}</p>

多個(gè)條目進(jìn)行選擇或者移除選擇,數(shù)組會(huì)同步刪除指定數(shù)據(jù)

下拉列表

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

在data中設(shè)置selValue值

selValue: '',

與復(fù)選框類(lèi)似,如果要多選時(shí),綁定的屬性要是數(shù)組類(lèi)型

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

如果此時(shí)selValue設(shè)定值還是字符串,Vue會(huì)在控制臺(tái)拋出警告,提醒你此時(shí)應(yīng)該使用數(shù)組數(shù)據(jù)

v-model修飾符

修飾符是對(duì)v-model的適當(dāng)補(bǔ)充,修飾符有:

.lazy       // 取代 input 監(jiān)聽(tīng) change 事件
.number     // 輸入字符串轉(zhuǎn)為數(shù)字
.trim       // 輸入首尾空格過(guò)濾

上文文本的事例:

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

v-model對(duì)input元素默認(rèn)進(jìn)行的input監(jiān)聽(tīng),添加.lazy修飾符后,進(jìn)行的change監(jiān)聽(tīng)

v-model.number

換成number修飾符,就會(huì)把輸入的字符串轉(zhuǎn)為數(shù)字的形式,比如你輸入:1.34e+5; 1.34e-5,Vue會(huì)進(jìn)行string-to-number的操作

v-model.trim

使用trim修飾符會(huì)對(duì)輸入框進(jìn)行頭尾去除空格的操作

v-pre

該指令是用來(lái)跳過(guò)這個(gè)元素和它的子元素的編譯過(guò)程,跳過(guò)大量沒(méi)有指令的節(jié)點(diǎn)會(huì)加快編譯。按指令設(shè)定的意義來(lái)看,是想當(dāng)模版元素是純靜態(tài)內(nèi)容時(shí),使用這個(gè)指令,指定某個(gè)區(qū)域不行編譯,提升Vue的編譯速度。不過(guò)可能就只有純背景的DOM區(qū)域會(huì)需求這個(gè)指令吧

v-cloak

很少用,按現(xiàn)在構(gòu)建代碼的形式,只要在進(jìn)入頁(yè)面加上一個(gè)全局loading,load完再展示頁(yè)面

自定義指令

除了Vue自帶的指令,還可以自定義一些自己需要的指令,自定義指令可以放在組件內(nèi)部使用directives屬性,也可以把公用的指令,放到外部文件然后引用擴(kuò)展到內(nèi)部屬性directives上

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

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

此時(shí)渲染頁(yè)面進(jìn)到頁(yè)面就會(huì)自動(dòng)獲取輸入框焦點(diǎn),如果是要把公共指令放到外部文件可以這么操作

  • 在src根目錄下,建立directives文件夾,然后建立directives.js,在其內(nèi)部輸入
let focus = {
    inserted: function (el) {
        el.focus()
    }
};

export { focus }
  • 在組件的目錄下引入該文件
<script>
    import * as directives from './../../directives/directives'; // 引入公共指令
    ...
  • 在組件內(nèi)部的相關(guān)屬性上擴(kuò)展相關(guān)屬性
directives: {
    ...directives
},  

自定義指令的相關(guān)鉤子函數(shù)

自定義指令的鉤子一共有如下幾個(gè)

bind:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。

inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 (僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)。

update:所在組件的 VNode 更新時(shí)調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒(méi)有。但是你可以通過(guò)比較更新前后的值來(lái)忽略不必要的模板更新 (詳細(xì)的鉤子函數(shù)參數(shù)見(jiàn)下)。

componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新后調(diào)用。

unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。

在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 }

模板中做如下調(diào)整

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

...
// data中設(shè)定focusValue屬性值
focusValue: ''

刷新頁(yè)面,在未進(jìn)行任何操作前控制臺(tái)輸出

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

在輸入框輸入任意字符觸發(fā)focus指令更新,控制臺(tái)輸出

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

多次輸入,控制也同樣多次輸出,證明update、componentUpdated鉤子函數(shù)會(huì)多次執(zhí)行,而bind、inserted鉤子函數(shù)只會(huì)在初始化時(shí)執(zhí)行一次

自定義指令的鉤子函數(shù),可以接收如下幾個(gè)參數(shù):

el:指令所綁定的元素,可以用來(lái)直接操作 DOM
binding:一個(gè)對(duì)象,包含以下屬性:
  - arg:指令的參數(shù)(可選值),修飾符和參數(shù)的順序會(huì)影響這個(gè)值的取值,比如:v-focus:gool.foo.rel:bar,arg的取值為gool,如果為v-focus.foo.rel:bar:gool,arg的取值就為空不展示,此時(shí)指令會(huì)把gool當(dāng)作修飾符rel的值
  - def:指令的方法,顯示的是添加的鉤子函數(shù)(添加了幾個(gè)鉤子函數(shù)就顯示幾個(gè))
  - expression:字符串形式的指令表達(dá)式。例如 v-my-directive="1 + 1" 中,表達(dá)式為 "1 + 1"。
  - modifiers:一個(gè)包含修飾符的對(duì)象。比如:v-focus:gool.foo.rel:bar中,修飾符對(duì)象為 {foo: true, rel:bar: true}
  - name:指令名,不包括 v- 前綴,不包括修飾符和參數(shù)。
  - rawName:指令名全稱(chēng),包括v-前綴,包括修飾符和參數(shù)
  - value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值為 2。
  - oldValue:指令綁定的前一個(gè)值,僅在 update 和 componentUpdated 鉤子中可用。無(wú)論值是否改變都可用。
vnode:Vue 編譯生成的虛擬節(jié)點(diǎn)
oldVnode:上一個(gè)虛擬節(jié)點(diǎn),僅在 update 和 componentUpdated 鉤子中可用

這四個(gè)參數(shù),在鉤子函數(shù)中都是通用的,做如下改造

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);
},

這時(shí)再控制臺(tái)就輸出相關(guān)信息binding,vnode和oldVnode的值了,這三個(gè)對(duì)象的值,官方建議是只進(jìn)行只讀操作,從中讀取需要的信息,并不要進(jìn)行相關(guān)修改

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

如果傳入的指令的值是對(duì)象,那么binding對(duì)象中的value也是對(duì)象,方便調(diào)用

計(jì)算屬性(computed)和觀(guān)察屬性(watch)

計(jì)算屬性在上文已經(jīng)使用了多次,只要插值展示的值包含一定程度的計(jì)算量,而不能直觀(guān)看出變量信息時(shí)均可使用計(jì)算屬性。

<p>你的姓名: {{name}}</p>
<p>你的性別: {{gender}}</p>
<p>你的學(xué)校: {{school}}</p>
<p>你的學(xué)院: {{college}}</p>

在data中建立相關(guān)信息

name: 'Tom',
gender: '男',
school: '深圳大學(xué)',
college: '軟件學(xué)院'

如所見(jiàn),這些信息都非常的直觀(guān),如果我們換一個(gè)稍微復(fù)雜些的信息

<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}`
    }
}

這個(gè)例子是一個(gè)不合理的例子,只為了簡(jiǎn)單說(shuō)明計(jì)算屬性包含了邏輯處理,在輸入框修改相關(guān)信息p標(biāo)簽信息就會(huì)更新,在開(kāi)發(fā)中還有可能碰到類(lèi)似這樣的需求,雖然插值fullName是依賴(lài)firstName和lastName計(jì)算出來(lái)的值,但偶爾需要直接修改fullName的情況,我們先直接在頁(yè)面上添加對(duì)應(yīng)方法

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

這時(shí)如果你直接點(diǎn)擊頁(yè)面上的按鈕,Vue會(huì)在控制臺(tái)報(bào)錯(cuò),告知fullName沒(méi)有setter屬性,那是因?yàn)槲覀冎苯邮褂糜?jì)算屬性時(shí),其實(shí)是直接使用getter屬性,要解決這個(gè)問(wèn)題我們可以做如下改造

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

再次點(diǎn)擊頁(yè)面按鈕,頁(yè)面邏輯就會(huì)正常,我們要記得這個(gè)思路,就算我們要修改一個(gè)計(jì)算屬性值,實(shí)際上是修改計(jì)算屬性相關(guān)聯(lián)的屬性中的值(比如這里的firstName和lastName),依賴(lài)這些關(guān)聯(lián)值更新觸發(fā)相關(guān)計(jì)算屬性的更新,不要嘗試直接在set中直接返回相關(guān)值,比如這樣:

set (newValue) {
    return newValue;
}

計(jì)算屬性可以滿(mǎn)足我們常見(jiàn)的數(shù)據(jù)關(guān)聯(lián)更新需求,但還有些會(huì)是自更新觸發(fā)不相關(guān)數(shù)據(jù)更新的需求,不如下面的問(wèn)題:

<p>你的職業(yè)為:{{occupation}}</p>

在模板中添加上面內(nèi)容,對(duì)應(yīng)data中設(shè)置occupation的值

occupation: '程序員',

我們現(xiàn)在有這么一個(gè)需求,就是當(dāng)用戶(hù)姓名變成指定姓名時(shí),occupation的值變成特定值,也就是當(dāng)firstName或lastName發(fā)生更新時(shí),觸發(fā)器occupation值的更新,這個(gè)時(shí)候我們?nèi)绻麌L試用計(jì)算屬性來(lái)處理,會(huì)感覺(jué)有些邏輯上的問(wèn)題,畢竟occupation的值和firstName與lastName值,并不像它們和fullName那樣有很強(qiáng)的邏輯關(guān)聯(lián)型,這時(shí)如果使用觀(guān)察屬性來(lái)進(jìn)行處理(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 = '程序員';
        }
    }
}

這時(shí)的邏輯就是當(dāng)firstName或lastName數(shù)據(jù)發(fā)生變化時(shí),判斷變化后的值是不是指定名稱(chēng),然后進(jìn)行相關(guān)更新

過(guò)濾(filter)

過(guò)濾是對(duì)插值和v-bind指令的補(bǔ)充,是對(duì)其返回值再進(jìn)行一次篩選,過(guò)濾器的使用分為組件內(nèi)和全局兩種

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

此時(shí)我們先使用組件內(nèi)的過(guò)濾器,在組件內(nèi)使用filters屬性

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

過(guò)濾器默認(rèn)接收的第一個(gè)參數(shù)是表達(dá)式的值,如果在使用過(guò)濾器時(shí)需求傳參數(shù),可以用下面這種形式

<p>{{info | upperCase('進(jìn)行大寫(xiě)轉(zhuǎn)換后為:')}}</p>

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

過(guò)濾器支持多個(gè)條件過(guò)濾,按如下寫(xiě)法進(jìn)行

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

多個(gè)過(guò)濾器類(lèi)型管道的寫(xiě)法,下一個(gè)過(guò)濾器會(huì)把上一個(gè)過(guò)濾器處理后的值當(dāng)作入?yún)⑻幚?/p>

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

以上都是過(guò)濾器在組件內(nèi)的使用方法,這些過(guò)濾器只適合處理沒(méi)有很強(qiáng)復(fù)用型的過(guò)濾器,其他比如幣種的轉(zhuǎn)換,金額的格式化,這些都是那種很通用的過(guò)濾器,還用組件內(nèi)的寫(xiě)法就不合適了,這時(shí)就需要把過(guò)濾器放在公共的地方,通過(guò)全局方式進(jìn)行引用

  • 在src的根目錄下建立filters文件夾,然后在文件夾內(nèi)建立filters.js文件,里面內(nèi)容如下
let upperCase = (val) => {
    return `${val.toUpperCase()}`;
};

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

export { upperCase, reverse }
  • 在組件內(nèi)引入剛剛添加的文件
<script>
    import * as filter from './../../filters/filters'; // 引入公共過(guò)濾器
    ...
  • 在filters下把剛剛引入的文件擴(kuò)展到組件內(nèi)過(guò)濾器(把上一步相關(guān)過(guò)濾器刪除)
filters:{
    ...filter
}

這樣頁(yè)面的過(guò)濾器一樣正常生效,不過(guò)這種把部分過(guò)濾器提取到一個(gè)公共文件夾下做法,比較易于維護(hù)

關(guān)于data,computed,watch和filters的使用

這四個(gè)屬性是我們處理數(shù)據(jù)最常用的操作屬性,很多操作都是圍繞這些屬性展開(kāi),下面說(shuō)下我對(duì)這些屬性的理解:

data通常是定義基礎(chǔ)數(shù)據(jù),在模板中使用的插值,一般都是由這些數(shù)據(jù)演變而來(lái),比如現(xiàn)在是一個(gè)產(chǎn)品詳情頁(yè),在頁(yè)面模板中,有關(guān)于產(chǎn)品的信息,而這些信息通常來(lái)源于接口,一般會(huì)在接口請(qǐng)求的回調(diào)函數(shù)中,把接口返回的產(chǎn)品數(shù)據(jù)賦值給某個(gè)data中代表產(chǎn)品的屬性值(比如為prdInfo,這個(gè)值默認(rèn)會(huì)是空值,在接口請(qǐng)求的成功回調(diào)中會(huì)把這個(gè)接口返回的產(chǎn)品信息賦給prdInfo),模版中會(huì)引用多個(gè)計(jì)算屬性來(lái)動(dòng)態(tài)的設(shè)定包括比如產(chǎn)品名稱(chēng),產(chǎn)品圖片之類(lèi)的信息,而這些計(jì)算屬性(computed)觸發(fā)更新的條件就是prdInfo值的變化watch關(guān)注的是值的更新,這個(gè)值可以是data中的值,也可以是computed的值,只要值發(fā)生變化就會(huì)觸發(fā)watch,至于filters是對(duì)模板插值的補(bǔ)充,會(huì)對(duì)模板輸出的插值(以及v-bind的綁定數(shù)據(jù))再進(jìn)行一次過(guò)濾

組件

組件是Vue最強(qiáng)大的功能,對(duì)Html進(jìn)行了很強(qiáng)的擴(kuò)展,對(duì)相關(guān)代碼進(jìn)行了封裝。

在整個(gè)項(xiàng)目中,因?yàn)槲覀兪褂昧藇ue-loader,所以會(huì)使用.vue的形式去創(chuàng)建組件。

創(chuàng)建一個(gè)組件

上面所有例子,其實(shí)都是在index.vue這個(gè)組件中進(jìn)行的,為了強(qiáng)化理解組件的感念,下面所進(jìn)行的例子都是在組件內(nèi)進(jìn)行,這也是Vue官方推崇的一種思路,即把界面抽象成一個(gè)組件樹(shù),頁(yè)面由一個(gè)個(gè)獨(dú)立的組件拼接而成,組件也可以由多個(gè)組件拼接而成

全局組件和局部組件

通過(guò)Vue.component進(jìn)行注冊(cè)的組件就是全局組件,不過(guò)在這里有一個(gè)需要注意的地方,因?yàn)槲覀兪峭ㄟ^(guò)npm進(jìn)行安裝的Vue,最新版的Vue是一個(gè)運(yùn)行時(shí)的版本,不包含支持template的編譯功能,我們上面可以使用template是因?yàn)槭褂昧藇ue-loader并使用了vue-template-compiler這個(gè)插件,如果在我們當(dāng)前的項(xiàng)目中使用全局組件,需要使用render而不是template(后面渲染函數(shù)會(huì)詳細(xì)講解)

在index.js中添加全局插件的注冊(cè)

...
Vue.component('anchored-heading', {
    render: function (createElement) {
        return createElement(
            'h' + this.level,   // tag name 標(biāo)簽名稱(chēng)
            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>

這樣頁(yè)面就能渲染出如下內(nèi)容

<h1>Hello world!</h1>

在實(shí)例選項(xiàng)components中注冊(cè)的組件就是局部組件,只在組件內(nèi)使用,比如在hello-world的目錄下建立一個(gè)新的組件header.vue,在其中輸入如下內(nèi)容

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

在index.vue中引入這個(gè)組件,在components中組冊(cè)該組件

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

...
components: {
    cusHeader
}

在模板中引用這個(gè)組件

<cusHeader></cusHeader>

最終渲染結(jié)果為

<header>這是頭部</header>
由于我們使用的是.vue組件形式,所以無(wú)需考慮自定義組件無(wú)法在普通DOM中不能使用(渲染出錯(cuò))的情況(比如table標(biāo)簽下出現(xiàn)了出現(xiàn)了自定義標(biāo)簽),如果碰到這種情況需要使用is關(guān)鍵字,告訴編譯器使用的是特殊字符,這個(gè)不在我的考慮,如要了解,請(qǐng)參考這個(gè)特殊處理

組件組合

像上面局部組件的例子,就是在一個(gè)組件內(nèi)引用另外一個(gè)組件,這個(gè)例子并沒(méi)有涉及父子組件之間的通信,下面用一個(gè)例子來(lái)說(shuō)明父子組件之間如何通信:

父組件向子組件傳遞數(shù)據(jù)

// 在模板中添加title屬性,在自定義組件添加的屬性就會(huì)被作為props傳遞給組件
<cusHeader title="頭部信息"></cusHeader>

在組件內(nèi)部設(shè)置接收這個(gè)屬性的參數(shù)(props):

props: ['title'],

這時(shí)如果直接渲染Vue會(huì)提示你data中的title被聲明為prop,要使用默認(rèn)值代替在data中聲明,因?yàn)樯弦徊轿覀冊(cè)赿ata中聲明了title,而在這里我們用props來(lái)接收一個(gè)傳入的title值,從這個(gè)例子我們推測(cè)在Vue中props的優(yōu)先級(jí)應(yīng)該是高于data的優(yōu)先級(jí)(?這個(gè)地方有點(diǎn)疑問(wèn),按官方文檔所說(shuō),因?yàn)閜rop會(huì)在組件實(shí)例創(chuàng)建之前進(jìn)行校驗(yàn),此時(shí)data,computed,methods等實(shí)例屬性都還不能使用,這里應(yīng)該能解釋為什么props的優(yōu)先級(jí)為什么會(huì)比data的優(yōu)先級(jí)高,但判斷使用默認(rèn)值代替data中聲明的邏輯是在哪里進(jìn)行的?),解決方法就是刪除在data中的聲明,如果你確實(shí)希望這個(gè)title有個(gè)默認(rèn)值要像下面這樣做改造:

props: {
    title: {
        default: '這是頭部'
    }
}

這時(shí)如果你刪除模板中cusHeader設(shè)定的title值,頁(yè)面就渲染成這個(gè)指定的默認(rèn)值,上面對(duì)象的寫(xiě)法,除了可以指定默認(rèn)值以外還可以對(duì)接收參數(shù)的類(lèi)型以及值進(jìn)行設(shè)定

  • 接收參數(shù),直接跟一個(gè)數(shù)據(jù)類(lèi)型,這時(shí)是指定當(dāng)前值只接收某類(lèi)數(shù)據(jù)
props: {
    title: Number
}

這樣寫(xiě)就表示接收的值必須是數(shù)字類(lèi)型,如果傳入其他類(lèi)似的值,Vue會(huì)渲染出來(lái),但是會(huì)在控制臺(tái)報(bào)錯(cuò),此例中,控制臺(tái)會(huì)報(bào)錯(cuò)提示title期望的是一個(gè)Number類(lèi)型,但是接收的卻是一個(gè)String類(lèi)型,Vue可以設(shè)置如下的參數(shù)類(lèi)型:

String
Number
Boolean
Function
Object
Array
Symbol

如果要設(shè)置可以同時(shí)接收多種數(shù)據(jù)類(lèi)型,可以使用數(shù)組的形式

title: [Number, String], // 接收數(shù)字或字符類(lèi)型
  • 接收參數(shù),跟一個(gè)對(duì)象,可以在對(duì)象中對(duì)接收參數(shù)設(shè)置更多參數(shù)

參數(shù)必傳:required設(shè)為true

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

如果做了這種設(shè)定,在cusHeader中如果不傳title值,控制臺(tái)報(bào)錯(cuò)告知缺少必須的參數(shù)

默認(rèn)值:default跟相關(guān)值

如上所述,可以使用default跟一個(gè)值,表示接收參數(shù)的默認(rèn)值,這個(gè)跟隨的值,根據(jù)type類(lèi)型的不同,其值也不同

// 如果是數(shù)字類(lèi)型,默認(rèn)值為數(shù)字
type: Number,
default: 100
...
// 如果是數(shù)組或值對(duì)象,默認(rèn)值也必須是工廠(chǎng)函數(shù)的返回值(如果不用工廠(chǎng)函數(shù),直接把數(shù)組值賦值給default,控制臺(tái)會(huì)報(bào)錯(cuò),提示props的type類(lèi)型為數(shù)組/對(duì)象,必須使用工廠(chǎng)函數(shù)返回)
title: {
    type: Array,
    default: function () {
        return ['這是頭部', '這是主體']
    }
}

自定義驗(yàn)證函數(shù):validator

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

如果在index.vue中模板這么寫(xiě)

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

控制臺(tái)會(huì)報(bào)錯(cuò),提示傳值的類(lèi)型錯(cuò)誤,因?yàn)閜rops進(jìn)行父子組件通信時(shí)默認(rèn)傳值類(lèi)型是String,如果要傳其他類(lèi)型數(shù)據(jù),要使用綁定屬性,做如下處理

// 在data中建立一個(gè)titleNum
titleNum: 12
...
// 模板中改為
<cusHeader :title="titleNum"></cusHeader>

這時(shí)頁(yè)面就能正常渲染,如果把titleNum換成1,控制臺(tái)報(bào)錯(cuò)提示不符合驗(yàn)證條件

普通HTML屬性添加到組件上

如果在組件內(nèi)部的props沒(méi)有設(shè)置對(duì)某個(gè)屬性的接收,那么那個(gè)屬性就會(huì)當(dāng)作普通HTML屬性加載到組件內(nèi)最外層的DOM元素上

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

這時(shí)頁(yè)面會(huì)渲染為

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

替換/合并現(xiàn)有特性

如果組件內(nèi)部的外層元素和要添加的屬性存在相同屬性,那么這個(gè)屬性就會(huì)進(jìn)行替換或合并,例如:

// 模板內(nèi)做如下修改,添加一個(gè)自定義的info屬性
<cusHeader info="header" style="color: red" class="header" :title="titleNum"></cusHeader>
...
// 組件內(nèi)部也一樣做調(diào)整
<template>
    <div class="basis" info="basis" style="color: yellow; font-size: 14px">
        <header>{{title}}</header>
    </div>
</template>

此時(shí)頁(yè)面渲染為

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

我們發(fā)現(xiàn)info屬性被替換,class屬性進(jìn)行了合并,而style屬性被有選擇型合并了(組件內(nèi)的color被父組件的color覆蓋了,而font-size被保留),Vue在處理這些組件傳遞的特性時(shí)大部分會(huì)被直接替換,只有在處理class和style時(shí)才會(huì)有選擇性的合并

子組件向父組件傳遞數(shù)據(jù)

使用props我們可以很容易的從父組件向子組件傳遞數(shù)據(jù),但是如果反過(guò)來(lái),要從子組件向父組件傳遞數(shù)據(jù)時(shí),就要借助自定義事件來(lái)完成:

// 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);
}

渲染完成的頁(yè)面,當(dāng)你點(diǎn)擊頁(yè)面元素,就會(huì)發(fā)現(xiàn),對(duì)應(yīng)數(shù)值發(fā)生了改變,在這里主要關(guān)心這幾個(gè)方面:

// concat是自定義事件,這里表示監(jiān)聽(tīng)自定義事件concat,當(dāng)concat被觸發(fā)時(shí),再觸發(fā)changeNum
@concat="changeNum"; 
...
// 表示觸發(fā)concat事件,并傳值20
this.$emit('concat', 20); 
...
// 在changeNum中接收的參數(shù)val,就是上面從子組件中傳遞而來(lái)
changeNum (val) {
    this.titleNum = val;
}

從上面的例子可以看出,借用自定義事件,我們實(shí)現(xiàn)了從子組件向父組件傳遞數(shù)據(jù)的能力,Vue推崇的單向數(shù)據(jù)流,所以才會(huì)用父組件到子組件通過(guò)props,子組件到父組件使用自定義事件,下圖是官網(wǎng)解釋父子組件通信的圖片:


父子通信

不過(guò)Vue針對(duì)子組件對(duì)父組件通信,提供了一個(gè)語(yǔ)法糖修飾符sync,讓子組件可以通過(guò)事件直接觸發(fā)父組件中值或狀態(tài)的修改,形成類(lèi)似雙向綁定。

<cusHeader :title.sync="titleNum"></cusHeader>
...
// 在header.vue做如下改動(dòng),update:title是顯示觸發(fā)相關(guān)更新
clickEvt () {
    this.$emit('update:title', 30)
}

這時(shí)點(diǎn)擊頁(yè)面元素,我們發(fā)現(xiàn)對(duì)應(yīng)數(shù)值也一樣發(fā)生了變動(dòng),此時(shí)頁(yè)面的數(shù)據(jù)就形成了一種類(lèi)似雙向綁定的交互邏輯:
子組件傳值到父組件,父組件更新數(shù)據(jù),又回傳給子組件,子組件數(shù)據(jù)更新

使用native修飾符觸發(fā)事件

// 比如,你想直接在組件上綁定一個(gè)click事件
<cusHeader :title="titleNum" @click="changeNum"></cusHeader>
...
methods: {
    changeNum () {
        console.log('changeNum');
    }
}
...
// 此時(shí)Vue是會(huì)把@click="changeNum"中的click識(shí)別成一個(gè)自定義事件,如果想正常觸發(fā),需要在組件內(nèi)添加相應(yīng)的事件進(jìn)行觸發(fā)
// 在header.vue中的事件做改造,這時(shí)組件上的click事件就能正常觸發(fā)
clickEvt () {
    console.log('從子組件觸發(fā)');
    this.$emit('click');
}

為了解決上面的問(wèn)題,Vue制定了一個(gè).native的修飾符

<cusHeader :title="titleNum" @click.native="changeNum"></cusHeader>
...
// header.vue把觸發(fā)click的代碼刪除
clickEvt () {
    console.log('從子組件觸發(fā)');
}

這時(shí)組件的click事件就能正常觸發(fā)

子組件之間的通信

上面探討了父組件和子組件,子組件和父組件通信的問(wèn)題,為了解決子組件之間的通信,官方提供了Vuex,內(nèi)容偏多,分開(kāi)討論

使用插槽分發(fā)內(nèi)容

如果我們把新建的組件當(dāng)作普通的HTML元素,在其中引用普通元素

<cusHeader>
    <p>內(nèi)部元素</p>
</cusHeader>

最終渲染會(huì)忽略插在組件內(nèi)部的元素,如果要解決這個(gè)問(wèn)題,需要借助插槽(slot),在header.vue組件做如下改造:

<template>
    <div>
        <header>{{title}}</header>
        <slot>只有在沒(méi)有要分發(fā)的內(nèi)容時(shí)才會(huì)顯示</slot>
    </div>
</template>

這時(shí)頁(yè)面就可以正常渲染出p標(biāo)簽中的內(nèi)容,現(xiàn)在如果刪除p標(biāo)簽,那么頁(yè)面就顯示slot中的文字,這個(gè)地方就會(huì)有一個(gè)問(wèn)題,比如slot中這段文字,你希望是按需展示出來(lái),而不是當(dāng)沒(méi)有插槽內(nèi)容就直接展示,這時(shí)可以借助作用域插槽來(lái)解決這個(gè)問(wèn)題

// header.vue做如下修改,把插槽的默認(rèn)值放到text(自定義屬性,可以是任意值)屬性中
<slot text="只有在沒(méi)有要分發(fā)的內(nèi)容時(shí)才會(huì)顯示"></slot>
...
// index.vue做如下修改,在組件引用的地方調(diào)用這個(gè)插槽存儲(chǔ)的值
// 下面slot-scope可以用在p標(biāo)簽是在Vue 2.5+以上的版本
// prop也可以是任意值
<cusHeader>
    <p slot-scope="prop">{{prop.text}}</p>
</cusHeader>

通過(guò)這種方式,我們就可以在組件內(nèi)定義一些默認(rèn)信息,同時(shí)可以按需進(jìn)行加載,形成來(lái)類(lèi)似父組件通過(guò)props向子組件傳值的能力,此時(shí)綁在組件插槽的內(nèi)容只能在組件調(diào)用的域中可用。

slot最初被設(shè)定為備用內(nèi)容,但當(dāng)和name屬性一并使用時(shí),其就具備了分發(fā)內(nèi)容的能力

// 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>這個(gè)會(huì)放到默認(rèn)body中</p>
    <h1 slot="footer">這是底部</h1>
</cusHeader>
...
// 頁(yè)面渲染為
<div>
    <div class="header">
        <h1>這是頭部</h1>
    </div> 
    <div class="body"> 
        <p>這個(gè)會(huì)放到默認(rèn)body中</p> 
    </div> 
    <div class="footer">
        <h1>這是底部</h1>
    </div>
</div>

使用具名插槽在組件設(shè)計(jì)上,會(huì)使組件更加靈活

動(dòng)態(tài)組件

官網(wǎng)上提到的動(dòng)態(tài)組件并不是動(dòng)態(tài)加載的組件,而是可以動(dòng)態(tài)切換的組件,是指利用保留的component標(biāo)簽,在結(jié)合is特性,對(duì)某一處的加掛點(diǎn)動(dòng)態(tài)切換組件,在這里我們實(shí)現(xiàn)一個(gè)相對(duì)復(fù)雜點(diǎn)tab頁(yè)切換

// 在hello-world目錄下,新建三個(gè).vue文件,分別表示三個(gè)新聞塊的內(nèi)容
game.vue
sport.vue
news.vue
...
// game.vue內(nèi)添加如下內(nèi)容
<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添加如下內(nèi)容
<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添加如下內(nèi)容
<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引入添加的三個(gè)組件
import gameList from './game.vue'; // 引入游戲列表
import newsList from './news.vue'; // 引入新聞列表
import sportList from './sport.vue'; // 引入運(yùn)動(dòng)新聞列表
...
// index.vue的components注冊(cè)這三個(gè)組件
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';
    }
}

當(dāng)然這個(gè)例子是有點(diǎn)不合適的,這三個(gè)列表應(yīng)該是復(fù)用一個(gè)列表組件,只不過(guò)根據(jù)不同數(shù)據(jù)源展示不同,我們?cè)邳c(diǎn)擊進(jìn)行切換時(shí),會(huì)發(fā)現(xiàn)控制臺(tái)會(huì)一直輸出我們?cè)O(shè)置在created中的log,證明在切換組件時(shí),組件每一次都是重新加載,這個(gè)從性能上考慮是需要進(jìn)行優(yōu)化的,使用Vue盡可能的復(fù)用重復(fù)代碼

使用keep-alive標(biāo)簽保留組件狀態(tài)以及靜態(tài)資源,避免重新渲染

// 在動(dòng)態(tài)加載組件的標(biāo)簽外添加keep-alive標(biāo)簽
<keep-alive>
    <component :is="showTabView"></component>
</keep-alive>

這時(shí)如果我們?cè)冱c(diǎn)擊頁(yè)面進(jìn)行組件切換,會(huì)發(fā)現(xiàn)各個(gè)組件created方法,只會(huì)執(zhí)行一次,再次切換回相關(guān)組件時(shí),created就不會(huì)執(zhí)行,不過(guò)這樣雖然達(dá)到了組件的復(fù)用保留了靜態(tài)資源,而且也不會(huì)進(jìn)行也避免了不必要的渲染,但是要如何知道這個(gè)組件發(fā)生了切換或值停用?Vue針對(duì)keep-alive的特性,添加了兩個(gè)鉤子函數(shù)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');
}

這時(shí)我們點(diǎn)擊頁(yè)面元素,進(jìn)行組件切換時(shí),發(fā)現(xiàn)控制臺(tái),會(huì)按上一個(gè)組件的deactivated切換后組件的activated的順序執(zhí)行,這樣我們?cè)谶M(jìn)行組件切換時(shí)依然可以通過(guò)這兩個(gè)鉤子函數(shù),動(dòng)態(tài)控制組件的加載

include/exclude控制緩存組件

默認(rèn)Vue會(huì)對(duì)keep-alive包裹下的所有切換組件進(jìn)行緩存,但在某些情況下,我們可能只打算對(duì)部分組件進(jìn)行緩存,這時(shí)就要借助include和exclude進(jìn)行細(xì)化控制

// 字符形式:這么就表示緩存gameList,newsList兩個(gè)模塊,注意“,”后面不能有空格,要不Vue識(shí)別有誤,模塊名直接使用,不需添加引號(hào)
<keep-alive include="gameList,newsList">
...
// 數(shù)組形式:這么也表示緩存gameList和newsList兩個(gè)模塊,模塊名要用字符形式表示
<keep-alive :include="['gameList', 'newsList']">
...
// 正則表達(dá)式:模塊名直接使用無(wú)需添加引號(hào)
<keep-alive :include="/gameList|newsList/">

exclude使用方式和include一致,區(qū)別在于exclude表示除此之外,與include功能相反

組件的設(shè)計(jì)

官方文檔提示我們?cè)谠O(shè)計(jì)組件時(shí),要考慮下面內(nèi)容:

  • Prop:接收從組件外部傳遞數(shù)據(jù);

  • 事件: 從組件內(nèi)觸發(fā)組件外部的方法達(dá)到從組件內(nèi)部向外傳遞數(shù)據(jù)的能力;

  • 插槽: 允許從組件外部將額外的內(nèi)容組合在組件中。

使用ref獲取子組件引用

ref是用來(lái)獲取元素或子組件的引用信息,針對(duì)元素ref獲取就是元素對(duì)應(yīng)的Dom對(duì)象,可以使用原聲js寫(xiě)法獲取相關(guān)屬性,針對(duì)組件獲取的是一個(gè)VueComponent

// 在組件和p標(biāo)簽都添加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的引用,如果你把這個(gè)引用放到created中是無(wú)法生效的,因?yàn)?refs只有在元素渲染完成才會(huì)被添加到實(shí)例對(duì)象上
// 如果用在普通標(biāo)簽上,獲取其引用可以使用原生js屬性
mounted () {
    console.log(this.$refs);
    console.log(this.$refs.info.className);
}

異步組件

異步組件要結(jié)合webpack來(lái)進(jìn)行使用,可以參看webpack中的介紹

遞歸組件

遞歸組件和遞歸調(diào)用類(lèi)似,是組件反復(fù)套用組件本身,當(dāng)符合某個(gè)條件時(shí)不再引用

// 在header-world目錄下,建立list.vue,在其中輸入如下內(nèi)容
<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: '這時(shí)遞歸內(nèi)部調(diào)用的數(shù)據(jù)',
                listFlag: false
            }
        },
        computed: {
            closeList () {
                return this.flag;
            },
            listInfo () {
                return this.info;
            }
        }
    }
</script>
...
// index.vue中引入list.vue,并引用
<script>
    import list from './list.vue'; // 引入遞歸組件
...
    components:{
        list
    }
...
// 模板中調(diào)用這個(gè)組件,info,showFlag都是之前設(shè)定的值
<list :info="info" :flag="showFlag"></list>

這時(shí)頁(yè)面就會(huì)渲染為:

<div class="list">
    <p>Hello vue!</p> 
    <p><div class="list"><p>這時(shí)遞歸內(nèi)部調(diào)用的數(shù)據(jù)</p> <!----></div></p>
</div>

我們可以看到,我們?cè)诮M件內(nèi)部復(fù)用了組件本身,在這里有幾個(gè)點(diǎn)需要注意

  • 組件內(nèi)部的name屬性需要添加,要不在組件內(nèi)使用list組件是編程出錯(cuò)的
  • 組件遞歸的終止條件一定要明確,要不組件會(huì)陷入一種死循環(huán)

遞歸組件非常適合去開(kāi)發(fā)那種不確定有幾層循環(huán)的組件,比如多級(jí)菜單,后續(xù)在實(shí)例開(kāi)發(fā)部分會(huì)有案例

組件之間的循環(huán)引用

遞歸組件可以認(rèn)為成是相同組件之間的循環(huán)調(diào)用,當(dāng)是不同組件形成類(lèi)似遞歸組件的調(diào)用,就會(huì)出現(xiàn)組件調(diào)用異常的情況

// 在項(xiàng)目目錄新建兩個(gè)文件moduleA.vue和moduleB.vue
// moduleA.vue內(nèi)容如下
<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內(nèi)容如下
<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并添加相關(guān)數(shù)據(jù)
<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: '社會(huì)新聞'},
                 {title: '社會(huì)民生'}
               ]
            }, {title: '游戲新聞'}, {title: '體育新聞'}],
    }
    ...
    components: {
        moduleA
    }

這時(shí)控制臺(tái)會(huì)報(bào)錯(cuò),提示你在moduleB.vue文件中module-a沒(méi)有注冊(cè),我們回看整個(gè)調(diào)用邏輯會(huì)發(fā)現(xiàn),moduleA組件中引用了moduleB組件,moduleB組件又引用了moduleA組件,并且根據(jù)我們?cè)斓臄?shù)據(jù)能看出,moduleA和moduleB也彼此產(chǎn)生了調(diào)用,這個(gè)時(shí)候兩個(gè)組件形成了類(lèi)似遞歸組件的那種調(diào)用的情況,只不過(guò)區(qū)別在于遞歸組件調(diào)用自己,是一定存在,但是moduleA和moduleB就無(wú)法保證在彼此調(diào)用時(shí)一定存在,而且這個(gè)問(wèn)題按官方的說(shuō)法,只有在使用webpack等模塊管理工具才會(huì)有這問(wèn)題,如果使用Vue.component進(jìn)行注冊(cè),Vue會(huì)直接處理這個(gè)沖突,要解決此問(wèn)題,就需要借助beforeCreate這個(gè)鉤子函數(shù),我們?cè)趍oduleA(因?yàn)閙oduleA針對(duì)moduleB是入口組件)

// moduleA.vue中進(jìn)行相關(guān)修改,保證moduleB不會(huì)在moduleA之前引用
<script>
    export default {
        data () {
            return {
                info: 'from moduleA'
            }
        },
        props: ['listData'],
        beforeCreate () {
            this.$options.components.moduleB = require('./moduleB.vue').default;
        },
        components: {
        }
    }
</script>

經(jīng)過(guò)上面的改造,頁(yè)面就可以正常展示,以上就是Vue的基礎(chǔ)內(nèi)容,下一章討論Vue其他的一些混合特性,以及過(guò)渡和動(dòng)畫(huà)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1.安裝 可以簡(jiǎn)單地在頁(yè)面引入Vue.js作為獨(dú)立版本,Vue即被注冊(cè)為全局變量,可以在頁(yè)面使用了。 如果希望搭建...
    Awey閱讀 11,096評(píng)論 4 129
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對(duì)于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,079評(píng)論 0 29
  • Vue 實(shí)例 屬性和方法 每個(gè) Vue 實(shí)例都會(huì)代理其 data 對(duì)象里所有的屬性:var data = { a:...
    云之外閱讀 2,241評(píng)論 0 6
  • 從前的你,愿意跑很遠(yuǎn)的路,來(lái)看我,買(mǎi)我喜歡的東西給我,現(xiàn)在,你總是一句話(huà)對(duì)不起或讓你委屈了
    薇薇安1802785閱讀 254評(píng)論 0 0
  • 每個(gè)地方都有不一樣的風(fēng)景,身處一個(gè)地方久了,便會(huì)有一些想出去走走的心思。 趕巧~遇上了小朋友畢業(yè)后的閑暇時(shí)間,擠著...
    凈染閱讀 776評(píng)論 9 7