1. 谈谈你对MVVM的理解

MVVM 分为 Model、View、ViewModel:

  • Model代表数据模型,数据和业务逻辑都在Model层中定义;
  • View代表视图,负责数据的展示;
  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
image.png

2. data为什么是一个函数而不是对象

JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?

Vue2.0的数据响应是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty () 来劫持各个属性的setter、getter,但是它并不算是实现数据的响应式的完美方案,某些情况下需要对其进行修补这也是它的缺陷,主要表现在两个方面:

  1. vue 实例创建后,无法检测到对象属性的新增或删除,只能追踪到数据是否被修改
  2. 不能监听数组的变化

解析:

  1. vue 实例创建后,无法检测到对象属性的新增或删除,只能追踪到数据是否被修改(Object.defineProperty只能劫持对象的属性)。当创建一个Vue实例时,将遍历所有DOM对象,并为每个数据属性添加了get和set。get和set 允许Vue观察数据的更改并触发更新。但是,如果你在Vue实例化后添加(或删除)一个属性,这个属性不会被vue处理,改变get和set。解决方案:

    1. Vue.set(obj, propertName/index, value)
    2. // 响应式对象的子对象新增属性,可以给子响应式对象重新赋值
    3. data.location = {
    4. x: 100,
    5. y: 100
    6. }
    7. data.location = {...data, z: 100}
  2. 不能监听数组的变化

vue在实现数组的响应式时,它使用了一些hack,把无法监听数组的情况通过重写数组的部分方法来实现响应式,这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,其他数组方法及数组的使用则无法检测到,例如如下两种使用方式

  1. vm.items[index] = newValue;
  2. vm.items.length

vue实现数组响应式的方法
通过重写数组的Array.prototype对应的方法,具体来说就是重新指定要操作数组的prototype,并重新该prototype中对应上面的7个数组方法,通过下面代码简单了解下实现原理:

  1. const methods = ['pop','shift','unshift','sort','reverse','splice', 'push'];
  2. // 复制Array.prototype,并将其prototype指向Array.prototype
  3. let proto = Object.create(Array.prototype);
  4. methods.forEach(method => {
  5. proto[method] = function () { // 重写proto中的数组方法
  6. Array.prototype[method].call(this, ...arguments);
  7. viewRender() // 视图更新
  8. function observe(obj) {
  9. if (Array.isArray(obj)) { // 数组实现响应式
  10. obj.__proto__ = proto; // 改变传入数组的prototype
  11. return;
  12. }
  13. if (typeof obj === 'object') {
  14. ... // 对象的响应式实现
  15. }
  16. }
  17. }
  18. })

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: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;

  • method 调用总会执行该函数。

    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中的组件的状态将会被保留:

  1. <keep-alive>
  2. <router-view v-if="$route.meta.keepAlive"></router-view>
  3. </kepp-alive>

router.js

  1. {
  2. path: '/',
  3. name: 'xxx',
  4. component: ()=>import('../src/views/xxx.vue'),
  5. meta:{
  6. keepAlive: true // 需要被缓存
  7. }
  8. },

8. 常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
  • .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
  • .capture :与事件冒泡的方向相反,事件捕获由外到内;
  • .self :只会触发自己范围内的事件,不包含子元素;
  • .once :只会触发一次。

    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 作为事件。

在普通标签上

  1. <input v-model="sth" /> //这一行等于下一行
  2. <input v-bind:value="sth" v-on:input="sth = $event.target.value" />

在组件上

  1. <currency-input v-model="price"></currentcy-input>
  2. <!--上行代码是下行的语法糖
  3. <currency-input :value="price" @input="price = arguments[0]"></currency-input>
  4. -->
  5. <!-- 子组件定义 -->
  6. Vue.component('currency-input', {
  7. template: `
  8. <span>
  9. <input
  10. ref="input"
  11. :value="value"
  12. @input="$emit('input', $event.target.value)"
  13. >
  14. </span>
  15. `,
  16. props: ['value'],
  17. })

12. 对keep-alive的理解,它是如何实现的,具体缓存的是什么?

keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
作用:

  1. 它能够把不活动的组件实例保存在内存中,而不是直接将其销毁
  2. 它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中

使用方式:

  1. 常用的两个属性include/exclude,允许组件有条件的进行缓存。
  2. 两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
  3. 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
Vue - 图2
这里创建了一个白名单和一个黑名单。表明哪些需要需要做缓存,哪些不需要做缓存。以及最大的缓存个数。

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中。

  1. 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函数:

  1. // 将模板编译为render函数
  2. const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)

CompileToFunctions中的主要逻辑如下∶
(1)调用parse方法将template转化为ast(抽象语法树)

  1. const ast = parse(template.trim(), options)
  • parse的目标:把tamplate转换为AST树,它是一种用 JavaScript对象的形式来描述整个模板。
  • 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造AST树的目的。

AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
(2)对静态节点做优化

  1. optimize(ast,options)

