2022 vue2/3 技术题整理

1.vue中组件之间的通信方式?

 

  • 父子组件
    • props
    • $emit/$on
    • $parent / $children
    • ref:用在普通的DOM 元素上,指向 DOM 元素;用在子组件上,指向组件实例
    • $attrs / $listeners
  • 兄弟组件
    • $parent
  • 跨层级关系
    • provide/inject
    • $root
    • eventbus
    • vuex
    • pinia

2.v-if和v-for哪个优先级更高?

  1. 在 Vue2 中,v-for 优先于 v-if ;在 Vue3 中,v-if 的优先级高于 v-for
  2. 官网明确指出永远不要把 v-ifv-for 同时用在同一个元素上,实践中也不应该把它们放一起。
  3. 通常有两种情况下导致我们这样做:
    • 为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。此时不应该这么做,因为vue2中哪怕只渲染列表中一小部分,也得在每次重渲染的时候遍历整个列表;vue3中会报错,因为v-if先执行,取不到user。应该定义一个计算属性 (比如 activeUsers),让其返回过滤后的列表,这样重新渲染时遍历的是过滤后的列表。
    • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShow")。此时不应该这么做,因为vue2中哪怕不渲染该列表,也得在每次重渲染的时候遍历整个列表;这种情况,虽然vue3中可以,但也不推荐,因为使用风格不统一。把 v-if 移动至容器元素上 (比如 ulol)即可。

3.Vue组件生命周期以及每个阶段做的事

  1. Vue组件生命周期指的是组件从创建到卸载的整个过程。在这个过程中的特定节点,会运行特定的钩子函数,就叫做生命周期函数,它的作用是便于用户在特定阶段添加自己的代码。
  2. Vue生命周期主要分为8个阶段:创建前后, 载入前后, 更新前后, 销毁前后,以及一些特殊场景的生命周期。vue3中新增了三个、修改了两个,详情如下:
    生命周期v2生命周期v3描述
    beforeCreatebeforeCreate组件实例被创建之初
    createdcreated组件实例已经完全创建
    beforeMountbeforeMount组件挂载之前
    mountedmounted组件挂载到实例上去之后
    beforeUpdatebeforeUpdate组件数据发生变化,更新之前
    updatedupdated数据数据更新之后
    beforeDestroybeforeUnmount组件实例销毁之前
    destroyedunmounted组件实例销毁之后
    activatedactivatedkeep-alive 缓存的组件激活时
    deactivateddeactivatedkeep-alive 缓存的组件停用时调用
    errorCapturederrorCaptured捕获一个来自子孙组件的错误时被调用
    -renderTracked调试钩子,响应式依赖被收集时调用
    -renderTriggered调试钩子,响应式依赖被触发时调用
    -serverPrefetchssr only,组件实例在服务器上被渲染前调用

Hook inside (setup) 生命周期: 除了没有 beforeCreate 和 created,其它的都有。因为setup是设置阶段,在 beforeCreate 之前执行,所以不需要再定义它们,直接在 setup 写代码即可。 setup勾子写法:onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured、onRenderTracked、onRenderTriggered

  1. Vue 生命周期流程图:

    6e30f5bcdef1c66d50c8b86022f4a82b.png

  4.结合实践:

        beforeCreate:通常用于插件开发中执行一些初始化任务

        created:组件初始化完毕,可以访问各种数据,获取接口数据等

        mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。

        beforeUpdate:此时view层还未更新,可用于获取更新前各种状态

        updated:完成view层的更新,更新后,所有状态已是最新

        beforeUnmount:实例被销毁前调用,可用于一些定时器或订阅的取消

        unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

Vue生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • vue的生命周期钩子核心实现是利用发布订阅模式,先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储),然后在创建/更新组件实例的过程中、会依次执行对应的钩子方法(发布)
  • 由于setup函数先执行,会先把setup里的生命周期函数推入数组,执行时也就先执行setup里的生命周期,然后执行选项里对应的生命周期。

4.双向绑定以及它的实现原理

  1. 双向数据绑定是通过指令或修饰符实现的一种特性,是一种绑定数据和事件的语法糖。数据修改可以驱动视图更新,视图也能够修改数据。
  2. 双向数据绑定可以减少大量繁琐的事件处理代码,提高开发效率和代码可读性
  3. 数据驱动视图是基于vue数据响应式的原理,视图修改数据是基于vue事件绑定的原理。

5.v-model和sync修饰符有什么区别

vue2中v-model只能用一个,.sync可以多个:

<input v-model="num"/>
等价于
<input :value="num" @input="val=>num=val"/>

<com1 :a.sync="num"></com1>
等价于
<com1 :a="num" @update:a="val=>num=val"></com1>

vue3中不再支持.sync,取而代之的是v-model,可以多个:

<com1 v-model="num"></com1>
等价于
<com1 :modelValue="num" @update:modelValue="val=>num=val"></com1>

<com1 v-model:a="num"></com1>
等价于
<com1 :a="num" @update:a="val=>num=val"></com1>

6. nextTick是干什么的,实现原理是什么?

官方定义:

Vue.nextTick( [callback, context] )

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello';
// DOM 还没有更新
Vue.nextTick(function () {
    // DOM 更新了
});
  1. nextTick是Vue提供的一个全局API,为了在数据改变后,获取到最新的DOM信息,可以在数据修改后,立即使用this.$nextTick(callback),这样回调函数将在dom信息更新完成后被调用。
  2. Vue更新DOM是异步执行的,将多个更新任务放在同一个更新队列里,多次调度同一个更新任务,只会往队列里放一次,然后在异步回调里执行更新队列,之所以在异步回调里,是基于事件轮询机制,等到所有设置数据的操作(设置数据是同步操作)完成后,才执行回调函数里的内容,实现批量更新,大大减少更新频繁的问题。
  3. nextTick就是把你的回调函数也放到更新队列里,然后一起在异步回调里执行,由于是先设置数据,后调用用户的nextTick,所以能获取更新后的内容。nextTick里的异步,会先判断Promise是否存在,如果存在,就使用微任务,不存在就使用宏任务。

7. Vue监听相关api有哪些?

watchEffect与watch: watch既要指明监视的属性,也要指明监视的回调。watchEffect不需要指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。watchEffect传入的函数会立刻执行一次。watch默认情况下不会执行回调函数,除非手动设置immediate选项。

watchEffect与computed: computed函数的返回值是ComputedRefImpl响应式数据,watchEffect函数的返回值是一个停止监听函数。computed注重计算结果,回调函数必须要写返回值。watchEffect注重计算过程,回调函数不写返回值。

watch与computed: computed函数的返回值是ComputedRefImpl响应式数据,watch函数的返回值是一个停止监听函数。computed不支持异步,当内有异步操作时无效,而watch支持异步。computed默认第一次加载的时候就开始监听;watch默认第一次加载不做监听,如果需要第一次加载做监听,需要设置 immediate: true

computed与方法:computed只会在相关响应式依赖发生改变时重新求值,方法只要调用就会重新求值。

computed: {
  // 这个计算属性的值永远不会更新。但如果声明的是方法,只要调用就会重新计算。
  now() {
    return Date.now();
  }
}

watch原理:

 1.watch内部会通过ReactiveEffect类创建一个观察者(订阅者) - Watcher 对象,创建Watcher对象时,会传入依赖收集函数和调度函数,如果传入的是依赖数据,会处理成依赖收集函数。

 2.当依赖数据发生改变时,它内部的Watcher会执行调度函数。

watchEffect原理:

 1.watchEffect内部也会通过ReactiveEffect类创建一个观察者(订阅者) - Watcher 对象,创建Watcher对象时,只传入了依赖收集函数。

 2.当依赖数据发生改变时,因为没有调度函数,它内部的Watcher会执行run函数,run函数是依赖收集函数的包装函数。

computed原理:

 1.computed内部也通过ReactiveEffect类创建的一个内部的Watcher(订阅者)对象,创建内部Watcher对象时传入了用户的依赖收集函数和内部的调度函数。

 2.computed内部还有一个重要的属性_dirty,用来记录该computed数据所依赖的数据是否发生了改变。当依赖数据发生改变时,computed内部的Watcher会执行调度函数,把_dirty属性设置为true,并且通知(dep属性里的)所有依赖该computed数据的外部Watcher(订阅者) 完成更新

 3.外部Watcher(订阅者)更新时,会通过.value属性获取依赖的computed数据的值时。computed内部会先检查_dirty是否为true,如果不为true,还返回老数据;如果为true,就调用内部Watcher(订阅者)的run函数获取最新数据并返回,然后把_dirty设为false

8. Vue子组件和父组件创建和挂载顺序

  1. 创建过程自上而下,挂载过程自下而上;即:

    • parent created
    • child created
    • child mounted
    • parent mounted
  2. 因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

