- 1. 谈谈你对MVVM的理解
- 2. data为什么是一个函数而不是对象
- 3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?
- 4. Computed 和 Watch 的区别
- 5. Computed 和 Methods 的区别
- 6. slot是什么?有什么作用?原理是什么?
- 7. 如何保存页面的当前的状态
- 8. 常见的事件修饰符及其作用
- 9. v-if、v-show、v-html 的原理
- 10. v-if和v-show的区别
- 11. v-model 原理
- 12. 对keep-alive的理解,它是如何实现的,具体缓存的是什么?
- 13. $nextTick 原理及作用
- 14. Vue template 到 render 的过程
- 15. 描述下Vue自定义指令
- 16. 子组件可以直接改变父组件的数据吗?
- 17. Vue是如何收集依赖的?
- 18. 什么是 mixin ?
- 19. 对SSR的理解
- 20. Vue的性能优化有哪些
- 21. 对 SPA 单页面的理解,它的优缺点分别是什么?
- 22. new Vue 以后发生的事情
- 23. 说一下Vue的生命周期
- 24. Vue 子组件和父组件执行顺序
- 25. created和mounted的区别
- 26. 一般在哪个生命周期请求异步数据
- 27. 组件通信
- 28. Vue3.0有什么更新
- 29. defineProperty和proxy的区别
- 30. 对虚拟DOM的理解?
- 31. 虚拟DOM的解析过程
- 32. DIFF算法的原理
- 33. Vue中key的作用
- 34. 为什么不建议用index作为key?
- 35. Vue-Router 的懒加载如何实现
- 36. 路由的hash和history模式的区别
- 37. 如何获取页面的hash变化
- 38. $route 和$router 的区别
- 39. 如何定义动态路由?如何获取传过来的动态参数?
- 40. Vue-router 路由钩子在生命周期的体现
- 41. Vue-router跳转和location.href有什么区别
- 42. params和query的区别
- 43. 对前端路由的理解
- 44. Vue router 原理, 哪个模式不会请求服务器
1. 谈谈你对MVVM的理解
MVVM 分为 Model、View、ViewModel:
- Model代表数据模型,数据和业务逻辑都在Model层中定义;
- View代表视图,负责数据的展示;
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
2. data为什么是一个函数而不是对象
JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?
Vue2.0的数据响应是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty () 来劫持各个属性的setter、getter,但是它并不算是实现数据的响应式的完美方案,某些情况下需要对其进行修补这也是它的缺陷,主要表现在两个方面:
- vue 实例创建后,无法检测到对象属性的新增或删除,只能追踪到数据是否被修改
- 不能监听数组的变化
解析:
vue 实例创建后,无法检测到对象属性的新增或删除,只能追踪到数据是否被修改(Object.defineProperty只能劫持对象的属性)。当创建一个Vue实例时,将遍历所有DOM对象,并为每个数据属性添加了get和set。get和set 允许Vue观察数据的更改并触发更新。但是,如果你在Vue实例化后添加(或删除)一个属性,这个属性不会被vue处理,改变get和set。解决方案:
Vue.set(obj, propertName/index, value)
// 响应式对象的子对象新增属性,可以给子响应式对象重新赋值
data.location = {
x: 100,
y: 100
}
data.location = {...data, z: 100}
不能监听数组的变化
vue在实现数组的响应式时,它使用了一些hack,把无法监听数组的情况通过重写数组的部分方法来实现响应式,这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,其他数组方法及数组的使用则无法检测到,例如如下两种使用方式
vm.items[index] = newValue;
vm.items.length
vue实现数组响应式的方法
通过重写数组的Array.prototype对应的方法,具体来说就是重新指定要操作数组的prototype,并重新该prototype中对应上面的7个数组方法,通过下面代码简单了解下实现原理:
const methods = ['pop','shift','unshift','sort','reverse','splice', 'push'];
// 复制Array.prototype,并将其prototype指向Array.prototype
let proto = Object.create(Array.prototype);
methods.forEach(method => {
proto[method] = function () { // 重写proto中的数组方法
Array.prototype[method].call(this, ...arguments);
viewRender() // 视图更新
function observe(obj) {
if (Array.isArray(obj)) { // 数组实现响应式
obj.__proto__ = proto; // 改变传入数组的prototype
return;
}
if (typeof obj === 'object') {
... // 对象的响应式实现
}
}
}
})
4. Computed 和 Watch 的区别
对于Computed:
- 它支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
对于Watch:
- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
- immediate:组件加载立即触发回调函数
- deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
总结:
- computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
运用场景:
- 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
5. Computed 和 Methods 的区别
可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的
不同点:computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
-
6. slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
7. 如何保存页面的当前的状态
既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:
- 前组件会被卸载
- 前组件不会被卸载
那么可以按照这两种情况分别得到以下方法:
组件会被卸载:
(1)将状态存储在LocalStorage / SessionStorage
只需要在组件即将被销毁的生命周期中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
优点:
- 兼容性好,不需要额外库或工具。
- 简单快捷,基本可以满足大部分需求。
缺点:
- 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)
- 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象
(2)路由传值
通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点:
- 简单快捷,不会污染 LocalStorage / SessionStorage。
- 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)
缺点:
- 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
优点:
- 代码量少
- 不需要考虑状态传递过程中的错误
缺点:
- 增加 A 组件维护成本
- 需要传入额外的 prop 到 B 组件
- 无法利用路由定位页面
除此之外,在Vue中,还可以用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行 被包裹在keep-alive中的组件的状态将会被保留:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>
router.js
{
path: '/',
name: 'xxx',
component: ()=>import('../src/views/xxx.vue'),
meta:{
keepAlive: true // 需要被缓存
}
},
8. 常见的事件修饰符及其作用
- .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
- .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
- .capture :与事件冒泡的方向相反,事件捕获由外到内;
- .self :只会触发自己范围内的事件,不包含子元素;
-
9. v-if、v-show、v-html 的原理
v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
- v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;
v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。
10. v-if和v-show的区别
手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
- 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
11. v-model 原理
v-model 只是语法糖而已
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:text 和 textarea 元素使用 value property 和 input 事件;
- checkbox 和 radio 使用 checked property 和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
在普通标签上
<input v-model="sth" /> //这一行等于下一行
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />
在组件上
<currency-input v-model="price"></currentcy-input>
<!--上行代码是下行的语法糖
<currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->
<!-- 子组件定义 -->
Vue.component('currency-input', {
template: `
<span>
<input
ref="input"
:value="value"
@input="$emit('input', $event.target.value)"
>
</span>
`,
props: ['value'],
})
12. 对keep-alive的理解,它是如何实现的,具体缓存的是什么?
keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
作用:
- 它能够把不活动的组件实例保存在内存中,而不是直接将其销毁
- 它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中
使用方式:
- 常用的两个属性include/exclude,允许组件有条件的进行缓存。
- 两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
- keep-alive的中还运用了LRU(Least Recently Used)算法。
原理:Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode节点,所以,keep- alive的缓存也是基于VNode节点的而不是直接存储DOM结构。
当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。其实就是将需要缓存的VNode节点保存在this.cache中/在render时,如果VNode的name符合在缓存条件(可以用include以及exclude控制),则会从this.cache中取出之前缓存的VNode实例进行渲染。
使用有两个场景,一个是动态组件,一个是router-view
这里创建了一个白名单和一个黑名单。表明哪些需要需要做缓存,哪些不需要做缓存。以及最大的缓存个数。
13. $nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
- 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。
this.$nextTick(() => { // 获取数据的操作...})
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中。
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
14. Vue template 到 render 的过程
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就称为模板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
- 解析阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
- 优化阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
- 生成阶段:将最终的AST转化为render函数字符串。
vue 在模版编译版本的代码中会执行 compileToFunctions 将template转化为render函数:
// 将模板编译为render函数
const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
CompileToFunctions中的主要逻辑如下∶
(1)调用parse方法将template转化为ast(抽象语法树)
const ast = parse(template.trim(), options)
- parse的目标:把tamplate转换为AST树,它是一种用 JavaScript对象的形式来描述整个模板。
- 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造AST树的目的。
AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
(2)对静态节点做优化
optimize(ast,options)
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
const code = generate(ast, options)
generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(render
) 生成render函数。
15. 描述下Vue自定义指令
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。 一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数据;
(1)自定义指令基本内容
- 全局定义:Vue.directive(“focus”,{})
- 局部定义:directives:{focus:{}}
- 钩子函数:指令定义对象提供钩子函数
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
- ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
- 钩子函数参数
- el:绑定元素
- bing: 指令核心对象,描述指令全部信息属性
- name
- value
- oldValue
- expression
- arg
- modifers
- vnode 虚拟节点
- oldVnode:上一个虚拟节点(更新钩子函数中才有用)
(2)使用场景
- 普通DOM元素进行底层操作的时候,可以使用自定义指令
- 自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。
(3)使用案例
初级应用:
- 鼠标聚焦
- 下拉菜单
- 相对时间转换
- 滚动动画
高级应用:
- 自定义指令实现图片懒加载
-
16. 子组件可以直接改变父组件的数据吗?
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
只能通过 $emit派发一个自定义事件,父组件接收到后,由父组件修改。17. Vue是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
function defineReactive (obj, key, val){
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if(Dep.target){
dep.depend();
...
}
return val
}
...
})
}
以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。
(1)Dep Dep是整个依赖收集的核心,其关键代码如下:class Dep {
static target;
subs;
constructor () {
...
this.subs = [];
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.sub, sub)
}
depend () {
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subds.slice();
for(let i = 0;i < subs.length; i++){
subs[i].update()
}
}
}
Dep 是一个 class ,其中有一个关键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
(2)Watcherclass Watcher {
getter;
...
constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)
}
...
}
function pushTarget (_target) {
Dep.target = _target
}
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶ 初始化状态initState,这中间便会通过defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。 初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent)
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。18. 什么是 mixin ?
Mixin 使我们能够为 Vue 组件编写可重用的功能。
- 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优x先于组件自已的 hook。
19. 对SSR的理解
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
SSR的优势:更好的SEO
- 首屏加载速度更快
SSR的缺点:
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
- 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境;
-
20. Vue的性能优化有哪些
(1)编码阶段
尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
(2)SEO优化
- 预渲染
- 服务端渲染SSR
(3)打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
(4)用户体验
- 骨架屏
- PWA
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
21. 对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
22. new Vue 以后发生的事情
- new Vue会调用 Vue 原型链上的_init方法对 Vue 实例进行初始化;
- 首先是initLifecycle初始化生命周期,对 Vue 实例内部的一些属性(如 children、parent、isMounted)进行初始化;
- initEvents,初始化当前实例上的一些自定义事件(Vue.$on);
- initRender,解析slots绑定在 Vue 实例上,绑定createElement方法在实例上;
- 完成对生命周期、自定义事件等一系列属性的初始化后,触发生命周期钩子beforeCreate;
- initInjections,在初始化data和props之前完成依赖注入(类似于 React.Context);
- initState,完成对data和props的初始化,同时对属性完成数据劫持内部,启用监听者对数据进行监听(更改);
- initProvide,对依赖注入进行解析;
- 完成对数据(state 状态)的初始化后,触发生命周期钩子created;
- 进入挂载阶段,将 vue 模板语法通过vue-loader解析成虚拟 DOM 树,虚拟 DOM 树与数据完成双向绑定,触发生命周期钩子beforeMount;
将解析好的虚拟 DOM 树通过 vue 渲染成真实 DOM,触发生命周期钩子mounted;
23. 说一下Vue的生命周期
Vue 实例有⼀个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
生命周期经历的阶段和钩子函数:实例化vue(组件)对象:new Vue()
- 初始化事件和生命周期 init events 和 init cycle
- beforeCreate函数:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。即此时vue(组件)对象被创建了,但是vue对象的属性还没有绑定,如data属性,computed属性还没有绑定,即没有值。此时还没有数据和真实DOM。即:属性还没有赋值,也没有动态创建template属性对应的HTML元素(二阶段的createUI函数还没有执行)
- 挂载数据(属性赋值)包括 属性和computed的运算
- Created函数:vue对象的属性有值了,但是DOM还没有生成,$el属性还不存在。此时有数据了,但是还没有真实的DOM即:data,computed都执行了。属性已经赋值,但没有动态创建template属性对应的HTML元素,所以,此时如果更改数据不会触发updated函数如果:数据的初始值就来自于后端,可以发送ajax,或者fetch请求获取数据,但是,此时不会触发updated函数
6.1 检查是否有el属性 检查vue配置,即new Vue{}里面的el项是否存在,有就继续检查template项。没有则等到手动绑定调用 vm.$mount()完成了el的绑定。
6.2 检查是否有template属性
检查配置中的template项,如果没有template进行填充被绑定区域,则被绑定区域的el对outerHTML(即 整个#app DOM对象)都作为被填充对象替换掉填充区域。即: 如果vue对象中有 template属性,那么,template后面的HTML会替换$el对应的内容。如果有render属性,那么render就会替换template。 即:优先关系时: render > template > el
- beforeMount函数:模板编译(template)、数据挂载(把数据显示在模板里)之前执行的钩子函数此时 this.$el有值,但是数据还没有挂载到页面上。即此时页面中的{{}}里的变量还没有被数据替换
- 模板编译:用vue对象的数据(属性)替换模板中的内容
- Mounted函数:模板编译完成,数据挂载完毕即:此时已经把数据挂载到了页面上,所以,页面上能够看到正确的数据了。
- beforeUpdate函数:组件更新之前执行的函数,只有数据更新后,才能调用(触发)beforeUpdate,注意:此数据一定是在模板上出现的数据,否则,不会,也没有必要触发组件更新(因为数据不出现在模板里,就没有必要再次渲染)数据更新了,但是,vue(组件)对象对应的dom中的内部(innerHTML)没有变,所以叫作组件更新前
- updated函数:组件更新之后执行的函数vue(组件)对象对应的dom中的内部(innerHTML)改变了,所以叫作组件更新之后
- activated函数:keep-alive组件激活时调用
- deactivated函数:keep-alive组件停用时调用
- beforeDestroy:vue(组件)对象销毁之前
- destroyed:vue组件销毁后
keep-alive
解析: 比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用
24. Vue 子组件和父组件执行顺序
加载渲染过程:
父组件 beforeCreate
父组件 created
父组件 beforeMount
子组件 beforeCreate
子组件 created
子组件 beforeMount
子组件 mounted
父组件 mounted
更新过程:
父组件 beforeUpdate
子组件 beforeUpdate
子组件 updated
父组件 updated
销毁过程:
父组件 beforeDestroy
子组件 beforeDestroy
子组件 destroyed
父组件 destoryed
25. created和mounted的区别
created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
26. 一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
27. 组件通信
父子组件通信
常用的方法有:
方案 | 父组件向子组件 | 子组件向父组件 |
---|---|---|
props / emits | props | emits |
v-model / emits | v-model | emits |
ref / emits | ref | emits |
provide / inject | provide | inject |
EventBus | emit / on | emit / on |
Vuex | - | - |
- props / emits
这是Vue跨组件通信最常用,也是基础的一个方案,它的通信过程是:
- Father.vue 通过 prop 向 Child.vue 传值(可包含父级定义好的函数)
- Child.vue 通过 emit 向 Father.vue 触发父组件的事件执行
下发 props
下发的过程是在 Father.vue 里完成的,父组件在向子组件下发 props 之前,需要导入子组件并启用它作为自身的模板,然后在 setup 里处理好数据,return 给 template 用。
<template>
<Child
title="用户信息"
:index="1"
:uid="userInfo.id"
:user-name="userInfo.name"
/>
</template>
<script>
import { defineComponent } from 'vue'
import Child from '@cp/Child.vue'
interface Member {
id: number,
name: string
};
export default defineComponent({
// 需要启用子组件作为模板
components: {
Child
},
// 定义一些数据并return给template用
setup () {
const userInfo: Member = {
id: 1,
name: 'Petter'
}
// 不要忘记return,否则template拿不到数据
return {
userInfo
}
}
})
</script>
接收 props
接收的过程是在 Child.vue 里完成的,在 script 部分,子组件通过与 setup 同级的 props 来接收数据。
它可以是一个数组,每个 item 都是 String 类型,把你要接受的变量名放到这个数组里,直接放进来作为数组的 item:
export default defineComponent({
props: [
'title',
'index',
'userName',
'uid'
]
})
但这种情况下,使用者不知道这些属性到底是什么类型的值,是否必传。
带有类型限制的 props
注:这一小节的步骤是在 Child.vue 里操作。
既然我们最开始在决定使用 Vue 3.0 的时候,为了更好的类型限制,已经决定写 TypeScript ,那么我们最好不要出现这种使用情况。
推荐的方式是把 props 定义为一个对象,以对象形式列出 prop,每个 property 的名称和值分别是 prop 各自的名称和类型,只有合法的类型才允许传入。
于是我们把 props 再改一下,加上类型限制:
export default defineComponent({
props: {
title: String,
index: Number,
userName: String,
uid: Number
}
})
这样我们如果传入不正确的类型,程序就会抛出警告信息,告知开发者必须正确传值。
如果你需要对某个 prop 允许多类型,比如这个 uid 字段,它可能是数值,也可能是字符串,那么可以在类型这里,使用一个数组,把允许的类型都加进去。
export default defineComponent({
props: {
// 单类型
title: String,
index: Number,
userName: String,
// 这里使用了多种类型
uid: [ Number, String ]
}
})
可选以及带有默认值的 props
注:这一小节的步骤是在 Child.vue 里操作。
有时候我们想对一些 prop 设置为可选,然后提供一些默认值,还可以再将 prop 再进一步设置为对象,支持的字段有:
字段 | 类型 | 含义 |
---|---|---|
type | string | prop 的类型 |
required | boolean | 是否必传,true=必传,false=可选 |
default | any | 与 type 字段的类型相对应的默认值,如果 required 是 false ,但这里不设置默认值,则会默认为 undefined |
validator | function | 自定义验证函数,需要 return 一个布尔值,true=校验通过,false=校验不通过,当校验不通过时,控制台会抛出警告信息 |
我们现在再对 props 改造一下,对部分字段设置为可选,并提供默认值:
export default defineComponent({
props: {
// 可选,并提供默认值
title: {
type: String,
required: false,
default: '默认标题'
},
// 默认可选,单类型
index: Number,
// 添加一些自定义校验
userName: {
type: String,
// 在这里校验用户名必须至少3个字
validator: v => v.length >= 3
},
// 默认可选,但允许多种类型
uid: [ Number, String ]
}
})
使用 props
注:这一小节的步骤是在 Child.vue 里操作。
在 template 部分,3.x 的使用方法和 2.x 是一样的,比如要渲染我们上面传入的 props :
<template>
<p>标题:{{ title }}</p>
<p>索引:{{ index }}</p>
<p>用户id:{{ uid }}</p>
<p>用户名:{{ userName }}</p>
</template>
但是 script 部分,变化非常大!
在 2.x ,只需要通过 this.uid、this.userName 就可以使用父组件传下来的 prop 。
但是 3.x 没有了 this, 需要给 setup 添加一个入参才可以去操作。
export default defineComponent({
props: {
title: String,
index: Number,
userName: String,
uid: Number
},
// 在这里需要添加一个入参
setup (props) {
// 该入参包含了我们定义的所有props
console.log(props);
}
})
TIP
- prop 是只读,不允许修改
- setup 的第一个入参,包含了我们定义的所有props(如果在 Child.vue 里未定义,但 父组件 Father.vue 那边非要传过来的,不会拿到,且控制台会有警告信息)
- 该入参可以随意命名,比如你可以写成一个下划线 ,通过 .uid 也可以拿到数据,但是语义化命名,是一个良好的编程习惯。
传递非 Prop 的 Attribute
上面的提示里有提到一句:
如果在 Child.vue 里未定义,但 父组件 Father.vue 那边非要传过来的,不会拿到,且控制台会有警告信息
但并不意味着你不能传递任何未定义的属性数据,在父组件,除了可以给子组件绑定 props,你还可以根据实际需要去绑定一些特殊的属性。
比如给子组件设置 class、id,或者 data-xxx 之类的一些自定义属性,如果 Child.vue 组件的 template 只有一个根节点,这些属性默认自动继承,并渲染在 node 节点上。
在 Father.vue 里,对 Child.vue 传递了 class、id 和 data-hash:
<template>
<Child
class="child"
keys="aaaa"
data-hash="afJasdHGUHa87d688723kjaghdhja"
/>
</template>
渲染后(2个 data-v-xxx 是父子组件各自的 css scoped 标记):
<div
class="child"
keys="aaaa"
data-hash="afJasdHGUHa87d688723kjaghdhja"
data-v-2dcc19c8=""
data-v-7eb2bc79=""
>
<!-- Child的内容 -->
</div>
你可以在 Child.vue 配置 inheritAttrs 为 false,来屏蔽这些自定义属性的渲染。
export default defineComponent({
inheritAttrs: false,
setup () {
// ...
}
})
获取非 Prop 的 Attribute
想要拿到这些属性,原生操作需要通过 element.getAttribute ,但 Vue 也提供了相关的 API :
在 Child.vue 里,可以通过 setup 的第二个参数 context 里的 attrs 来获取到这些属性。
export default defineComponent({
setup (props, { attrs }) {
// attrs 是个对象,每个 Attribute 都是它的 key
console.log(attrs.class);
// 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
console.log(attrs['data-hash']);
}
})
TIP
- attr 和 prop 一样,都是只读的
- 不管 inheritAttrs 是否设置,都可以通过 attrs 拿到这些数据,但是 element.getAttribute 则只有 inheritAttrs 为 true 的时候才可以。
Vue 3 的 template 还允许多个根节点,多个根节点的情况下,无法直接继承这些属性,需要在 Child.vue 指定继承在哪个节点上,否则会有警告信息。
<template>
<!-- 指定继承 -->
<p v-bind="attrs"></p>
<!-- 指定继承 -->
<!-- 这些不会自动继承 -->
<p></p>
<p></p>
<p></p>
<!-- 这些不会自动继承 -->
</template>
当然,前提依然是,setup 里要把 attrs 给 return 出来。
查看详情:多个根节点上的 Attribute 继承open in new window
绑定 emits
最开始有介绍到,子组件如果需要向父组件告知数据更新,或者执行某些函数时,是通过 emits 来进行的。
每个 emit 都是事件,所以需要先由父组件先给子组件绑定,子组件才能知道应该怎么去调用。
TIP
当然,父组件也是需要先在 setup 里进行定义并 return,才能够在 template 里绑定给子组件。
比如要给 Child.vue 绑定一个更新用户年龄的方法,那么在 Father.vue 里需要这么处理:
先看 script 部分(留意注释部分):
import { defineComponent, reactive } from 'vue'
import Child from '@cp/Child.vue'
interface Member {
id: number,
name: string,
age: number
};
export default defineComponent({
components: {
Child
},
setup () {
const userInfo: Member = reactive({
id: 1,
name: 'Petter',
age: 0
})
// 定义一个更新年龄的方法
const updateAge = (age: number): void => {
userInfo.age = age;
}
return {
userInfo,
// return给template用
updateAge
}
}
})
再看 template 部分(为了方便阅读,我把之前绑定的 props 先去掉了):
<template>
<Child
@update-age="updateAge"
/>
</template>
接收 emits
注:这一小节的步骤是在 Child.vue 里操作。
和 props 一样,你可以指定是一个数组,把要接收的 emit 名称写进去:
export default defineComponent({
emits: [
'update-age'
]
})
其实日常这样配置就足够用了。
TIP
- 这里的 emit 名称指 Father.vue 在给 Child.vue 绑定事件时,template 里面给子组件指定的 @aaaaa=”bbbbb” 里的 aaaaa
- 当在 emits 选项中定义了原生事件 (如 click ) 时,将使用组件中的事件替代原生事件侦听器
接收 emits 时做一些校验
当然你也可以对这些事件做一些验证,配置为对象,然后把这个 emit 名称作为 key, value 则配置为一个方法。
比如上面的更新年龄,只允许达到成年人的年龄才会去更新父组件的数据:
export default defineComponent({
emits: {
// 需要校验
'update-age': (age: number) => {
// 写一些条件拦截,记得返回false
if ( age < 18 ) {
console.log('未成年人不允许参与');
return false;
}
// 通过则返回true
return true;
},
// 一些无需校验的,设置为null即可
'update-name': null
}
})
调用 emits
注:这一小节的步骤是在 Child.vue 里操作。
和 props 一样,也需要在 setup 的入参里引入 emit ,才允许操作。
setup 的第二个入参 expose 是一个对象,你可以完整导入 expose 然后通过 expose.emit 去操作,也可以按需导入 { emit } (推荐这种方式):
export default defineComponent({
emits: [
'update-age'
],
setup (props, { emit }) {
// 2s 后更新年龄
setTimeout( () => {
emit('update-age', 22);
}, 2000);
}
})
- v-model / emits
对比 props / emits ,这个方式更为简单:
- 在 Father.vue ,通过 v-model 向 Child.vue 传值
- Child.vue 通过自身设定的 emits 向 Father.vue 通知数据更新
v-model 的用法和 props 非常相似,但是很多操作上更为简化,但操作简单带来的 “副作用” ,就是功能上也没有 props 那么多。
绑定 v-model
它的和下发 props 的方式类似,都是在子组件上绑定 Father.vue 定义好并 return 出来的数据。
TIP
- 和 2.x 不同, 3.x 可以直接绑定 v-model ,而无需在子组件指定 model 选项。
- 另外,3.x 的 v-model 需要使用 : 来指定你要绑定的属性名,同时也开始支持绑定多个 v-model
我们来看看具体的操作:
<template>
<Child
v-model:user-name="userInfo.name"
/>
</template>
如果你要绑定多个数据,写多个 v-model 即可
<template>
<Child
v-model:user-name="userInfo.name"
v-model:uid="userInfo.id"
/>
</template>
看到这里应该能明白了,一个 v-model 其实就是一个 prop,它支持的数据类型,和 prop 是一样的。
所以,子组件在接收数据的时候,完全按照 props 去定义就可以了。
点击回顾:接收 props ,了解在 Child.vue 如何接收 props,以及相关的 props 类型限制等部分内容。
配置 emits
注:这一小节的步骤是在 Child.vue 里操作。
虽然 v-model 的配置和 prop 相似,但是为什么出这么两个相似的东西?自然是为了简化一些开发上的操作。
使用 props / emits,如果要更新父组件的数据,还需要在父组件定义好方法,然后 return 给 template 去绑定事件给子组件,才能够更新。
而使用 v-model / emits ,无需如此,可以在 Child.vue 直接通过 “update:属性名” 的格式,直接定义一个更新事件:
export default defineComponent({
props: {
userName: String,
uid: Number
},
emits: [
'update:userName',
'update:uid'
]
})
这里的 update 后面的属性名,支持驼峰写法,这一部分和 2.x 的使用是相同的。
这里也可以对数据更新做一些校验,配置方式和 接收 emits 时做一些校验 是一样的。
调用自身的 emits
注:这一小节的步骤是在 Child.vue 里操作。
在 Child.vue 配置好 emits 之后,就可以在 setup 里直接操作数据的更新了:
export default defineComponent({
// ...
setup (props, { emit }) {
// 2s 后更新用户名
setTimeout(() => {
emit('update:userName', 'Tom')
}, 2000);
}
})
在使用上,和 调用 emits 是一样的。
- ref / emits
在学习 响应式 API 之 ref 的时候,我们了解到 ref 是可以用在 DOM 元素与子组件 上面。
父组件操作子组件
所以,父组件也可以直接通过对子组件绑定 ref 属性,然后通过 ref 变量去操作子组件的数据或者调用里面的方法。
比如导入了一个 Child.vue 作为子组件,需要在 template 处给子组件标签绑定 ref:
<template>
<Child ref="child" />
</template>
然后在 script 部分定义好对应的变量名称(记得要 return 出来):
import { defineComponent, onMounted, ref } from 'vue'
import Child from '@cp/Child.vue'
export default defineComponent({
components: {
Child
},
setup () {
// 给子组件定义一个ref变量
const child = ref<HTMLElement>(null);
// 请保证视图渲染完毕后再执行操作
onMounted( () => {
// 执行子组件里面的ajax函数
child.value.getList();
// 打开子组件里面的弹窗
child.value.isShowDialog = true;
});
// 必须return出去才可以给到template使用
return {
child
}
}
})
子组件通知父组件
子组件如果想主动向父组件通讯,也需要使用 emit,详细的配置方法可见:绑定 emits
爷孙组件通信
顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件”):
C组件引入到B组件里,B组件引入到A组件里渲染,此时A是C的爷爷级别(可能还有更多层级关系),如果你用 props ,只能一级一级传递下去,那就太繁琐了,因此我们需要更直接的通信方式。
他们之间的关系如下,Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (可能有多个):
Grandfather.vue
└─Son.vue
└─Grandson.vue
这一 Part 就是讲一讲 C 和 A 之间的数据传递,常用的方法有:
方案 | 爷组件向孙组件 | 孙组件向爷组件 |
---|---|---|
provide / inject | provide | inject |
EventBus | emit / on | emit / on |
Vuex | - | - |
为了方便阅读,下面的父组件统一叫 Grandfather.vue,子组件统一叫 Grandson.vue,但实际上他们之间可以隔无数代…
- provide / inject
这个特性有两个部分:Grandfather.vue 有一个 provide 选项来提供数据,Grandson.vue 有一个 inject 选项来开始使用这些数据。
- Grandfather.vue 通过 provide 向 Grandson.vue 传值(可包含定义好的函数)
- Grandson.vue 通过 inject 向 Grandfather.vue 触发爷爷组件的事件执行
无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者。
TIP
这一部分的内容变化都特别大,但使用起来其实也很简单,不用慌,也有相同的地方:
- 父组件不需要知道哪些子组件使用它 provide 的 property
- 子组件不需要知道 inject property 来自哪里
另外,要切记一点就是:provide 和 inject 绑定并不是可响应的。这是刻意为之的,但如果传入了一个可监听的对象,那么其对象的 property 还是可响应的。
发起 provide
我们先来回顾一下 2.x 的用法:
export default {
// 定义好数据
data () {
return {
tags: [ '中餐', '粤菜', '烧腊' ]
}
},
// provide出去
provide () {
return {
tags: this.tags
}
}
}
旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数。
3.x 的新版 provide, 和 2.x 的用法区别比较大。
TIP
在 3.x , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法。
每次要 provide 一个数据的时候,就要单独调用一次。
每次调用的时候,都需要传入 2 个参数:
参数 | 类型 | 说明 |
---|---|---|
key | string | 数据的名称 |
value | any | 数据的值 |
来看一下如何创建一个 provide:
// 记得导入provide
import { defineComponent, provide } from 'vue'
export default defineComponent({
// ...
setup () {
// 定义好数据
const msg: string = 'Hello World!';
// provide出去
provide('msg', msg);
}
})
操作非常简单对吧哈哈哈,但需要注意的是,provide 不是响应式的,如果你要使其具备响应性,你需要传入响应式数据,详见:响应性数据的传递与接收
接收 inject
也是先来回顾一下 2.x 的用法:
export default {
inject: [
'tags'
],
mounted () {
console.log(this.tags);
}
}
旧版的 inject 用法和 props 类似,3.x 的新版 inject, 和 2.x 的用法区别也是比较大。
TIP
在 3.x, inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法。
每次要 inject 一个数据的时候,就要单独调用一次。
每次调用的时候,只需要传入 1 个参数:
参数 | 类型 | 说明 |
---|---|---|
key | string | 与 provide 相对应的数据名称 |
来看一下如何创建一个 inject:
// 记得导入inject
import { defineComponent, inject } from 'vue'
export default defineComponent({
// ...
setup () {
const msg: string = inject('msg') || '';
}
})
也是很简单(写 TS 的话,由于 inject 到的值可能是 undefined,所以要么加个 undefined 类型,要么给变量设置一个空的默认值)。
响应性数据的传递与接收
之所以要单独拿出来说, 是因为变化真的很大 - -
在前面我们已经知道,provide 和 inject 本身不可响应,但是并非完全不能够拿到响应的结果,只需要我们传入的数据具备响应性,它依然能够提供响应支持。
我们以 ref 和 reactive 为例,来看看应该怎么发起 provide 和接收 inject。
对这 2 个 API 还不熟悉的同学,建议先阅读一下 响应式 API 之 ref 和 响应式 API 之 reactive 。
先在 Grandfather.vue 里 provide 数据:
export default defineComponent({
// ...
setup () {
// provide一个ref
const msg = ref<string>('Hello World!');
provide('msg', msg);
// provide一个reactive
const userInfo: Member = reactive({
id: 1,
name: 'Petter'
});
provide('userInfo', userInfo);
// 2s 后更新数据
setTimeout(() => {
// 修改消息内容
msg.value = 'Hi World!';
// 修改用户名
userInfo.name = 'Tom';
}, 2000);
}
})
在 Grandsun.vue 里 inject 拿到数据:
export default defineComponent({
setup () {
// 获取数据
const msg = inject('msg');
const userInfo = inject('userInfo');
// 打印刚刚拿到的数据
console.log(msg);
console.log(userInfo);
// 因为 2s 后数据会变,我们 3s 后再看下,可以争取拿到新的数据
setTimeout(() => {
console.log(msg);
console.log(userInfo);
}, 3000);
// 响应式数据还可以直接给 template 使用,会实时更新
return {
msg,
userInfo
}
}
})
非常简单,非常方便!!!
TIP
响应式的数据 provide 出去,在子孙组件拿到的也是响应式的,并且可以如同自身定义的响应式变量一样,直接 return 给 template 使用,一旦数据有变化,视图也会立即更新。
但上面这句话有效的前提是,不破坏数据的响应性,比如 ref 变量,你需要完整的传入,而不能只传入它的 value,对于 reactive 也是同理,不能直接解构去破坏原本的响应性。
切记!切记!!!
引用类型的传递与接收
这里是针对非响应性数据的处理
provide 和 inject 并不是可响应的,这是官方的故意设计,但是由于引用类型的特殊性,在子孙组件拿到了数据之后,他们的属性还是可以正常的响应变化。
先在 Grandfather.vue 里 provide 数据:
export default defineComponent({
// ...
setup () {
// provide 一个数组
const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
provide('tags', tags);
// provide 一个对象
const userInfo: Member = {
id: 1,
name: 'Petter'
};
provide('userInfo', userInfo);
// 2s 后更新数据
setTimeout(() => {
// 增加tags的长度
tags.push('叉烧');
// 修改userInfo的属性值
userInfo.name = 'Tom';
}, 2000);
}
})
在 Grandsun.vue 里 inject 拿到数据:
export default defineComponent({
setup () {
// 获取数据
const tags: string[] = inject('tags') || [];
const userInfo: Member = inject('userInfo') || {
id: 0,
name: ''
};
// 打印刚刚拿到的数据
console.log(tags);
console.log(tags.length);
console.log(userInfo);
// 因为 2s 后数据会变,我们 3s 后再看下,能够看到已经是更新后的数据了
setTimeout(() => {
console.log(tags);
console.log(tags.length);
console.log(userInfo);
}, 3000);
}
})
引用类型的数据,拿到后可以直接用,属性的值更新后,子孙组件也会被更新。
WARNING
由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用 响应式 API 。
基本类型的传递与接收
这里是针对非响应性数据的处理
基本数据类型被直接 provide 出去后,再怎么修改,都无法更新下去,子孙组件拿到的永远是第一次的那个值。
先在 Grandfather.vue 里 provide 数据:
export default defineComponent({
// ...
setup () {
// provide 一个数组的长度
const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
provide('tagsCount', tags.length);
// provide 一个字符串
let name: string = 'Petter';
provide('name', name);
// 2s 后更新数据
setTimeout(() => {
// tagsCount 在 Grandson 那边依然是 3
tags.push('叉烧');
// name 在 Grandson 那边依然是 Petter
name = 'Tom';
}, 2000);
}
})
在 Grandsun.vue 里 inject 拿到数据:
export default defineComponent({
setup () {
// 获取数据
const name: string = inject('name') || '';
const tagsCount: number = inject('tagsCount') || 0;
// 打印刚刚拿到的数据
console.log(name);
console.log(tagsCount);
// 因为 2s 后数据会变,我们 3s 后再看下
setTimeout(() => {
// 依然是 Petter
console.log(name);
// 依然是 3
console.log(tagsCount);
}, 3000);
}
})
很失望,并没有变化。
TIP
那么是否一定要定义成响应式数据或者引用类型数据呢?
当然不是,我们在 provide 的时候,也可以稍作修改,让它能够同步更新下去。
我们再来一次,依然是先在 Grandfather.vue 里 provide 数据:
export default defineComponent({
// ...
setup () {
// provide 一个数组的长度
const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
provide('tagsCount', (): number => {
return tags.length;
});
// provide 字符串
let name: string = 'Petter';
provide('name', (): string => {
return name;
});
// 2s 后更新数据
setTimeout(() => {
// tagsCount 现在可以正常拿到 4 了
tags.push('叉烧');
// name 现在可以正常拿到 Tom 了
name = 'Tom';
}, 2000);
}
})
这次可以正确拿到数据了,看出这2次的写法有什么区别了吗?
TIP
基本数据类型,需要 provide 一个函数,将其 return 出去给子孙组件用,这样子孙组件每次拿到的数据才会是新的。
但由于不具备响应性,所以子孙组件每次都需要重新通过执行 inject 得到的函数才能拿到最新的数据。
按我个人习惯来说,使用起来挺别扭的,能不用就不用……
WARNING
由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用
兄弟组件通信
如果他们之间要交流,目前大概有这两类选择:
全局组件通信
常用的方法有:
方案 | 发起方 | 接收方 |
---|---|---|
EventBus | emit | on |
Vuex | - | - |
EventBus
EventBus 通常被称之为 “全局事件总线” ,它是用来在全局范围内通信的一个常用方案,它的特点就是: “简单” 、 “灵活” 、“轻量级”。
TIP
在中小型项目,全局通信推荐优先采用该方案,事件总线在打包压缩后不到 200 个字节, API 也非常简单和灵活。
回顾 Vue 2
在 2.x,使用 EventBus 无需导入第三方插件,直接在自己的 libs 文件夹下创建一个 bus.ts 文件,暴露一个新的 Vue 实例即可。
import Vue from 'vue';
export default new Vue;
然后就可以在组件里引入 bus ,通过 $emit 去发起交流,通过 $on 去监听接收交流。
旧版方案的完整案例代码可以查看官方的 2.x 语法 - 事件 API
了解 Vue 3
Vue 3 移除了 $on 、 $off 和 $once 这几个事件 API ,应用实例不再实现事件触发接口。
根据官方文档在 迁移策略 - 事件 API的推荐,我们可以用 mitt或者 tiny-emitter 等第三方插件来实现 EventBus 。
创建 3.x 的 EventBus
这里以 mitt 为例,示范如何创建一个 Vue 3 的 EventBus 。
首先,需要安装 mitt :
npm install --save mitt
然后在 libs 文件夹下,创建一个 bus.ts 文件,内容和旧版写法其实是一样的,只不过是把 Vue 实例,换成了 mitt 实例。
import mitt from 'mitt';
export default mitt();
然后就可以定义发起和接收的相关事件了,常用的 API 和参数如下:
方法名称 | 作用 |
---|---|
on | 注册一个监听事件,用于接收数据 |
emit | 调用方法发起数据传递 |
off | 用来移除监听事件 |
on 的参数:
参数 | 类型 | 作用 |
---|---|---|
type | string | symbol | 方法名 |
handler | function | 接收到数据之后要做什么处理的回调函数 |
这里的 handler 建议使用具名函数,因为匿名函数无法销毁。
emit 的参数:
参数 | 类型 | 作用 |
---|---|---|
type | string | symbol | 与 on 对应的方法名 |
data | any | 与 on 对应的,允许接收的数据 |
off 的参数:
参数 | 类型 | 作用 |
---|---|---|
type | string | symbol | 与 on 对应的方法名 |
handler | function | 要删除的,与 on 对应的 handler 函数名 |
更多的 API 可以查阅 插件的官方文档,在了解了最基本的用法之后,我们来开始配置一对交流。
TIP
如果你需要把 bus 配置为全局 API ,不想在每个组件里分别 import 的话,可以参考之前的章节内容: 全局 API 挂载 。
创建和移除监听事件
在需要暴露交流事件的组件里,通过 on 配置好接收方法,同时为了避免路由切换过程中造成事件多次被绑定,多次触发,需要在适当的时机 off 掉:
import { defineComponent, onBeforeUnmount } from 'vue'
import bus from '@libs/bus'
export default defineComponent({
setup () {
// 定义一个打招呼的方法
const sayHi = (msg: string = 'Hello World!'): void => {
console.log(msg);
}
// 启用监听
bus.on('sayHi', sayHi);
// 在组件卸载之前移除监听
onBeforeUnmount( () => {
bus.off('sayHi', sayHi);
})
}
})
关于销毁的时机,可以参考 组件的生命周期 。
调用监听事件
在需要调用交流事件的组件里,通过 emit 进行调用:
import { defineComponent } from 'vue'
import bus from '@libs/bus'
export default defineComponent({
setup () {
// 调用打招呼事件,传入消息内容
bus.emit('sayHi', '哈哈哈哈哈哈哈哈哈哈哈哈哈哈');
}
})
Pinia
Pinia 和 Vuex 一样,也是 Vue 生态里面非常重要的一个成员,也都是运用于全局的状态管理。
但面向 Componsition API 而生的 Pinia ,更受 Vue 3 喜爱,已被钦定为官方推荐的新状态管理工具。
为了阅读上的方便,对 Pinia 单独开了一章,请跳转至 全局状态的管理 阅读。
28. Vue3.0有什么更新
- 响应式原理的改变 Vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
- 组件选项声明方式 Vue3.x 使用 Composition API setup 是 Vue3.x 新增的一个选项, 他是组件内使用 Composition API 的入口。
- 模板语法变化 slot 具名插槽语法 自定义指令 v-model 升级
- 其它方面的更改 Suspense 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。 基于 treeshaking 优化,提供了更多的内置功能。
Vue3.0 新特性以及使用经验总结 传送门
29. defineProperty和proxy的区别
Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。
但是这样做有以下问题:
- 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过$set 来调用Object.defineProperty()处理。
- 无法监控到数组下标和长度的变化。
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty(),其有以下特点:
- Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
- Proxy 可以监听数组的变化。
30. 对虚拟DOM的理解?
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。31. 虚拟DOM的解析过程
虚拟DOM的解析过程:
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
32. DIFF算法的原理
在新老虚拟DOM对比时:
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
33. Vue中key的作用
vue 中 key 值的作用可以分为两种情况来考虑:
- 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
- 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
- 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
34. 为什么不建议用index作为key?
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2…这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
35. Vue-Router 的懒加载如何实现
使用箭头函数+import动态加载
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
36. 路由的hash和history模式的区别
Vue-Router有两种模式:hash模式和history模式。默认的路由模式是hash模式。
1. hash模式
简介: hash模式是开发中默认的模式,它的URL带着一个#,例如:www.abc.com/#/vue,它的hash值就是#/vue。
特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。
原理: hash模式的主要原理就是onhashchange()事件:window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。
2. history模式
简介: history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
特点: 当使用history模式时,URL就像这样:abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
API: history api可以分为两大部分,切换历史状态和修改历史状态:修改历史状态:包括了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。
- 切换历史状态: 包括forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作。
虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。
如果想要切换到history模式,就要进行以下配置(后端也要进行配置):
const router = new VueRouter({
mode: 'history',
routes: [...]
})
3. 两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- pushState() 可额外设置 title 属性供后续使用。
- hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。
hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
37. 如何获取页面的hash变化
(1)监听$route的变化
// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
},
(2)window.location.hash读取#值 window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
38. $route 和$router 的区别
- $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
$router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
39. 如何定义动态路由?如何获取传过来的动态参数?
(1)param方式
配置路由格式:/router/:id
- 传递的方式:在path后面跟上对应的值
- 传递后形成的路径:/router/123
1)路由定义
//在APP.vue中
<router-link :to="'/user/'+userId" replace>用户</router-link>
//在index.js
{
path: '/user/:userid',
component: User,
},
2)路由跳转
// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link>
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' + wade)
3)参数获取 通过 $route.params.userid 获取传递的值
(2)query方式
- 配置路由格式:/router,也就是普通配置
- 传递的方式:对象中使用query的key作为传递方式
- 传递后形成的路径:/route?id=123
1)路由定义
//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobe",
age: "28",
height: 198
}
});
}
2)跳转方法
// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
3)获取参数
通过$route.query 获取传递的值
40. Vue-router 路由钩子在生命周期的体现
一、Vue-Router导航守卫
有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的
- 全局路由钩子
vue-router全局有三个路由钩子;
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
- router.afterEach 全局后置钩子 进入路由之后
具体使用∶
beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {
let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
if (!ifInfo) {
// sessionStorage里没有储存user信息
if (to.path == '/') {
//如果是登录页面路径,就直接next()
next();
} else {
//不然就跳转到登录
Message.warning("请重新登录!");
window.location.href = Vue.prototype.$loginUrl;
}
} else {
return next();
}
})
afterEach (跳转之后滚动条回到顶部)
router.afterEach((to, from) => {
// 跳转之后滚动条回到顶部
window.scrollTo(0,0);
});
- 单个路由独享钩子
beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next
export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即将进入登录页面')
next()
}
}
]
- 组件内钩子
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
这三个钩子都有三个参数∶to、from、next
- beforeRouteEnter∶ 进入组件前触发
- beforeRouteUpdate∶ 当前地址改变并且组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foo组件,这个钩子在这种情况下就会被调用
- beforeRouteLeave∶ 离开组件被调用
注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:
beforeRouteEnter(to, from, next) {
next(target => {
if (from.path == '/classProcess') {
target.isFromProcess = true
}
})
}
二、Vue路由钩子在生命周期函数的体现
- 完整的路由导航解析流程(不包括其他生命周期)
- 触发进入其他路由。
- 调用要离开路由的组件守卫beforeRouteLeave
- 调用全局前置守卫∶ beforeEach
- 在重用的组件里调用 beforeRouteUpdate
- 调用路由独享守卫 beforeEnter。
- 解析异步路由组件。
- 在将要进入的路由组件中调用 beforeRouteEnter
- 调用全局解析守卫 beforeResolve
- 导航被确认。
- 调用全局后置钩子的 afterEach 钩子。
- 触发DOM更新(mounted)。
- 执行beforeRouteEnter 守卫中传给 next 的回调函数
- 触发钩子的完整顺序
路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
- beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。
- beforeEnter:路由独享守卫
- beforeRouteEnter:路由组件的组件进入路由前钩子。
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能访问tAis。
- created;组件生命周期,可以访问tAis,不能访问dom。
- beforeMount:组件生命周期
- deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
- mounted:访问/操作dom。
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行beforeRouteEnter回调函数next。
- 导航行为被触发到导航完成的整个过程
- 导航行为被触发,此时导航未被确认。
- 在失活的组件里调用离开守卫 beforeRouteLeave。
- 调用全局的 beforeEach守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnteY。
- 解析异步路由组件(如果有)。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。
-
41. Vue-router跳转和location.href有什么区别
使用 location.href= /url 来跳转,简单方便,但是刷新了页面;
- 使用 history.pushState( /url ) ,无刷新页面,静态跳转;
- 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为vue-router就是用了 history.pushState() ,尤其是在history模式下。
42. params和query的区别
- 引入方式不同: query要使用path来引入,params要使用name来引入,接受参数格式类似,引用分别是this.r o u t e . q u e r y . n a m e 和 t h i s . route.query.name和this.route.query.name和this.route.params.name
- 形成的路径不同(或者url地址显示不同):
使用query传参的话,会在浏览器的url栏看到传的参数类似于get请求,使用params传参的话则不会,类似于post请求。
params传递后形成的路径:/router/123,/router/zhangsan
query传递后形成的路径:/router?id=666&name=zhangsan
- 是否受动态路径参数影响
Query传递的参数不会受路径参数的影响,会全部展示到路径上,刷新不会丢失query里面的数据;
params传递的参数会受路径参数的影响,只会展示含有动态路径参数的部分,刷新会丢失没有设置动态路径参数的params的数据。
43. 对前端路由的理解
在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。这个体验并不好,不过在最初也是无奈之举——用户只有在刷新页面的情况下,才可以重新去请求数据。
后来,改变发生了——Ajax 出现了,它允许人们在不刷新页面的情况下发起请求;与之共生的,还有“不刷新页面即可更新页面内容”这种需求。在这样的背景下,出现了 SPA(单页面应用)。
SPA极大地提升了用户体验,它允许页面在不刷新的情况下更新页面内容,使内容的切换更加流畅。但是在 SPA 诞生之初,人们并没有考虑到“定位”这个问题——在内容切换前后,页面的 URL 都是一样的,这就带来了两个问题:
- SPA 其实并不知道当前的页面“进展到了哪一步”。可能在一个站点下经过了反复的“前进”才终于唤出了某一块内容,但是此时只要刷新一下页面,一切就会被清零,必须重复之前的操作、才可以重新对内容进行定位——SPA 并不会“记住”你的操作。
- 由于有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,搜索引擎无法收集全面的信息
为了解决这个问题,前端路由出现了。
前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。
那么如何实现这个目的呢?首先要解决两个问题:
- 当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为我们的 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。
- 单页面应用对服务端来说,就是一个URL、一套资源,那么如何做到用“不同的URL”来映射不同的视图内容呢?
从这两个问题来看,服务端已经完全救不了这个场景了。所以要靠咱们前端自力更生,不然怎么叫“前端路由”呢?作为前端,可以提供这样的解决思路:
- 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化掉。
- 感知 URL 的变化。这里不是说要改造 URL、凭空制造出 N 个 URL 来。而是说 URL 还是那个 URL,只不过我们可以给它做一些微小的处理——这些处理并不会影响 URL 本身的性质,不会影响服务器对它的识别,只有我们前端感知的到。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。
44. Vue router 原理, 哪个模式不会请求服务器
Vue router 的两种方法,hash模式不会请求服务器
解析:
- url的hash,就是通常所说的锚点#,javascript通过hashChange事件来监听url的变化。比如这个 URL:http://www.abc.com/#/hello,hash 的值为#/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
- HTML5的History模式,它使url看起来像普通网站那样,以“/”分割,没有#,单页面并没有跳转。不过使用这种模式需要服务端支持,服务端在接收到所有请求后,都指向同一个html文件,不然会出现404。因此单页面应用只有一个html,整个网站的内容都在这一个html里,通过js来处理。