Vue2基础

94 阅读24分钟

Vue配置

配置开发者传递给Vue构造函数的参数,可以传入以下配置:

  • name

    组件的名称

    可用于实现组件递归

  • data

    可能会使用到的数据

  • methods

    可能会使用到的方法

  • template

    配置模板

  • render

    渲染方法,用于生成虚拟DOM树

  • el

    挂载的目标元素,元素内容也可作为模板存在

  • computed

    计算属性

  • components

    局部注册组件

  • props

    声明组件的属性

  • router

    配置vue路由

  • store

    配置vuex仓库

  • hooks

    钩子函数,具体包含哪些钩子函数请看《组件生命周期》小节

  • mixins

    混入组件配置

  • name

    组件名称,可以实现组件递归

  • watch

    观察某个响应式数据的变化,一旦该数据发生变化,就会自动运行一段函数

Vue配置中的this,均指向该构造函数创建出来的Vue实例

Vue指令

  • v-bind

    用于绑定属性,可简写为:

    被绑定的属性,属性值就是一个JS表达式

    例如:

    <img v-bind:src="JS表达式" alt="">
    

    可简写为:

    <img :src="JS表达式" alt="">
    

    对于一些特殊的属性,经过v-bind绑定后,其属性值就可以不是字符串:

    <div :style="{
    	width: '100px',
    	height: '100px'
    }"></div>
    
  • v-for

    用于循环数组,也可以用于循环数字

    例如:

    循环数组:

    <span v-for="(item, i) in 数组">
    	{{item}}
    </span>
    

    循环数字:

    <span v-for="num in 10">
    	{{num}}
    </span>
    <!-- 从 1 遍历到 10 -->
    

    Vue建议:在元素中使用v-for指令时,最好再给被循环的元素绑定一个属性key,key的值建议选择一个唯一且稳定的值,这有助于提高Vue的重渲染效率

    <span v-for="(item, i) in 数组" :key="唯一且稳定的值">
    	{{item}}
    </span>
    
  • v-on

    用于绑定事件,可简写为@

    例如:

    <button v-on:click="事件处理函数"></button>
    

    可简写为:

    <button @click="事件处理函数"></button>
    
  • v-if

    控制是否生成虚拟DOM节点

    可以搭配v-else-if和v-else一起使用

    v-if.png

  • v-show

    控制元素显示隐藏

    v-show.png

    v-if与v-show的区别:

    v-if可以控制是否生成vnode,也就间接控制了是否生成对应的真实dom

    当v-if为true时,会生成对应的vnode,并生成对应的dom元素;当v-if为false时,不会生成对应的vnode,自然也不会生成对应的dom

    v-show始终会生成vnode,也就间接导致始终会生成dom,它只是控制dom的display属性

    当v-show为true时,不做任何处理;当v-show为false时,生成的dom的display属性为none

    使用v-if可以减少树的结点数量和渲染量,但会导致树不稳定;使用v-show可以保持树的稳定,但不能减少树的节点数量和渲染量

    而vue的渲染效率取决于两个方面:树越稳定,渲染效率越高;树的节点数量越少,渲染效率越高

    因此,在实际开发中,对于显示状态会频繁变化的情况应该使用v-show,以保持树的稳定,显示状态变化较少的情况应该使用v-if,以减少树的节点数量和渲染量

  • v-slot

    向指定的插槽传递内容

  • v-html

    设置vnode对应真实dom的innerHTML的内容

  • v-model

    双向绑定表单元素的数据

这些指令的属性值,都会被Vue视为JS表达式

核心概念

注入

Vue会将构造函数的参数中的某些配置属性提取到Vue实例中,这个过程称之为注入

以下配置中的内容都会被注入到Vue实例中:

  • data
  • computed
  • methods
  • props

注入.png

Vue实例中的以$开头的属性,是Vue提供给我们使用的一些实用方法和属性

以_开头的属性,是Vue不希望我们使用的方法和属性,这些是Vue内部使用的属性

在Vue模板中可以直接使用Vue实例中的成员

例如模块中的{{}}内部,以及模块中的指令部分,都可以直接使用Vue实例中的成员

Vue的数据响应式的特点,是在配置被注入完成后才具有的

虚拟DOM树

直接操作真实的DOM会带来严重的效率问题,因此Vue使用虚拟DOM的方式来描述要渲染的内容

在传递给Vue构造函数的el配置中,它所引用的元素,其自身以及其内部的所有内容均会被当做模板

模板本质上就是一个字符串,它是不会直接渲染到页面中的

Vue中内置了一个编译器,该编译器会将模板编译为一棵虚拟DOM树,之后Vue会根据这棵虚拟DOM树来生成一棵真实的DOM树,最终页面所呈现的是真实DOM树中的内容

虚拟DOM树.png

当模板中使用到的数据发生变化时,Vue会重新生成一棵虚拟DOM树

Vue会将这棵新的虚拟DOM树与数据变化之前的虚拟DOM树进行对比,找出它们之间的差异,并把该差异应用到真实DOM中,而不是更新整棵真实DOM树,因此提高了效率

重新渲染.png

Vue的配置中包含一个render配置方法,该方法的返回值,就是虚拟DOM树

new Vue({
    data: {
        title: "Hello World",
        author: "张三"
    },
    render(h) {
        return h("div", [
            h("div", `第一个vue应用:${this.title}`),
            h("p", `作者:${this.author}`)
        ]);
    }
});