9. 怎么缓存组件?缓存后怎么更新?

  1. 缓存组件使用 keep-alive 组件,keep-alive 是 vue 内置组件,keep-alive 包裹动态组件component,当组件切换时会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

    <keep-alive>
      <component :is="view"></component>
    </keep-alive>
  2. keep-alive 有属性三个 include、exclude、max,以及2个生命周期 activated、deactivated

    1. include: 值为字符串或正则表达式或数组,只有名称匹配的组件才会被缓存。
    2. exclude: 值为字符串或正则表达式或数组,排除名称匹配的组件,其它组件都会缓存。
    3. 值为字符串时可以用","隔开表示多个,值为正则或数组时,要使用v-bind。exclude的优先级高于include。
    4. max表示最多缓存多少个。
    5. vue3 中结合 vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component"></component>
      </keep-alive>
    </router-view>
  3. 缓存后如果要获取数据,解决方案可以有以下两种:

    • beforeRouteEnter:每次进入路由的时候,都会执行beforeRouteEnter

      beforeRouteEnter(to, from, next){
        next(vm=>{
          // 每次进入路由执行
          vm.getData()  // 获取数据
        })
      }
    • actived:被keep-alive缓存的组件被激活的时候,都会执行actived钩子

      activated(){
         this.getData() // 获取数据
      }
  4. 原理:keep-alive是一个组件,它内部定义了一个map对象和一个set列表,map用于缓存创建过的组件实例,set保存组件激活的顺序。内嵌组件激活时,keep-alive会先查找map中有没有该内嵌组件,有就复用并渲染,并把他移到set列表的末尾;如果没有,除了渲染,还会判断是否符合缓存条件,符合就缓存。

  5. 缓存策略:LRU算法 -- 如果缓存数量超过了max,就删除最久未使用的,缓存最新的。keep-alive组件内部定义的set列表,缓存组件的key,没有key,就把组件本身作为key,这个key就是map对象的key。删除缓存的组件时,总是删除set列表头部的第一个元素,添加新元素时往set列表的末尾add,这样就实现了LRU算法。

10.你写过自定义指令吗?使用场景有哪些?

当我们需要进行DOM操作时,可以使用自定义指令。自定义指令就是带有生命周期钩子的对象,或者单独一个函数。自定义指令用到的生命周期函数:created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted

1.先创建自定义指令:
import { Directive } from 'vue';
// 对象指令
export const vMyDir: Directive = {
    mounted(el, dir, vNode) {
        console.log('在绑定元素的父组件挂载之后调用', el, dir, vNode);
        el.style.background = dir.value.background;
        el.style.color = dir.value.color;
    },
    updated(el, dir, vNode) {
        console.log('在包含组件的VNode及其子组件的VNode更新之后调用', el, dir, vNode);
    },
}
// 函数指令
export const vMyDir1: Directive = (el, dir, vNode) => {
    // 这将被作为 `mounted` 和 `updated` 调用
}

2.自定义局部指令:
    1)在该组件引入刚才创建的指令
        import { vMyDir } from '../public/data';
        const data = reactive<StringObj>({
            background: 'red',
            color: '#eee',
        });
    2)只能在该组件的template模板里使用: <div v-myDir="data">MyDirective</div>

3.自定义全局指令:
    1)在入口js文件(一般为main.ts)中通过 app.directive方法注册, 如:
        import { vMyDir } from '../public/data';
        app.directive('myGlobalDir', vMyDir);
    2)在所有组件的template模板里都能使用: <div v-myGlobalDir="data">myGlobalDir</div>

11.说下 $attrs 和 $listeners 的使用场景

  1. child组件的一些属性没在props中定义,就会传入到$attrs中;添加native修饰符的事件,就会绑定在child组件的原生dom上,不添加就传到child组件的$listeners里了。v-bind="$attrs" v-on="$listeners",可以直接把$attrs$listeners透传给它的子组件,常用于爷孙组件的传值。
  2. vue2中使用$listeners获取事件,vue3中已移除,均合并到$attrs中,使用起来更简单了。

12.v-once的使用场景有哪些?

<span v-once>This will never change: {{msg}}</span>
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<my-component v-once :comment="msg"></my-component>
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>
<!-- valueA 若不发生变化,则不会进行更新 -->
<div v-memo="[valueA]">
  <div class="box" v-for="item in arr" :key="item">{{foodObj[food]}}</div>
</div>
  1. v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来更新。
  2. 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段。
  3. vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。
  4. 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。

13.什么是递归组件?举个例子说明下?

<template>
  <li>
    <div> {{ model.name }}</div>
    <ul v-show="isOpen" v-if="isFolder">
      <!-- 组件通过组件名称,递归渲染了它自己 -->
      <TreeItem
        class="item"
        v-for="model in model.children"
        :model="model">
      </TreeItem>
    </ul>
  </li>
<script>
export default {
  name: 'TreeItem',
}
</script>
  1. 如果组件通过组件名称调用它自己,或者在组件内部引入它自己然后调用,这种情况就是递归组件。
  2. 实际开发中、树形结构的组件往往会使用递归组件。类似Menu这类组件,组件的子节点结构和父节点往往是相同的。
  3. 组件内部要有递归结束条件。

    组件name的作用:

        vue-tools插件调试时,需要组件的name,没有会警告。

        做递归组件,通过自己的名字来使用自己。

        使用keep-alive时,可搭配组件name进行缓存过滤。

14.异步组件是什么?使用场景有哪些?

import { defineAsyncComponent } from 'vue';
// 函数返回promise
const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...可以从服务器加载组件
    setTimeout(() => {
        resolve(/* loaded component */);
    }, 300);
  });
});
// ES模块动态导入
const AsyncComp = defineAsyncComponent(() => import('./components/MyComponent.vue'));
// 传入对象
const AsyncComponent = defineAsyncComponent({
    loader: returnPromise, // 加载函数
    loadingComponent: { // 加载异步组件时使用的loading组件
        render: () => h('div', `loading...`),
    },
    delay: 200, // 展示加载组件前的延迟时间,默认为 200ms
    errorComponent: { // 加载失败展示的组件
        render: () => h('div', `errorComponent`),
    },
    timeout: 1000, // 时间限制,默认值是 Infinity。如果超时会显示errorComponent,如果后来又加载正常了,依然显示正常组件。
});
  1. 在大型应用中,我们需要把应用分割成一个个的小模块,并且在需要的时候加载它们。
  2. 可以使用路由懒加载组件;还可以在页面中继续使用异步组件,从而实现更细粒度的分割。
  3. 使用异步组件可以通过defineAsyncComponent,结合import动态导入或函数返回promise。另外Vue3中还可以结合Suspense组件使用异步组件。
  4. 异步组件底层是一个高阶函数,内部返回了一个新组件。符合渲染条件时,新组件的render函数上会渲染传入的异步组件,否则渲染loadingComponent或errorComponent。

15.vue3对比vue2的变化:

  1. vue3支持vue2的大多数特性,实现对vue2的兼容
  2. 更好的支持TypeScript
  3. vue3对比vue2具有明显的性能提升:打包大小减少41%;内存占用减少54%;初次渲染加快55%,更新加快133%;

    虚拟dom性能优化:

    • vue2做dom-diff时,对虚拟dom进行全量对比,每个节点不分静态和动态,都会一层一层比较。vue3把虚拟节点创建成block节点,增添了patchFlag 来标识哪些节点是动态的,并把动态节点搜集在dynamicChildren数组中,dom-diff时只比较数组中的动态节点,省掉了非动态节点的比较。对于不稳定结构、如:循环、v-if,把它们也创建成block节点,实现blocktree,做到靶向更新(静态虚拟节点提升、静态属性提升、事件缓存、预解析字符串等)。
    • hoistStatic静态提升,vue2里每次触发更新时,不管元素是否参与更新,都会重新创建。vue3为了避免不必要的重新创建元素,会把不参与更新的元素和属性保存起来,提升到渲染函数render之外,只创建一次,每次复用。
    • cacheHandlers事件缓存,vue2每次渲染时,绑定事件都会重新生成新的function,vue3可在第一次渲染后缓存绑定的事件,重新渲染时会先读取缓存,如果缓存里没有,就把事件存到缓存里。
    • 预解析字符串,当连续静态节点超过十个时,会通过静态字符串一次性创建静态节点。
  4. 重写双向数据绑定:
    1. vue2基于Object.defineProperty(obj, prop, desc)实现双向数据绑定, vue3基于new Proxy(obj, desc) 。
    2. new Proxy具有以下优势:
      1. 监听的是整个对象,而不是对象的属性,省去了for in循环。Object.defineProperty只能监听对象属性,所以要用for in循环和递归,性能不太好。
      2. 丢掉了麻烦的备份数据,Object.defineProperty需要操作备份数据,否则会死循环。
      3. 能监听对象属性的增删操作,也能监听数组元素的变化和增删,Object.defineProperty不能。
      4. 编程体验是一致的,不需要使用特殊api,能监听数组元素的变化和增删,也支持map、set这些新的数据类型,Object.defineProperty不能
      5. Proxy的第二个参数可以有 13 种拦截方:不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的
  5. Composition Api:即setup函数式编程,也叫 vue hooks

    选项式API的坏处:

        1.代码碎片化,分为data、methods、computed、props等,数据跟方法逻辑是分离的。

        2.逻辑不复用,首先在data选项声明的变量才是响应式数据,其次在methods选项声明的函数,才可以操控组件里的响应式数据,这样导致复用外部逻辑不太方便。通常采用mixins来复用数据和逻辑,但这种方法有一些弊端。

     

    组合式api完美解决了以上问题,它具有以下优点:

        1.代码集中化,组合式API的组合就在于它把变量、函数都集中在了一起,更容易组织代码结构。

        2.数据和代码逻辑可复用性强,通过setup语法糖,可以直接引用公共代码块,逻辑复用很方便。

  6. Tree-sharking的支持:vue3源码引入了Tree-sharking特性,将vue3的全局api进行分块。对于没有用到的vue3功能,打包时就不会打包进去。
  7. Fragments多根节点:vue2组件只能有一个根节点。vue3支持多个根节点,同时也支持在render函数里写jsx语法。
  8. 其他特性:
    • vue2: v-for优先级高于v-if,vue3: v-if优先级高于v-for。
    • vue3还增加了teleport 、Suspense,以及给组件绑定多个v-model。
    • zhuanlan.zhihu.com/p/571426648