这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
(3)生成代码

  1. 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 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶

    1. function defineReactive (obj, key, val){
    2. const dep = new Dep();
    3. ...
    4. Object.defineProperty(obj, key, {
    5. ...
    6. get: function reactiveGetter () {
    7. if(Dep.target){
    8. dep.depend();
    9. ...
    10. }
    11. return val
    12. }
    13. ...
    14. })
    15. }

    以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。
    (1)Dep Dep是整个依赖收集的核心,其关键代码如下:

    1. class Dep {
    2. static target;
    3. subs;
    4. constructor () {
    5. ...
    6. this.subs = [];
    7. }
    8. addSub (sub) {
    9. this.subs.push(sub)
    10. }
    11. removeSub (sub) {
    12. remove(this.sub, sub)
    13. }
    14. depend () {
    15. if(Dep.target){
    16. Dep.target.addDep(this)
    17. }
    18. }
    19. notify () {
    20. const subs = this.subds.slice();
    21. for(let i = 0;i < subs.length; i++){
    22. subs[i].update()
    23. }
    24. }
    25. }

    Dep 是一个 class ,其中有一个关键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
    (2)Watcher

    1. class Watcher {
    2. getter;
    3. ...
    4. constructor (vm, expression){
    5. ...
    6. this.getter = expression;
    7. this.get();
    8. }
    9. get () {
    10. pushTarget(this);
    11. value = this.getter.call(vm, vm)
    12. ...
    13. return value
    14. }
    15. addDep (dep){
    16. ...
    17. dep.addSub(this)
    18. }
    19. ...
    20. }
    21. function pushTarget (_target) {
    22. Dep.target = _target
    23. }

    Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
    (3)过程
    在实例化 Vue 时,依赖收集的相关过程如下∶ 初始化状态initState,这中间便会通过defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。 初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,

    1. updateComponent = () => {
    2. vm._update(vm._render())
    3. }
    4. 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 以后发生的事情

    image.png
  1. new Vue会调用 Vue 原型链上的_init方法对 Vue 实例进行初始化;
  2. 首先是initLifecycle初始化生命周期,对 Vue 实例内部的一些属性(如 children、parent、isMounted)进行初始化;
  3. initEvents,初始化当前实例上的一些自定义事件(Vue.$on);
  4. initRender,解析slots绑定在 Vue 实例上,绑定createElement方法在实例上;
  5. 完成对生命周期、自定义事件等一系列属性的初始化后,触发生命周期钩子beforeCreate;
  6. initInjections,在初始化data和props之前完成依赖注入(类似于 React.Context);
  7. initState,完成对data和props的初始化,同时对属性完成数据劫持内部,启用监听者对数据进行监听(更改);
  8. initProvide,对依赖注入进行解析;
  9. 完成对数据(state 状态)的初始化后,触发生命周期钩子created;
  10. 进入挂载阶段,将 vue 模板语法通过vue-loader解析成虚拟 DOM 树,虚拟 DOM 树与数据完成双向绑定,触发生命周期钩子beforeMount;
  11. 将解析好的虚拟 DOM 树通过 vue 渲染成真实 DOM,触发生命周期钩子mounted;

    23. 说一下Vue的生命周期

    Vue 实例有⼀个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
    image.png
    image.png
    生命周期经历的阶段和钩子函数:

  12. 实例化vue(组件)对象:new Vue()

  13. 初始化事件和生命周期 init events 和 init cycle
  14. beforeCreate函数:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。即此时vue(组件)对象被创建了,但是vue对象的属性还没有绑定,如data属性,computed属性还没有绑定,即没有值。此时还没有数据和真实DOM。即:属性还没有赋值,也没有动态创建template属性对应的HTML元素(二阶段的createUI函数还没有执行)
  15. 挂载数据(属性赋值)包括 属性和computed的运算
  16. 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

  1. beforeMount函数:模板编译(template)、数据挂载(把数据显示在模板里)之前执行的钩子函数此时 this.$el有值,但是数据还没有挂载到页面上。即此时页面中的{{}}里的变量还没有被数据替换
  2. 模板编译:用vue对象的数据(属性)替换模板中的内容
  3. Mounted函数:模板编译完成,数据挂载完毕即:此时已经把数据挂载到了页面上,所以,页面上能够看到正确的数据了。
  4. beforeUpdate函数:组件更新之前执行的函数,只有数据更新后,才能调用(触发)beforeUpdate,注意:此数据一定是在模板上出现的数据,否则,不会,也没有必要触发组件更新(因为数据不出现在模板里,就没有必要再次渲染)数据更新了,但是,vue(组件)对象对应的dom中的内部(innerHTML)没有变,所以叫作组件更新前
  5. updated函数:组件更新之后执行的函数vue(组件)对象对应的dom中的内部(innerHTML)改变了,所以叫作组件更新之后
  6. activated函数:keep-alive组件激活时调用
  7. deactivated函数:keep-alive组件停用时调用
  8. beforeDestroy:vue(组件)对象销毁之前
  9. 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跨组件通信最常用,也是基础的一个方案,它的通信过程是:

  1. Father.vue 通过 prop 向 Child.vue 传值(可包含父级定义好的函数)
  2. Child.vue 通过 emit 向 Father.vue 触发父组件的事件执行

下发 props
下发的过程是在 Father.vue 里完成的,父组件在向子组件下发 props 之前,需要导入子组件并启用它作为自身的模板,然后在 setup 里处理好数据,return 给 template 用。

  1. <template>
  2. <Child
  3. title="用户信息"
  4. :index="1"
  5. :uid="userInfo.id"
  6. :user-name="userInfo.name"
  7. />
  8. </template>
  9. <script>
  10. import { defineComponent } from 'vue'
  11. import Child from '@cp/Child.vue'
  12. interface Member {
  13. id: number,
  14. name: string
  15. };
  16. export default defineComponent({
  17. // 需要启用子组件作为模板
  18. components: {
  19. Child
  20. },
  21. // 定义一些数据并return给template用
  22. setup () {
  23. const userInfo: Member = {
  24. id: 1,
  25. name: 'Petter'
  26. }
  27. // 不要忘记return,否则template拿不到数据
  28. return {
  29. userInfo
  30. }
  31. }
  32. })
  33. </script>

