VUE常见面试题
1.单页应用(SPA)
•概念
单页应用(只有一个html文件,整个网站的所有内容都在这一个html里,通过js来处理)不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的。为了实现单页应用 ==> 前后端分离 + 前端路由。(更新视图但不重新请求页面)
前后端分离 ===> 利用Ajax,可以在不刷新浏览器的情况下异步数据请求交互。
•优缺点
举例:比较单例应用和多页应用
单页应用(SPA:单页复应用)
1)页面跳转->JS渲染(JS能感知URL的变化,动态地把页面的内容删除掉,新的页面上的结构渲染出来)
2)首屏展示:一次HTML请求 一次JS请求(两者都回来之后首屏进行展示)
一次请求 节约HTTP请求时延
3)优点:页面切换块
4)缺点:首屏时间长 SEO差
2 多页应用
1)页面跳转->返回HTML
2)优点:首屏时间快 SEO效果好
3)缺点:页面切换慢
2.MVVM
理解 Vue 的 MVVM
MVVM --> model-view-viewModel
model:模型,数据对象(data)
view:视图,模板页面
viewModel:视图模型(vue 的实例)
MVVM 本质上是 MVC (Model-View- Controller)的改进版。即模型-视图-视图模型。
模型
指的是后端传递的数据,视图
指的是所看到的页面。
视图模型
是 mvvm 模式的核心,它是连接 view 和 model 的桥梁。它有两个方向:
- 将
模型
转化成视图
,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。 - 将
视图
转化成模型
,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
这两个方向都实现的,我们称之为数据的双向绑定。
**3.**Vue的响应式原理(双向数据绑定)
简单概述 : 通过Object.defineProperty 完成对于数据的劫持, 通过发布者 - 订阅者模式, 完成对于节点的数据更新
vue.js 是采用数据劫持结合发布者 - 订阅者模式的方式,通过 Object.defineProperty () 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
1、需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
2、compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3、Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器 (dep) 里面添加自己 ②自身必须有一个 update () 方法 ③待属性变动 dep.notice () 通知时,能调用自身的 update () 方法,并触发 Compile 中绑定的回调,则功成身退。
4、MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化 (input) -> 数据 model 变更的双向绑定效果。
vue中双向数据绑定的简单实现:
<script>
let obj = {};
let nameValue = "";
Object.defineProperty(obj, "name", {
set(value){
// ins.value = nameValue = value;
nameValue = value;
ins.value = value;
},
get(){
return nameValue;
}
})
// 双向绑定:其实描述的就是视图和数据的关系,视图的变化会同步更新数据,数据变化也会同步更新视图
// 1. 视图中用户输入数据的时候,我们需要获取到用户的输入,然后赋值给obj.name
// 1.1 注册事件
let ins = document.querySelector("#ins");
ins.oninput = function() {
// 1.2 获取用户的输入
// 1.3 赋值给obj.name
obj.name = this.value;
};
// 2. 数据在发生改变的时候,我们需要把数据赋值给input框
// 当用户通过=给obj.name赋值的时候,会触发上面的set方法
// 我们只需要在set方法中,把当前的更新的数据,赋值给input框即可
// obj.name = "123";
</script>
</body>
</html>
**4.**data为什么是函数
当我们定义这个 <button-counter>
组件时,你可能会发现它的 data
并不是像这样直接提供一个对象:
data: {
count: 0
}
取而代之的是,一个组件的 data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {
return {
count: 0
}
}
如果 Vue 没有这条规则,点击一个按钮就可能影响到其它所有实例
对象为引用类型,当复用组件时,由于数据对象都指向同一个 data 对象,当在一个组件中修改 data 时,其他重用的组件中的 data 会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object 的实例),引用地址不同,则不会出现这个问题。
5、v-model原理
v-model是一个指令,限制在、、、components中使用,修饰符.lazy(取代 input 监听 change 事件)、.number(输入字符串转为有效的数字)、.trim(输入首尾空格过滤)。它其实是一个语法糖,其原理实际上就是双向绑定原理
6.v-if和 v-show
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-if 和 v-show 看起来似乎差不多,当条件不成立时,其所对应的标签元素都不可见,但是这两个选项是有区别的:
1、v-if 在条件切换时,会对标签进行适当的创建和销毁,而 v-show 则仅在初始化时加载一次,因此 v-if 的开销相对来说会比 v-show 大。
2、v-if 是惰性的,只有当条件为真时才会真正渲染标签;如果初始条件不为真,则 v-if 不会去渲染标签。v-show 则无论初始条件是否成立,都会渲染标签,它仅仅做的只是简单的 CSS 切换。
7.computed、watch和method
•使用方法
computed:事件计算属性缓存:响应式依赖缓存
watch:侦听属性
method:方法
•区别
- 计算属性 computed:
支持缓存,只有依赖数据发生改变,才会重新进行计算
不支持异步,当 computed 内有异步操作时无效,无法监听数据的变化
computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 中声明过或者父组件传递的 props 中的数据通过计算得到的值
如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用 computed
如果 computed 属性属性值是函数,那么默认会走 get 方法;函数的返回值就是属性的属性值;在 computed 中的,属性都有一个 get 和一个 set 方法,当数据变化时,调用 set 方法。
- 侦听属性 watch:
不支持缓存,数据变,直接会触发相应的操作;
watch 支持异步;
监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
当一个属性发生变化时,需要执行对应的操作;一对多;
监听数据必须是 data 中声明过或者父组件传递过来的 props 中的数据,当数据变化时,触发其他操作,函数有两个参数:
计算属性缓存 vs 方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message
还没有发生改变,多次访问 reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now()
不是响应式依赖:
computed: {
now: function () {
return Date.now()
}
}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。
计算属性 vs 侦听属性
区别:
1. `computed` 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 `watch` 是监听已经存在且已挂载到 `vm` 上的数据,所以用 `watch` 同样可以监听 `computed` 计算属性的变化(其它还有 `data`、`props`)
2. `computed` 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 `computed` 属性,才会计算新的值,而 `watch` 则是当数据发生变化便会调用执行函数
3. 从使用场景上说,`computed` 适用一个数据被多个数据影响,而 `watch` 适用一个数据影响多个数据;
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch
——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。细想一下这个例子:
<div id="demo">{{ fullName }}</div>
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
好得多了,不是吗?
computed原理
当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer遍历 data 中每个属性设置 get/set 数据拦截
初始化 computed 会调用 initComputed 函数
注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher )
调用计算属性时会触发其Object.defineProperty的get访问器函数
调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。
Watch原理
8.Vue的生命周期
举例:哪个阶段能操作DOM
mounted
9.父子组件生命周期顺序
通过上边一步步的debugger,我们可以发现父子组件在加载的时候,执行的先后顺序为父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted。
**10.Vue 组件间通信的方式
方法一、props
/$emit
- 父组件向子组件传值
父组件 A 通过 props 的方式向子组件 B 传递,B to A 通过在 B 组件中 $emit, A 组件中 v-on 的方式实现。
总结:父组件通过 props 向下传递数据给子组件。注:组件中的数据共有三种形式:data、props、computed
- 子组件向父组件传值(通过事件形式)
总结:子组件通过 events 给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。
方法二、$emit
/$on
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案 vuex。
1.具体实现方式:
var Event=new Vue();
Event.$emit(事件名,数据);
Event.$on(事件名,data => {});
方法三 Vuex与localStorage
vuex 是 vue 的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在 vuex 里数据改变的时候把数据拷贝一份保存到 localStorage 里面,刷新之后,如果 localStorage 里有保存的数据,取出来再替换 store 里的 state。
方法四、$attrs
/$listeners
1. 简介
多级组件嵌套需要传递数据时,通常使用的方法是通过 vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此 Vue2.4 版本提供了另一种方法----$attrs
/$listeners
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
格式为{属性名:属性值}。Vue2.4 提供了$attrs
, $listeners
来传递数据与事件,跨级组件之间的通讯变得更简单。
简单来说:$attrs
与$listeners
是两个对象,$attrs
里存放的是父组件中绑定的非 Props 属性,$listeners
里存放的是父组件中绑定的非原生事件。
方法五、provide/inject
1. 简介
Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
方法六、$parent
/ $children
与 ref
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/$children
:访问父 / 子实例
常见使用场景可以分为三类:
- 父子通信:
父向子传递数据是通过 props,子向父是通过 events($emit
);通过父链 / 子链也可以通信($parent
/$children
);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
- 兄弟通信:
Bus;Vuex - 跨级通信:
Bus;Vuex;provide / inject API、$attrs/$listeners
11.Vue的单向数据流
为什么是单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
这里有两种常见的试图改变一个 prop 的情形:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
**12.keep-alive组件 **
<keep-alive>
是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
<keep-alive>
与 <transition>
相似,只是一个抽象组件,它不会在DOM树中渲染(真实或者虚拟都不会),也不在父组件链中存在,比如:你永远在 this.$parent
中找不到 keep-alive
。
1、prop:
- include: 字符串或正则表达式。只有匹配的组件会被缓存。
- exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
exclude优先级大于include
// routes 配置
export default [
{
path: '/',
name: 'home',
component: Home,
meta: {
keepAlive: true // 需要被缓存
}
}, {
path: '/:id',
name: 'edit',
component: Edit,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
2、结合router,缓存部分页面
使用$route.meta的keepAlive属性:
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 这里是会被缓存的视图组件,比如 Home! -->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>
【加盐】使用 router.meta 拓展
假设这里有 3 个路由: A、B、C。
- 需求:
- 默认显示 A
- B 跳到 A,A 不刷新
- C 跳到 A,A 刷新
- 实现方式
- 在 A 路由里面设置 meta 属性:
{
path: '/',
name: 'A',
component: A,
meta: {
keepAlive: true // 需要被缓存
}
}
- 在 B 组件里面设置 beforeRouteLeave:
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = true; // 让 A 缓存,即不刷新
next();
}
};
- 在 C 组件里面设置 beforeRouteLeave:
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = false; // 让 A 不缓存,即刷新
next();
}
};
这样便能实现 B 回到 A,A 不刷新;而 C 回到 A 则刷新。
13.slot插槽
匿名插槽
具名插槽:v-slot :name (只能绑定在template标签上)
作用域插槽:在插槽中绑定数据
14.Vue检测数组或对象的变化
在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。可以直接在子组件修改对象或数组,但是并不会数据改变就会引起变化。
检测对象变化
1、不能检测到对象属性的添加或删除
var vm = new Vue({
data:{
data111:{
a = 1
}
}
})
data111.a = 2;//这个可以引起变化
但data111.b = 2;和vm.b = 2这个不能检测到变化
需要用
Vue.set(object, key, value)
比如$set(data111, b, 2);
或者:
$set(key,value)
比如vm.$set(‘b’, 2);
检测数组变化
下面两种情况不能检测到变化:
1、直接通过索引设置元素,如arr[0]=12;
2、直接修改数组的长度,如vm.arr.length
Vue.set( object, key, value )
用法:
this.$set(this.arr,0,12)
举例:vm.$set()的作用
15.虚拟DOM
https://www.cnblogs.com/fundebug/p/vue-virtual-dom.html
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。
•原理
•优缺点
优点:
1、跨平台
2、将dom对比操作放在js层。效率高
3、提升渲染性能
举例:diff算法
diff 算法包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了
16.Vue中的key的作用
https://blog.csdn.net/zyj362633491/article/details/86654014
当 Vue.js 用v-for
正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
总体来说,当使用列表渲染时,永远添加key
属性,这样可以提高列表渲染的效率,提高了页面的性能。
使用key属性强制替换元素
key
属性还有另外一种使用方法,即强制替换元素,从而可以触发组件的生命周期钩子或者触发过渡。因为当key
改变时,Vue认为一个新的元素产生了,从而会新插入一个元素来替换掉原有的元素。
**17.**nextTick的原理
vue 实现响应式并不是数据发生变化后 dom 立即变化,而是按照一定的策略来进行 dom 更新。
nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 nextTick,则可以在回调中获取更新后的 DOM
**18.**Vuex
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
getter: {
doneTodos: (state, getters) => {
return state.todos.filter(todo => todo.done)
}
},
mutations: {
increment (state, payload) {
state.count++
}
},
actions: {
addCount(context) {
// 可以包含异步操作
// context 是一个与 store 实例具有相同方法和属性的 context 对象
}
}
})
// 注入到根实例
new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
template: '<App/>',
components: { App }
})
-
state:
包含了
store
中存储的各个状态。 -
getter
类似于 Vue 中的计算属性,根据其他 getter 或 state 计算返回值,
注意: getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
注意: getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
-
mutation
一组方法,是改变
store
中状态的执行者,只能是同步操作。更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。也就是说,前面两个都是状态值本身,
mutations
才是改变状态的执行者。注意:
mutations
只能是同步地更改状态。const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 变更状态 state.count++ } } }) store.commit('increment')
-
action:
一组方法,其中可以包含异步操作。
想要异步地更改状态,就需要使用
action
。action
并不直接改变state
,而是发起mutation
。Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用
context.commit
提交一个 mutation,或者通过context.state
和context.getters
来获取 state 和 getters。发起
action
的方法形式和发起mutation
一样,只是换了个名字dispatch
。5、Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
这时我们可以将 store 分割为模块(module),每个模块拥有自己的
state
、getters
、mutations
、actions
、甚至是嵌套子模块——从上至下进行同样方式的分割。
举例:dispatch和commit的区别
commit执行的结果是变更store中的状态,直接执行
store.dispatch
返回相应action
的执行结果,而action的处理函数返回的就是Promise,所以store.dispatch
仍然返回一个Promise。
一个 store.dispatch
在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
Action与Mutation的区别
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作,而Mutation只能且必须是同步操作。
Vuex 结构分析
**19.vue-router的两种模式
hash 模式
只能改变 # 后面的 url 片段即 hash 值。hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。每次 hash 值的变化,会触发 hashchange
这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听 hashchange
来实现更新页面部分内容的操作:
hash 模式的工作原理是 hashchange 事件,可以在 window 监听 hash 的变化。
<div id="test" style="height: 500px;width: 500px;margin: 0 auto"></div>
<script>
window.onhashchange = function (event) {
console.log(event) // HashChangeEvent {..., newURL: "...test.html#red", oldURL: "...test.html", ...}
console.log(location) // location {..., hash: "#red", ...}
let hash = location.hash.slice(1); // red
document.body.style.color = hash;
document.getElementById('test').style.backgroundColor = hash
}
</script>
在 url 后面随便添加一个 #xx 会触发 onhashchange 事件。打印 event,里边有两个属 性 newURL 和 oldURL。可以通过模拟改变 hash 的值,动态改变页面数据。
history 模式
前面的 hashchange,只能改变 # 后面的 url 片段,而 history api 则给了前端完全的自由。
通过history api,我们丢掉了丑陋的 #,但是它也有个毛病:
不怕前进,不怕后退,就怕f5刷新,刷新是实实在在地去请求服务器的。在 hash 模式下,前端路由修改的是 # 中的信息,而浏览器请求时是跟它无关的,所以没有问题。
但是在 history 下,你可以自由的修改 path,当刷新时,如果服务器中没有相应的响应或者资源,会刷出404来。
多了两个 API,pushState()
和 replaceState()。
通过这两个 API:
1)可以改变 url 地址且不会发送请求
2)不仅可以读取历史记录栈,还可以对浏览器历史记录栈进行修改。
区别
- 前面的hashchange,你只能改变 # 后面的 url 片段。而 pushState 设置的新 URL 可以是与当前 URL 同源的任意 URL。
- history 模式则会将 URL 修改得就和正常请求后端的 URL 一样,如后端没有配置对应 /user/id 的路由处理,则会返回404错误
$router
与$route
的区别
$route
是一个跳转的路由对象,每一个路由都会有一个 route 对象,是一个局部的对象。可以获取对应的 name、path、query、params 等(<router-link>
传的参数由this.$route.query
或者this.$route.params
接收)$router
为通过 Vue.use(VueRouter) 和 VueRouter 构造函数得到的一个 router 的实例对象,这个对象是一个全局的对象。想要导航到不同 URL,则使用$router.push
方法;返回上一个 history 也是使用$router.go
方法
20.vue-router 有哪几种导航钩子
1、全局守卫: router.beforeEach
2、全局解析守卫: router.beforeResolve
3、全局后置钩子: router.afterEach
4、路由独享的守卫: beforeEnter
5、组件内的守卫: beforeRouteEnter:进入组件路由之前
beforeRouteUpdate (2.2 新增):在本路由的下级路由切换时触发
beforeRouteLeave:离开组件路由
导航表示路由正在发生改变,vue-router 提供的导航守卫主要用来:通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
注意:参数或查询的改变并不会触发进入/离开的导航守卫。 你可以通过 观察 $route 对象 来应对这些变化,或使用 beforeRouteUpdate的组件内守卫。
21.常用的事件修饰符
.stop: 阻止冒泡
.prevent: 阻止默认行为
.self: 仅绑定元素自身触发
.once: 2.1.4 新增,只触发一次
passive: 2.3.0 新增,滚动事件的默认行为 (即滚动行为) 将会立即触发,不能和.prevent 一起使用
.sync 修饰符