16.Vue3模板编译

  1. vue的模板编译是vue自己实现的,本质是将html语法转化成js语法。就是把vue模板编译成一个render函数。之所以需要这个编译过程:一是为了开发者可以更方面高效的编写视图模板;二是手写render函数不仅效率底下,而且失去了编译期的优化能力。
  2. 解析html模板生成为ast抽象语法树。
    1. 先截取html代码字符串,一个个字符进行判断,“<”开头就是元素、“{{”开头是模板,其它是文本。截取元素时截到“>”,截取模板时截到“}}”,截取文本时截到“<”或“{{”,截取一部分就解析一部分,解析完就删掉,代码字符串为空了,就解析完毕了。
    2. 使用大量的正则表达式,解析截取到的字符串,标识类型(标签、指令、属性、文本、模板等)、保存内容和位置信息等。如果是元素,还要分隔空格和“=”,获取元素名、属性名、属性值等并保存,然后递归解析儿子。
  3. 遍历转化ast,给节点添加patchFlag标记、添加预处理函数(如:openBlock、createTextVNode、createElementVNode、toDisplayString)等
  4. 遍历转化后的ast语法树,用字符串拼接生成js代码(render函数)。

Vue中编译器何时执行?

        created之后,beforeMount之前,如果是运行时编译,即不存在 render function 但是存在 template的情况,需要先进行编译步骤。

 

React有没有编译器?

   react 使用babelJSX解析成虚拟dom

17.ref和reactive对比

  1. ref和reactive都是用来定义响应式数据的,一般ref定义基本类型、reactive定义复杂类型。
    • ref函数:
      • 语法: const xxx = ref(initValue);
      • 接收的数据类型: 基本类型,引用类型
      • 作用: 把参数加工成一个响应式对象,全称为reference对象(下面一律简称为ref对象)
      • 核心原理: 内部封装一个RefImpl类,并设置get value / set value,拦截用户对value属性的访问,从而实现响应式。如果接收引用类型参数,ref会调用reactive实现响应式数据。
    • reactive函数:
      • 语法: const xxx = reactive(源对象);
      • 接收的数据类型: 引用类型
      • 作用: 把参数加工成一个代理对象,全称为proxy对象。
      • 核心原理: 基于Es6的Proxy实现,通过代理操作源对象。
  2. ref与reactive的其他区别:
    1. reactive只能定义复杂的数据类型,定义基本数据类型时会抛出警告。
    2. reactive定义的数据可直接访问,ref定义的数据访问时要多一个 .value 。
    3. watch监听时,deep参数只对ref起作用,对reactive都是深度监听。深度监听时newVal等于oldVal。
    4. reactive数据重新赋值会破坏自动更新,ref数据的value重新赋值不会破坏数据的自动更新。

18.用Vue3.0 实现一个全局loading

  1. 因为是全局loading,应该可以通过js调用,所以做成插件。
  2. 创建一个loading组件
<style scoped>
/* ...*/
</style>
<template>
    <div class="loading-container" v-show="isShow">
        <div class="loading"></div> 
        <span class="loading-text">loading...</span>   
    </div>
</template>
<script lang="ts">
    export default {
        name: 'my-loading'
    }
</script>
<script setup lang="ts">
import { ref } from 'vue';
const isShow = ref(false);
const show = () => {
    isShow.value = true;
}
const hide = () => {
    isShow.value = false;
}
defineExpose({
    show,
    hide,
});
</script>

     3.将这个loading组件组成插件 

import { App, VNode, createVNode, render } from 'vue';
import Loading from './index.vue';
export default {
    install(app: App) {
        const vnode: VNode = createVNode(Loading);
        render(vnode, document.body);
        app.config.globalProperties.$loading = vnode.component?.exposed;
    }
}

19.插件编写与使用

编写插件

  1. 插件是一个普通函数或者一个包含install函数的对象。如果是一个对象,就会自动执行它的install函数,如果是一个function,则函数本身会被自动调用。这两种情况都会收到两个参数:由createApp生成的app对象和用户传入的参数。
  2. 由于插件里可以访问app对象,所以可以使用app对象的所有功能,如:component、config、mixin、directive、provide等,通过编写app对象的这些功能,可以给所有的组件提供一些额外的公共能力。

使用插件

  1. 在使用createApp()初始化Vue应用后,可以通过调用app.use()方法将插件安装到应用中。use方法会自动阻止多次安装同一插件,它有两个参数,第一个是要安装的插件,第二个参数是可选的、是用户自主传入的参数。
  2. 安装完插件,在任何组件中都可以通过getCurrentInstance方法获取组件实例,然后就能使用插件里了。

20.Vue组件的data为什么必须是函数

  1. 组件本质上是个对象(也有函数组件 - 纯渲染组件,vue3不推荐使用了,因为对象组件的性能已经和纯渲染组件不相上下了),每引用一次组件,都会基于同一个组件对象创建出组件实例。所以组件实例间共享了组件上的所有属性,包括data属性。如果把组件的data定义成普通对象,每一个组件实例的data就会指向同一份data数据,改变其中一个,所有组件实例的data数据都会改变。
  2. 把组件中的data写成一个函数,data以函数返回值的形式定义,这样每引用一次组件,就会返回一份新的data,就给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据,互不影响。

21.怎么扩展Vue组件?

  1. 常见的组件扩展方法有:mixins,slots,extends,composition api,高阶函数,render props

  2. mixins(混入),用于vue组件间复用数据和逻辑

    1. mixins本质是一个js对象,它可以包含组件中任意功能选项,如data、components、methods、created、computed等。
    2. 当组件使用mixins对象时,所有mixins对象的选项都将被混入该组件本身的选项中。当组件存在与mixin对象相同的选项时,组件的选项会覆盖mixin的选项,但是如果相同选项是生命周期函数,则会合并成一个数组,先执行mixin的钩子,再执行组件的钩子。
    3. 混入有点类似于react的高阶函数,在组件间共享数据和逻辑。有全局混入和局部混入两种方式,使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件),全局混入常用于插件的编写。
      1. 局部混入:通过组件的 mixins 属性,mixins: [myMixin]
      2. 全局混入:Vue.mixin(myMixin);
    4. mixin的不足:
      1. 命名冲突时,本地选项将覆盖混合器选项(生命周期勾子函数里的会叠加)。多个mixin的property都被合并到同一个组件中,容易造成property名冲突。

      2. 数据相互依赖,强耦合难以维护,数据来源不明。混合器和消耗它的组件之间没有层次关系,组件可以使用混入器中定义的数据,混入器也可以使用组件中定义的数据。重构组件时,如果改变了mixin需要的变量名称,运行就会报错,如果有一大堆mixin的组件,我们可以重构本地数据吗?如果多个组件使用了同一个mixin,我们能重构这个mixin么?

  3. 插槽用于向vue组件中插入内容,常用于扩展组件的内容。

    1. 使用方式:当组件包裹的template标签带有v-slot或#xxx时,就是一个插槽,插槽会被插入到组件模板中slot标签所在的位置。

    2. 作用域插槽:作用域插槽就是带数据的插槽。如果要使用被插入组件的数据,可以使用作用域插槽。组件模板中的slot标签通过v-bind绑定数据,组件的插槽通过v-slot接收。

    3. 具名插槽:如果要精确插入到固定位置,可以使用具名插槽。给插槽的v-slot添加名字,同时给solt标签添加name属性,对应到v-slot的名字,就能将插槽插入到相应的位置。

    4. 插槽的原理:插槽本质上是个函数、返回虚拟dom。插槽保存在父组件虚拟dom的children属性上,此时children值是个对象,key为插槽名字,value为插槽函数。父组件实例化时,会把虚拟dom的children赋值到组件实例的slots属性上,并传入到setup函数的context参数里。最后在父组件的render函数中按照插槽的名字调用对应的插槽。

  4. extends和mixins的类似,只是有以下区别:

    1. extends会比mixins先执行,执行顺序:extends > mixins > 组件

    2. 一个组件只能使用一个extends对象(extends: extend)

    3. Vue.extend(options),是扩展Vue这个类;Vue.mixin(options)是全局混入

      // 扩展Vue这个类
      const ExtendVue = Vue.extend({
        data: function () { // extend 的data必须是函数返回形式
          return {
            firstName: 'Walter',
            lastName: 'White',
            alias: 'Heisenberg'
          }
        }
      });
      // 创建 ExtendVue 实例,并挂载到元素上
      new ExtendVue().$mount('#app');
    4. 参考链接
      1. vue--extends使用及理解_闲人不梦卿的博客-优快云博客_vue extendsVue中 Vue.extend() 详解及使用_前端 贾公子的博客-优快云博客_vue.extend
      2. vue--extends使用及理解_闲人不梦卿的博客-优快云博客_vue extends
  5. vue3中引入的composition api,可以很好解决mixin的不足,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用。

  6. vue3支持了jsx的写法,所以也可以使用高阶函数或render props扩展组件。

22.子组件可以直接修改父组件的数据么,说明原因

  1. 组件化开发有个单向数据流的原则,不在子组件中修改父组件数据是个常识问题。
  2. 属性在父子之间传递,要遵循单向数据流原则:父级 props 的更新会向下流动到子组件中,但是反过来则不行。这样防止了子组件意外修改父级组件的数据,从而避免程序的数据流向难以理解。
  3. 每次父级组件发生变更时,子组件中所有的 props 都将会刷新为最新的值。这意味着你不应该在子组件内部改变 props 。如果你这样做了,Vue 会在浏览器控制台中发出警告。
    const props = defineProps(['foo']);
    props.foo = 'bar'; // ❌ 下面行为会被警告, props是只读的!
  4. 当子组件希望把属性做为一个初始值,会涉及到属性修改。这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

    const props = defineProps(['initialCounter']);
    const counter = ref(props.initialCounter);
  5. 实践中如果确实需要改变父组件的属性,应该emit一个事件让父组件去做这个变更。虽然我们可以直接修改复杂数据类型内嵌的对象或属性,但不要这么做,要按标准做。

23.Vue权限管理怎么做?控制到按钮级别的权限怎么做?

  1. 权限管理一般分为页面权限按钮权限的管理

  2. 页面权限实现的时候分前端和后端两种方案:

    1. 前端方案:把所有路由信息放在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表

      比如配置一个staticRoutes数组和一个asyncRoutes数组;staticRoutes包含所有用户都能访问的路由,如:登录页面路由;asyncRoutes数组保存剩余所有路由信息。然后根据后端返回的数据,获取asyncRoutes数组里所有的有权限路由,最后通过router.addRoute(accessRoute)动态添加路由即可。
    2. 后端方案:把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoute动态添加路由信息

    3. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!

  3. 按钮权限的控制通过 v-if 判断当前角色是否存有该按钮权限

24.从0搭建一个vue项目,说说有哪些步骤、哪些重要插件、目录结构怎么组织

  1. 从0创建一个项目大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件

  2. 目前vue3项目我会用vite或者create-vue创建项目

  3. 接下来引入必要插件:路由插件vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus和antd-vue、http工具我会选axios

  4. 代码规范:eslint、stylelint

  5. 提交规范,可以使用husky,lint-staged,commitlint前端规范之Git工作流规范(Husky + Commitlint + Lint-staged) - Yellow_ice - 博客园

  6. 目录结构我有如下习惯: 

    .vscode:用来放项目中的 vscode 配置

    plugins:用来放 vite 插件的 plugin 配置

    public:用来放一些诸如 页头icon、图片 之类的公共文件,会被打包到dist根目录下

    src:用来放项目代码文件

    src/api:用来放http的一些接口配置

    src/assets:用来放一些 CSS 之类的静态资源

    src/components:用来放项目通用组件

    src/layout:用来放项目的布局

    src/router:用来放项目的路由配置

    src/store:用来放状态管理Pinia的配置

    src/utils:用来放项目中的工具方法类

    src/views:用来放项目的页面文件

25.实际工作中,你总结的vue最佳实践有哪些?

  1. 编码风格方面:
    • 命名组件使用“多词”风格,避免和HTML元素冲突
    • 定义属性加上类型而不是只有一个属性名
    • 组件名、属性名用“驼峰”,在模板中使用时用'-'隔开
  2. 性能方面:
    • keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件的状态
    • v-if 和 v-show 区分使用场景
    • computed 和 watch 区分使用场景
    • v-for 遍历必须为 item 添加 key,并保证同一组key的唯一与稳定,且避免同时使用 v-if
    • 利用v-once渲染那些不需要更新的内容、v-memo做性能优化
    • 事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。自己绑定的原生事件、定时器需要手动移除。
    • 对于深层嵌套对象的大数组可以使用shallowRef或shallowReactive降低开销
    • 路由懒加载:借助import()实现
    • 使用defineAsyncComponent(也可结合Suspense)实现异步组件
  3. 安全:小心使用v-html,:url,:style等,避免html、url、样式等注入

26.SPA、SSR的区别是什么

  1. SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。还有 多页面应用(Mulpile Page Application),简称 MPA。
  2. SPA应用只会首次请求html文件,后续只需要Ajax请求即可,而且路由切换超快,因此用户体验更好,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案。HTML内容在服务器上一次性生成出来,首屏加载更快快,搜索引擎也可以方便的抓取页面信息。但同时SSR方案也会有性能、开发受限等问题。
  3. 在选择上,如果我们的应用存在首屏加载优化需求,SEO需求时,就可以考虑SSR。

        Next.js: 一个轻量级的 React 服务端渲染应用框架。

        Nuxt.js: 用于 Vue.js 开发服务端渲染的应用。

27.谈谈你对SPA单页面的理解

SPA( single-page application )单页面应用,仅在 Web 页面初始化时加载相应的 HTMLJavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,避免页面的重新加载。

优点:

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

缺点:

  • 首屏加载耗时多
  • 不能使用浏览器的前进后退功能:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,需要自己建立堆栈管理。
  • 不利于SEO:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势

28.使用vue渲染大量数据时应该怎么优化?说下你的思路!

  • 可以采取分页的方式获取,避免渲染大量数据
  • vue-virtual-scroller等虚拟列表方案,只渲染视口范围内的数据
    • 处理巨量列表的展示。原理是它只把展示给用户的那部分渲染出来,比如滚到上面的dom 就回收掉。它会生成一个很长的 dom,然后根据这个 dom 的滚动距离,计算应该渲染展示的内容列表。
    • Vue 展示巨量数据 vue-virtual-scroller

29.说一说你对vue响应式的理解?

  1. 数据响应式是Vue的核心特性之一,数据驱动视图,就是数据变化可以被检测到并对这种变化做出响应的机制。
  2. vue的数据响应式,是通过“数据劫持 + 观察者模式实现”的,获取数据时收集依赖,数据变化时触发调度更新(通过render创建虚拟节点,创建完虚拟节点,让新旧虚拟节点做patch、dom-diff...最后更新视图)。可以使我们只需要操作数据,完全不用接触繁琐的dom操作,从而大大提升开发效率,降低开发难度。
  3. vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty做数据拦截,当数据被访问或发生变化时,作出响应;如果是数组则通过重写数组的7个原生方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api;对于es6中新产生的Map、Set这些数据结构不支持;数组通过下标修改还不能检查到等问题
  4. vue3重写了数据响应式,利用ES6的Proxy代理机制实现,它有很多好处:
    1. 监听的是整个对象,而不是对象的属性,省去了for in循环。Object.defineProperty只能监听对象属性,所以要用for in循环和递归,性能不太好。
    2. 丢掉了麻烦的备份数据,Object.defineProperty需要操作备份数据,否则会死循环。
    3. 能监听对象属性的增删操作,也能监听数组元素的变化和增删,Object.defineProperty不能。
    4. 编程体验是一致的,不需要使用特殊api,能监听数组元素的变化和增删,也支持map、set这些新的数据类型,Object.defineProperty不能
    5. Proxy的第二个参数可以有 13 种拦截方:不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的

30.Vue事件绑定的原理

       1.vue中事件绑定有两种,一种是原生事件绑定,另一种是组件的事件绑定。

       2.原生事件的绑定是通过addEventLister绑给真实dom

       3.vue2组件绑定事件是通过$on方法来实现的,$on是采用经典的发布订阅模式实现,首先定义一个事件中心,通过$on订阅事件,将事件存储在事件中心里面,然后通过$emit触发事件中心里存储的订阅事件。

// 发布订阅模式
class EventBus {
  constructor() {
    // 事件中心,存储事件
  	this.events = {};
  }
  // 订阅者,往事件中心注册事件
  $on (eventType, handler) {
    this.events[eventType] = this.events[eventType] || [];
    this.events[eventType].push(handler);
  }
  // 发布者,发布调用事件
  $emit (eventType, ...args) {
    if (this.events[eventType]) {
      this.events[eventType].forEach(handler => {
        handler(...args);
      });
    }
  }
}
const bus = new EventBus();
bus.$on('click', (...args) => {
  console.log('$on --> click1 args:', args);
});
bus.$on('click', (...args) => {
  console.log('$on --> click2 args:', args);
});
bus.$emit('click', 123);

        4.vue3删除了$on;组件绑定事件的原理:子组件在emit函数里,通过eventName寻找虚拟dom属性中、从父组件传入的函数,然后执行对应的函数。

31.Vue数据响应式与双向数据绑定的区别

  • 数据响应式是Vue的核心特性之一,数据驱动视图,就是数据变化可以被检测到并对这种变化做出响应的机制。
  • 双向数据绑定是通过指令或修饰符实现的一种特性,是一种绑定数据和事件的语法糖。
  • 简单的说:数据响应式是指通过数据驱动DOM视图的变化,是单向的过程;而双向数据绑定的数据和DOM是一个双向关系。

32.说一说vue中的观察者模式?

        观察者模式: 当一个变量修改时,就会通知所有关注这个变量的所有对象,让他们重新获得这个变量的新值。

var data = {
  money: 1000,
  setMoney(money) {
    this.money = money;
    // 只要修改money,就调用notifyAll
    this.notifyAll();
  },
  observers: [], // 观察者
  notifyAll() {
    this.observers.forEach((obj) => {
  	    // 凡是observers数组中的对象,必须携带一个getMoney函数
      	obj.getMoney();
    });
  }
}
var obj1 = {
  money: 0,
  getMoney() {
    this.money = data.money;
  },
}
var obj2 = {
  money: 0,
  getMoney() {
    this.money = data.money;
  },
}
data.observers.push(obj1);
data.observers.push(obj2);
data.setMoney(900);
console.log(obj1.money, obj2.money);
data.setMoney(800);
console.log(obj1.money, obj2.money);	

33.说说你对虚拟 DOM 的理解?

1.虚拟dom就是用 JS 对象,去描述一个真实的dom结构,它本质是个js对象。

2.vdom的好处:

    1)有效减少不必要的 dom 操作,从而提高程序性能 

        操作 dom 是比较昂贵的,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象的 VNode 进行中间处理,通过diff算法,可以有效减少不必要的dom操作,从而提高性能。

    2)实现跨平台

        1.同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、还可以实现 SSR 、还可以渲染到 WebGL 中等等

        2.Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。

    3)内容经过了XSS处理,可以防范XSS攻击。