接收 props
接收的过程是在 Child.vue 里完成的,在 script 部分,子组件通过与 setup 同级的 props 来接收数据。
它可以是一个数组,每个 item 都是 String 类型,把你要接受的变量名放到这个数组里,直接放进来作为数组的 item:

  1. export default defineComponent({
  2. props: [
  3. 'title',
  4. 'index',
  5. 'userName',
  6. 'uid'
  7. ]
  8. })

但这种情况下,使用者不知道这些属性到底是什么类型的值,是否必传。
带有类型限制的 props
注:这一小节的步骤是在 Child.vue 里操作。
既然我们最开始在决定使用 Vue 3.0 的时候,为了更好的类型限制,已经决定写 TypeScript ,那么我们最好不要出现这种使用情况。
推荐的方式是把 props 定义为一个对象,以对象形式列出 prop,每个 property 的名称和值分别是 prop 各自的名称和类型,只有合法的类型才允许传入。
于是我们把 props 再改一下,加上类型限制:

  1. export default defineComponent({
  2. props: {
  3. title: String,
  4. index: Number,
  5. userName: String,
  6. uid: Number
  7. }
  8. })

这样我们如果传入不正确的类型,程序就会抛出警告信息,告知开发者必须正确传值。
如果你需要对某个 prop 允许多类型,比如这个 uid 字段,它可能是数值,也可能是字符串,那么可以在类型这里,使用一个数组,把允许的类型都加进去。

  1. export default defineComponent({
  2. props: {
  3. // 单类型
  4. title: String,
  5. index: Number,
  6. userName: String,
  7. // 这里使用了多种类型
  8. uid: [ Number, String ]
  9. }
  10. })

可选以及带有默认值的 props
注:这一小节的步骤是在 Child.vue 里操作。
有时候我们想对一些 prop 设置为可选,然后提供一些默认值,还可以再将 prop 再进一步设置为对象,支持的字段有:

字段 类型 含义
type string prop 的类型
required boolean 是否必传,true=必传,false=可选
default any 与 type 字段的类型相对应的默认值,如果 required 是 false ,但这里不设置默认值,则会默认为 undefined
validator function 自定义验证函数,需要 return 一个布尔值,true=校验通过,false=校验不通过,当校验不通过时,控制台会抛出警告信息

我们现在再对 props 改造一下,对部分字段设置为可选,并提供默认值:

  1. export default defineComponent({
  2. props: {
  3. // 可选,并提供默认值
  4. title: {
  5. type: String,
  6. required: false,
  7. default: '默认标题'
  8. },
  9. // 默认可选,单类型
  10. index: Number,
  11. // 添加一些自定义校验
  12. userName: {
  13. type: String,
  14. // 在这里校验用户名必须至少3个字
  15. validator: v => v.length >= 3
  16. },
  17. // 默认可选,但允许多种类型
  18. uid: [ Number, String ]
  19. }
  20. })

使用 props
注:这一小节的步骤是在 Child.vue 里操作。
在 template 部分,3.x 的使用方法和 2.x 是一样的,比如要渲染我们上面传入的 props :

  1. <template>
  2. <p>标题:{{ title }}</p>
  3. <p>索引:{{ index }}</p>
  4. <p>用户id:{{ uid }}</p>
  5. <p>用户名:{{ userName }}</p>
  6. </template>

但是 script 部分,变化非常大!
在 2.x ,只需要通过 this.uid、this.userName 就可以使用父组件传下来的 prop 。
但是 3.x 没有了 this, 需要给 setup 添加一个入参才可以去操作。

  1. export default defineComponent({
  2. props: {
  3. title: String,
  4. index: Number,
  5. userName: String,
  6. uid: Number
  7. },
  8. // 在这里需要添加一个入参
  9. setup (props) {
  10. // 该入参包含了我们定义的所有props
  11. console.log(props);
  12. }
  13. })

TIP

  1. prop 是只读,不允许修改
  2. setup 的第一个入参,包含了我们定义的所有props(如果在 Child.vue 里未定义,但 父组件 Father.vue 那边非要传过来的,不会拿到,且控制台会有警告信息)
  3. 该入参可以随意命名,比如你可以写成一个下划线 ,通过 .uid 也可以拿到数据,但是语义化命名,是一个良好的编程习惯。

传递非 Prop 的 Attribute
上面的提示里有提到一句:
如果在 Child.vue 里未定义,但 父组件 Father.vue 那边非要传过来的,不会拿到,且控制台会有警告信息
但并不意味着你不能传递任何未定义的属性数据,在父组件,除了可以给子组件绑定 props,你还可以根据实际需要去绑定一些特殊的属性。
比如给子组件设置 class、id,或者 data-xxx 之类的一些自定义属性,如果 Child.vue 组件的 template 只有一个根节点,这些属性默认自动继承,并渲染在 node 节点上
在 Father.vue 里,对 Child.vue 传递了 class、id 和 data-hash:

  1. <template>
  2. <Child
  3. class="child"
  4. keys="aaaa"
  5. data-hash="afJasdHGUHa87d688723kjaghdhja"
  6. />
  7. </template>

