高级特性
自定义model
- input元素的value = this.name
- 绑定input事件,this.name = $event.target.value
$nextTick
Vue是异步渲染的
data改变之后,Dom不会立刻渲染
$nextTick会在Dom渲染之后出发,以回去最新Dom节点
插槽
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 [Web Components](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md)
规范草案,将元素作为承载分发内容的出口。
匿名插槽(默认插槽)
没有命名默认只有一个。
// 子组件
<button type="submit">
<slot>Submit</slot> // Submit作为默认值
</button>
<submit-button></submit-button> // 渲染出默认值Submit
<submit-button>
Save // 替换默认值,渲染出Save
</submit-button>
// 子组件
<button type="submit">
<slot>Submit</slot> // Submit作为默认值
</button>
<submit-button></submit-button> // 渲染出默认值Submit
<submit-button>
Save // 替换默认值,渲染出Save
</submit-button>
具名插槽
与匿名插槽对应,slot标签带name命名。
- 子组件
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个<template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
- 父组件
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
作用域插槽
子组件内数据可以被父页面拿到(解决了数据只能从父组件传递到子组件的问题)。
- 子组件
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
- 父组件
<current-user>
<template v-slot:default="slotProps" />
<template scope="slotProps"> // 另一种写法
{{ slotProps.user.firstName }}
</template>
</current-user>
<current-user>
<template v-slot:default="slotProps" />
<template scope="slotProps"> // 另一种写法
{{ slotProps.user.firstName }}
</template>
</current-user>
自定义指令
提供一种机制,将数据的变化映射为Dom的行为。
Vue.directive()
局部指令
directives: {
focus: {
// 绑定完成
bind: function () {
console.log('bind', ...arguments);
},
//
update: function () {
console.log('update', ...arguments);
},
// 解绑
unbind: function () {
console.log('unbind', ...arguments);
},
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
}
}
directives: {
focus: {
// 绑定完成
bind: function () {
console.log('bind', ...arguments);
},
//
update: function () {
console.log('update', ...arguments);
},
// 解绑
unbind: function () {
console.log('unbind', ...arguments);
},
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
}
}
全局指令
Vue.directive('focus', {
// 绑定完成
bind: function () {
console.log('bind', ...arguments);
},
//
update: function () {
console.log('update', ...arguments);
},
// 解绑
unbind: function () {
console.log('unbind', ...arguments);
},
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
Vue.directive('focus', {
// 绑定完成
bind: function () {
console.log('bind', ...arguments);
},
//
update: function () {
console.log('update', ...arguments);
},
// 解绑
unbind: function () {
console.log('unbind', ...arguments);
},
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。
钩子函数的参数
指令钩子函数会被传入以下参数:
el
:指令所绑定的元素,可以用来直接操作 DOM。binding
:一个对象,包含以下 property:name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
。
vnode
:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
动态、异步组件
动态组件
通过 is
属性来实现动态组件的切换。
<component v-bind:is="currentTabComponent"></component>
<component v-bind:is="currentTabComponent"></component>
异步组件
异步组件是指在渲染过程中,需要等待的组件。
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
const AsyncComp = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComp.vue'),
// 异步组件加载时使用的组件
loading: LoadingComp,
// 加载失败时使用的组件
error: ErrorComp,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
// 简写
const AsyncComp = () => import('./MyComp.vue')
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
const AsyncComp = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComp.vue'),
// 异步组件加载时使用的组件
loading: LoadingComp,
// 加载失败时使用的组件
error: ErrorComp,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
// 简写
const AsyncComp = () => import('./MyComp.vue')
Vue组件库的二次封装
实现一个自动loading的按钮。
attrs属性介绍
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
组件代码
通过 v-bind="$attrs"
,去继承父组件传过来的属性。
或者通过 $props
去接受所有的参数。
<template>
<Button :loading="loadingStatus" v-bind="$attrs" @click="handleClick"><slot /></Button>
</template>
<script>
import { Button } from 'vant';
export default {
name: "MuButton",
/**
props: {
...Button.props,
autoLoading: {
type: Boolean,
default: false
}
},
*/
props: {
autoLoading: {
type: Boolean,
default: false
}
},
data () {
return {
loadingStatus: false,
}
},
components: {
Button
},
methods: {
handleClick() {
if (this.autoLoading) {
// 判断是否开启自动loading
this.loadingStatus = true;
}
// 点击的回调
this.$emit('click', () => {
// 在异步完成之后,去除loading
this.loadingStatus = false;
})
}
}
}
</script>
<style scoped>
</style>
<template>
<Button :loading="loadingStatus" v-bind="$attrs" @click="handleClick"><slot /></Button>
</template>
<script>
import { Button } from 'vant';
export default {
name: "MuButton",
/**
props: {
...Button.props,
autoLoading: {
type: Boolean,
default: false
}
},
*/
props: {
autoLoading: {
type: Boolean,
default: false
}
},
data () {
return {
loadingStatus: false,
}
},
components: {
Button
},
methods: {
handleClick() {
if (this.autoLoading) {
// 判断是否开启自动loading
this.loadingStatus = true;
}
// 点击的回调
this.$emit('click', () => {
// 在异步完成之后,去除loading
this.loadingStatus = false;
})
}
}
}
</script>
<style scoped>
</style>
页面中使用
<template>
<div class="home">
<MyButton type="primary" round :autoLoading="true" :loading-text="'提交中...'" @click="handleClick">提交</MyButton>
</div>
</template>
<script>
import { mapState } from 'vuex';
import MyButton from '@/components/MyButton';
export default {
name: "Home",
data() {
return {
}
},
computed: {
...mapState('router', [
'routers'
])
},
components: {
MyButton
},
methods: {
handleClick (done) {
setTimeout(() => {
// 执行该回调,去关闭loading
done();
}, 1500)
}
}
}
</script>
<template>
<div class="home">
<MyButton type="primary" round :autoLoading="true" :loading-text="'提交中...'" @click="handleClick">提交</MyButton>
</div>
</template>
<script>
import { mapState } from 'vuex';
import MyButton from '@/components/MyButton';
export default {
name: "Home",
data() {
return {
}
},
computed: {
...mapState('router', [
'routers'
])
},
components: {
MyButton
},
methods: {
handleClick (done) {
setTimeout(() => {
// 执行该回调,去关闭loading
done();
}, 1500)
}
}
}
</script>
keep-alive
keep-alive
是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
缓存 vdom,避免重新渲染。
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
路由
路由的模式
- hash
- history
hash
- hash变化会触发网页跳转,即浏览器的前进、后退
- hash变化不会刷新页面,SPA必须得特点
- hash不会提交到server端
// 监听路由变化
window.addEventListener("hashchange", function () {
});
// 监听路由变化
window.onhashchange = function (event) {
}
// 监听路由变化
window.addEventListener("hashchange", function () {
});
// 监听路由变化
window.onhashchange = function (event) {
}
history
- history变化不会触发网页跳转
- history.pushState
- window.onpopstate
history.pushState({
name: 'pageName'
});
// 监听浏览器前进后退
window.onpopstate = function (event) {
console.log(event.state);
}
history.pushState({
name: 'pageName'
});
// 监听浏览器前进后退
window.onpopstate = function (event) {
console.log(event.state);
}
路由自动加载
- 思路
我们可以将用到的页面都引入,然后通过文件名去筛选拿到路由名称
可以通过webpack的api来获取对应的文件,即上下文来实现。
require.context();
可以给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
require.context(directory, useSubdirectories = false, regExp = /^\.\//);
// require.context(
// directory: String, 要搜索的文件夹目录
// includeSubdirs: Boolean /* optional, default true */, 搜索它的子目录
// filter: RegExp /* optional, default /^\.\/.*$/, any file */, 匹配文件的正则表达式
// mode: String /* optional, 'sync' | 'eager' | 'weak' | 'lazy' | 'lazy-once', default 'sync' */
// )
require.context(directory, useSubdirectories = false, regExp = /^\.\//);
// require.context(
// directory: String, 要搜索的文件夹目录
// includeSubdirs: Boolean /* optional, default true */, 搜索它的子目录
// filter: RegExp /* optional, default /^\.\/.*$/, any file */, 匹配文件的正则表达式
// mode: String /* optional, 'sync' | 'eager' | 'weak' | 'lazy' | 'lazy-once', default 'sync' */
// )
- 实现
import Vue from 'vue';
import Router from 'vue-router';
// 先定义一个空的路由数组
let routes = [
];
let views = require.context('../views/', true, /\.vue$/);
// 导出的方法有 3 个属性: resolve, keys, id。
// - resolve 是一个函数,它返回请求被解析后得到的模块 id。
// - keys 也是一个函数,它返回一个数组,由所有可能被上下文模块处理的请求组成。
// - id 是上下文模块里面所包含的模块 id. 它可能在你使用 module.hot.accept 的时候被用到
// 这里只用到 keys,返回搜索到的数组
views.keys().forEach((key) => {
let routerName = views(key).default.name;
// 将对应路由push到路由的数组
routes.push({
path: routerName === 'Home' ? '/' : `/${routerName}`,
title: routerName,
name: routerName,
component: views(key).default
});
});
// console.log(routes);
Vue.use(Router);
export default new Router({
routes
});
import Vue from 'vue';
import Router from 'vue-router';
// 先定义一个空的路由数组
let routes = [
];
let views = require.context('../views/', true, /\.vue$/);
// 导出的方法有 3 个属性: resolve, keys, id。
// - resolve 是一个函数,它返回请求被解析后得到的模块 id。
// - keys 也是一个函数,它返回一个数组,由所有可能被上下文模块处理的请求组成。
// - id 是上下文模块里面所包含的模块 id. 它可能在你使用 module.hot.accept 的时候被用到
// 这里只用到 keys,返回搜索到的数组
views.keys().forEach((key) => {
let routerName = views(key).default.name;
// 将对应路由push到路由的数组
routes.push({
path: routerName === 'Home' ? '/' : `/${routerName}`,
title: routerName,
name: routerName,
component: views(key).default
});
});
// console.log(routes);
Vue.use(Router);
export default new Router({
routes
});
组件的自动加载
同理,我们也可以通过该api实现组件的自动加载
- 在组件文件夹下新建
index.js
,编码如下
const files = require.context('./', false, /\.vue$/); //引入当前路径下所有的vue组件
let components = {};
files.keys().forEach((key) => {
components[files(key).default.name] = files(key).default;
});
console.log(components);
export default components;
const files = require.context('./', false, /\.vue$/); //引入当前路径下所有的vue组件
let components = {};
files.keys().forEach((key) => {
components[files(key).default.name] = files(key).default;
});
console.log(components);
export default components;
- 在需要用到组件的地方
import subComponents from '/components';
component: {
subComponent
}
import subComponents from '/components';
component: {
subComponent
}