3.虚拟dom的不足:

    1.首次渲染大量DOM时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

    2.无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

4.vdom如何生成真实dom和更新的:

    1.vue组件中的模板 - template, 会被编译器 - compiler编译为渲染函数。组件挂载(mount)过程中会调用render函数,返回对应的虚拟dom,然后开始patch,在patch过程中转化为真实dom。

    2.挂载完成后,组件进入更新流程。如果某些响应式数据发生变化,会触发组件更新,将重新调用render函数生成新的vdom,然后让新的vdom和旧的vdom做patch操作、dom-diff,能复用的就复用,不能复用的才新建或修改,从而最小量的操作dom,达到高效更新视图的目的。

34.你了解vue中的diff算法吗?

        1.Vue中的diff算法也称为patching算法,就是将新旧虚拟DOM进行patch对比,最后转化成真实dom的过程。

        2.diff算法的目的是找到能复用的节点并复用,不能复用的才新建或修改,从而最小量的操作dom,达到高效更新视图的目的。

        3.组件内响应式数据变化时,组件内的观察者对象会触发其调度函数,然后会执行render函数获得最新的虚拟DOM,接着执行patch函数,并传入新旧两个虚拟DOM,通过比对两者找到变化的地方,然后更新到真实dom。

        4.patch过程是一个递归过程,遵循深度优先、同层比较的策略,同层节点通过唯一的key进行区分,key和type都相同才复用;新老节点的子节点都是数组时,diff算法详情如下:

         Vue2的diff算法是使用双指针移动来进行新老节点对比的。依次判断新老节点的头头、尾尾、头尾、尾头,是否能复用,如果能就复用,并进行相应的指针移动(头头、尾尾、头尾、尾头)。如果都不能复用,则进行暴力对比,如果找到了,就把老的子节点移动到相应位置,然后对老的子节点递归进行patch处理,如果找不到就新建。最后老的子节点有多的就删掉,新的子节点有多的就在相应的位置新建。

        Vue3的diff算法是先使用双指针移动来进行新老节点对比的。依次判断新老子节点的头头、尾尾能否复用,如果能就复用,并进行相应的指针移动(头头、尾尾)操作。如果都不能,则停止遍历,并声明一个map对象保存剩余的新节点,然后遍历剩下的老节点。如果当前节点的key在map对象里能找到且对应的元素类型相同,则复用这个节点。如果这个节点的key在map对象里找不到,则删除这个节点。遍历结束后,如果map对象里有未访问的元素,则就是新增的元素,就新增它们。

        5.vue3模板编译时做了优化,把虚拟节点创建成block节点,增添了patchFlag 来标识哪些节点是动态的,并把动态节点搜集在dynamicChildren数组中,dom-diff时只比较数组中的动态节点,省掉了非动态节点的比较。对于不稳定结构、如:循环、v-if,也把它们创建成block节点,实现blocktree,做到靶向更新(静态虚拟节点提升、静态属性提升、事件缓存、预解析字符串等)。vue2是对虚拟dom进行全量对比,每个节点不分静态和动态,都会一层一层比较。