每当界面中使用到的数据变化时,render方法都会重新运行一次,因此可以得到一棵全新的虚拟DOM树

由于render方法使用起来比较困难,因此Vue提供给开发者使用模板来间接生成虚拟DOM

Vue会根据模板编译成为render方法,该render方法返回的内容就是与模板相应的虚拟DOM树

模板并不是只能来源于HTML元素,它也可以作为一个配置直接提供给Vue:template配置,它的内容是一个字符串,该字符串也可以作为模板

若配置中显示地提供了render方法,则直接根据该方法得到虚拟DOM,而不管el所引用的元素以及template配置

若配置中没有显式提供render方法时,Vue就会寻找配置中的template,如果有就将其内容作为模板,若没有则将配置中的el所引用元素的outerHTML作为模板,然后将模板编译为render方法,并通过调用render方法生成对应的虚拟DOM树

因此,虚拟DOM树一定需要通过render方法得到

<!DOCTYPE html>
<div id="app">
    <h1>第一个vue应用:{{title}}</h1>
    <p>作者:{{author}}</p>
</div>

<script>
    var vm = new Vue({
        el: "#app",
        template: `
            <div id="app">
                <h1>第一个vue应用:{{title}}</h1>
                <p>作者:{{author}}</p>
            </div>
        `,
        data: {
            title: "Hello World",
            author: "张三"
        },
        render(h) {
            return h("div", [
                h("div", `第一个vue应用:${this.title}`),
                h("p", `作者:${this.author}`)
            ]);
        }
    });
</script>

优先级:render > template > el

挂载

有了虚拟DOM树,就能够生成对应的真实DOM树

将生成的真实DOM树替换到页面中的某个元素位置,这个过程称之为挂载

可以通过两种方式来指定挂载的位置:

  1. el: "css选择器"
  2. vue实例.$mount("css选择器")

完整流程

完整流程.png

组件

组件的出现是为了达到下面两个目的:

  1. 降低整体复杂度,提升代码的可读性和可维护性
  2. 提升局部代码的可复用性

一般情况下,一个组件就代表着页面中的一小块区域,组件中通常包含三个模块:

  1. 功能(JS代码)
  2. 内容(模板代码)
  3. 样式(CSS代码)

要在组件中包含样式,需要构建工具的支持

创建组件

一个组件其实就是一个对象

var myComp = {
    template: `...`,
    data(){
		return {
       		...
        }
    },
    methods: {
        ...
    }
}

该配置对象和传递给Vue构造函数的参数配置对象非常类似,但也有些许不同:

  1. 组件配置中没有el配置,模板只能使用template配置进行传递(或者直接书写render方法)

  2. 组件配置中的data配置需要写为一个函数,函数的返回值才是真正会使用到的数据

    每当其他组件使用到该组件时,就会调用一次data函数,并返回一个独立的数据,保证了不同数据之间不会相互影响

在 Vue2.x 版本中,要求组件模版中的根元素只能有一个

注册组件

创建完组件后,其他地方需要注册该组件,才能使用它

组件注册分为局部注册和全局注册

局部注册

局部注册的组件,只能在一个另一个组件中使用,并且在使用之前,还需要先在自己的components配置中进行引用

var myComp = {...};

var vm = new Vue({
	components: {
        MyComp: myComp
    },
    template: `
    	<div id="app">
        	<MyComp></MyComp>
        </div>
    `
});

也可以通过render进行局部注册,使用render进行局部注册时就不需要在components中引入组件:

var myComp = {...};

var vm = new Vue({
	render: (h) => h(myComp)
});

上面的代码相当于:

var myComp = {...};

var vm = new Vue({
	components: {
        MyComp: myComp
    },
    template: `<MyComp></MyComp>`
});
全局注册

全局注册的组件,在整个应用的任何地方都可以使用

使用下面的方法对组件进行全局注册:

Vue.component("组件名称", 组件对象);

全局注册的组件,可以直接使用,无需再向配置对象中通过components配置对使用的组件进行引用

var myComp = {...};

Vue.component("MyComp", myComp);

var vm = new Vue({
    template: `
        <div id="app">
        	<MyComp></MyComp>
        </div>
    `
});

建议只对通用性很强的组件进行全局注册,因为对于全局注册的组件,无法做到对它的按需加载,即全局注册的组件会使用存在于打包结果中,不利于打包结果优化

组件的命名

组件的命名通常有两种:

  • 短横线命名
  • 大驼峰命名
new Vue({
    components: {
        "my-comp": comp1,		// 短横线命名方式
        MyComp: comp2			// 大驼峰命名方式
    }
});

推荐使用大驼峰命名方式

向组件传递数据 —— props

常见的向组件传递数据的方式是利用组件的props配置

在使用注册的组件时,通过给组件加入一些属性来传递数据给组件

var myComp = {
    props: {
        title: {
            required: true,			// 属性必须传递	
            type: String,			// 属性的类型为字符串
            default: "标题"			// 属性的默认值为"标题"
        }
    },
    template: `<h1>{{title}}</h1>`
}

new Vue({
    template: `
    	<div id="app">
        	<MyComp title="标题1"></MyComp>
        	<MyComp title="标题2"></MyComp>
        	<MyComp title="标题3"></MyComp>
        </div>
    `,
    components: {
		MyComp: myComp
	}
});

