vue

304 阅读29分钟

vue基础

1. Vue的基本原理

Vue实例创建时,vue当中的data属性,通过object.defineProperty定义成为一个响应式的数据,通过getter方法获取相应的值,通过setter方法修改相应的值,在属性被访问和修改时,通知相应的watcher实例 把使用当前属性的元素进行收集起来,当属性的setter的方法被调用时,watcher就会重新计算,通知相关的元素进行更新

vue基本原理.png

2. 双向数据绑定的原理

Vue采用数据劫持(object.defineProperty) 结合发布者-订阅者模式,通过object.defineProperty来劫持各个属性的setter getter (访问器) ,在数据变动时发布消息给订阅者,触发相应的监听回调

1.     Observe(劫持器 监听器)先将所有的数据进行递归遍历,通过object.defineProperty来劫持各个属性的setter getter将数据进行劫持起来,给对象中的某个值进行赋值就会触发setter,可以监听到数据的变化

2.     通过compile(模板编译器)来解析模板指令(render渲染的细节),将模板中的变量替换成真实的数据,并通过计算将template转成真正的html在页面展示出来, 把当前属性的依赖每个节点绑定更新函数,添加到watcher订阅者中,一旦数据有变动,收到通知,更新相关的依赖

3.     watcher订阅者是Observe(劫持器 监听器)和compile(模板编译器)之间的桥梁 watcher在new自身时往属性订阅器(dep)添加相关属性的依赖,属性自身有一个update()方法,属性调用setter方法时,通过dep.notice()通知watcher来调用属性自身的update()方法,并触发compile中的回调函数,完成视图的更新

4.     MVVM整合了Observe(劫持器 监听器) compile(模板编译器) watcher订阅者 通过Observe(劫持器 监听器)来监听model数据变化,

通过compile(模板编译器)来编译模板指令

最后利用watcher订阅者是Observe(劫持器 监听器)和compile(模板编译器)之间的桥梁,达到双向数据绑定的效果

Model层数据发生变化就会被watcher监听到 来通知compile完成视图的重新渲染

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

  • 使用 Object.defineProperty()在对一些属性进行操作时是无法拦截的。
  • 修改数组中的某条数据或者给对象新增属性和删除属性,都不能触发组件的重新渲染,
  • 因为 Object.defineProperty()只是能对现有的属性进行获取和修改不能删除现有属性。     
  • Vue3.0 而是通过使用 Proxy 对对象进行代理,可以监听到任何方式的数据改变,从而实现数据劫持。
  • 使用Proxy 的好处是它可以监听到任何方式的数据改变,缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。

4. MVVM、MVC、MVP的区别

(1)MVC

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。

  • View 负责数据的显示
  • Model 负责存储数据,以及对相应数据的操作。
  • 并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。
  • Controller 层是 View 层和 Model 层的纽带,当用户与页面产生交互的时候,Controller通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
  • image.png

(2)MVVM

MVVM 分为 Model、View、ViewModel:

  • Model 负责存储数据,以及对相应数据的操作;

  • View 负责数据的展示;

  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

  • Model和View并无直接关联,而是通过ViewModel来进行联系的,

  • Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。 image.png

(3)MVP

  • MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。 View 层和 Model 层耦合在一起,
  • MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。
  • MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,
  • MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,

5. Computed 和 Watch 的区别

Computed(计算属性):

  1. 依赖其他响应式属性计算得出的结果。

  2. 自动缓存计算结果,只有在依赖属性变化时才会重新计算。

  3. 可以在模板中直接使用Computed属性,会自动追踪依赖关系。

  4. 不支持异步,当computed中有异步操作时,无法监听数据的变化

 

Watch(监听器):

  1. 用于监听特定数据的变化,并在变化时执行自定义的操作。

  2. 可以监听单个或多个数据的变化,包括深度监听对象或数组的内部属性的变化。

  3. 适合执行异步、复杂或耗时的操作,如发送网络请求或触发副作用操作。

  4. Watch可以提供先前值和当前值的参数,以便在变化时执行特定逻辑。

 

总结:

  • Computed用于声明计算属性,根据依赖自动计算,适合简单的计算逻辑。

  • Watch用于监听数据的变化,在变化时触发特定操作,适合处理复杂、异步操作。

  • Computed是给模板使用的属性,而Watch是监听数据的变化执行对应的副作用操作。

watch和watchEffect的区别

  • watch
watch(特定数据,(新值,旧值)=>{},{ deep: true })
{ immediate: true }创建监听以后,立即执行
监听具体的某一个数据的变化,变化时执行相应的操作,支持异步操作
监听响应式对象,默认开启深度监听
watch([x, () => y.value], ([newX, newY]) => {},{deep:true})
监听多个数据的变化
  • watchEffect
  watchEffect(() => {
      console.log('doubled的值变为:', doubled.value);
    });
   回调函数会立即执行,自动追踪相关的依赖,依赖发生改变以后,回调函数再次执行

取消事件监听