35.vue中key的作用?

        1.key主要是在diff算法时使用的,判断节点能否复用的条件就是key和type是不是都相同,都相同才能复用,有一个不同就不能复用。

        2.diff算法时,没有key就是undefined,如果元素还是同类型元素,会造成一种结果:元素会按照下标顺序依次复用老节点。这样虽然diff算法更快,但只适用于简单无状态的组件,因为对于有状态的组件,复用元素时可能会出现数据错位(如顺序变了)的问题。对于大部分场景,组件都有自己的数据,虽然带上key会增加一些开销(key不是为了提高diff算法的速度),但可以保证节点能被正确的复用,而不出现数据错位的问题。

       3.key还有其它作用,如keep-alive组件的LRU算法需要。

36.Vue实例挂载的过程中发生了什么?

  1. 挂载过程指的是app.mount()的过程,这是个初始化过程,整体上做了两件事:初始化、建立更新机制
  2. 初始化会创建组件实例给组件实例赋值(data、props、attrs、slots、setupState、设置代理
  3. 如果是运行时编译,即通过template编写,不存在 render function的情况,还需要把template模板编译成render函数。
  4. 建立更新机制这一步,会在组件内部创建一个Watcher(观察者)对象,然后把Watcher对象的run函数赋给组件实例的update函数,接着调用update函数,首次执行一次组件渲染。渲染时先通过组件的render函数获取组件的虚拟dom,接着执行patch函数,将虚拟dom转换为真实dom;执行render函数时,Watcher会收集它依赖的响应式数据,以后这些依赖的响应式数据变化时会通知Watcher,然后由Watcher执行更新调度,进而再次执行update函数。当然,执行update函数时,还会执行相应的组件生命周期钩子函数。

37.vue-loader是什么?它有什么作用?

  1. vue-loader是用于处理单文件组件(SFC,Single-File Component)的loader
  2. 因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template>、<script>和<style>,代码会更加清晰。结合其他loader我们还可以用less编写<style>,用TS编写<script>。我们的<style>还可以单独作用于当前组件。
  3. webpack打包时,会以loader的方式调用vue-loader
  4. vue-loader被执行时,它会对SFC中的每个语言块调用单独的loader链处理。最后将这些单独的块装配成最终的组件模块。

 38.你是怎么处理vue项目中的错误的?

  1. 应用中的错误类型分为"接口异常"和“代码逻辑异常
  2. 我们需要根据不同错误类型做相应处理:
    1. 接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。
    2. 代码逻辑异常是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler收集错误。也可用边界组件捕获子代的错误,展示降级的ui(errorCaptured)
  3. vue中全局捕获异常:
    app.config.errorHandler = (err, instance, info) => {
      // report error to tracking services
    }
  4. axios拦截器中处理捕获异常:
    import axios from 'axios';
    
    const myRequest = axios.create({
    	baseURL: 'https://www.abc.com',
    	timeout: 5000,
    	headers: {
    		"Content-Type": "application/json;charset=utf-8"
    	}
    });
     
    // 请求拦截器
    myRequest.interceptors.request.use(
        function (config) {
            // 在发送请求之前进行操作
            return config;
        },
        function (error) {
            // 对请求错误进行操作
            return Promise.reject(error);
        }
    );
     
    // 响应拦截器
    myRequest.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (error) => {
        // 存在response说明服务器有响应
        if (error.response) {
          const response = error.response;
          if (response.status >= 400) {
            // ...
          }
        } else {
          // ...
        }
        return Promise.reject(error);
      },
    );

39.怎么定义动态路由?怎么获取传过来的动态参数?

  1. 将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。例如,有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。可以在Vue Router的path中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中id就是路径参数
  2. 路径参数 用冒号表示。它的值在每个组件中通过 this.$route.params 来获取。
  3. 参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query$route.hash 等。
    1. 相应路由参数变化
    2. 如何处理404 Not Found路由

40.怎么实现路由懒加载

  1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载,能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。可以对所有的路由都使用动态导入

  2. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:

    { path: '/users/:id', component: () => import('./views/UserDetails') }

  3. 结合注释() => import(/* webpackChunkName: "group-user" */'./UserDetails.vue')可以做webpack代码分块

    vite中结合rollupOptions定义分块

  4. 路由中不能使用异步组件

41.router-link和router-view是如何起作用的?

  • vue-router中两个重要组件router-linkrouter-view,分别起到路由导航作用和组件内容渲染作用
  • 使用中router-link默认生成一个a标签,设置to属性定义跳转path。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。
  • router-link是一个组件,它的模板是一个a标签,a标签的内容通过插槽渲染。它不是通过超链接跳转,而是绑定点击事件,执行push操作,click时还阻止了a标签默认事件。router-view也是一个组件,它渲染组件的原理类似于高阶函数,通过路由信息找到对应组件后,在router-view组件的render函数里渲染目标组件。

42.vue-router中如何保护路由?

  • vue-router中保护路由的方法叫做路由守卫,通过跳转或取消的方式来守卫导航。
  • 路由守卫有三个级别:全局路由独享组件级,影响范围由大到小。可以通过router.beforeEach注册一个全局路由守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候,可以加入单路由独享守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;还可以为组件添加守卫配置,例如beforeRouteEnter,进入该组件前调用,控制的范围更精确了。
  • 路由守卫原理:通过promise链式执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入reject流程取消原本导航
// 全局路由守卫
const router = createRouter({ ... });
router.beforeEach((to, from, next) => {
  /*
    next(); // 进入to所指路由
    next(false); // 取消导航
    next({ // 进入指定路由
        name,
        params
        // query: params,
    });
    next('/abc?a=1&b=2'); // 进入指定路由
  */
  return false; // 返回 false 也可取消导航
});