注意:

  • 组件不可以更改自己的props中的数据,因为这些数据是父组件传递给自己的,若实在是要更改,则应该向父组件发出一个通知,让父组件对该数据进行更改

  • 当属性的默认值是一个引用值时,应该将引用值设置为函数的返回值

    export default {
        props: {
            list: {
                type: Array,
                default: () => []
            }
        }
    }
    

vue-cli

vue-cli是一个脚手架工具,用于搭建vue工程

vue-cli内部使用其实是webpack,并往webpack中加入了许多插件和加载器,例如:

  • babel
  • webpack-dev-server
  • eslint
  • postcss
  • less-loader

全局安装@vue/cli

npm install -g @vue/cli

创建工程

vue create Project-Name

SFC

单文件组件,Single File Component,即一个文件就包含了一个组件所需的全部代码

Vue中的一个.vue文件就是一个SFC

<template>
	<!-- 组件模板 -->
</template>

<script>
export default {
    // 组件配置
}
</script>

<style>
	/* 组件样式 */
</style>

预编译

当vue-cli进行打包时,会直接将SFC中的<template></template>的内容编译为render函数,之后运行打包结果时,就可以直接执行render函数来生成虚拟DOM树,这叫做模板预编译

模板预编译带来了以下好处:

  1. 不需要在运行时才将模板编译为render函数,提高了运行效率
  2. 打包结果中不需要加入编译模版的代码以及模版代码,减小了打包体积

预编译.png

注意:预编译只对使用元素形式的template模版有效,对处于组件配置对象中的template属性中的模版不生效

作用域样式

Vue借鉴了css module的思想,当在组件中的style元素中加上scope后,style就变为了带有作用域的style

<style scoped>
	/* 组件样式 */
</style>
实现原理

当加入了scope后,Vue会给该组件中的元素都加上一个自定义属性data-v-[hash:8],并且会给style中的每个样式中的元素选择器和类选择器组合上一个属性选择器,属性选择器匹配的就是该元素添加的自定义属性data-v-[hash:8]

如果组件内部没有使用其他组件,则style中的样式就只可能匹配自己组件中的元素,而不可能匹配其他组件中的元素

如果组件(父组件)中使用到了其它组件(子组件),则子组件的根元素也会加上自定义属性data-v-[hash:8],因此父组件的作用域样式可以应用在子组件的根元素上

作用域样式.jpg

计算属性

计算属性的完整写法:

computed: {
	propName: {
        get(){
            // getter
        },
		set(val){
            // setter
        }
    }	
}

只包含getter的计算属性:

computed: {
	propName(){
        // getter
    }	
}

computed和methods的区别:

  • 计算属性中包含了getter和setter,获取计算属性实际上是调用计算属性的getter方法

    计算属性的getter和setter方法参数固定,getter没有参数,setter只有一个参数

    方法的参数数量不限

  • 计算属性有缓存,vue会记录计算属性中所使用到的依赖,并缓存计算属性的返回结果,当计算属性的依赖全都没有改变的情况下,再次使用计算属性使用的其实是缓存的结果,当计算属性的依赖发生了改变时,计算属性才会重新进行计算

    vue只会把响应式数据视为依赖

    方法没有缓存,每次调用时方法都会重新运行一次

  • 计算属性通常是根据已有数据得到新的数据,并且在得到数据的过程中不建议使用异步、获取当前时间、随机数等操作

  • 含义上,计算属性只是一个数据,可以读取也可以赋值

    方法则是一个操作,用于处理一些事情

组件事件

父组件传递给子组件的数据,子组件不可以对其进行更改,因为这违背了“谁的数据谁负责”的原则

当子组件需要更新数据时,应该向父组件发出一个通知,让更改任务由父组件来完成

子组件使用$emit向父组件发出通知,这个过程称之为抛出事件,抛出事件时子组件可以传递一些信息给父组件

父组件通过监听相应的事件来接收来自子组件发给自己的通知,并对相应数据进行处理

组件事件.png

例如:

<!-- 父组件 -->
<template>
<Child @event="handleEvent" />
</template>

<script>
import Child from "./Child.vue"

export default {
    components: {
        Child
    },
    methods: {
        handleEvent(a, b, c){	// 对于在组件上注册的事件,所有参数均是由子组件传递过来的
			// a = 1, b = 2, c = 3
        }
    }
}
</script>
<!-- 子组件Child -->
<template>
<button @click="handleClick">Click</button>
</template>

<script>
export default {
    methods: {
        handleClick(e){		// 对于在元素上注册的事件,第一个参数为事件对象
			this.$emit("event", 1, 2, 3);
        }
    }
}
</script>

绑定事件处理函数的位置也可以是一个函数调用(但不是真的调用,只是看上去是在调用)

例如:

<template>
<button @click="handleClick(1, $event)">Click</button>
</template>

<script>
export default {
    methods: {
        handleClick(num, e){
            // 和$event同一位置的参数就是事件对象
            // num = 1, e为事件对象
        }	
    }
}
</script>

事件修饰符

针对dom的原生事件,vue提供了多种事件修饰符以简化代码

常见的事件修饰符有:

  • .stop

    用于事件冒泡

  • .prevent

    用于阻止事件默认行为

  • .capture

    事件在捕获阶段运行

例如:

<a @click.prevent="handleClick"></a>

除了针对dom的事件修饰符,vue还提供了针对键盘事件的按钮修饰符和系统修饰符,例如:.enter.alt,以及针对表单事件的修饰符,例如:.lazy.number.trim