事件监听返回一个回调函数,调用回调函数来取消事件监听
const unwatch = watchEffect(() => {}) 
当该侦听器不再需要时 调用unwatch()

<template>
  <div>
    <input v-model="count" type="text" />
    <p>Count doubled: {{ doubled }}</p>
  </div>
</template>

<script>
import { ref, watch, watchEffect } from 'vue';

export default {
  setup() {
    const count = ref(1);
    const doubled = ref(2);

    // 使用watch来监视count的变化
    watch(count, (newVal, oldVal) => {
        doubled.value = newVal * 2;
        console.log(`count值发生了变化,新值为${newVal},旧值为${oldVal}`);
    });

    // 使用watchEffect来自动追踪其依赖并执行响应的计算
    watchEffect(() => {
      console.log('doubled的值变为:', doubled.value);
      // 这里可以进行一些副作用的操作,比如根据doubled的值更新UI或者进行网络请求等操作
    });

    return {
      count,
      doubled
    };
  }
};
</script>

6. Computed 和 Methods 的区别

不同点:

  • computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
  • method 调用总会执行该函数。

7. slot是什么?有什么作用?原理是什么?

  • 插槽slot作为插槽的出口,父组件提供的插槽内容将在哪里被渲染。

  • 使组件更加灵活和具有可复用性。

  • 默认插槽:又名匿名插槽,没有name属性值,一个组件内只有有一个匿名插槽。

  • 具名插槽:带有name属性的slot,一个组件可以出现多个具名插槽。

  • 作用域插槽:可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件

实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

8. 过滤器的作用,如何实现一个过滤器

在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出

使用场景:

  • 时间、价格等数据格式的输出显示。
  • 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用fliters过滤器来处理数据。

过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。 过滤器用在插值表达式 {{ }}v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。

例如,在显示金额,给商品价格添加单位:

<li>商品价格:{{item.price | filterPrice}}</li>

 filters: {
    filterPrice (price) {
      return price ? ('¥' + price) : '--'
    }
  }

9. 如何保存页面的当前的状态

两种情况:

  • 前组件会被卸载
  • 前组件不会被卸载

组件会被卸载:

(1)将状态存储在LocalStorage / SessionStorage

在组件即将被销毁的生命周期中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来

优点:

  • 兼容性好,不需要额外库或工具。
  • 简单快捷,基本可以满足大部分需求。

缺点:

  • 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。

(2)路由传值

通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。

在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。

优点:

  • 简单快捷,不会污染 LocalStorage / SessionStorage。
  • 可以传递 Date、RegExp 等特殊对象

缺点:

  • 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。

组件不会被卸载:

(1)单页面渲染

要切换的组件作为子组件,父组件中正常储存页面状态。

优点:

  • 代码量少
  • 不需要考虑状态传递过程中的错误

缺点:

  • 增加 A 组件维护成本
  • 需要传入额外的 prop 到 B 组件
  • 无法利用路由定位页面

用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 // 需要被缓存
  }
},

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

  • .stop:等同于 event.stopPropagation() ,防止事件冒泡;
  • .prevent :等同于 event.preventDefault() ,取消默认行为;
  • .capture :与事件冒泡的方向相反,事件捕获由外到内;
  • .self :只会触发自己范围内的事件,不包含子元素;
  • .once :只会触发一次。

11. v-if、v-show、v-html 的原理

  • v-if生成vnode的时候会忽略对应节点,render的时候就不会渲染;
  • v-show生成vnode,render的时候也会渲染成真实节点,只是在render过程中修改了display;
  • v-html设置innerHTML为v-html的值。

12. 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有更高的初始渲染消耗;

13. v-model 是如何实现的,语法糖实际是什么?

(1)作用在表单元素上 动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值:

<input v-model="sth" />
//  等同于
<input 
    v-bind:value="message" 
    v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;

(2)作用在组件上 在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件

本质是一个父子组件通信的语法糖,通过prop和$.emit实现。 因此父组件 v-model 语法糖本质上可以修改为:

<child :value="message"  @input="function(e){message = e}"></child>

在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。 例子:

// 父组件
<aa-input v-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>

// 子组件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>

props:{value:aa,}
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}

15. v-model 可以被用在自定义组件上吗?如果可以,如何使用?

可以。v-model 实际上是一个语法糖,如:

<input v-model="searchText">

实际上相当于:

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

用在自定义组件上也是同理:

<custom-input v-model="searchText">

相当于:

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

显然,custom-input 与父组件的交互如下:

  1. 父组件将searchText变量传入custom-input 组件,使用的 prop 名为value
  2. custom-input 组件向父组件传出名为input的事件,父组件将接收到的值赋值给searchText

所以,custom-input 组件的实现应该类似于这样:

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})

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

data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。 data 是一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。

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

实现组件缓存,当组件切换时不会对当前组件进行卸载。

参数

keep-alive接收三个参数:

  • include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存
  • exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存
  • max:可传数字,限制缓存组件的最大数量,超过max则按照LRU算法进行置换

