vue基础
谈一下对vue数据双向绑定的理解
view=>data的实现的原理
给input框绑定oninput事件,在方法中去修改data中对应属性的值,就会立刻调用底层对象对应的set方法,进而实现data中数据的更新,再将数据渲染到view中
data => view
通过实现以下 4 个步骤,来实现数据的双向绑定:
1 、实现一个监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
Observer.prototype = {
walk: function(data) {
var self = this;
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
};
- 递归遍历data中的属性,通过Object.defineProperty(obj, prop, descriptor),为每一项属性添加get(获取值触发)和set(修改值触发)方法,实现数据的可观测性
2 、实现一个订阅器
Dep
,用来收集订阅者,对监听器Observer
和 订阅者Watcher
进行统一管理,即当数据变化的时候后执行对应订阅者的更新函数;
-
dep的属性subs:存的是watcher对象
this.subs = []
- 每次为data的属性defineProperty时,就会生成对应属性的dep实例
-
dep的方法addSub:实现将watcher添加的subs数组中。
- 判断Dep在什么时候添加wather,是通过在get中会判断Dep.target(初始化为null),如果为null,就不会为dep添加watcher,即初始化时,dep数组时不会有值的
-
dep的方法notify:遍历通知每个watcher,触发watcher的update回调函数
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
3 、实现一个订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 全局变量 订阅者 赋值
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 全局变量 订阅者 释放
return value;
}
};
-
Watcher类的属性
- vm:一个 Vue 的实例对象
- exp:是
node
节点的v-model
等指令的属性值 或者插值符号中的属性。如v-model="name"
,exp
就是name
; - cb:是
Watcher
绑定的更新函数;
-
watcher的方法get:当去实例化一个渲染
watcher
的时候,首先进入watcher
的构造函数逻辑,就会执行它的this.get()
方法,进入get
函数,会执行get: function() { Dep.target = this; // 缓存自己 var value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; }
- 利用触发defineProperty中的get方法,同时将当前watcher实例赋值给Dep.target,就实现了 为dep中添加watcher,同时当前watcher也绑定了vm.data中的数据
-
watcher的方法update:当数据发生变化时调用
Watcher
自身的更新函数进行更新的操作。- 先通过
let value = this.vm.data[this.exp];
获取到最新的数据, - 然后将其与之前
get()
获得的旧数据进行比较,如果不一样,则调用更新函数cb
进行更新。
- 先通过
提出问题:应该在什么时候创建watcher实例呢?
4 、实现一个解析器
Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化,将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器。
-
首先通过递归去遍历复制template模版的整个dom节点树,拿到
fragment
-
再挨个遍历根结点的每个孩子节点,进行判断再做相应的操作
-
如果是文本节点
node.nodeType == 3
,且满足了{{exp}}
差值表达式,就创建一个watcher实例,并传入data中对应的属性exp
,就添加对this.vm[exp]
的订阅者,实现了model->view的数据绑定 -
如果是标签元素
node.nodeType == 1
,则拿到当前节点的属性,- 如果是v-model='name',则需要绑定数据。通过
modelUpdater
方法,为视图中的值初始化绑定vm中的数据,同时创建watcher实例,并将modelUpdater以更新数据的回调函数传递过去
modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value }
- 如果是v-on:click='handel',则需要绑定事件。通过
attr.name
拿到click,attr.value
拿到handel,并为当前节点注册对应的事件函数
- 如果是v-model='name',则需要绑定数据。通过
-
var Watcher = require('./watcher')
function Compile(el, vm) {
this.vm = vm
this.el = document.querySelector(el)
this.fragment = null
this.init()
}
Compile.prototype = {
init: function () {
if (this.el) {
// 复制整个dom节点树
this.fragment = this.nodeToFragment(this.el)
// 挨个遍历根节点的所有孩子
this.compileElement(this.fragment)
// 进行模版替换后,将真实的dom添加到页面上
this.el.appendChild(this.fragment)
} else {
console.log('Dom元素不存在')
}
},
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment()
// 获取第一个 节点,包含文本节点
var child = el.firstChild
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElement: function (el) {
// el.childNodes按代码顺序获取所有的节点,包括文本换行节点等
// el.children 只获取标签元素,不包括文本换行节点等
var childNodes = el.childNodes
var self = this
;[].slice.call(childNodes).forEach(function (node) {
var reg = /\{\{(.*)\}\}/
// 拿到当前节点的文本
var text = node.textContent
// 判断是不是标签
if (self.isElementNode(node)) {
self.compile(node)
} else if (self.isTextNode(node) && reg.test(text)) {
// RegExp.$1------reg.exec(text)[1]
self.compileText(node, reg.exec(text)[1])
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node)
}
})
},
// 编译 标签
compile: function (node) {
var nodeAttrs = node.attributes
var self = this
Array.prototype.forEach.call(nodeAttrs, function (attr) {
// attr.name---获取 key
var attrName = attr.name
// 如果是以v-开头的指定
if (self.isDirective(attrName)) {
// attr.value--- 获取value
var exp = attr.value
// 去掉v-,比如为on:click
var dir = attrName.substring(2)
// 判断是否为事件指令
if (self.isEventDirective(dir)) {
self.compileEvent(node, self.vm, exp, dir)
} else {
// v-model 指令
self.compileModel(node, self.vm, exp, dir)
}
node.removeAttribute(attrName)
}
})
},
// 编译文本 {{exp}}
compileText: function (node, exp) {
var self = this
var initText = this.vm[exp]
this.updateText(node, initText)
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value)
})
},
// 编译 绑定的事件 比如 v-on:click='handel',exp是绑定的值handel,dir就是on:click,
compileEvent: function (node, vm, exp, dir) {
// 拿到click
var eventType = dir.split(':')[1]
// 拿到 handel方法
var cb = vm.methods && vm.methods[exp]
if (eventType && cb) {
// 为节点注册事件
node.addEventListener(eventType, cb.bind(vm), false)
}
},
compileModel: function (node, vm, exp, dir) {
var self = this
// 拿到data里面exp对应的值
var val = this.vm[exp]
// 判断 view的value===model里面的data
this.modelUpdater(node, val)
// 生成watcher实例,data更新时,就会触发cb回调函数(即实现 data--》view的同步)
new Watcher(this.vm, exp, function (value) {
self.modelUpdater(node, value)
})
node.addEventListener('input', function (e) {
var newValue = e.target.value
if (val === newValue) {
return
}
self.vm[exp] = newValue
val = newValue
})
},
updateText: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value
},
modelUpdater: function (node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value
},
isDirective: function (attr) {
return attr.indexOf('v-') == 0
},
isEventDirective: function (dir) {
return dir.indexOf('on:') === 0
},
isElementNode: function (node) {
return node.nodeType == 1
},
isTextNode: function (node) {
return node.nodeType == 3
}
}
module.exports = Compile
vue-router的实现原理
谈一下对Vue是渐进式框架的理解
组件中data为什么必须是函数
当一个组件被定义,
data
必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果data
仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供data
函数,每次创建一个新实例后,我们能够调用data
函数,从而返回初始数据的一个全新副本数据对象。 为了保证组件的独立性和可复用性,数据不被污染
data是一个函数,组件实例化的时候这个函数将会被调用,返回一个对象,计算机会给这个对象分配一个内存地址,你实例化几次,就分配几个内存地址,他们的地址都不一样,所以每个组件中的数据不会相互干扰,改变其中一个组件的状态,其它组件不变。
虚拟dom和diff算法
vue数据更新
采用虚拟DOM+diff算法提高更新性能
- 生成新的虚拟DOM结构
- 和旧的虚拟DOM结构对比
- 利用diff算法, 找不同, 只更新变化的部分(重绘/回流)到页面 - 也叫打补丁
虚拟DOM
真实的DOM属性好几百个, 没办法快速的知道哪个属性改变了
虚拟DOM保存在内存中,只记录dom关键信息, 配合diff算法提高DOM更新的性能
virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。 比如dom是这样的:
<div>
<p>123</p>
</div>
对应的virtual DOM(伪代码):
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};
注意:VNode
和oldVNode
都是对象
diff算法
在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。
diff流程图
当数据发生改变时,set方法会让调用Dep.notify
通知所有订阅者Watcher,订阅者就会调用patch
给真实的DOM打补丁,更新相应的视图。
diff算法如何比较新旧虚拟DOM?
- 根元素改变 – 删除当前DOM树重新建
- 根元素未变, 属性改变 – 更新属性
- 根元素未变, 子元素/内容改变
- 无key – 就地更新 / 有key – 按key比较
v-for中key的作用
key 的特殊属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。
- 如果不使用 key,Vue会采用就地复地原则:使用一种最大限度减少移动元素并且尽可能的尝试修复/再利用相同类型元素的算法,做patch或者reuse。
- 使用key,Vue会根据keys的顺序记录element,它会基于key的变化重新排列元素顺序,并且会移除 key 不存在的元素。
渲染函数 & JSX(待补充)
组件通信(待补充)
1. 父子组件
a. 通过发送和接受消息
父组件
例如
通过v-bind或:实现把父组件的数据传给子组件
通过v-on或@,实现监听子组件事件和接收数据
<son :uname="uname" @update-uname="uname = $event"></son>
子组件
例如
export default {
//通过props接收父组件传过来的数据
props: ['uname'],
methods: {
handle(){
//通过$emit触发事件,发送数据给父组件
this.$emit('update-uname','ls')
}
}
}
语法糖
v-model
父组件
<son v-model="uname"></son>
子组件
export default {
props: ['value'],
methods: {
handle(){
this.$emit('input','ls')
}
}
}
.sync
父组件
<son :uname.sync="uname"></son>
子组件
export default {
props: ['uname'],
methods: {
handle(){
this.$emit('update:uname','ls')
}
}
}
b.通过组件实例直接通信
父->子
通过ref和$refs
<template>
<son ref="sonRef"></son>
</template>
<script>
export default = {
methods: {
handle(){
this.$refs.sonRef.uname //访问数据
this.$refs.sonRef.updateUname('ls')//访问方法
}
}
}
</script>
子->父
通过$parent
export default {
methods: {
handle(){
this.$parent.uname //访问数据
this.$parent.upateUname('ls') //访问方法
}
}
}
2. 兄弟组件
a通过事件中心
事件中心
例如
import Vue from 'vue'
const eventBus = new Vue()
export funtion on(eventName, fn){
eventBus.$on(eventName, fn)
}
export funtion emit(eventName, data){
eventBus.$emit(eventName, data)
}
兄弟A
例如
import {emit} from '@/utils/eventBus.js'
export default {
methods: {
handle(){
//发消息
emit('add-success', 4)
}
}
}
兄弟B
例如
import {on} from '@/utils/eventBus.js'
export default {
data(){
return {
list: [1,2,3]
}
},
mounted(){
//接受消息
on('add-success', function(data){
this.list.push(data)
})
}
}
b.通过父组件通信(通过消息和直接通信)
父组件
例如
<template>
<son-a @add-success="handle"><son-a>
<son-b ref="sonbRef"><son-b>
</template>
<script>
export defalut {
methods: {
handle(data){
//父组件之间操作子组件实例实现通信
this.$refs.sonbRef.list.push(data)
}
}
}
</script>
兄弟A
例如
export default {
methods: {
handle(){
//发消息
this.$emit('add-success', 4)
}
}
}
兄弟B
例如
export default {
data(){
return {
list: [1,2,3]
}
}
}
c.通过父组件通信(通过消息)
父组件
例如
<template>
<son-a @add-success="handle"><son-a>
<son-b :list="list"><son-b>
</template>
<script>
export defalut {
data(){
list: []
},
methods: {
handle(data){
//父子组件通过数据绑定实现通信
this.list.push(data)
}
}
}
</script>
兄弟A
例如
export default {
methods: {
handle(){
//发消息
this.$emit('add-success', 4)
}
}
}
兄弟B
例如
export default {
props: {
//通过props接收父组件传过来的数据
list: {
type: Array,
default: ()=>[]
}
}
}
d.通过插槽
父组件
<template>
<son-a><button @click="handle(4)">add</button><son-a>
<son-b :list="list"><son-b>
</template>
<script>
export defalut {
data(){
return {
list: [1,2,3]
}
},
methods: {
handle(data){
//父组件之间操作子组件实例实现通信
this.list.push(data)
}
}
}
</script>
兄弟A
例如
<template>
<div class="son-a">
<slot></slot>
</div>
</template>
兄弟B
例如
export default {
props: {
//通过props接收父组件传过来的数据
list: {
type: Array,
default: ()=>[]
}
}
}
3. 爷孙组件
a.通过父子通信
<body>
<div id="app">
<grand-father></grand-father>
</div>
</body>
<script>
// 爷爷组件
Vue.component('grand-father', {
data() {
return {
money: 1000
}
},
template: `
<div>
<div>爷爷</div>
<father :money="money" @pay="money-=$event"/>
</div>
`
})
// 父亲组件
Vue.component('father', {
props: ['money'],
template: `
<div>
<div>父亲</div>
<son :money="money" @pay="$emit('pay',$event)"/>
</div>
`
})
// 孙子组件
Vue.component('son', {
props: ['money'],
template: `
<div>
<div>孙子-{{money}}</div>
<button @click="$emit('pay', 100)">-100</button>
</div>
`
})
var vm = new Vue({
el: '#app',
data: {
},
methods: {
},
mounted() {
},
})
b.通过$parent
<body>
<div id="app">
<grand-father></grand-father>
</div>
</body>
<script>
// 爷爷组件
Vue.component('grand-father', {
data() {
return {
money: 1000
}
},
template: `
<div>
<div>爷爷</div>
<father ref="father"/>
</div>
`,
methods: {
pay(num){
if(this.money>num){
this.money -= num
}
}
}
})
// 父亲组件
Vue.component('father', {
template: `
<div>
<div>父亲</div>
<son/>
</div>
`
})
// 孙子组件
Vue.component('son', {
template: `
<div>
<div>孙子-{{$parent.$parent.money}}</div>
<!--可以,但是不要做-->
<!--<button @click="$parent.$parent.money-=100">-100</button>-->
<button @click="$parent.$parent.pay(100)">-100</button>
</div>
`
})
var vm = new Vue({
el: '#app',
data: {
},
methods: {
},
mounted() {
},
})
</script>
c.通过provide和inject
<body>
<div id="app">
<grand-father></grand-father>
</div>
</body>
<script>
// 爷爷组件
Vue.component('grand-father', {
data() {
return {
money: 1000
}
},
provide(){
return {
grandFather: this
}
},
template: `
<div>
<div>爷爷</div>
<father/>
</div>
`,
methods: {
pay(num){
if(this.money>num){
this.money -= num
}
}
}
})
// 父亲组件
Vue.component('father', {
template: `
<div>
<div>父亲</div>
<son/>
</div>
`
})
// 孙子组件
Vue.component('son', {
inject: ['grandFather'],
mounted() {
},
template: `
<div>
<div>孙子-{{grandFather.money}}</div>
<button @click="grandFather.pay(100)">-100</button>
</div>
`
})
var vm = new Vue({
el: '#app',
data: {
},
methods: {
},
mounted() {
},
})
</script>
4. 没关系组件-Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 定义数据
state: {
count: 0
},
// 同步代码
mutations: {
addCount (state) {
state.count++
},
//接受参数
addCountN (state, payload) {
state.count += payload
}
},
// 异步代码
actions: {
addCountSync (context) {
setTimeout(() => {
context.commit('addCount')
}, 1000)
},
// 接受参数
addCountNSync (context, payload) {
setTimeout(() => {
context.commit('addCountN', payload)
}, 1000)
}
},
// 计算属性
getters: {
showCount (state) {
return `当前的count值=${state.count}`
},
// 简化模块中state的获取
token (state) {
return state.user.token
}
},
// 模块化
modules: {
user: {
// 命名空间
namespaced: true,
state: {
token: 'abc'
},
mutations: {
updateToken () {
console.log('2')
}
}
}
}
})
// state
<div>{{$store.state.count}}</div>
// mutations
<button @click="$store.commit('addCount')">+1</button>
// actions
<button @click="$store.dispatch('addCountSync')">+1 sync</button>
// getters
<div>{{$store.getters.showCount}}</div>
<script>
import {mapState,mapMutations,mapActions,mapGetters} from 'vuex'
export default {
computed: {
...mapState(['count']),
...mapGetters(['showCount'])
},
methods: {
...mapMutations(['addCount', 'addCountN']),
...mapActions(['addCountSync', 'addCountNSync']),
}
}
</script>
自定义事件
v-model自定义
.native
修饰符
想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on
的 .native
修饰符:
核心:通过冒泡的语法糖
<!-- 通过冒泡 -->
<!-- <div @click="handleClick">
<el-button></el-button>
</div> -->
<!-- 语法糖 -->
<el-button @click.native="handleClick"></el-button>
<!-- 注意:.stop,.keyup,.prevent只能作用于dom事件对象,不能作用于组件通过$emit通信!!! -->
<el-input @keyup.native.enter="handleEnter"></el-input>
</div>
</body>
<script>
Vue.component('el-button', {
template: `
<div><button>按钮</button></div>
`
})
Vue.component('el-input', {
template: `
<input type="text"/>
`
})
new Vue({
el: '#app',
methods: {
handleClick() {
console.log('click')
},
handleEnter() {
console.log('enter')
}
}
})
</script>
注意:
.stop,.keyup,.prevent
只能作用于dom事件对象,不能作用于组件通过$emit通信!!!
.sync
修饰符
使用场景:父子组件相互传参
注意:带有
.sync
修饰符的v-bind
不能和表达式一起使用 (例如v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似v-model
。 父组件 传给子组件值visible,并接收子组件update:visible的事件
<!-- :visible="showDialog" @update:visible="showDialog = $event" -->
<!-- 语法糖 -->
:visible.sync="showDialog"
子组件 接收父组件传的值visible,通过@update:visible触发父组件的事件
Vue.component('el-dialog', {
props: ['visible'],
template: `
<div v-if="visible">这是一个弹层, <a @click="$emit('update:visible', false)">关闭</a></div>
`
})
v-bind.sync(多个prop)
注意:将
v-bind.sync
用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。 父组件
<el-dialog v-bind.sync="info"></el-dialog>
子组件
Vue.component('el-dialog', {
props: ['visible', 'name'],
template: `
<div v-if="visible">这是{{name}}的一个弹层, <a @click="$emit('update:visible', false)">关闭</a></div>
`
})
组件注册的方式
1.全局注册
注意:全局注册的行为必须在根 Vue 实例 (通过
new Vue
) 创建之前发生
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
2.局部注册
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
3.注册路由组件
src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export const routes = [
{
path: '/login',
component: () => import('@/views/login/index'),
}
]
const router = new Router({
routes
})
export default router
4.以插件模式注册组件
src/components/index.js
该文件负责所有的公共的组件的全局注册,并默认导出
// 该文件负责所有的公共的组件的全局注册 Vue.use
import PageTools from './PageTools'
import UploadExcel from './UploadExcel/index'
import ImageUpload from './ImageUpload'
export default {
install(Vue) {
// 注册全局的通用栏组件对象
Vue.component('PageTools', PageTools)
// 注册导入excel数据的文件
Vue.component('UploadExcel', UploadExcel)
// 注册导入上传组件
Vue.component('ImageUpload', ImageUpload)
}
}
在main.js中引入,并全局注册自己的插件
import Component from '@/components'
Vue.use(Component) // 注册自己的插件
搭建vue开发环境
自己搭建过vue开发环境吗?
路由
路由传参
组件传参
hash和history
hash
优点
- 只需要前端配置路由表, 不需要后端的参与
- 兼容性好, 浏览器都能支持
- hash值改变不会向后端发送请求, 完全属于前端路由
- 同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置
缺点
- hash值前面需要加#, 不符合url规范,也不美观
history
优点
- 符合url地址规范, 不需要#, 使用起来比较美观
缺点
- 在用户手动输入地址或
刷新页面时会发起url请求
, 后端需要配置index.html页面用户匹配不到静态资源的情况, 否则会出现404错误 - 兼容性比较差, 是利用了 HTML5 History对象中新增的 pushState() 和 replaceState() 方法,需要特定浏览器的支持.
路由守卫
调用顺序:
全局守卫--》路由独享守卫--》路由组件内守卫
1.全局守卫
vue-router全局有三个守卫:
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
- router.afterEach 全局后置钩子 进入路由之后
使用方法:
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
每个守卫方法接收三个参数:
-
to: Route
: 即将要进入的目标 路由对象 -
from: Route
: 当前导航正要离开的路由 -
next: Function
: 一定要调用该方法来 resolve 这个钩子。执行效果依赖next
方法的调用参数。-
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。 -
next(false)
: 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from
路由对应的地址。 -
next('/')
或者next({ path: '/' })
: 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在router-link
的to
prop 或router.push
中的选项。 -
next(error)
: (2.4.0+) 如果传入next
的参数是一个Error
实例,则导航会被终止且该错误会被传递给router.onError()
注册过的回调。
-
2.路由独享守卫
为某些路由单独配置守卫:
注意:
调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
// ...
}
}
]
})
3.路由组件内守卫
-
beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例
this
,组件实例还没被创建 -
beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例
this
-
beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例
this
beforeRouteEnter (to, from, next) {
// 在路由独享守卫后调用 不!能!获取组件实例 `this`,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 `this`
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,可以访问组件实例 `this`
}
补充应用
beforeRouteEnter访问this
因为钩子在组件实例还没被创建的时候调用,所以不能获取组件实例 this
,可以通过传一个回调给next
来访问组件实例 。
但是回调的执行时机在mounted后面,所以在我看来这里对this的访问意义不太大,可以放在created
或者mounted
里面。
beforeRouteEnter (to, from, next) {
console.log('在路由独享守卫后调用');
next(vm => {
// 通过 `vm` 访问组件实例`this` 执行回调的时机在mounted后面,
})
}
复制代码
beforeRouteLeave
导航离开该组件的对应路由时调用,我们用它来禁止用户离开,比如还未保存草稿,或者在用户离开前,将setInterval
销毁,防止离开之后,定时器还在调用。
beforeRouteLeave (to, from , next) {
if (文章保存) {
next(); // 允许离开或者可以跳到别的路由 上面讲过了
} else {
next(false); // 取消离开
}
}
复制代码
路由钩子函数的错误捕获
如果我们在全局守卫/路由独享守卫/组件路由守卫的钩子函数中有错误,可以这样捕获:
router.onError(callback => {
// 2.4.0新增 并不常用,了解一下就可以了
console.log(callback, 'callback');
});
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
完整的路由导航解析流程(不包括其他生命周期):
- 触发进入其他路由。
- 调用要离开路由的组件守卫
beforeRouteLeave
- 调用局前置守卫:
beforeEach
- 在重用的组件里调用
beforeRouteUpdate
- 调用路由独享守卫
beforeEnter
。 - 解析异步路由组件。
- 在将要进入的路由组件中调用
beforeRouteEnter
- 调用全局解析守卫
beforeResolve
- 导航被确认。
- 调用全局后置钩子的
afterEach
钩子。 - 触发DOM更新(
mounted
)。 - 执行
beforeRouteEnter
守卫中传给 next 的回调函数
生命周期
常用钩子
-
ajax请求最好放在
created
里面,因为此时已经可以访问this
了,请求到数据就可以直接放在data
里面。这里也碰到过几次,面试官问:ajax请求应该放在哪个生命周期。
-
关于dom的操作要放在
mounted
里面,在mounted
前面访问dom会是undefined
。 -
每次进入/离开组件都要做一些事情,用什么钩子:
-
不缓存:
进入的时候可以用
created
和mounted
钩子,离开的时候用beforeDestory
和destroyed
钩子,beforeDestory
可以访问this
,destroyed
不可以访问this
。 -
缓存了组件:
缓存了组件之后,再次进入组件不会触发
beforeCreate
、created
、beforeMount
、mounted
,如果你想每次进入组件都做一些事情的话,你可以放在activated
进入缓存组件的钩子中。同理:离开缓存组件的时候,
beforeDestroy
和destroyed
并不会触发,可以使用deactivated
离开缓存组件的钩子来代替。