插槽

使用插槽可以让父组件控制子组件的某一部分区域

插槽的简单用法

在子组件中需要被控制的区域加入一个Vue的内置组件slot

父组件在应用子组件时,使用带有结束标记的组件标签形式,在标签的内部写入具体的内容,之后,子组件标签中的内容会“替换”掉子组件中的slot

插槽.png

具名插槽

组件内部可以多次使用slot,为了区分不同的slot,需要给不同的slot起名字

在父组件中,需要在不同slot的对应内容的外部包裹一个template组件,并通过为template组件加入v-slot指令来指定控制哪个slot,v-slot后面跟随的就是要控制的slot的名称

slot的默认名称为default,而对于默认名称的slot,父组件中可以不使用template组件包裹该slot对应的内容(即插槽的简单用法)

具名插槽.png

v-slot可以简写为#

扩展

  • slot内部可以书写一些默认的内容,如果父组件在使用子组件时没有为该插槽指定内容,则该插槽就会将默认内容显示出来

    <template>
    <div class="container">
        <slot>
    		<span>default display content</span>
        </slot>
    </div>
    </template>
    
  • 父组件中为子组件传递的插槽内容中使用的数据仍是父组件自己的

    换句话说,父组件为子组件传递的插槽内容之后并不是直接将子组件中的<slot>替换掉

    上面的示意图只是为了方便理解插槽的效果

    例如:

    父组件:

    <template>
    <div class="container">
        <Child>
        	<ul>
                <li v-for="item in list"></li>
    	    </ul>
        </Child>
    </div>
    </template>
    
    <script>
    import Child from "./Child.vue";
        
    export default {
        components: {
            Child
        },
        data(){
            return {
                list: [...]			// 插槽中使用的list是父组件自己的list
            }
        }
    }
    </script>
    

    子组件:

    <template>
    <div class="container">
        <slot></slot>
    </div>
    </template>
    
    <script>
    export default {
        data(){
    		return {
                list: [...]				// 子组件的list并不会被使用到
            }
        }
    }
    </script>
    
  • 子组件可以循环利用父组件所指定的插槽内容

    父组件:

    <template>
    <div class="container">
        <Child>
        	<div>slot content</div>
        </Child>
    </div>
    </template>
    
    <script>
    import Child from "./Child.vue";
        
    export default {
        components: {
            Child
        }
    }
    </script>
    

    子组件:

    <template>
    <div class="container">
    	<div v-for="num in 10">
            <slot></slot>
        </div>
        <!-- 将会生成10个【<div>slot content</div>】 -->
    </div>
    </template>
    

    需要注意的是,不允许直接对<slot>进行循环

路由

vue-router 官网:router.vuejs.org/zh/

安装路由插件

npm i -D vue-router

应用路由插件

// main.js

import Vue from "vue"
import VueRouter from "vue-router"

Vue.use(VueRouter);

var router = new VueRouter({
    // 路由配置
});

new Vue({
    router
}).$mount("#app");

为Vue应用完vue-router后,Vue中就多出来了两个全局组件:RouterLinkRouterView,同时vue-router还会给Vue的原型上增加两个属性:$route$router

$route中包含有与当前url相关的各种信息,$router用于实现函数式跳转(与RouterLink的功能一样)

路由配置

应用路由时,需要传递一个配置对象给vue-router所导出的构造函数,该对象中包含以下几个配置项:

  • mode

    路由模式

    路由模式决定了:

    ① 从页面url中的哪个部分获取匹配内容

    ② 改变页面url时应该改变url的哪个部分

    vue-router提供了三种路由模式:

    ① hash

    ​ 以页面url的hash部分作为匹配内容,改变时也只改变url的hash部分

    ​ 改变hash不会导致页面刷新

    ② history

    ​ 以页面url的path部分作为匹配内容,改变时也只改变url的path部分

    ​ Vue使用了H5的history api来做到改变url的path但不导致页面刷新

    ​ 相比hash,history的兼容性较差

    ③ abstract

    ​ 通常用于非浏览器环境中时

  • routes

    路由匹配规则

    vue-router会将该配置中的每一个元素的path属性与mode对应内容进行匹配

  • meta

    附加信息

    附加信息会出现在$route对象中

  • base

    设置路由的基础路径,其默认值为"/"

    vue-router在对url与路由规则两者进行路由匹配时,会将url中的base部分直接忽略,对剩余部分进行匹配

    路由匹配成功后,就会在匹配的路由的url的mode部分前面加上base

import Home from "@/views/Home.vue"
import Blog from "@/views/Blog.vue"
import BlogDetail from "@views/BlogDetail.vue"
import NotFound from "@views/NotFound.vue"

var router = new VueRouter({
    mode: "history",
    routes: [
        {
            path: "/",
            component: Home
        },
        {
            path: "/blog",
            component: Blog
        },
        {
            path: "/blogDetail",
            component: BlogDetail
        },
        {
            path: "*",					// 当前面的配置都没有匹配成功时,则会匹配该项
            component: NotFound
        }
    ]
});
RouterView

RouterView和slot一样,是一个占位组件,当vue-router匹配成功某个组件时,就会将该组件渲染到RouterView所在的位置

RouterLink

Vue在渲染时,会将RouterLink渲染为a元素,并且阻止了该a元素的默认行为,因此通过点击RouterLink来实现无刷新的页面切换

例如:

<RouterLink to="/blog">文章</RouterLink>