include和exclude,传数组情况居多。

  • 两个生命周期 activated/deactivated

  • activated 组件激活时调用

  • deactivated 组件失活时调用

  • created:初始化一个cache、keys,cache用来存缓组件的虚拟dom集合,keys用来存缓存组件的key集合。

  • mounted:实时监听include、exclude这两个的变化,并执行相应操作。

  • destroyed:删除掉所有缓存相关的东西。 在 destroyed 组件销毁时for循环执行pruneCacheEntry函数

    • 1:遍历集合,执行所有缓存组件的$destroy方法
    • 2:将cache对应key的内容设置为null
    • 3:删除keys中对应的元素

使用场景

  1. Tab 页面切换:
    在一个页面中有多个 Tab,每个 Tab 对应一个组件,使用 <keep-alive> 可以在 Tab 切换时保持各个 Tab 组件的状态,避免每次切换 Tab 都重新渲染组件。
  2. 路由页面缓存:
    对于一些频繁切换的路由页面,可以使用 <keep-alive> 缓存路由组件,提高路由切换时的效率,并且可以保持页面状态。
  3. 模态框/对话框:
    当打开模态框或对话框时,如果希望关闭后再次打开时能够保持上一次的状态,可以使用 <keep-alive> 缓存模态框的组件。
  4. 列表/表格数据:
    对于需要频繁展示的列表或表格数据,可以通过 <keep-alive> 缓存列表或表格组件,以保持数据的加载状态和滚动位置,提升用户体验。
  5. 含有表单数据的页面:
    在包含表单数据的页面中,通过 <keep-alive> 缓存页面组件,可以避免用户在填写表单数据后切换页面导致数据丢失,提供更好的用户体验。

18. $nextTick 原理及作用

  • nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
  • 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。

19. Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?

  • 新增属性没有被Vue转换为响应式的属性,不会触发视图的更新,这时就需要使用Vue的全局 api  $set()
  • $set()方法相当于手动的去处理成一个响应式的属性

20. Vue中封装的数组方法有哪些,其如何实现页面更新

在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,所以需要对这些操作进行一个改写,让Vue能监听到其中的变化。 image.png 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:

// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // 缓存原生数组方法
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    // 执行并缓存原生数组功能
    const result = original.apply(this, args);
    // 响应式处理
    const ob = this.__ob__;
    let inserted;
    switch (method) {
    // push、unshift会新增索引,所以要手动observer
      case "push":
      case "unshift":
        inserted = args;
        break;
      // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
      case "splice":
        inserted = args.slice(2);
        break;
    }
    // 
    if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
    // notify change
    ob.dep.notify();// 通知依赖更新
    // 返回原生数组方法的执行结果
    return result;
  });
});

简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。

21. Vue 单页应用与多页应用的区别

概念:

  • SPA单页面应用只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
  • MPA多页面应用,指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

区别: 775316ebb4c727f7c8771cc2c06e06dd.jpg

22. Vue template 到 render 的过程

Vue的template并不是真正的html结构,不是W3C的规范语法,浏览器不认得v-show类型的指令

先通过调用parse方法将template转化为ast(抽象语法树) 它是一种用JS对象的形式来描述整个模板 解析过程是利用正则表达式顺序解析模板,当解析到开始标签,闭合标签,文本的时候会分别执行对应的回调函数,ast以节点为单位来描述一个一个的类型 ast元素节点总共三种类型 type为1表示普通元素 2为表达式 3为纯文本

对静态节点做优化

通过optimize会深度遍历ast ,将ast中的静态节点区分出来,并做一个标记,由于静态节点的DOM不会改变,ast更新时不会重复计算静态节点,性能得到了优化

Ast转render函数

23. Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。

24. 简述 mixin、extends 的覆盖逻辑

mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。

  • Vue 的 mixin 用来抽离多个组件之间公共的业务逻辑,实现复用
<template>
  <div class="about">
    <div>{{ name }}</div>
    <button @click="get">获取了</button>
  </div>
</template>

<script>

import {myMixin} from '../mixin'

export default {
//使用mixins
  mixins: [myMixin]

}

</script>
  • extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。将传入组件的 options 和当前组件的 options 进行了合并

25.描述下Vue自定义指令

指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

  • 全局定义:Vue.directive("focus",{})
  • 局部定义:directives:{focus:{}}

自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind


1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。

4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。

5. unbind:只调用一次,指令与元素解绑时调用。

原理

1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性

2.通过 genDirectives 生成指令代码

3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子

4.当执行指令对应钩子函数时,调用对应指令定义的方法

26. 子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。

Vue提倡单向数据流,即父级 props 的更新会流向子组件。这是为了防止意外的改变父组件状态,

**只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

27. Vue是如何收集依赖的?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶

function defieneReactive (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)Watcher

class 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,这样便已经完成了一个依赖收集的过程。