// 独享路由守卫
const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from, next) => {
      return false; // 返回 false 也可取消导航
    },
  },
];

// 组件内路由守卫
const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在导航进入该组件前调用
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
  },
  beforeRouteLeave(to, from, next) {
    // 在导航离该组件前调用
  },
}

43.Vue Router完整的导航解析流程

        切换组件流程:beforeRouteLeave -> beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach

        复用组件流程:beforeEach -> beforeRouteUpdate -> beforeResolve -> afterEach

44.如果让你从零开始写一个vue路由,说说你的思路

一个SPA应用的路由需要解决的问题是 页面跳转内容改变、同时页面不刷新。因为路由需要以插件形式存在,所以:

  1. 首先我会定义一个createRouter函数,返回路由器实例routerrouter内部做几件事
    1. 保存用户传入的配置项
    2. 监听hashchange或者popstate事件,根据path匹配对应路由,找到对应组件
  2. router定义成一个Vue插件,即实现install方法,内部做两件事
    1. 注册两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示
    2. Vue实例上定义两个全局变量:$route$router,使组件内可以访问当前路由和路由器实例

45.Vue路由的底层原理是什么?hash路由和history路由有什么区别?

1.Vue路由是根据url的不同来渲染不同的组件,在无刷新的情况下修改页面内容。通常有两种模式的路由,hash路由和history路由。

2.hash路由利用location.hash做路由控制,通过a标签或location.hash可以改变路由的hash,用onhashchange事件来监听hash的变化。

3.history路由利用window.history做路由控制,通过history.pushState(stateObj, title, url)和history.replaceState(stateObj, title, url)来改变history的状态,用onpopstate事件来监听history的变化。onpopstate可以监听到浏览器的前进、后退、history.forward、history.back、history.go,因为这些都会修改浏览器历史堆栈的指针;但监听不到history.pushState 和 history.replaceState,需要自己重写history.pushState、history.replaceState。

        hash的url中带了一个 #, 而history没有。

        hash 就是前端锚点,不会向服务器发送请求,不能做seo优化。

        hash虽然出现在URL中,但不会被包括在HTTP请求中,对后端完全没有影响。

        hash不需要依赖后端,history需要后端做一些路由配置。

// 重写 history.pushState 和 history.replaceState
;(function (history) {
    const oldPushState = history.pushState;
    const oldReplaceState = history.replaceState;
    history.pushState = rewriteFn(oldPushState, 'pushState');
    history.replaceState = rewriteFn(oldReplaceState, 'replaceState');
    function rewriteFn(fn, eventName) {
        return function (state, title, pathname) {
            const result = fn.call(history, state, title, pathname); // 执行旧的
            if (typeof window.onpopstate === 'function') {
                const customEvent = new CustomEvent(eventName, {detail: {pathname, state}});
                // 调用window.onpopstate
                window.onpopstate(customEvent);
            }
            return result;
        }
    }
}(history));

window.onpopstate = function setContent(eve) {
    // ...
}
history路由后端配置:

Nginx 配置
location / {
  try_files $uri $uri/ /index.html;
}

node 配置
// 可以使用 connect-history-api-fallback 这个中间件
// npm install connect-history-api-fallback --save

// app.js
var history = require("connect-history-api-fallback");
var connect = require("connect");
var app = connect().use(history()).listen(3000);

// 或者使用 express
var express = require("express");
var app = express();
app.use(history());

46.$route和$router的区别?

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

47.说一说你对vuex理解?

  1. vuex是vue专用的状态管理库,解决了多个组件间共享状态的问题。
  2. 虽然利用各种组件间的通信方式,也能实现状态共享,但是当组件层级较深或组件间毫无关联时,组件间通信就会变得复杂、难以维护。vuex把整个应用的状态抽离出来,使用单向数据流动的模式,对全局的状态进行集中统一管理,这样任何组件都能通过同样的api获取和修改状态,使得数据的变化、流向都变很清晰、容易控制。
  3. vuex并非必须的,它帮我们管理共享状态,但却带来更多的概念和操作流程。一个小项目或者没有大量的全局状态需要维护的项目,可以不使用vuex。
  4. 我在使用vuex过程中有如下理解:
    1. 一个应用只有一个store,将全局状态放入store的state对象中,在组件中通过store实例的state属性直接访问状态、通过getters属性访问计算过的状态。也可以通过vuex的 mapState, mapGetters, mapMutations, mapActions,把store里的选项映射到组件选项中。
    2. 在mutations里添加修改状态的方法,并且遵循单向数据流动原则,只在mutations里修改状态;如果需要异步修改状态,在actions里添加方法,并在异步操作完成后,把修改操作提交给mutations,而不在actions里直接修改,遵循单向数据流动原则。
    3. 通过modules选项拆分出去的各个子模块:在访问状态时添加子模块的名称;给子模块有设置namespace,在提交mutations和actions时带上命名空间前缀;模块内部的 actionmutation 和 getter 是注册在全局命名空间的,不加namespace,各个模块间actionmutation 和 getter里的方法名会混淆。
  5. vuex的原理(从零写一个vuex):
    1. vuex 是一个插件,通过自己的 install 方法,使用 mixin 全局混入,在 beforeCreate 阶段给所有组件注入 Store 实例,让每个组件都能通过 this.$store 访问到 Store 实例。(当然供vue3使用的vuex,即4.x版本的vuex,是用 provide 向全局暴露出 Store 实例。用户调用 useStore 函数时,vuex 会在通过 inject 方法获取 Store 实例并返回。)

    2. vuex 的 Store 实例里有一个隐藏的 Vue 实例。创建 Store 实例时,将 Store 实例的 state 存入隐藏的 Vue 实例的 data 中,实现 state 数据的响应式;将 getters 存入隐藏的 Vue 实例的计算属性中,实现 getters 数据的实时监听;内部实现一个dispatch方法和一个commit方法,挂载在 Store 实例上。根据用户dispatch/commit的类型,在actions/mutations里找到对应的方法并调用,调用actions里的方法时传入 Store 实例和payload数据,调用mutations里的方法时传入state数据和payload数据,供用户做进一步操作。

    3. 用户在任意组件中修改 Store 实例里的 state 数据,本质上就是修改隐藏 Vue 实例上的 data 数据,而这些数据是响应式的,因此更新后其它组件的视图也会更新。

/*=== vuex modules 的使用 ===*/

/*== a文件start ==*/
import { createStore } from "vuex";

// 创建一个 module
const moduleA = {
    namespaced: true,
    state: () => {
        return {
            num: 0
        }
    },
    getters: {
        gettersHello: (state, getters, rootState, rootGetters) => {
            console.log("state", state);
            console.log("getters", getters);
            console.log("rootState", rootState);
            console.log("rootGetters", rootGetters);
            return state.num * 2;
        }
    },
    mutations: {
        increment: state => {
            state.num++;
        }
    }
}
// 创建一个 store 实例
const store = createStore({
    state() {
        return {
            seyHello: "Hello World"
        };
    },
    modules: {
        moduleA,
    }
});
export default store;
/*== a文件end ==*/

/*== b文件start ==*/
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';

export default {
    methods: mapMutations({
        increment: "moduleA/increment"
    }),
    computed: {
        ...mapState({
            num: state => state.moduleA.num,
        }),
        ...mapGetters(["moduleA/gettersHello"]), // 数组形式
        // ...mapGetters("moduleA", { // 对象形式 
        //   gettersHello: "gettersHello",
        // }),
    },
}
</script>

<template>
    <p>num:{{ num }}</p>
    <button @click="increment">increment</button>
</template>
/*== b文件end ==*/

48.怎么监听vuex数据的变化?

  • 可以通过watch、watchEffect、computed监听

  • 可以使用vuex提供的API:store.subscribe(cb)

49.vuex中actions和mutations有什么区别?

  1. actions里的方法通过dispatch调用,mutations里的方法通过commit调用
  2. 按照官方定义的规范:只能在mutations里修改状态,mutations里修改状态是同步的。如果需要异步修改状态,把异步逻辑写在actions里,等异步逻辑结束后,把修改状态的操做提交给mutations,让mutations完成状态修改,而不能在actions里直接修改,这样是遵循数据单向流动的原则。另外可以在actions里返回Promise实例,便于处理异步结果。

50.vuex有什么缺点?你在开发过程中有遇到什么问题吗?

1.设计有点复杂:引入了诸多概念,有state、gettersmutationsactions、modules、namespaced;访问state时要带上模块key,内嵌模块多的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错。

2.对ts的支持也不友好,在使用模块时没有代码提示

3.规范性问题,按照官方定义的规范:

        1.只能在mutations里修改状态,且mutations里修改状态只能同步;如果需要异步修改,只能在actions里操作,然后提交给mutations完成状态修改。

        2.但由于dispatchcommit两个API在用法和调用效果上几乎是一样的,容易引起混淆。实践中有些人会不管是同步还是异步,统一采用dispatch的方式;虽然官方在定义上要求在actions里做异步修改,但是我就要在actions里做同步修改,官方并没有拦截;还有我就要在mutations里写个setTimeout做更改,也是可以的,官方没有拦截;

        3.甚至我直接修改Store里的状态,也是可以更新页面的,官方定义的只能在mutations里修改状态,但在外面修改状态并没有拦截。

        4.总之官方虽然定义了规范,但没有去实现这些规范,所有的规范全靠开发者自觉遵守,但由于不同开发者对于官方文档的理解不尽相同,容易造成书写规范不统一的问题。