当mode为hash时,Vue会将RouterLink渲染为下面的a元素:

<a href="#/blog">文章</a>

当mode为history时,Vue会将RouterLink渲染为下面的a元素:

<a href="/blog">文章</a>

路由匹配

假设路由模式采用的是history

用户首次访问页面时,vue-router会根据页面url的path部分,与路由匹配规则进行匹配,匹配成功后,把相应的组件渲染到RouterView所在的位置

路由匹配.png

若用户点击了导航栏中的文章按钮(即点击了<RouterLink to="/blog">文章</RouterLink>),vue-router会将path更改为"/blog",然后根据新的path匹配路由规则,并将匹配成功的组件渲染到RouterView所在的位置

路由切换.png

激活状态

点击切换组件时,往往也需要改变导航栏中按钮的状态,例如:增加或删除按钮的激活状态

vue-router考虑到了这一点,在默认情况下,vue-router会将页面url中的mode部分与RouterLink的to属性进行匹配

若mode是以to为开头的,则认为匹配,于是将对应的RouterLink渲染为带有类名router-link-active的a元素

若mode与to完全相等,则认为精确匹配,于是将对应的RouterLink渲染为带有类名router-link-exact-activerouter-link-active的a元素

例如:

假设url中的mode部分为/blog

to类名
/router-link-active
/blogrouter-link-active router-link-exact-active
/about

可以为RouterLink添加一个属性exact-path,对于该属性为true的RouterLink,只有在精确匹配的情况下,才会为其添加类名router-link-active

默认RouterLink是不包含该属性的

例如:

假设url中的mode部分为/blog/detail

toexact-path类名
/true
/blogfalserouter-link-active
/abouttrue
/blog/detailtruerouter-link-active router-link-exact-active

另外,可以通过设置RouterLink的active-class属性来更改匹配时添加的类名,通过exact-active-class属性来更改精确匹配时添加的类名

命名路由

使用命名路由可以解除系统与路径之间的耦合

var router = new VueRouter({
    routes: [
        {
            name: "home",
            path: "/",
            component: Home
        },
        {
            name: "artical",
            path: "/blog",
            component: Blog
        }
    ]
});
<!-- 向to属性传递路由信息对象RouterLink会根据你传递的信息以及路由配置生成对应的路径 -->
<RouterLink :to="{ name: 'artical' }">foo</RouterLink>

动态路由

动态路由用于匹配mode满足条件的多个url,而非像静态路由那样只能匹配到固定的mode

var router = new VueRouter({
    mode: "history",
    routes: [
        {
            name: "artical",
            path: "/blog/cate/:categoryId",
            component: Blog
        }
    ]
});

匹配情况:

url中的path是否匹配
/blog/cate/1
/blog/cate/2
/blog/cate/1/2

可以在组件实例的原型上的$routes.params.categoryId来获取动态部分的实际内容

编程式导航

可以使用vue-router向Vue原型上注入的属性$router实现函数形式的跳转

this.$router.push("目标mode"); 		// 普通跳转

this.$router.push({ 				// 命名路由跳转
    name: "..."
});

this.$router.go(-1); 				// 回退,类似于 history.go(-1)

导航守卫

当页面首次渲染以及或者导航切换的前一刻,会触发导航前置守卫,用法如下:

var router = new VueRouter({});

router.beforeEach((to, from, next)=>{
    // ...
});

export default router;

to为目标导航的$route对象,from为当前(还没发生切换)的$route对象

只有调用了next方法才能真正发生导航切换:如果next是无参调用的,则将会进入to对应导航;若是传参调用的,则会根据参数进入相应导航

next中传入的参数和编程式导航中的this.$router.push的参数一致

导航守卫中除了有beforeEach外,还有afterEach,当导航切换完成后会立即触发afterEach,afterEach中也包含一个回调函数,不过该回调函数中没有next参数,to和from参数不变

var router = new VueRouter({});

router.afterEach((to, from)=>{
    // ...
});

export default router;

watch

利用watch配置,可以观察到某个数据的变化

一旦被观察的数据发生了变化,就会运行相应的函数

export default {
    watch: {
        $route(newVal, oldVal){				// 属性名为要观察的数据名称
            // this.$route变化时运行
        }
    }
}

上面的只是简写的形式,完整的形式如下:

export default {
    watch: {
        $route: {
            handler(newVal, oldVal){},	// this.$route变化时运行
            deep: false,				// 是否监听$route内部的属性的变化,默认为false
            immediate: false			// 是否在一开始就执行一次handler,默认为false
        }
    }
}

当路由切换发生后,vue-router会将Vue实例中的route属性赋值为一个新对象,因此watch才能在deep为false的情况下也能观察到\router的改变

观察对象中某个属性的变化:

export default {
    watch: {
        ["$route.params"](newVal, oldVal){				// 属性名为要观察的数据名称
            // this.$route.params变化时运行
        }
    }
}

watch配置中只支持使用.操作符来观察某个对象内部的某个属性,而不支持使用[]操作符

例如:

export default {
    watch: {
        ["$route['params']"](newVal, oldVal){ }
    }
}

上面的代码运行时会报错

$watch

在Vue实例中有一个$watch属性,利用该属性也可以实现对某个数据的观察

例如:

export default {
    created(){
        this.$watch(
            ()=>{
            	return this.$route.params;		// 返回内容为要观察的数据名称
        	},
            (newVal, oldVal)=>{
                // this.$route.params变化时运行
            },
            {
                immediate: true
            }
        );
    }
}