28. 对 React 和 Vue 的理解,它们的异同

    1. 数据驱动

    • React 使用单向数据流,通过props来传递数据,通过回调函数来处理数据变化。
    • Vue 使用双向数据绑定,通过v-model指令可以实现表单数据的双向绑定。
    1. 语法

    • React 使用 JSX,它将组件的结构和行为以一种类似 HTML 的语法嵌入到 JavaScript 代码中。
    • Vue 使用类似 HTML 的模板语法,可以直接在模板中编写组件的结构和交互行为。
    1. 状态管理
    • 在React中,常用的状态管理方案是Redux,而在Vue中,推荐使用Vuex。
    1. diff算法的不同
    • react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。 Vue 使用双向指针,边对比,边更新DOM

相同之处

    1. 组件化
    • React 和 Vue 都采用了组件化的思想,允许开发者将UI划分为独立的、可复用的组件来构建应用。
    1. 虚拟 DOM
    • React 和 Vue 都使用虚拟 DOM 技术,使得页面更新更加高效,并能够提供更好的性能。

29. Vue的优点

  1. 响应式数据绑定:Vue采用了响应式的数据绑定机制,数据变化时可自动更新视图,减少手动操作DOM。
  2. 组件化开发:Vue鼓励组件化开发,提高代码复用性和可维护性,使得开发过程更高效。
  3. 性能优化:Vue采用虚拟DOM和提供性能优化工具,帮助开发者提升应用性能。

30. assets和static的区别

相同点: assetsstatic 两个都是存放静态资源文件

不相同点: assets 中存放的资源文件在项目打包时,会进行打包上传,最终也都会放置在 static 文件中一同上传至服务器。static 中放置的资源文件就不会要走打包,直接上传至服务器。 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件大

31. delete和Vue.delete删除数组的区别

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete 直接删除了数组元素 改变了数组的长度。

32. vue如何监听对象或者数组某个属性的变化

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。

解决方式:

  • this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)
this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
  • 调用以下几个数组的方法
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()

vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作

vm.$set 的实现原理是:

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;
  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

35. 对SSR的理解

SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。

优点:

SSR 有着更好的 SEO、并且首屏加载速度更快

缺点: 开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,

服务器会有更大的负载需求

36. 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压缩等。

37. 对 SPA 单页面的理解,它的优缺点分别是什么?

SPA仅在页面初始化时加载相应的 HTML、JavaScript 和 CSS。利用路由机制实现 HTML 内容的变换,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:初始加载大量的css,js文件,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

38. template和jsx的有什么分别?

  • JSX具有更高的灵活性,在复杂的组件中,更具有优势
  • template 更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
  • JSX,它将组件的结构和行为以一种类似 HTML 的语法嵌入到 JavaScript 代码中。
  • template 使用类似 HTML 的模板语法,可以直接在模板中编写组件的结构和交互行为。

39. vue初始化页面闪动问题

vue初始化之前,由于div是不归vue管的

首先:在css里加上以下代码:

[v-cloak] {    display: none;}

如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"

40. MVVM的优缺点?

优点:

  1. 模型和视图相分离:视图负责展示数据,模型则负责处理业务逻辑,从而降低了各个部分之间的耦合性。使得代码更加清晰可读并且易于维护。

  2. 双向数据绑定:当模型中的数据发生变化,相关的视图会自动更新。

  3. 可测试性:由于视图和业务逻辑的分离,视图模型中的业务逻辑可以更容易地进行单元测试。

  4. 提高开发效率:由于数据绑定的存在,可以减少编写重复的界面更新代码的工作量,提高开发效率。

  5. 可维护性:模型、视图和视图模型的分层设计使得代码更加结构化和可维护。开发者可以更容易地理解和修改特定功能的实现。

缺点:

  1. 性能开销:MVVM模式在处理视图和数据之间的双向绑定时,会引入一定的性能开销。特别是在处理大规模数据和复杂计算时,可能会对性能产生一定的影响。

  2. 灵活性限制:MVVM模式在数据和视图之间有严格的约定,这对于一些复杂的交互场景可能存在一定的限制。在需要灵活控制数据和视图交互的情况下,可能需要额外的工作来实现。

生命周期

1. 说一下Vue的生命周期

beforeCreate 创建前,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问

created 创建后。组件实例已创建完成,数据和事件回调已初始化,但尚未挂载到DOM上。

beforeMount 挂载前:相关的 render 函数首次被调用。

mounted 挂载后,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点

beforeUpdate 更新前,此时数据是新的,DOM是旧的。

updated 更新后,当前阶段组件 Dom 已完成更新。

beforeDestroy 销毁前。在这一步,实例仍然完全可用。清除计时器。

destroyed 销毁后。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

activated keep-alive 专属,组件被激活时调用

deactivated keep-alive 专属,组件被销毁时调用

异步请求在哪一步发起?

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

2. Vue 子组件和父组件执行顺序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

3. created和mounted的区别

  • created 在组件实例被创建后执行,未挂载到 DOM 树上,无法访问到组件的 DOM
  • mounted 在组件实例被挂载到 DOM 树之后执行,可以访问到组件的 DOM 元素

4. 一般在哪个生命周期请求异步数据

我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 ​

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
  • SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。

5. keep-alive 中的生命周期哪些