vuex有严格模式:

        1.严格模式下,无论何时发生了状态变更,如果不是由 mutation 函数引起的,将会抛出错误。这保证了单向数据流原则,保证所有的状态变更都能被调试工具跟踪到。

         2.开启严格模式,仅需在创建 store 的时候传入 strict: true

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

51.页面刷新,vuex重置问题

  1. vuex是在内存中保存状态,刷新之后就会丢失。如果要持久化保存,localStorage就很合适。数据更新时,在subscribe方法里把需要的数据存入localStorage,页面刷新时从localStorage中把值取出作为state的初始值即可。
  2. 也可以使用插件,如:vuex-persistvuex-persistedstate,内部的实现就是通过subscribe
// npm install --save vuex-persist
// import VuexPersistence from "vuex-persist";

const vuexLocal = new VuexPersistence({ // 将vuex数据存入localStorage中
  storage: window.localStorage,
});
export default new Vuex.Store({
  state: {},
  modules: {},
  plugins: [vuexLocal.plugin],
});

/*
store.subscribe((mutation, state) => {
  console.log(mutation.type); // 监听到mutation中被调用的方法
  console.log(mutation.payload);
});
store.subscribeAction((action, state) => {
    console.log(action.type); // 监听到Action中被调用的方法
    console.log(action.payload);
});
*/

52.用过pinia吗?有什么优点?

1. pinia是什么?

  • Vuex 是 Vue 之前的官方状态管理库。由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式,它仍可以正常工作,但不再更新。对于新的应用,建议使用 Pinia。Pinia 是 Vue 的存储库,由 Vue 核心团队维护,对 Vue 2 和 Vue 3 都可用,它允许跨组件/页面共享状态。
  • 事实上,Pinia 最初正是为了探索 Vuex 的下一个版本而开发的,整合了核心团队关于 Vuex 5 的许多想法。可以认为pinia是新版的Vuex,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,而且老版本vuex不再更新,所以以后改用pinia即可

  • 相比于 Vuex,Pinia 提供了更简洁的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。

Vuex: state、getters、mutations(同步)、actions(异步)、modules、namespaced

Pinia: state、getters、actions(同步异步都支持)

pinia 原理:

    1.createPinia 时会创建一个 pinia 对象,通过该对象的 install 方法,用 provide 向全局暴露出该 pinia 对象,该对象上有一个 state 数据、为 ref 响应式数据。

    2.当用户在任意组件调用 defineStore 返回的 useStore 函数时,pinia 会在通过 inject 方法获取 pinia 对象,然后从 state 数据里获取当前 id 对应的数据并返回。如果获取不到,就先添加再获取。往 pinia 对象的 ref 数据里添加数据时,会把用户传的 state 数据处理成 ref 数据、getters 处理成 computed 数据、然后合并上 actions 里的函数,一起返回给用户。

    3.用户在任意组件中修改 useStore 返回的数据,本质上就是修改 pinia 对象上 state 的数据,而这些数据是响应式的,因此更新后其它组件的视图也会更新。

2. 为什么要使用pinia?

  • Vue2Vue3都支持,这让使用Vue2Vue3的开发者都能很快上手。
  • pinia中只有stategettersactions,抛弃了Vuex中的mutationsVuexmutations和actions容易引起混淆,pinia直接抛弃它了,使框架更简洁。
  • piniaactions支持同步和异步,Vuex中的actions原则上只让使用异步,其实同步和异步都支持。
  • 良好的Typescript支持,Vue3都推荐使用TS来编写,这个时候使用pinia非常合适
  • 没有命名空间、没有模块嵌套结构,可以通过多个 store 做更好的代码分割,实现数据扁平化。
  • 体积非常小,只有1KB左右。
  • 完美结合 Composition Api 这种组合式的开发风格,可以把一个 hook 定义成 store。这样不但可以实现组件间复用数据和逻辑,还能实时同步、共享数据状态。

  • pinia支持插件来扩展自身功能
  • 支持服务端渲染

3. pinna使用

3.1 基本使用

/*== main.ts ==*/
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import './style.css';
import App from './App.vue';

createApp(App).use(createPinia()).mount('#app');


/*== countStore.ts ==*/
import { defineStore } from 'pinia';
import { useCount } from '../hooks/countHook';

/*
第一个参数是 store 的唯一 id,不能和其它 store 的重复。
也可以不要第一个参数,在第二个参数里加一个 id 属性。
*/
// export const useCountStore = defineStore('count', {
//     state: () => {
//         return {
//             num: 0,
//         }
//     },
//     getters: {
//         doubleNum: (state: { num: number; }) => state.num * 2,
//     },
//     actions: {
//         add(value: number) {
//             this.num += value;
//         },
//         minus(value: number) {
//             this.num -= value;
//         },
//     }
// });

export const useCountStore = defineStore('count', useCount);


/*== countHook.ts ==*/
import { ref, computed } from 'vue';

export const useCount = () => {
    const num = ref(0);
    const doubleNum = computed(() => num.value * 2);
    const add = (value: number) => {
        num.value += value;
    }
    const minus = (value: number) => {
        num.value -= value;
    }
    return {
        num,
        doubleNum,
        add,
        minus,
    }
}


/*== Test.vue ==*/
<script setup lang="ts">
import { useCountStore } from '../stores/countStore';
import { useCount } from '../hooks/countHook';

const countHook = useCount();
const countStore = useCountStore();
</script>

<template>
  <h1>Test</h1>
  <div>countHook: {{countHook.num}}, doubleNum: {{countHook.doubleNum}}</div>
  <div>countStore: {{countStore.num}}, doubleNum: {{countStore.doubleNum}}</div>
  <button @click="countStore.add(1)">countStore add</button>
  <button @click="countStore.minus(1)">countStore minus</button>
  <button @click="countHook.add(1)">countHook add</button>
  <button @click="countHook.minus(1)">countHook minus</button>
</template>

3.2 重置state

<button @click="reset">重置store</button>
// 重置store
const reset = () => {
  store.$reset();
};

当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新

3.3 批量更改state数据

<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
  store.$patch({
    name: "张三",
    age: 100,
    sex: "女",
  });
};

        采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数。

store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 });
  state.hasChanged = true;
});

3.4 直接替换整个state

store.$state = { counter: 666, name: '张三' }

上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少

53.v-show指令,v-if的区别?

相同点:都是条件渲染指令

不同的是:

        1.v-if只有条件为true的才会添加到dom树中, v-show不管条件为true还是false都会添加到dom树中, 只是设置了display为none这个样式。

        2.v-if 适用于不需要频繁切换条件的场景;v-show 则适用于需要频繁切换条件的场景

        3.v-show不支持 <template> 元素,也不支持 v-else。

54.scoped 的原理及样式穿透

原理:给dom添加唯一的属性,同时对应的在CSS选择器上添加这个属性,使得样式私有化。

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性

1.使用:deep()

<!-- Parent -->
<template>
<div class="wrap">
    <Child />
</div>
</template>
<style lang="scss" scoped>
.wrap :deep(.box) {
    background: red;
}
</style>


<!-- Child -->
<template>
    <div class="box"></div>
</template>

2.使用两个style标签

<!-- Parent -->
<template>
<div class="wrap">
    <Child />
</div>
</template>
<style lang="scss" scoped>
/* 其他样式 */
</style>
<style lang="scss">
.wrap .box{
  background: red;
}
</style>


<!-- Child -->
<template>
  <div class="box"></div>
</template>

55.vue2 $set的实现原理

1、$set会先判断设置的是对象还是数组

2、如果是对象,通过Object.defineProperty实现

3、如果是数组,通过vue重写的数组方法splice实现

vue2通过对 7 种数组(push,pop,unshift,shift,splice,sort,reverse)方法进行重写,实现对数据变化的监听。

56.vue常用的事件修饰符?

事件修饰符

  • .stop 阻止事件冒泡
  • .prevent 阻止标签默认行为
  • .capture 使用事件捕获模式
  • .self 只当在 event.target 是当前元素自身时触发处理函数
  • .once 事件将只会触发一次
  • .passive 告诉浏览器你不想阻止事件的默认行为

v-model 的修饰符

  • .lazy 通过这个修饰符,绑定事件为 change 事件,不加 .lazy 绑定的为input事件
  • .number 自动将用户的输入值转化为数值类型
  • .trim 自动过滤用户输入的首尾空格

键盘事件的修饰符

.enter .tab .delete (捕获“删除”和“退格”键)  .esc .space .up .down .left .right

系统修饰键

.ctrl  .alt  .shift  .meta

鼠标按钮修饰符

.left  .right  .middle

57.vue的核心及优缺点?

两个核心:数据驱动、指令系统。

优点:易学易用、性能出色,数据驱动、指令系统、mvvm、组件化。

缺点:不利于SEO的优化;首屏加载耗时相对长一些。

58.Vue中的provide/inject

1.provide可以在父级组件中提供数据给后代组件,而在任何后代组件中,都可以使用inject来接收provide提供的数据。

2.provide/inject绑定并不是可响应的,但如果传入了一个响应式对象,那么这个绑也是可响应的。

3.Vue 官方不推荐在普通项目中使用provide/inject,因为provide/inject 破坏了单向数据流动原则,一方面增加了耦合度,也使得数据的变化、流向都变不可控,难以追踪。这在多人协作开发中,这将成为一个噩梦。