$watch会返回一个函数,调用这个返回的函数可以取消对相应数据的观察:

export default {
    created(){
        this.unwatch = this.$watch(
            ()=>{
            	return this.$route.params;		// 返回内容为要观察的数据名称
        	},
            (newVal, oldVal)=>{
                // this.$route.params变化时运行
            },
            {
                immediate: true
            }
        );
    },
    destroyed(){
        this.unwatch();			// 取消对this.$route.params的观察
    }
}

获取远程数据

在开发阶段,一般会需要跨域请求资源,这往往就会带来跨域问题

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>后端测试服务器: ajax 跨域:http://test-data:3000/api/news
后端测试服务器->>浏览器: JSON数据
rect rgb(224,74,74)
Note right of 浏览器: 浏览器阻止数据移交
end

而在生产环境下,通常是不存在跨域问题的

sequenceDiagram
浏览器->>服务器: http://www.my-site.com/
服务器->>浏览器: 页面
浏览器->>服务器: ajax:http://www.my-site.com/api/news
服务器->>浏览器: JSON数据
sequenceDiagram
浏览器->>静态资源服务器: http://www.my-site.com/
静态资源服务器->>浏览器: 页面
浏览器->>数据服务器: ajax 跨域:http://api.my-site.com/api/news
数据服务器->>浏览器: [允许www.my-site.com]JSON数据

要解决开发环境下的跨域问题,可以使用代理

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: JSON数据
前端开发服务器->>浏览器: JSON数据

在Vue的配置文件vue.config.js中将开发服务器配置为代理服务器:

// vue.config.js

module.exports = {
    devServer: {
        proxy: {
            "/api": {
                target: "http://www.mysite.com"
            }
        }
    }
}

配置规则和webpack-dev-server的配置规则一模一样

组件生命周期

组件生命周期.png

自定义指令

全局定义

Vue.directive("mydirec", {
    // 指令配置
});

全局定义的指令所有组件都可以直接使用

<template>
<div class="container" v-mydirec="js表达式"></div>
</template>

局部定义

<template>
<div class="container" v-mydirec="js表达式"></div>
</template>

<script>
export default {
    directives: {
        mydirec: {
            // 指令配置
        }
    }
}
</script>

指令配置

vue支持为指令配置一些钩子函数,这些钩子函数会在合适的时候调用

export default {
    bind(el, binding){
		// 元素被创建出来时调用(元素此时还没有被加入到页面中)
    },
    inserted(el, binding){
        // 被绑定的元素插入到父元素中时被调用(元素已经被加入到页面中)
    },
	update(el, binding){
		// 所在组件的vnode更新时被调用
    },
    unbind(el, binding){
        // 元素被移除时调用
    }
}

如果bind和update钩子函数的处理过程相同,并且自定义指令中也只需要用到这两个钩子,则可以简写为

export default function(el, binding){
	// bind & update hooks
}

每个钩子函数在调用时,Vue都会向其传递一些参数,其中最重要的是前两个参数:

  1. el

    被绑定元素对应的真实dom

  2. binding

    一个对象,其中包含了传递给指令的信息

    自定义指令.png

混合

利用混入可以将多个组件中的相同配置项提取出来:

组件混入.png

混入后.png

使用mixins来实现混入:

// 公共代码
var common = {
    data(){
        return {
            a: 1,
            b: 2
        }
    },
    created(){
        console.log("common created");
    },
    computed:{
        sum(){
            return this.a + this.b + this.c;
        }
    }
}

var comp = {
    mixins: [common],
    data(){
        return {
            c: 3
        }
    }
    created(){
        console.log("comp created");
        console.log(this.a, this.b, this.c, this.sum);
    }
}

混合中的代码的this会被Vue自动绑定为使用该混合的组件的组件实例对象

当混入的配置中包含了与组件配置中相同的钩子函数时,混入的配置中的钩子会先运行

$listeners

$listeners是vue实例中的一个属性,其中记录了父组件中所有注册过的事件的处理函数

<!-- 父组件 -->
<Child @event1="handleEvent1" @event2="handleEvent2" />
// 子组件
this.$listeners;		// { event1: handleEvent1, event2: handleEvent2 }

v-model

v-model用于双向绑定某个数据,当一端的数据发生变化时,另一端也会立即发生改变

v-model专门使用在表单元素上,不同类型的表单元素使用v-model时会产生不同的效果

对于type="text"的input元素,v-model是元素的value属性与input事件的结合

<template>
<input type="text" v-model="text" />
</template>

<script>
export default {
    data(){
		return {
            text: ""
        }
    }
}
</script>

等价于:

<template>
<input type="text" :value="text" @input="text = $event.target.value" />
</template>

<script>
export default {
    data(){
		return {
            text: ""
        }
    }
}
</script>

事件总线

利用事件总线,可以完成任意关系的两个组件之间的数据通信,甚至是组件与普通的js模块之间的通信

var listeners = {};

export default {
    // 绑定事件
    $on(eventName, handler){
        if(!listeners[eventName]){
            listeners[eventName] = new Set();
        }
        listeners[eventName].add(handler);
    },
    // 解绑事件
    $off(eventName, handler){
        if(!listeners[eventName]){
            return;
        }
        listeners[eventName].delete(handler);
    },
    // 运行事件
    $emit(eventName, ...args){
        if(!listeners[eventName]){
            return;
        }
        for (var handler of listeners[eventName]){
            handler(...args);
        }
    }
}