渲染后(2个 data-v-xxx 是父子组件各自的 css scoped 标记):

  1. <div
  2. class="child"
  3. keys="aaaa"
  4. data-hash="afJasdHGUHa87d688723kjaghdhja"
  5. data-v-2dcc19c8=""
  6. data-v-7eb2bc79=""
  7. >
  8. <!-- Child的内容 -->
  9. </div>

你可以在 Child.vue 配置 inheritAttrs 为 false,来屏蔽这些自定义属性的渲染。

  1. export default defineComponent({
  2. inheritAttrs: false,
  3. setup () {
  4. // ...
  5. }
  6. })

获取非 Prop 的 Attribute
想要拿到这些属性,原生操作需要通过 element.getAttribute ,但 Vue 也提供了相关的 API :
在 Child.vue 里,可以通过 setup 的第二个参数 context 里的 attrs 来获取到这些属性。

  1. export default defineComponent({
  2. setup (props, { attrs }) {
  3. // attrs 是个对象,每个 Attribute 都是它的 key
  4. console.log(attrs.class);
  5. // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
  6. console.log(attrs['data-hash']);
  7. }
  8. })

TIP

  1. attr 和 prop 一样,都是只读的
  2. 不管 inheritAttrs 是否设置,都可以通过 attrs 拿到这些数据,但是 element.getAttribute 则只有 inheritAttrs 为 true 的时候才可以。

Vue 3 的 template 还允许多个根节点,多个根节点的情况下,无法直接继承这些属性,需要在 Child.vue 指定继承在哪个节点上,否则会有警告信息。

  1. <template>
  2. <!-- 指定继承 -->
  3. <p v-bind="attrs"></p>
  4. <!-- 指定继承 -->
  5. <!-- 这些不会自动继承 -->
  6. <p></p>
  7. <p></p>
  8. <p></p>
  9. <!-- 这些不会自动继承 -->
  10. </template>

当然,前提依然是,setup 里要把 attrs 给 return 出来。
查看详情:多个根节点上的 Attribute 继承open in new window
绑定 emits
最开始有介绍到,子组件如果需要向父组件告知数据更新,或者执行某些函数时,是通过 emits 来进行的。
每个 emit 都是事件,所以需要先由父组件先给子组件绑定,子组件才能知道应该怎么去调用。
TIP
当然,父组件也是需要先在 setup 里进行定义并 return,才能够在 template 里绑定给子组件。
比如要给 Child.vue 绑定一个更新用户年龄的方法,那么在 Father.vue 里需要这么处理:
先看 script 部分(留意注释部分):

  1. import { defineComponent, reactive } from 'vue'
  2. import Child from '@cp/Child.vue'
  3. interface Member {
  4. id: number,
  5. name: string,
  6. age: number
  7. };
  8. export default defineComponent({
  9. components: {
  10. Child
  11. },
  12. setup () {
  13. const userInfo: Member = reactive({
  14. id: 1,
  15. name: 'Petter',
  16. age: 0
  17. })
  18. // 定义一个更新年龄的方法
  19. const updateAge = (age: number): void => {
  20. userInfo.age = age;
  21. }
  22. return {
  23. userInfo,
  24. // return给template用
  25. updateAge
  26. }
  27. }
  28. })

再看 template 部分(为了方便阅读,我把之前绑定的 props 先去掉了):

  1. <template>
  2. <Child
  3. @update-age="updateAge"
  4. />
  5. </template>

接收 emits
注:这一小节的步骤是在 Child.vue 里操作。
和 props 一样,你可以指定是一个数组,把要接收的 emit 名称写进去:

  1. export default defineComponent({
  2. emits: [
  3. 'update-age'
  4. ]
  5. })

其实日常这样配置就足够用了。
TIP

  1. 这里的 emit 名称指 Father.vue 在给 Child.vue 绑定事件时,template 里面给子组件指定的 @aaaaa=”bbbbb” 里的 aaaaa
  2. 当在 emits 选项中定义了原生事件 (如 click ) 时,将使用组件中的事件替代原生事件侦听器

接收 emits 时做一些校验
当然你也可以对这些事件做一些验证,配置为对象,然后把这个 emit 名称作为 key, value 则配置为一个方法。
比如上面的更新年龄,只允许达到成年人的年龄才会去更新父组件的数据:

  1. export default defineComponent({
  2. emits: {
  3. // 需要校验
  4. 'update-age': (age: number) => {
  5. // 写一些条件拦截,记得返回false
  6. if ( age < 18 ) {
  7. console.log('未成年人不允许参与');
  8. return false;
  9. }
  10. // 通过则返回true
  11. return true;
  12. },
  13. // 一些无需校验的,设置为null即可
  14. 'update-name': null
  15. }
  16. })

调用 emits
注:这一小节的步骤是在 Child.vue 里操作。
和 props 一样,也需要在 setup 的入参里引入 emit ,才允许操作。
setup 的第二个入参 expose 是一个对象,你可以完整导入 expose 然后通过 expose.emit 去操作,也可以按需导入 { emit } (推荐这种方式):

  1. export default defineComponent({
  2. emits: [
  3. 'update-age'
  4. ],
  5. setup (props, { emit }) {
  6. // 2s 后更新年龄
  7. setTimeout( () => {
  8. emit('update-age', 22);
  9. }, 2000);
  10. }
  11. })
  • v-model / emits

对比 props / emits ,这个方式更为简单:

  1. 在 Father.vue ,通过 v-model 向 Child.vue 传值
  2. Child.vue 通过自身设定的 emits 向 Father.vue 通知数据更新