keep-alive用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时组件销毁前后的生命周期不会再被触发,因为组件不会被真正销毁。

activated 组件被激活时调用

deactivated 组件被销毁时调用

组件通信

组件通信的方式如下:

(1) props  /   $emit

父组件通过props向子组件传递数据,子组件通过$emit和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
  • props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
  • props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式
// 父组件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

<script>
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父组件数据";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};
</script>
// 子组件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按钮</button>
    </div>
</template>
<script>
export default {
    name: "son",
    props: ["msg", "fn"]
};
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
// 父组件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}
</script>
//子组件
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
    emitIndex(index) {
      this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
    }
  }
}
</script>

(2)eventBus事件总线($emit / $on

eventBus事件总线适用于父子组件非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 假设有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>

firstCom组件中发送事件:

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

(3)接收事件secondCom组件中发送事件:

<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

(3)依赖注入(provide / inject)

这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。

provide / inject是Vue提供的两个钩子,和datamethods是同级的。并且provide的书写形式和data一样。

  • provide 钩子用来发送数据或方法
  • inject钩子用来接收数据或方法

在父组件中:

provide() { 
    return {     
        num: this.num  
    };
}

在子组件中:

inject: ['num']

还可以这样写,这样写就可以访问父组件中的所有属性:

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

inject: ['app']
console.log(this.app.num)

注意: 依赖注入所提供的属性是非响应式的。

(3)ref / $refs

这种方式也是实现父子组件之间的通信。

ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

在子组件中:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

(4)$parent / $children

  • 使用$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
  • 使用$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。

在子组件中:

<template>
  <div>
    <span>{{message}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

在父组件中:

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change">点击改变子组件值</button>
  </div>
</template>

<script>
import child from './child.vue'
export default {
  components: { child },
  data() {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    change() {
      // 获取到子组件
      this.$children[0].message = 'JavaScript'
    }
  }
}
</script>

在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值。 需要注意:

  • 通过$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例
  • 在组件中使用$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的
  • 在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组
  • $children 的值是数组,而$parent是个对象

(5)$attrs / $listeners

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。

先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。

  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
  • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A组件(APP.vue):

<template>
    <div id="app">
        //此处监听了两个事件,可以在B组件或者C组件中直接触发 
        <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
    </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};
</script>

B组件(Child1.vue):

<template>
    <div class="child-1">
        <p>props: {{pChild1}}</p>
        <p>$attrs: {{$attrs}}</p>
        <child2 v-bind="$attrs" v-on="$listeners"></child2>
    </div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
        this.$emit('test1'); // 触发APP.vue中的test1方法
    }
};
</script>

C 组件 (Child2.vue):

<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>
<script>
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');// 触发APP.vue中的test2方法
    }
};
</script>

在上述代码中:

  • C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
  • 在B组件中通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

父组件怎么调用子组件中的方法

  1. 为子组件设置 ref:
    在父组件中,在子组件的标签上添加 ref 属性,这样可以在父组件中通过 ref 获取子组件的实例。例如:

    <template>
      <div>
        <child-component ref="childRef"></child-component>
      </div>
    </template>
    
  2. 在父组件中调用子组件方法:
    使用 this.$refs.childRef 来访问子组件的实例,然后就可以直接调用子组件的方法了。例如:

    this.$refs.childRef.childMethod();
    
  3. 子组件方法定义:
    在子组件中定义需要被调用的方法。例如,在子组件的 methods 中定义一个方法:

    methods: {
      childMethod() {
        // 子组件的具体逻辑
      }
    }
    

(6)总结

(1)父子组件间通信

  • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。
  • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

(2)兄弟组件间通信

  • 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。

(3)任意组件之间

  • 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

路由

1. Vue-Router 的懒加载如何实现

非懒加载:

import List from '@/components/list.vue'
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})

(1)方案一(常用):使用箭头函数+import动态加载

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

(2)方案二:使用箭头函数+require动态加载

const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})

2. 路由的hash和history模式的区别

Vue-Router有两种模式:默认的路由模式是hash模式。

1. hash模式

1.hash 路由模式的实现主要是基于下面几个特点:

  • hash模式是开发中默认的模式,#以及#后面的字符称之为 hash, 用 window.location.hash 读取。
  • URL 中的 hash 值只是客户端的url中进行显示,向服务端发送请求的时候,hash 部分不会被发送。
  • 可以通过hashchang 事件来监听 hash 值的变化,从而对页面进行跳转。
  • hash 值的改变会增加浏览器的访问记录,所以可以通过浏览器的回退、前进控制 hash 值的改变。
  • 可以通过 a 标签设置 href 值或者通过 js 给location.hash 赋值来改变 hash 值。
window.onhashchange = function(event){
	console.log(event.oldURL, event.newURL);
	let hash = location.hash.slice(1);
}

2. history模式