Vue实例中自带on、\off和$emit方法,可以直接使用一个Vue实例来充当事件总线:

import Vue from "vue"

export default new Vue({})

vuex

利用vuex可以创建一个可以共享数据的仓库,任意数量以及任意关系的组件都可以访问或操作仓库中的同一份数据

仓库数据也是响应式数据

安装vuex:

npm i vuex

应用vuex:

// main.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

var store = new Vuex.Store({			// 创建仓库
    // 仓库配置对象
});

new Vue({
    store
}).$mount("#app");

为Vue应用完vuex后,Vue的原型上就多出了一个属性$store,利用该属性就可以访问和操作仓库中的共享数据

仓库配置

  • state

    模块状态(即模块数据)

  • mutations

    可能会对仓库数据进行的操作的集合

  • actions

    异步处理仓库数据

  • strict

    是否开启严格模式

    开启后将只允许通过mutation来改变仓库状态

  • namespaced

    是否开启命名空间

  • modules

    添加模块

  • getters

    类似于vue配置中的computed

    new Vuex.Store({
        state: {
            score: 100
        },
        getters: {
            grade(state){			// state为当前
                if(state.score >= 90){
                    return "优";
                }else if(state.score >= 80){
                    return "良";
                }else if(state.score >= 60){
                    return "中";
                }else{
                    return "差";
                }
            }
        }
    });
    

    在组件中使用getters中的属性:

    console.log($store.getters.grade)
    

更改仓库数据

可以利用Vue实例的原型上的$store属性直接对仓库数据进行更改,但这样vuex就无法记录数据变化时的相关信息,这不利于开发者进行调试

vuex建议使用mutation来更改数据,因为这有助于vuex跟踪数据的变化(在调试工具vue-devtools中可以查看到这里的变化)

在mutation中定义针对仓库数据可能会使用到的操作:

new Vuex.Store({
    state: {
        num: 1
    },
    mutations: {
        increase(state){			// state是当前模块的状态
            state.num++;
        },
        decrease(state){
            state.num--;
        },
        power(state, payload){
            state.num **= payload;
        }
    }
})

当需要修改仓库数据时,应该通过提交mutation中的操作来更新仓库数据:

$store.commit("increase");
$store.commit("power", 3);	// 第一个参数对应操作的名称,第二个参数对应mutation的payload形参

注意:

  • mutation中不能出现异步操作
  • mutation是数据变更的唯一途径

异步处理

如果要在vuex中进行异步操作,就需要使用action

new Vuex.Store({
    state: {
        num: 1
    },
    mutations: {
        increase(state){			// state是当前模块的仓库状态
            state.num++;
        },
        decrease(state){
            state.num--;
        },
        power(state, payload){
            state.num **= payload;
        }
    },
    actions: {
        asyncIncrease(context){
            setTimeout(()=>{
                context.commit("increase");
            }, 1000);
            // return ...;
        },
        asyncDecrease(context){
            setTimeout(()=>{
                context.commit("decrease");
            }, 1000);
            // return ...;
        },
        asyncPower(context, payload){
            setTimeout(()=>{
                context.commit("power", payload);
            }, 1000);
            // return ...;
        }
    }
})

当需要进行异步操作时,应该通过触发action中的操作来完成:

$store.dispatch("asyncIncrease").then(...);
$store.dispatch("asyncPower", 3).then(...);

$store.dispatch()会返回一个Promise,完成后的相关数据就是对应的action操作函数的返回值

模块

var moduleA = {
    state: {...},
    mutations: {...},
    actions: {...}
};

var moduleB = {
    state: {...},
    mutations: {...},
    actions: {...}
};

var store = new Vuex.Store({
    modules: {
        a: moduleA,
        b: moduleB
    }
});

不同的模块中可能会出现相同名称的state或mutation或action,因此会造成冲突

为了解决命名冲突问题,就需要让每一个模块开启命名空间:

var moduleA = {
    namespaced: true,				// 为模块开启命名空间
    state: {...},
    mutations: {...},
    actions: {...}
};

var moduleB = {
    namespaced: true,				// 为模块开启命名空间
    state: {...},
    mutations: {...},
    actions: {...}
};

var store = new Vuex.Store({
    modules: {
        a: moduleA,
        b: moduleB
    }
});

开启命名空间后,当需要通过$store为某个模块提交mutation或触发action时,就需要使用下面的方式:

// $store.commit("moduleName/mutationName")
// $store.dispatch("moduleName/actionName")
$store.commit("a/×××");
$store.dispatch("a/×××");

如果是在某个具体的模块内部进行的mutation提交或action触发,则可以省略模块名称前缀:

var moduleA = {
    namespaced: true,					// 为模块开启命名空间
    state: {
    	num: 1
    },
    mutations: {
        increase(state){
            state.num++;
        }
    },
    action: {
        async asyncIncrease(ctx){
            await delay(1000);
            ctx.commit("increase");		// 使用ctx就可以省略模块名称前缀
        }
    }
};

watch

store.watch和Vue实例中的\watch的功能和用法完全一致

异步组件

Vue支持使用异步组件

使用异步组件时,需要注册组件为一个函数,而函数的返回值需要是一个Promise或一个普通的对象

如果是Promise,则其完成之后的数据应该是一个组件配置对象