v-model 的用法和 props 非常相似,但是很多操作上更为简化,但操作简单带来的 “副作用” ,就是功能上也没有 props 那么多。
绑定 v-model
它的和下发 props 的方式类似,都是在子组件上绑定 Father.vue 定义好并 return 出来的数据。
TIP

  1. 和 2.x 不同, 3.x 可以直接绑定 v-model ,而无需在子组件指定 model 选项。
  2. 另外,3.x 的 v-model 需要使用 : 来指定你要绑定的属性名,同时也开始支持绑定多个 v-model

我们来看看具体的操作:

  1. <template>
  2. <Child
  3. v-model:user-name="userInfo.name"
  4. />
  5. </template>

如果你要绑定多个数据,写多个 v-model 即可

  1. <template>
  2. <Child
  3. v-model:user-name="userInfo.name"
  4. v-model:uid="userInfo.id"
  5. />
  6. </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:属性名” 的格式,直接定义一个更新事件:

  1. export default defineComponent({
  2. props: {
  3. userName: String,
  4. uid: Number
  5. },
  6. emits: [
  7. 'update:userName',
  8. 'update:uid'
  9. ]
  10. })

这里的 update 后面的属性名,支持驼峰写法,这一部分和 2.x 的使用是相同的。
这里也可以对数据更新做一些校验,配置方式和 接收 emits 时做一些校验 是一样的。
调用自身的 emits
注:这一小节的步骤是在 Child.vue 里操作。
在 Child.vue 配置好 emits 之后,就可以在 setup 里直接操作数据的更新了:

  1. export default defineComponent({
  2. // ...
  3. setup (props, { emit }) {
  4. // 2s 后更新用户名
  5. setTimeout(() => {
  6. emit('update:userName', 'Tom')
  7. }, 2000);
  8. }
  9. })

在使用上,和 调用 emits 是一样的。

  • ref / emits

在学习 响应式 API 之 ref 的时候,我们了解到 ref 是可以用在 DOM 元素与子组件 上面。
父组件操作子组件
所以,父组件也可以直接通过对子组件绑定 ref 属性,然后通过 ref 变量去操作子组件的数据或者调用里面的方法。
比如导入了一个 Child.vue 作为子组件,需要在 template 处给子组件标签绑定 ref:

  1. <template>
  2. <Child ref="child" />
  3. </template>

然后在 script 部分定义好对应的变量名称(记得要 return 出来):

  1. import { defineComponent, onMounted, ref } from 'vue'
  2. import Child from '@cp/Child.vue'
  3. export default defineComponent({
  4. components: {
  5. Child
  6. },
  7. setup () {
  8. // 给子组件定义一个ref变量
  9. const child = ref<HTMLElement>(null);
  10. // 请保证视图渲染完毕后再执行操作
  11. onMounted( () => {
  12. // 执行子组件里面的ajax函数
  13. child.value.getList();
  14. // 打开子组件里面的弹窗
  15. child.value.isShowDialog = true;
  16. });
  17. // 必须return出去才可以给到template使用
  18. return {
  19. child
  20. }
  21. }
  22. })

子组件通知父组件
子组件如果想主动向父组件通讯,也需要使用 emit,详细的配置方法可见:绑定 emits

爷孙组件通信
顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件”):
C组件引入到B组件里,B组件引入到A组件里渲染,此时A是C的爷爷级别(可能还有更多层级关系),如果你用 props ,只能一级一级传递下去,那就太繁琐了,因此我们需要更直接的通信方式。
他们之间的关系如下,Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (可能有多个):

  1. Grandfather.vue
  2. └─Son.vue
  3. └─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 选项来开始使用这些数据。

  1. Grandfather.vue 通过 provide 向 Grandson.vue 传值(可包含定义好的函数)
  2. Grandson.vue 通过 inject 向 Grandfather.vue 触发爷爷组件的事件执行

无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者。
TIP
这一部分的内容变化都特别大,但使用起来其实也很简单,不用慌,也有相同的地方:

  1. 父组件不需要知道哪些子组件使用它 provide 的 property
  2. 子组件不需要知道 inject property 来自哪里

另外,要切记一点就是:provide 和 inject 绑定并不是可响应的。这是刻意为之的,但如果传入了一个可监听的对象,那么其对象的 property 还是可响应的。
发起 provide
我们先来回顾一下 2.x 的用法:

  1. export default {
  2. // 定义好数据
  3. data () {
  4. return {
  5. tags: [ '中餐', '粤菜', '烧腊' ]
  6. }
  7. },
  8. // provide出去
  9. provide () {
  10. return {
  11. tags: this.tags
  12. }
  13. }
  14. }

旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数。
3.x 的新版 provide, 和 2.x 的用法区别比较大。
TIP
在 3.x , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法。
每次要 provide 一个数据的时候,就要单独调用一次。
每次调用的时候,都需要传入 2 个参数:

参数 类型 说明
key string 数据的名称
value any 数据的值

来看一下如何创建一个 provide:

  1. // 记得导入provide
  2. import { defineComponent, provide } from 'vue'
  3. export default defineComponent({
  4. // ...
  5. setup () {
  6. // 定义好数据
  7. const msg: string = 'Hello World!';
  8. // provide出去
  9. provide('msg', msg);
  10. }
  11. })