history 采用 HTML5 的新特性;且提供了两个新方法:

  • pushState()用于在历史中添加一条记录

  • replaceState()对历史记录进行修改

  • popState 事件的监听到状态变更

  • 通过 pushState 和 replaceState 两个API 来操作实现 URL 的变化,不会向后端发送请求,也不会触发popstate事件的执行

  • popstate事件的执行是在点击浏览器的前进后退按钮的时候,会被触发,监听 到URL 的变化,从而对页面进行跳转

history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。

在vue的router中,通过修改vueRouter的mode属性来决定使用history还是hash。默认为hash模式。
const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

3. 如何获取页面的hash变化

(1)监听$route的变化

// 监听,当路由发生变化的时候执行
watch: {
  $route: {
    handler: function(val, oldVal){
      console.log(val);
    },
    // 深度观察监听
    deep: true
  }
},

(2)window.location.hash读取#值 window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。

4. $route 和$router 的区别

  • $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
  • $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。

5. 如何定义动态路由?如何获取传过来的动态参数?

(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: "kobi",
        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 获取传递的值

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

1.全局导航守卫 2.路由独享守卫 3.组件内导航守卫

一、Vue-Router导航守卫

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

  1. 全局路由钩子

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

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫在 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);
});
  1. 单个路由独享钩子

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

export default [    
    {        
        path: '/',        
        name: 'login',        
        component: login,        
        beforeEnter: (to, from, next) => {          
            console.log('即将进入登录页面')          
            next()        
        }    
    }
]
  1. 组件内钩子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

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

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

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

beforeRouteEnter(to, from, next) {      
    next(target => {        
        if (from.path == '/classProcess') {          
            target.isFromProcess = true        
        }      
    })    
}

二、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 的回调函数。
  • 导航完成

7. Vue-router跳转和location.href有什么区别

  • 使用 location.href= /url 来跳转,刷新页面;
  • 使用 history.pushState( /url ) ,无刷新页面,静态跳转;
  • 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。

8. params和query的区别

用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name

url地址显示:query更加类似于get传参url中显示参数,params则类似于post传参url中不显示参数

注意:query刷新不会丢失数据 params刷新会丢失数据。

9. Vue-router 导航守卫有哪些

  • 全局前置/钩子:beforeEach、beforeResolve、afterEach
  • 路由独享的守卫:beforeEnter
  • 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

10. 对前端路由的理解

前端路由是指在前端应用中,通过URL来管理不同页面,而无需刷新整个页面,不需要向服务器发送请求获取新的页面。

前端路由通常使用一种叫做“单页面应用”的开发模式来实现。SPA将整个应用加载到浏览器中的一个页面中,然后通过前端路由来根据URL的变化切换显示不同的页面内容。

在前端路由中,常用的工具库有React Router和Vue Router等。它们提供了一组API,用于定义路由规则、处理URL变化和页面切换等功能。同时,它们还支持路由参数、嵌套路由、重定向等高级功能,使得前端路由更加灵活和强大。

vuex

1.vuex的理解

为vue提供的全局状态管理系统,用于多个组件中的数据共享

Vuex的状态是响应式的,状态发生改变,相应的组件也会相应的更新

改变store中的状态唯一的途径是mutation,方便追踪每个状态的变化

image.png

主要的模块

State  定义响应式的数据

Getter  state对象的读取方法

Mutation 唯一更改state中状态的方法,必须是同步函数

Action  用于提交mutation,而不是直接变更状态,可以是异步操作

Module  允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

2. Vuex中action和mutation的区别

  • Mutation是修改State的唯一途径 同步执行;
  • Action不能直接操作state通过调用mutation来完成state状态的修改 异步执行。
  • mutation的参数是state,包含store中的数据;store的参数是context,它是 state 的父级,包含 state、getters

3. Vuex 和 localStorage 的区别

(1)最重要的区别

  • vuex存储在内存中
  • localstorage 则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON的stringify和parse方法进行处理。 读取内存比读取硬盘速度要快

(2)应用场景

  • Vuex 它采用集中式存储管理应用的所有组件的状态,vuex用于组件之间的传值。
  • localstorage是本地存储,是将数据存储到浏览器,一般是在跨页面传递数据时使用 。
  • Vuex能做到数据的响应式,localstorage不能

(3)永久性

刷新页面时vuex存储的值会丢失,localstorage不会。

4. Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex区别

  • Vuex以mutations变化函数取代Reducer函数,无需switch,只需在对应的mutation函数里改变state值即可
  • Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可
  • Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)

(2)共同思想

  • 单—的数据源
  • 变化可以预测

5. 为什么要用 Vuex 或者 Redux

对于多层嵌套的组件传递参数会非常繁琐,并且对于兄弟组件间的传值需要定义在共同的父组件中。通常会导致代码无法维护。

将组件的公共状态抽取出来,采用集中式的管理。代码将会变得更结构化且易维护

6. Vuex和单纯的全局对象有什么区别?

  • Vuex 的状态存储是响应式的。若 store 中的状态发生变化,那么相应的组件也会相应地更新。
  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径mutation。这样可以方便地跟踪每一个状态的变化。