4.provide/inject在开发插件和库的时候可以使用,因为插件和库都会封装暴露出来统一修改数据的api,通过这些api对数据做统一的监控。

5.provide/inject原理:在组件内调用provide函数时,会把数据保存在组件实例的provides属性上。等到子组件初始化时,会一级一级的把父级的provides数据保存到自己的provides属性上。组件内调用inject时,会根据inject的第一个参数,从provides数据里获取相应数据。

59.Vue中如何重置reactive数据:

声明时,reactive的参数是一个函数返回的形式。重置时,通过Object.assign方法。

const data = reactive(getInitialData());

Object.assign(data, getInitialData());

60.Vue3踩过的坑

多人协作开发时,有人用options api、有人用setup api、有人用template模板、有人用jsx模板,多种开发形式混在一起比较混乱。

61.Vue中动画如何实现

1.把要做动画的容器添加到transition组件中

2.然后可通过transition或animation两个属性实现css动画:

        transition动画需要在enter-from、leave-to、enter-to、leave-from等几个阶段里编辑动画效果,然后在enter-active、leave-active里调用transition实现动画;

        animation动画不需要在上面几个阶段里编辑,只需要在@keyframes里编辑动画效果,然后在enter-active、leave-active里调用animation实现动画。

        也可以借助animate.css第三方动画库实现css动画效果。

3.transition组件上可以绑定一些事件函数,在事件函数里用js编辑样式也能实现动画效果。也可以借助gsap.js第三方动画库实现动画效果。

<style scoped>
.box {
    display: inline-block;
    margin: 0 20px;
    width: 200px;
    height: 200px;
    line-height: 200px;
    text-align: center;
    color: #eee;
    background-color: red;
}

/* transition要写这六个类 */
.test-enter-from,
.test-leave-to {
    width: 0;
    height: 0;
    color: transparent;
}
.test-enter-to,
.test-leave-from {
    width: 200px;
    height: 200px;
}
.test-enter-active,
.test-leave-active {
    transition: all 2s linear;
}


/* animation时只需要写这两个类,但要声明动画。 */
.test1-enter-active {
    animation: loading 2s linear 0s 1 normal;
}
.test1-leave-active {
    animation: loading 2s linear 0s 1 reverse;
}
@keyframes loading {
    0% {
        width: 0;
        height: 0;
        color: transparent;
    }
    100% {
        width: 200px;
        height: 200px;
    }
}


.testAnimation {
    animation: loading 2s linear 0s infinite alternate;
}
</style>

<template>
    <button @click="isAdd = !isAdd">MyTransition 显示/隐藏</button>&ensp;
    <button @click="isAdd1 = !isAdd1">MyAnimation 显示/隐藏</button>&ensp;
    <button @click="toggle">my_animatTest 开始/结束</button>
    <!--
        不自定义类名,类名默认以name开头的。
        自定义类名:
        enter-from-class
        leave-from-class
        appear-class
        enter-to-class
        leave-to-class
        appear-to-class
        enter-active-class
        leave-active-class
        appear-active-class
     -->
    <transition name="test">
        <div class="box" v-if="isAdd">MyTransition</div>
    </transition>
    <transition name="test1">
        <div class="box" v-if="isAdd1">MyAnimation</div>
    </transition>
    <div :class="toggleClass" @click="toggle">my_animatTest</div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue';

const isAdd = ref(true);
const isAdd1 = ref(true);
const toggleClass = reactive(['box']);

const toggle = () => {
    if (toggleClass.includes('testAnimation')) {
        toggleClass.splice(toggleClass.indexOf('testAnimation'), 1);
    } else {
        toggleClass.push('testAnimation');
    }
}
</script>

62.谈谈你对MVVM的理解

MVC:实际应用开发场景中,常用的一种设计模式

- M(Model):数据模型层。是应用程序中用于处理应用程序数据逻辑的部分,模型对象负责在数据库中存取数据。  
- V(View):视图层。是应用程序中处理数据显示的部分,视图是依据模型数据创建的。 
- C(Controller):控制层。是应用程序中处理用户交互的部分,控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。

MVVM:它是MCV的改进版,并不是用VM取代了C,只是在MVC的基础上增加了一层VM,它只不过弱化了C的概念。它就是将view的状态和行为抽象化,将视图UI和业务逻辑分开,实现了View和Model的自动同步。也就是当Model的数据改变时,不需要手动操作Dom来改变View的显示,而是通过数据驱动View层的改变。
- M(Model):数据模型层。就是业务逻辑相关的数据对象,通常从数据库映射而来,我们可以说是与数据库对应的model。 
- V(View):视图层。就是展现出来的用户界面。 
- VM(ViewModel):视图模型层。就是与界面(view)对应的Model,也叫业务逻辑层。

af71f4bfaa1a40fd98a98545a041a33f.png

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现

<!-- View 层 -->

<div id="app">
    <p>{{message}}</p>
    <button v-on:click="showMessage()">Click me</button>
</div>
// ViewModel 层

var app = new Vue({
    el: '#app',
    data: {  // 用于描述视图状态   
        message: 'Hello Vue!', 
    },
    methods: {  // 用于描述视图行为  
        showMessage(){
            let vm = this;
            alert(vm.message);
        }
    },
    created(){
        let vm = this;
        // Ajax 获取 Model 层的数据
        ajax({
            url: '/your/server/data/api',
            success(res){
                vm.message = res;
            }
        });
    }
})
// Model 层

{
    "url": "/your/server/data/api",
    "res": {
        "success": true,
        "name": "test",
        "domain": "www.baidu.com"
    }
}

63.vue如何进行依赖收集的

1.watcher(观察者)和响应式数据之间是多对多的关系,一个观察者可以依赖多个响应式数据,一个响应式数据也可以被多个观察者依赖。

2.通过proxy数据劫持,监听到数据获取的时候(例如调用组件render函数会进行数据获取操作),就会调用track方法收集依赖,把所有依赖该数据的watcher保存到一个WeakMap中。当监听到数据变化时,会通过trigger通知所有观察者触发各自的调度函数;当然watcher被保存到WeakMap中时,也会在自己的deps属性上保存所有保存了自己的dep(发布者),便于取消监听。

64.谈一谈对Vue组件化的理解

  • 组件化开发能大幅提高复用性、测试性,从而提高开发效率
  • 降低更新频率,只重新渲染变化的组件
  • 组件的特点:高内聚、低耦合、单向数据流

65.为什么Vue采用异步渲染

     Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,浪费性能。所以 Vue 会在本轮数据更新后,在异步中更新视图。核心思想 nextTick

66.对Vue SSR的理解

SSR也就是服务端渲染,也就是将Vue组件渲染成 HTML 的工作放在了服务端,然后再把 html 返回给客户端。

  • 优点SSR 有着更好的 SEO、并且首屏加载速度更快
    • 因为搜索引擎获取到的 SPA 页面的内容是模板内容;而 SSR 是直接由服务端返回已经渲染好的页面,所以SEO更好。
    • 更快的内容到达时间(首屏加载更快): SPA 会等待所有 js 文件都下载完成后,才开始进行页面的渲染,文件下载和渲染都需要一定的时间,所以首屏加载慢;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件和渲染页面的时间,所以 SSR 有更快的内容到达时间
  • 缺点: 开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求
    • 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加占用CPU资源 ,因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略

67.Vue为什么没有类似于React中shouldComponentUpdate的生命周期

  • React父组件发生变化后,默认会更新所有子组件,但是很多子组件实际上没有发生变化的,这时候需要 shouldComponentUpdate 进行手动操作来减少子组件更新,从而提高程序整体的性能
  • Vue在一开始就知道那个组件发生了变化,不需要手动控制diff,而组件内部在更新前,也已经对组件的属性和插槽进行了类似于shouldComponentUpdate 的判断,用户再次优化的价值有限。

68.vue 中使用了哪些设计模式

  • 工厂模式 传入参数即可创建实例,如:虚拟 DOM 的创建、组件实例的创建
  • 单例模式 整个程序有且仅有一个实例:vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式: (@装饰器的用法)
  • 策略模式 策略模式指对象有某个行为,在不同的场景中,有不同的实现方案,比如选项的合并策略

69.Vue 中组件和插件有什么区别?

1.组件就是把ui和非ui的各种逻辑包装成一个统一的可复用实例,每一个.vue 文件都是一个组件。插件是一个包含install方法的对象,或者一个单独的方法。

2.编写组件可以有很多方式,最常见的就是 Vue 单文件的这种格式,每一个.vue 文件就是一个组件;还可以通过app.component来编写组件。编写插件是在install方法里,这个方法的第一个参数是 Vue 对象,第二个参数是一个可选的选项对象。

3.组件注册可以全局注册或局部注册,插件注册通过app.use(),且要在app.mount之前。

4.组件的使用场景是业务模块;插件是对 Vue 功能的增强或补充,用来添加全局功能。

70.什么是Vue ?

1.vue是一套用于构建用户界面的渐进式框架

2.vue的两大核心:数据驱动和组件化。

        (1)数据驱动(数据的变化会驱动页面发生变化,不用操作DOM)

        (2)组件化(封装视图和业务逻辑,便于复用)

3.mvvm框架

        将view的状态和行为抽象化,将视图UI和业务逻辑分开,实现了View和Model的自动同步。也就是当Model的数据改变时,不需要手动操作Dom来改变View的显示,而是通过数据驱动View层的改变。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值