操作非常简单对吧哈哈哈,但需要注意的是,provide 不是响应式的,如果你要使其具备响应性,你需要传入响应式数据,详见:响应性数据的传递与接收
接收 inject
也是先来回顾一下 2.x 的用法:

  1. export default {
  2. inject: [
  3. 'tags'
  4. ],
  5. mounted () {
  6. console.log(this.tags);
  7. }
  8. }

旧版的 inject 用法和 props 类似,3.x 的新版 inject, 和 2.x 的用法区别也是比较大。
TIP
在 3.x, inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法。
每次要 inject 一个数据的时候,就要单独调用一次。
每次调用的时候,只需要传入 1 个参数:

参数 类型 说明
key string 与 provide 相对应的数据名称

来看一下如何创建一个 inject:

  1. // 记得导入inject
  2. import { defineComponent, inject } from 'vue'
  3. export default defineComponent({
  4. // ...
  5. setup () {
  6. const msg: string = inject('msg') || '';
  7. }
  8. })

也是很简单(写 TS 的话,由于 inject 到的值可能是 undefined,所以要么加个 undefined 类型,要么给变量设置一个空的默认值)。
响应性数据的传递与接收
之所以要单独拿出来说, 是因为变化真的很大 - -
在前面我们已经知道,provide 和 inject 本身不可响应,但是并非完全不能够拿到响应的结果,只需要我们传入的数据具备响应性,它依然能够提供响应支持。
我们以 ref 和 reactive 为例,来看看应该怎么发起 provide 和接收 inject。
对这 2 个 API 还不熟悉的同学,建议先阅读一下 响应式 API 之 ref响应式 API 之 reactive
先在 Grandfather.vue 里 provide 数据:

  1. export default defineComponent({
  2. // ...
  3. setup () {
  4. // provide一个ref
  5. const msg = ref<string>('Hello World!');
  6. provide('msg', msg);
  7. // provide一个reactive
  8. const userInfo: Member = reactive({
  9. id: 1,
  10. name: 'Petter'
  11. });
  12. provide('userInfo', userInfo);
  13. // 2s 后更新数据
  14. setTimeout(() => {
  15. // 修改消息内容
  16. msg.value = 'Hi World!';
  17. // 修改用户名
  18. userInfo.name = 'Tom';
  19. }, 2000);
  20. }
  21. })

在 Grandsun.vue 里 inject 拿到数据:

  1. export default defineComponent({
  2. setup () {
  3. // 获取数据
  4. const msg = inject('msg');
  5. const userInfo = inject('userInfo');
  6. // 打印刚刚拿到的数据
  7. console.log(msg);
  8. console.log(userInfo);
  9. // 因为 2s 后数据会变,我们 3s 后再看下,可以争取拿到新的数据
  10. setTimeout(() => {
  11. console.log(msg);
  12. console.log(userInfo);
  13. }, 3000);
  14. // 响应式数据还可以直接给 template 使用,会实时更新
  15. return {
  16. msg,
  17. userInfo
  18. }
  19. }
  20. })

非常简单,非常方便!!!
TIP
响应式的数据 provide 出去,在子孙组件拿到的也是响应式的,并且可以如同自身定义的响应式变量一样,直接 return 给 template 使用,一旦数据有变化,视图也会立即更新。
但上面这句话有效的前提是,不破坏数据的响应性,比如 ref 变量,你需要完整的传入,而不能只传入它的 value,对于 reactive 也是同理,不能直接解构去破坏原本的响应性。
切记!切记!!!
引用类型的传递与接收
这里是针对非响应性数据的处理
provide 和 inject 并不是可响应的,这是官方的故意设计,但是由于引用类型的特殊性,在子孙组件拿到了数据之后,他们的属性还是可以正常的响应变化。
先在 Grandfather.vue 里 provide 数据:

  1. export default defineComponent({
  2. // ...
  3. setup () {
  4. // provide 一个数组
  5. const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
  6. provide('tags', tags);
  7. // provide 一个对象
  8. const userInfo: Member = {
  9. id: 1,
  10. name: 'Petter'
  11. };
  12. provide('userInfo', userInfo);
  13. // 2s 后更新数据
  14. setTimeout(() => {
  15. // 增加tags的长度
  16. tags.push('叉烧');
  17. // 修改userInfo的属性值
  18. userInfo.name = 'Tom';
  19. }, 2000);
  20. }
  21. })

在 Grandsun.vue 里 inject 拿到数据:

  1. export default defineComponent({
  2. setup () {
  3. // 获取数据
  4. const tags: string[] = inject('tags') || [];
  5. const userInfo: Member = inject('userInfo') || {
  6. id: 0,
  7. name: ''
  8. };
  9. // 打印刚刚拿到的数据
  10. console.log(tags);
  11. console.log(tags.length);
  12. console.log(userInfo);
  13. // 因为 2s 后数据会变,我们 3s 后再看下,能够看到已经是更新后的数据了
  14. setTimeout(() => {
  15. console.log(tags);
  16. console.log(tags.length);
  17. console.log(userInfo);
  18. }, 3000);
  19. }
  20. })