7. 为什么 Vuex 的 mutation 中不能做异步操作?

  • Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化
  • 每个mutation执行完成后都会对应到一个新的状态变更,如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难

8. Vuex的严格模式是什么,有什么作用,如何开启?

在严格模式下,状态变更且不是由mutation函数引起的,将会抛出错误。

在Vuex.Store 构造器选项中开启,如下

const store = new Vuex.Store({
    strict:true,
})

9. 如何在组件中批量使用Vuex的getter属性

使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中

import {mapGetters} from 'vuex'
export default{
    computed:{
        ...mapGetters(['total','discountTotal'])
    }
}

10. 如何在组件中重复使用Vuex的mutation

使用mapMutations辅助函数,在组件中这么使用

import { mapMutations } from 'vuex'
methods:{
    ...mapMutations({
        setNumber:'SET_NUMBER',
    })
}

然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)

11.vuex和pinia的区别

  • Vuex基于Vue2的Options API构建,而Pinia基于Vue3的Composition API构建,这使得Pinia更加灵活和可组合 (体现在不需要通过辅助函数进行注册)。
  • Vuex采用全局单例模式,通过一个store对象来管理所有的状态,而Pinia采用分离模式,每个组件都拥有自己的store实例。
  • pinia 具有可靠的类型推断支持
  • pinia中没有了mutations和modules,同步异步都可在actions进行操作。
  • Vuex体积相对较大,而Pinia的体积约1KB,相对较小。

vue3

1. Vue3.0有什么更新

image.png

image.png

2. defienProperty与proxy有何作用,区别是什么?

Object.defineProperty和Proxy都是用来实现对对象的监视和控制的机制

Object.defineProperty的作用和区别:

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

示例:

const obj = { name: 'Alice' };
Object.defineProperty(obj, 'name', { writable: false }); // 将name属性设为不可写

Proxy的作用和区别:

  • 可以监控整个对象的操作。
  • 通过Proxy可以代理整个对象,可以监听同级结构下的所有属性变化,并在代理过程中定义对应的操作方法(如get、set、deleteProperty等),从而实现对对象的行为进行拦截和自定义处理。
  • Proxy 可以监听数组的变化

示例:

const handler = {
  get: function(target, prop) {
    console.log(`Reading property ${prop}`);
    return target[prop];
  }
};
const proxy = new Proxy({ name: 'Alice' }, handler);
console.log(proxy.name); // 会触发get拦截器,打印"Reading property name"并返回"Alice"

3. Vue3.0 为什么要用 proxy?

  • proxy可以直接监听数组的变化;
  • proxy可以监听对象而非属性.它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。
  • Proxy直接可以劫持整个对象,并返回一个新对象。

Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异∶

  • get 收集依赖
  • Set、delete 等触发依赖

4. Vue 3.0 中的 Vue Composition API?

  1. 代码组织: 在 Vue 2 中,组件的属性、方法和生命周期钩子是按照固定的顺序进行组织的,导致在逻辑相关的代码之间需要来回跳转。而在 Vue 3 中,Composition API 可以按照逻辑关联代码,使得代码更易于维护、理解和重用。
  2. 逻辑复用: 在 Vue 2 中,逻辑的复用较为受限,通常需要使用 mixins 或较复杂的高阶组件来实现逻辑的共享。而在 Vue 3 中,Composition API 提供了更好的逻辑复用方式,可以更轻松地提取和重用逻辑部分。
  3. 更好的 TypeScript 支持: 可以更好地进行类型推断和类型校验。

5. Composition API与React Hook很像,区别是什么

React Hook是根据useState调用的顺序来确定下一次重渲染时的state是来源于哪个useState, 限制条件

  • 不能在循环、条件、嵌套函数中调用Hook
  • 必须确保总是在你的React函数的顶层调用Hook
  • useEffect、useMemo等函数必须手动确定依赖关系

Composition API是基于Vue的响应式系统实现的

  • 声明在setup函数内,组件实例化只调用一次setup
  • Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用
  • 响应式系统自动实现了依赖收集,而React Hook需要手动传入依赖,而且必须必须保证依赖的顺序

虚拟DOM

1.对虚拟DOM的理解?

虚拟Dom是一个JS对象,通过对象的形式来表示DOM结构,是由render函数生成,是render函数的返回值,是对模板的一种W3C的描述,模板上的一些指令是浏览器的识别不了的,vue在编译模板的时候要先将转为AST(抽象语法树),AST在转为render函数, render函数的返回值就是虚拟DOM

2. 虚拟DOM的解析过程

用JS对象来描述整个虚拟DOM树(虚拟DOM)

把虚拟DOM转成真实DOM并插入页面中(render)

虚拟DOM发生了更改,比较新老虚拟DOM树的差异,得到差异对象(diff)

把差异对象应用到真正的DOM树上(patch)

3. 为什么要用虚拟DOM

传统的 DOM 操作非常耗费时间和计算成本,每次数据变化后直接操作 DOM,会造成频繁的页面重绘和回流。而虚拟DOM可以将数据变化的结果先放在内存中,然后与之前的虚拟DOM进行比对,只在需要更新的地方进行真正的 DOM 操作,避免了频繁的 DOM 操作,提高了页面的渲染效率和性能。