<template>
<div id="app">
    <AsyncComps />				<!-- 该组件在1秒后显现 -->
</div>
</template>

<script>
var AsyncComp = ()=>{
    return new Promise(async (resolve)=>{
        setTimeout(()=>{
            resolve(await import("./Comp.vue"));
        }, 1000);
    });
}

export default {
    components: {
        AsyncComp				// 注册异步组件
    }
}
</script>

如果函数的返回值是一个对象,则可以对异步组件进行更多的操作:

<template>
<div id="app">
    <AsyncComps />				<!-- 该组件在1秒后显现 -->
</div>
</template>

<script>
import Comp from "./Comp.vue";
import LoadingComp from "./LoadingComp.vue";
import ErrorComp from "./ErrorComp.vue";

var AsyncComp = ()=>({
    component: import(Comp),
    loading: LoadingComp,
    error: ErrorComp,
    delay: 1000,
    timeout: 3000
})

export default {
    components: {
        AsyncComp				// 注册异步组件
    }
}
</script>
  • component

    Promise完成后所展示出的组件

  • loading

    Promise还处于pending状态时,会在组件区域上显示loading对应的组件

  • error

    Promise失败或加载时间超时时会显示出的组件

  • delay

    Promise完成后,还会经过delay毫秒后才显示出来

  • timeout

    异步组件加载的超时时长

直接让异步组件函数返回一个Promise的方式等同于异步组件函数返回的对象中仅仅加入了component属性

路由懒加载

利用异步组件,可以实现路由懒加载:

var router = new VueRouter({
    mode: "",
    routes: [
        {
            path: "/",
            component: () => import("@/views/Home.vue")
        },
        {
            path: "/blog",
            component: () => import("@/views/Blog.vue")
        },
        {
            path: "/blogDetail",
            component: () => import("@views/BlogDetail.vue")
        }
    ]
});

默认情况下,vue-cli会利用webpack将工程的src目录下的所有模块打包成为一个bundle

这就导致在初次访问页面时,需要一次性加载所有的js代码,造成页面响应时间的增长

而懒加载的组件,能够形成单独的chunk,进而在打包结果中形成独立的bundle

并且对于懒加载的组件,它在没有被使用到时页面是可以不用去请求它的,因此页面的首屏响应速度更快

例如,把某个页面设置为异步组件的形式,之后只有在用户点击了该切换到该页面的导航栏按钮后才去加载该页面对应的打包结果文件

其他知识

开启css module

Vue在默认情况下并没有开启css module,但可以通过指定样式文件的名称来为该文件开启css module

命名方式:file_name.module.css

less文件同样允许开启css module:file_name.module.less

得到组件渲染的根Dom

import Icon from "@/components/Icon.vue";

function getComponentRootDom(com, props){
    var vm = new Vue({
		render: h => h(com, {props})
    });
	vm.$mount();
    return vm.$el;
}

var icon = getComponentRootDom(Icon, {
    type: "home"
});

扩展Vue实例

往Vue.prototype中加入一些成员,这些成员就可以在任意组件中使用

扩展Vue实例.jpg

ref

通过ref可以获取到当前组件所渲染出的某个dom元素,也可以获取到子组件的实例

<template>
<div class="container">
    <span ref="span">content</span>
	<ChildComp ref="child"></ChildComp>
    <button @click="handleClick">click</button>
</div>
</template>

<script>
import ChildComp from "./ChildComp"

export default {
    component: {
        ChildComp
    },
    methods: {
        handleClick(){
			console.log(this.$refs.span);			// 得到的是真实的span元素
            console.log(this.$refs.child);			// 得到的是ChildComp的组件实例
        }
    }
}
</script>

内置组件component

利用component组件可以实现动态渲染组件或元素

通过设置component组件的is属性来指定component所要渲染的元素类型

is可以绑定为字符串(字符串可以是HTML元素名称,也可以是注册的组件的名称),也可以绑定为一个组件的配置

比如:

<template>
<div>
    <component is="h1">title</component>
</div>
</template>

等价于:

<template>
<div>
    <h1>title</h1>
</div>
</template>

再比如:

<template>
<div>
    <component :is="Child" />
</div>
</template>

<script>
import Child from "./Child.vue"

export default {
    data(){
        return {
            Child
        }
    }
}
</script>

等价于:

<template>
<div>
    <Child />
</div>
</template>

<script>
import Child from "./Child.vue"

export default {
    components: {
        Child
    }
}
</script>

利用component可以很方便地实现元素或组件的切换:

<template>
<div>
    <button @click="index = (index + 1) % 3">点击切换组件</button>
    <component :is="comps[index]" />
</div>
</template>

<script>
import Child1 from "./Child1.vue"
import Child2 from "./Child2.vue"
import Child3 from "./Child3.vue"

export default {
    data(){
		return {
            comps: [Child1, Child2, Child3],
            index: 0
        }
    }
}
</script>

等价于:

<template>
<div>
    <button @click="index = (index + 1) % 3">点击切换组件</button>
    <Child1 v-if="index===0" />
    <Child2 v-else-if="index===1"/>
    <Child3 v-else/>
</div>
</template>

<script>
import Child1 from "./Child1.vue"
import Child2 from "./Child2.vue"
import Child3 from "./Child3.vue"

export default {
    data(){
		return {
            index: 0
        }
    },
    components: {
        Child1,
        Child2,
        Child3
    }
}
</script>