引用类型的数据,拿到后可以直接用,属性的值更新后,子孙组件也会被更新。
WARNING
由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用 响应式 API
基本类型的传递与接收
这里是针对非响应性数据的处理
基本数据类型被直接 provide 出去后,再怎么修改,都无法更新下去,子孙组件拿到的永远是第一次的那个值。
先在 Grandfather.vue 里 provide 数据:

  1. export default defineComponent({
  2. // ...
  3. setup () {
  4. // provide 一个数组的长度
  5. const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
  6. provide('tagsCount', tags.length);
  7. // provide 一个字符串
  8. let name: string = 'Petter';
  9. provide('name', name);
  10. // 2s 后更新数据
  11. setTimeout(() => {
  12. // tagsCount 在 Grandson 那边依然是 3
  13. tags.push('叉烧');
  14. // name 在 Grandson 那边依然是 Petter
  15. name = 'Tom';
  16. }, 2000);
  17. }
  18. })

在 Grandsun.vue 里 inject 拿到数据:

  1. export default defineComponent({
  2. setup () {
  3. // 获取数据
  4. const name: string = inject('name') || '';
  5. const tagsCount: number = inject('tagsCount') || 0;
  6. // 打印刚刚拿到的数据
  7. console.log(name);
  8. console.log(tagsCount);
  9. // 因为 2s 后数据会变,我们 3s 后再看下
  10. setTimeout(() => {
  11. // 依然是 Petter
  12. console.log(name);
  13. // 依然是 3
  14. console.log(tagsCount);
  15. }, 3000);
  16. }
  17. })

很失望,并没有变化。
TIP
那么是否一定要定义成响应式数据或者引用类型数据呢?
当然不是,我们在 provide 的时候,也可以稍作修改,让它能够同步更新下去。
我们再来一次,依然是先在 Grandfather.vue 里 provide 数据:

  1. export default defineComponent({
  2. // ...
  3. setup () {
  4. // provide 一个数组的长度
  5. const tags: string[] = [ '中餐', '粤菜', '烧腊' ];
  6. provide('tagsCount', (): number => {
  7. return tags.length;
  8. });
  9. // provide 字符串
  10. let name: string = 'Petter';
  11. provide('name', (): string => {
  12. return name;
  13. });
  14. // 2s 后更新数据
  15. setTimeout(() => {
  16. // tagsCount 现在可以正常拿到 4 了
  17. tags.push('叉烧');
  18. // name 现在可以正常拿到 Tom 了
  19. name = 'Tom';
  20. }, 2000);
  21. }
  22. })

这次可以正确拿到数据了,看出这2次的写法有什么区别了吗?
TIP
基本数据类型,需要 provide 一个函数,将其 return 出去给子孙组件用,这样子孙组件每次拿到的数据才会是新的。
但由于不具备响应性,所以子孙组件每次都需要重新通过执行 inject 得到的函数才能拿到最新的数据。
按我个人习惯来说,使用起来挺别扭的,能不用就不用……
WARNING
由于不具备真正的响应性,return 给模板使用依然不会更新视图,如果涉及到视图的数据,请依然使用
兄弟组件通信
如果他们之间要交流,目前大概有这两类选择:

  1. 【不推荐】先把数据传给 Father.vue,再通过 父子组件通信 的方案去交流
  2. 【推荐】借助 全局组件通信 的方案才能达到目的。

全局组件通信
常用的方法有:

方案 发起方 接收方
EventBus emit on
Vuex - -

EventBus
EventBus 通常被称之为 “全局事件总线” ,它是用来在全局范围内通信的一个常用方案,它的特点就是: “简单” 、 “灵活” 、“轻量级”。
TIP
在中小型项目,全局通信推荐优先采用该方案,事件总线在打包压缩后不到 200 个字节, API 也非常简单和灵活。
回顾 Vue 2
在 2.x,使用 EventBus 无需导入第三方插件,直接在自己的 libs 文件夹下创建一个 bus.ts 文件,暴露一个新的 Vue 实例即可。

  1. import Vue from 'vue';
  2. 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 :

  1. npm install --save mitt

然后在 libs 文件夹下,创建一个 bus.ts 文件,内容和旧版写法其实是一样的,只不过是把 Vue 实例,换成了 mitt 实例。

  1. import mitt from 'mitt';
  2. 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 掉:

  1. import { defineComponent, onBeforeUnmount } from 'vue'
  2. import bus from '@libs/bus'
  3. export default defineComponent({
  4. setup () {
  5. // 定义一个打招呼的方法
  6. const sayHi = (msg: string = 'Hello World!'): void => {
  7. console.log(msg);
  8. }
  9. // 启用监听
  10. bus.on('sayHi', sayHi);
  11. // 在组件卸载之前移除监听
  12. onBeforeUnmount( () => {
  13. bus.off('sayHi', sayHi);
  14. })
  15. }
  16. })

关于销毁的时机,可以参考 组件的生命周期
调用监听事件
在需要调用交流事件的组件里,通过 emit 进行调用:

  1. import { defineComponent } from 'vue'
  2. import bus from '@libs/bus'
  3. export default defineComponent({
  4. setup () {
  5. // 调用打招呼事件,传入消息内容
  6. bus.emit('sayHi', '哈哈哈哈哈哈哈哈哈哈哈哈哈哈');
  7. }
  8. })

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 会被自动调用。
但是这样做有以下问题:

  1. 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过$set 来调用Object.defineProperty()处理。
  2. 无法监控到数组下标和长度的变化。

Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty(),其有以下特点:

  1. Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
  2. 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动态加载

    1. const List = () => import('@/components/list.vue')
    2. const router = new VueRouter({
    3. routes: [
    4. { path: '/list', component: List }
    5. ]
    6. })

    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()事件

    1. window.onhashchange = function(event){
    2. console.log(event.oldURL, event.newURL);
    3. let hash = location.hash.slice(1);
    4. }

    使用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模式,就要进行以下配置(后端也要进行配置):

  1. const router = new VueRouter({
  2. mode: 'history',
  3. routes: [...]
  4. })

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的变化

  1. // 监听,当路由发生变化的时候执行
  2. watch: {
  3. $route: {
  4. handler: function(val, oldVal){
  5. console.log(val);
  6. },
  7. // 深度观察监听
  8. deep: true
  9. }
  10. },