易于实现跨平台:虚拟 DOM 与平台无关,可以在客户端、服务端、小程序等多个平台中使用。

4. 虚拟DOM真的比真实DOM性能好吗

对于复杂的数据更新频繁的场景,使用虚拟 DOM 可以带来性能优化。

页面DOM比较少的情况下,由于多了一层虚拟DOM的计算,会比innerHTML插入慢,直接操作真实的DOM会更高效

5. DIFF算法的原理

DIFF算法只做同级比较,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换

先定类型,判断是否为同一类型,同种组件,如果不为同一类型, 同种组件,则不进行比较,直接进行替换

在进行定量,定量就是用key来描述一个组件的内容,如果新旧虚拟DOM的key值相同则表示是同一类型的组件内容,不需要去进行一一比对,节约了性能

6. Vue中key的作用

v-if中的key用来标识一个独立的元素避免切换前后相同元素的一个复用,使用key的元素就不会被复用

v-for中的key给每个列表项提供一个 key 值,更好的追踪元素,高效的更新虚拟DOM

key 是为 Vue中的唯一标记,通过这个 key,diff 操作可以更准确、更快速

7. 为什么不建议用index作为key?

如果使用索引,当对元素进行增删操作的时候,元素的 key 值发生改变,可能会导致所有元素的 key 值发生改变。所以 key 值不建议使用索引。

8.vue2中的diff算法和vue3中的diff算法的区别

  • 静态标记和提升
    • Vue 2 中的 diff 算法会重新渲染整个组件,导致大量的 DOM 操作。
    • Vue 3 对静态节点进行优化,在重新渲染时可以跳过静态节点,从而减少不必要的操作。
  • Fragments 和模板内的组合
    • Vue 2 中,模板中的组件必须有一个单一的根节点
    • Vue 3 引入了 Fragments (片段) 的概念,可以直接在模板中返回多个根节点
  • 动态 Props 的标记和缓存
    • Vue 2 中,所有的 Props 都会被当作响应式的,静态的 Props 在重新渲染时也会进行 diff 和更新。
    • Vue 3 中,组件会区分动态 Props 和静态 Props,只有动态 Props 才会触发 diff 过程。

说说你对发布订阅模式的理解?与观察者模式有何区别?

消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者可能存在

订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在

什么是洋葱模型

在洋葱模型中,每一层相当于一个中间件,其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机。

Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。

优化

1.骨架屏一般有几种方案实现

骨架屏在页面加载过程中展示布局结构,给用户一种页面正在加载的视觉反馈 以下是一些常见的骨架屏实现方案:

  1. 纯 CSS 方案:使用纯 CSS 绘制骨架屏,可以使用伪元素、背景颜色或渐变来实现。通过控制元素的大小、形状和样式,以及添加动画效果,模拟页面的布局结构和加载过程。
  2. 基于图片方案:将页面截图、图标或简单的 SVG 图像作为骨架屏的背景,然后使用 CSS 和动画效果来补充和改进骨架屏的展示效果。
  3. 基于预渲染方案:在后端服务器生成骨架屏的 HTML 片段,作为初始响应返回给浏览器。随后,前端 JavaScript 会将骨架屏替换为具体的内容,实现平滑的过渡效果。
  4. 基于组件方案:使用通用的组件库或框架中提供的骨架屏组件。通过配置组件参数,来自动生成基于布局结构的骨架屏。
  5. 基于第三方库方案:使用第三方的骨架屏库,如 react-loading-skeletonvue-content-loading 等。这些库提供了易于使用且高度可定制的骨架屏组件,通过简单的 API 调用即可实现骨架屏效果。

Tree-shaking(树摇)

Tree shaking 是指在打包过程中去除不被使用的代码,以减少最终生成的的体积

Tree shaking 的核心思想是通过静态代码分析的方式,识别出没有被使用的模块成员(函数、变量、类等),从而可以在打包过程中将这些未使用的部分剔除掉

Tree shaking 的好处很明显,首先可以大大减少 JavaScript 文件的大小,减少网络传输和加载时间;其次,减少了未使用代码的包含,减小了浏览器解析和执行 JavaScript 的成本;最后,还可以提供更好的用户体验,特别是在移动端或网络条件不佳的情况下。

vue2 vue3 中的 route区别

new Router 变成 createRouter

Vue Router 不再是一个类,而是一组函数。现在你不用再写 new Router(),而是要调用 createRouter

新的 history 配置取代 mode

mode: 'history' 配置已经被一个更灵活的 history 配置所取代。根据你使用的模式,你必须用适当的函数替换它:

  • "history"createWebHistory()
  • "hash"createWebHashHistory()
  • "abstract"createMemoryHistory()

移动了 base 配置

现在,base 配置被作为 createWebHistory (其他 history 也一样)的第一个参数传递:

删除了 fallback 属性

删除了 *(星标或通配符)路由