(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)路由定义

  1. //在APP.vue中
  2. <router-link :to="'/user/'+userId" replace>用户</router-link>
  3. //在index.js
  4. {
  5. path: '/user/:userid',
  6. component: User,
  7. },

2)路由跳转

  1. // 方法1:
  2. <router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link>
  3. // 方法2:
  4. this.$router.push({name:'users',params:{uname:wade}})
  5. // 方法3:
  6. this.$router.push('/user/' + wade)

3)参数获取 通过 $route.params.userid 获取传递的值
(2)query方式

  • 配置路由格式:/router,也就是普通配置
  • 传递的方式:对象中使用query的key作为传递方式
  • 传递后形成的路径:/route?id=123

1)路由定义

  1. //方式1:直接在router-link 标签上以对象的形式
  2. <router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
  3. // 方式2:写成按钮以点击事件形式
  4. <button @click='profileClick'>我的</button>
  5. profileClick(){
  6. this.$router.push({
  7. path: "/profile",
  8. query: {
  9. name: "kobe",
  10. age: "28",
  11. height: 198
  12. }
  13. });
  14. }

2)跳转方法

  1. // 方法1:
  2. <router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
  3. // 方法2:
  4. this.$router.push({ name: 'users', query:{ uname:james }})
  5. // 方法3:
  6. <router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
  7. // 方法4:
  8. this.$router.push({ path: '/user', query:{ uname:james }})
  9. // 方法5:
  10. this.$router.push('/user?uname=' + jsmes)

3)获取参数

  1. 通过$route.query 获取传递的值

40. Vue-router 路由钩子在生命周期的体现

一、Vue-Router导航守卫
有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的

  1. 全局路由钩子

vue-router全局有三个路由钩子;

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后

具体使用∶

  • beforeEach(判断是否登录了,没登录就跳转到登录页)

    1. router.beforeEach((to, from, next) => {
    2. let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
    3. if (!ifInfo) {
    4. // sessionStorage里没有储存user信息
    5. if (to.path == '/') {
    6. //如果是登录页面路径,就直接next()
    7. next();
    8. } else {
    9. //不然就跳转到登录
    10. Message.warning("请重新登录!");
    11. window.location.href = Vue.prototype.$loginUrl;
    12. }
    13. } else {
    14. return next();
    15. }
    16. })
  • afterEach (跳转之后滚动条回到顶部)

    1. router.afterEach((to, from) => {
    2. // 跳转之后滚动条回到顶部
    3. window.scrollTo(0,0);
    4. });
  1. 单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next

  1. export default [
  2. {
  3. path: '/',
  4. name: 'login',
  5. component: login,
  6. beforeEnter: (to, from, next) => {
  7. console.log('即将进入登录页面')
  8. next()
  9. }
  10. }
  11. ]
  1. 组件内钩子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
这三个钩子都有三个参数∶to、from、next

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 当前地址改变并且组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foo组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave∶ 离开组件被调用

注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:

  1. beforeRouteEnter(to, from, next) {
  2. next(target => {
  3. if (from.path == '/classProcess') {
  4. target.isFromProcess = true
  5. }
  6. })
  7. }

二、Vue路由钩子在生命周期函数的体现

  1. 完整的路由导航解析流程(不包括其他生命周期)
  • 触发进入其他路由。
  • 调用要离开路由的组件守卫beforeRouteLeave
  • 调用全局前置守卫∶ beforeEach
  • 在重用的组件里调用 beforeRouteUpdate
  • 调用路由独享守卫 beforeEnter。
  • 解析异步路由组件。
  • 在将要进入的路由组件中调用 beforeRouteEnter
  • 调用全局解析守卫 beforeResolve
  • 导航被确认。
  • 调用全局后置钩子的 afterEach 钩子。
  • 触发DOM更新(mounted)。
  • 执行beforeRouteEnter 守卫中传给 next 的回调函数
  1. 触发钩子的完整顺序

路由导航、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。
  1. 导航行为被触发到导航完成的整个过程
  • 导航行为被触发,此时导航未被确认。
  • 在失活的组件里调用离开守卫 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的区别

  1. 引入方式不同: 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
  2. 形成的路径不同(或者url地址显示不同):

使用query传参的话,会在浏览器的url栏看到传的参数类似于get请求,使用params传参的话则不会,类似于post请求。
params传递后形成的路径:/router/123,/router/zhangsan
query传递后形成的路径:/router?id=666&name=zhangsan

  1. 是否受动态路径参数影响

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模式不会请求服务器
    解析:
  1. url的hash,就是通常所说的锚点#,javascript通过hashChange事件来监听url的变化。比如这个 URL:http://www.abc.com/#/hello,hash 的值为#/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
  2. HTML5的History模式,它使url看起来像普通网站那样,以“/”分割,没有#,单页面并没有跳转。不过使用这种模式需要服务端支持,服务端在接收到所有请求后,都指向同一个html文件,不然会出现404。因此单页面应用只有一个html,整个网站的内容都在这一个html里,通过js来处理。