【Vue】Vue超全面的面试题

1.Vue的理解?

Vue.js(/vjuː/,或简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。

Vue 是一套用于构建用户界面的渐进式MVVM框架。那怎么理解渐进式呢?渐进式含义:强制主张最少。

Vue.js包含了声明式渲染、组件化系统、客户端路由、大规模状态管理、构建工具、数据持久化、跨平台支持等,但在实际开发中,并没有强制要求开发者之后某一特定功能,而是根据需求逐渐扩展。

Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。

Vue.js的核心库只关心视图渲染,且由于渐进式的特性,Vue.js便于与第三方库或既有项目整合。Vue.js 实现了一套声明式渲染引擎,并在runtime或者预编译时将声明式的模板编译成渲染函数,挂载在观察者 Watcher 中,在渲染函数中(touch),响应式系统使用响应式数据的getter方法对观察者进行依赖收集(Collect as Dependency),使用响应式数据的setter方法通知(notify)所有观察者进行更新,此时观察者 Watcher 会触发组件的渲染函数(Trigger re-render),组件执行的 render 函数,生成一个新的 Virtual DOM Tree,此时 Vue 会对新老 Virtual DOM Tree 进行 Diff,查找出需要操作的真实 DOM 并对其进行更新。

2. Vue双向绑定的理解?

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
在这里插入图片描述
即:

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。

  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

  1. 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
  2. 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
  3. 实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
  4. 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

在这里插入图片描述

3.Vue中的v-show和v-if有什么区别?

v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

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

  • new Vue的时候调用会调用_init方法

      定义 $set、 $get 、$delete、$watch 等方法
      定义 $on、$off、$emit、$off 等事件
      定义 _update、$forceUpdate、$destroy生命周期
      调用$mount进行页面的挂载
    
  • 挂载的时候主要是通过mountComponent方法

  • 定义updateComponent更新函数

  • 执行render生成虚拟DOM

  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

5. Vue生命周期的理解?

(1)生命周期是什么?

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

(2)各个生命周期的作用

生命周期 描述
beforeCreate 组件实例被创建之初,组件的属性生效之前
created 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后
activited keep-alive 专属,组件被激活时调用
deadctivated keep-alive 专属,组件被销毁时调用
beforeDestory 组件销毁前调用
destoryed 组件销毁后调用

(3)生命周期示意图

在这里插入图片描述

6.SPA首屏加载速度慢如何解决?

  • 使用代码分割:将代码拆分成小块并按需加载(懒加载),以避免不必要的网络请求和减少加载时间。
  • 缓存资源:利用浏览器缓存来存储重复使用的文件,例如 CSS 和 JS 文件、图片等。
  • 预加载关键资源:在首次渲染之前,先提前加载关键资源,例如首页所需的 JS、CSS 或数据,以保证关键内容的快速呈现。
  • 使用合适的图片格式:选择合适的图片格式(例如 JPEG、PNG、WebP 等),并根据需要进行压缩以减少文件大小。对于一些小图标,可以使用 iconfont 等字体文件来代替。
  • 启用 Gzip 压缩:使用服务器端的 Gzip 压缩算法对文件进行压缩,以减少传输时间和带宽消耗。
  • 使用 CDN:使用内容分发网络(CDN)来缓存和传递文件,以提高文件的下载速度和可靠性。
  • 优化 API 请求:尽可能地减少 API 调用的数量,并使用缓存和延迟加载等技术来优化 API 请求的效率。
  • 使用服务器端渲染:使用服务器端渲染(SSR)来生成 HTML,以减少客户端渲染所需的时间和资源。但需要注意,SSR 也可能增加了服务器的负担并使网站更复杂。

7.vue-router 路由钩子函数是什么?执行顺序是什么?

路由钩子的执行流程,钩子函数种类有:全局守卫、路由守卫、组件守卫。
完整的导航解析流程:
1 、导航被触发。
2 、在失活的组件里调用 beforeRouterLeave 守卫。
3 、调用全局的 beforeEach 守卫。
4 、在重用的组件调用 beforeRouterUpdate 守卫( 2.2+ )。
5 、在路由配置里面 beforeEnter 。
6 、解析异步路由组件。
7 、在被激活的组件里调用 beforeRouterEnter 。
8 、调用全局的 beforeResolve 守卫( 2.5+ )。
9 、导航被确认。
10 、调用全局的 afterEach 钩子。
11 、触发 DOM 更新。
12 、调用 beforeRouterEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

8.Vue给对象添加新属性时,界面不刷新怎么办?

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

Vue.set()
Object.assign()
$forcecUpdated()

1. Vue.set()

通过Vue.set向响应式对象中添加一个property,并确保这个新 property 同样是响应式的,且触发视图更新

function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用defineReactive方法,实现新增属性的响应式

关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截

大致代码如下:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                console.log(`set ${key}:${newVal}`);
                val = newVal
            }
        }
    })
}

2. Object.assign()

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

3. $forceUpdate

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate迫使 Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结

如果为对象添加少量的新属性,可以直接采用Vue.set()

如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象

如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

9.Vue中的$nextTick有什么用?

作用 :

vue 更新 DOM 是异步更新的,数据变化,DOM 的更新不会马上完成, nextTick的回调是在下次 DOM 更新循环结束之后执行的延迟回调 。

实现原理:

  • nextTick 主要使用了 宏任务和微任务 。根据执行环境分别尝试采用 Promise:可以将函数延迟到当前函数调用栈最末端

    MutationObserver :是 H5 新加的一个功能,其功能是监听 DOM 节点的变动,在所有 DOM

    变动完成后,执行回调函数setImmediate:用于中断长时间运行的操作,并在浏览器完成其他操作(如事件和显 示更新)后立即运行回调函数

    如果以上都不行则采用 setTimeout 把函数延迟到 DOM

    更新之后再使用,原因是宏任务消耗大于微任务,优先使用微任务,最后使用消耗最大的宏任务

10.对slot的理解?以及应用场景有哪些?

slot是什么

在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符

该占位符可以在后期使用自己的标记语言填充
使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

三、分类
slot可以分来以下三种:

默认插槽
具名插槽
作用域插槽

默认插槽
子组件用标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面

父组件在使用的时候,直接在子组件的标签内写入内容即可

子组件Child.vue

<template>
    <slot>
      <p>插槽后备的内容</p>
    </slot>
</template>

父组件
<Child>
  <div>默认插槽</div>  
</Child>

具名插槽
子组件用name属性来表示插槽的名字,不传为默认插槽

父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值

// 子组件Child.vue

<template>
    <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>

//父组件
<child>
    <template v-slot:default>具名插槽</template>
    <!-- 具名插槽⽤插槽名做参数 -->
    <template v-slot:content>内容...</template>
</child>

作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上

父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用

子组件Child.vue
<template> 
  <slot name="footer" testProps="子组件的值">
          <h3>没传footer插槽</h3>
    </slot>
</template>

父组件
<child> 
    <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
    <template v-slot:default="slotProps">
      来⾃⼦组件数据:{
   
   {slotProps.testProps}}
    </template>
  <template #default="slotProps">
      来⾃⼦组件数据:{
   
   {slotProps.testProps}}
    </template>
</child>

小结:

v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用

默认插槽名为default,可以省略default直接写v-slot

缩写为#时不能不写参数,写成#default

可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

11.Vue中mixin的理解?以及应用场景有哪些?

Vue的mixin的作用就是抽离公共的业务逻辑,原理类似对象的继承,当组件初始化的时候,会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。 如果混入的数据和本身组件的数据有冲突,采用本身的数据为准。

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

13.Vue.Observable 是什么?

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象

返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器

Vue.observable({ count : 1})

其作用等同于
new vue({ count : 1})

在 Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象

在 Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

使用场景

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择

14.Vue中key的原理?

在 Vue 中,key 是用于帮助 Vue 识别和跟踪虚拟 DOM 的变化的特殊属性。当 Vue 更新渲染真实 DOM 时,它使用 key 属性来比较新旧节点,并尽可能地复用已存在的真实 DOM 节点,以提高性能。

Vue 在进行虚拟 DOM 的 diff 算法时,会使用 key 来匹配新旧节点,以确定节点的更新、移动或删除。它通过 key 属性来判断两个节点是否代表相同的实体,而不仅仅是根据它们的内容是否相同。这样可以保留节点的状态和避免不必要的 DOM 操作。

key 的工作原理如下:

当 Vue 更新渲染真实 DOM 时,它会对新旧节点进行比较,找出它们之间的差异。
如果两个节点具有相同的 key 值,则 Vue 认为它们是相同的节点,会尝试复用已存在的真实 DOM 节点。
如果节点具有不同的 key 值,Vue 会将其视为不同的节点,并进行适当的更新、移动或删除操作。
使用 key 可以提供更准确的节点识别和跟踪,避免出现一些常见的问题,比如在列表中重新排序时导致的元素闪烁、输入框内容丢失等。

key 必须是唯一且稳定的,最好使用具有唯一标识的值,例如使用数据的唯一 ID。同时,不推荐使用随机数作为 key,因为在每次更新时都会生成新的 key,导致所有节点都重新渲染,无法复用已有的节点,降低性能。

更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下

function createKeyToOldIdx (children, beginIdx, endIdx) {
    
    
  let i, key
  const map = {
    
    }
  for (i = beginIdx; i <= endIdx; ++i) {
    
    
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

15.Vue中keep-alive的理解?

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;

  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;

  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

16.Vue中封装axios?

17.Vue项目中如何划分结构和组件?

文件夹和文件夹内部文件的语义一致性
单一入口/出口
就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
公共的文件应该以绝对路径的方式从根目录引用
/src 外的文件不应该被引入

18.Vue如何实现权限管理?控制到按钮级别的权限怎么做?

前端权限控制可以分为四个方面:

接口权限
按钮权限
菜单权限
路由权限

接口权限

接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录

登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token

axios.interceptors.request.use(config => {
    config.headers['token'] = cookie.get('token')
    return config
})
axios.interceptors.response.use(res=>{},{response}=>{
    if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
        router.push('/login')
    }
})

路由权限控制

方案一

初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

const routerMap = [
  {
    
    
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
    
    
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor'] // you can set roles in root nav
    },
    children: [{
    
    
      path: 'page',
      component: () => import('@/views/permission/page'),
      name: 'pagePermission',
      meta: {
    
    
        title: 'pagePermission',
        roles: ['admin'] // or you can only set roles in sub nav
      }
    }, {
    
    
      path: 'directive',
      component: () => import('@/views/permission/directive'),
      name: 'directivePermission',
      meta: {
    
    
        title: 'directivePermission'
        // if do not set roles, means: this page does not require permission
      }
    }]
  }]

这种方式存在以下四种缺点:

加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。

全局路由守卫里,每次路由跳转都要做权限判断。

菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译

菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

方案二

初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制

登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

import router from './router'
import store from './store'
import {
    
     Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import {
    
     getToken } from '@/utils/auth' // getToken from cookie

NProgress.configure({
    
     showSpinner: false })// NProgress Configuration

// permission judge function
function hasPermission(roles, permissionRoles) {
    
    
  if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
  if (!permissionRoles) return true
  return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect']// no redirect whitelist

router.beforeEach((to, from, next) => {
    
    
  NProgress.start() // start progress bar
  if (getToken()) {
    
     // determine if there has token
    /* has token*/
    if (to.path === '/login') {
    
    
      next({
    
     path: '/' })
      NProgress.done() // if current page is dashboard will not trigger    afterEach hook, so manually handle it
    } else {
    
    
      if (store.getters.roles.length === 0) {
    
     // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetUserInfo').then(res => {
    
     // 拉取user_info
          const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
          store.dispatch('GenerateRoutes', {
    
     roles }).then(() => {
    
     // 根据roles权限生成可访问的路由表
            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
            next({
    
     ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
          })
        }).catch((err) => {
    
    
          store.dispatch('FedLogOut').then(() => {
    
    
            Message.error(err || 'Verification failed, please login again')
            next({
    
     path: '/' })
          })
        })
      } else {
    
    
        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {
    
    
          next()//
        } else {
    
    
          next({
    
     path: '/401', replace: true, query: {
    
     noGoBack: true }})
        }
        // 可删 ↑
      }
    }
  } else {
    
    
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
    
     // 在免登录白名单,直接进入
      next()
    } else {
    
    
      next('/login') // 否则全部重定向到登录页
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
  }
})

router.afterEach(() => {
    
    
  NProgress.done() // finish progress bar
})

按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

这种方式也存在了以下的缺点:

全局路由守卫里,每次路由跳转都要做判断

菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译

菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限可以理解成将页面与理由进行解耦

方案一

菜单与路由分离,菜单由后端返回

前端定义路由信息

{
    
    
    name: "login",
    path: "/login",
    component: () => import("@/pages/Login.vue")
}

name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

全局路由守卫里做判断

function hasPermission(router, accessMenu) {
    
    
  if (whiteList.indexOf(router.path) !== -1) {
    
    
    return true;
  }
  let menu = Util.getMenuByName(router.name, accessMenu);
  if (menu.name) {
    
    
    return true;
  }
  return false;

}

Router.beforeEach(async (to, from, next) => {
    
    
  if (getToken()) {
    
    
    let userInfo = store.state.user.userInfo;
    if (!userInfo.name) {
    
    
      try {
    
    
        await store.dispatch("GetUserInfo")
        await store.dispatch('updateAccessMenu')
        if (to.path === '/login') {
    
    
          next({
    
     name: 'home_index' })
        } else {
    
    
          //Util.toDefaultPage([...routers], to.name, router, next);
          next({
    
     ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
        }
      }  
      catch (e) {
    
    
        if (whiteList.indexOf(to.path) !== -1) {
    
     // 在免登录白名单,直接进入
          next()
        } else {
    
    
          next('/login')
        }
      }
    } else {
    
    
      if (to.path === '/login') {
    
    
        next({
    
     name: 'home_index' })
      } else {
    
    
        if (hasPermission(to, store.getters.accessMenu)) {
    
    
          Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
        } else {
    
    
          next({
    
     path: '/403',replace:true })
        }
      }
    }
  } else {
    
    
    if (whiteList.indexOf(to.path) !== -1) {
    
     // 在免登录白名单,直接进入
      next()
    } else {
    
    
      next('/login')
    }
  }
  let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
  Util.title(menu.title);
});

Router.afterEach((to) => {
    
    
  window.scrollTo(0, 0);
});

每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的

如果根据路由name找不到对应的菜单,就表示用户有没权限访问

如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载

这种方式的缺点:

菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用

全局路由守卫里,每次路由跳转都要做判断

方案二

菜单和路由都由后端返回

前端统一定义路由组件

const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
    
    
    home: Home,
    userInfo: UserInfo
};
后端路由组件返回以下格式

[
    {
    
    
        name: "home",
        path: "/",
        component: "home"
    },
    {
    
    
        name: "home",
        path: "/userinfo",
        component: "userInfo"
    }
]

在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件

如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

这种方法也会存在缺点:

全局路由守卫里,每次路由跳转都要做判断
前后端的配合要求更高

按钮权限

方案一

按钮权限也可以用v-if判断

但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断

这种方式就不展开举例了

方案二

通过自定义指令进行按钮权限的判断

首先配置路由

{
    
    
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: {
    
    
        btnPermissions: ['admin', 'supper', 'normal']
    },
    //页面需要的权限
    children: [{
    
    
        path: 'supper',
        component: _import('system/supper'),
        name: '权限测试页',
        meta: {
    
    
            btnPermissions: ['admin', 'supper']
        } //页面需要的权限
    },
    {
    
    
        path: 'normal',
        component: _import('system/normal'),
        name: '权限测试页',
        meta: {
    
    
            btnPermissions: ['admin']
        } //页面需要的权限
    }]
}
自定义权限鉴定指令

import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
    
    
    bind: function (el, binding, vnode) {
    
    
        // 获取页面按钮权限
        let btnPermissionsArr = [];
        if(binding.value){
    
    
            // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
            btnPermissionsArr = Array.of(binding.value);
        }else{
    
    
            // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
            btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
        }
        if (!Vue.prototype.$_has(btnPermissionsArr)) {
    
    
            el.parentNode.removeChild(el);
        }
    }
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
    
    
    let isExist = false;
    // 获取用户按钮权限
    let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
    if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    
    
        return false;
    }
    if (value.indexOf(btnPermissionsStr) > -1) {
    
    
        isExist = true;
    }
    return isExist;
};
export {
    
    has}

在使用的按钮中只需要引用v-has指令

<el-button @click='editClick' type="primary" v-has>编辑</el-button>

19.Vue如何解决跨域?

JSONP

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

Proxy

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

module.exports = {
    
    
    devServer: {
    
    
        host: '127.0.0.1',
        port: 8084,
        open: true,// vue项目启动时自动打开浏览器
        proxy: {
    
    
            '/api': {
    
     // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
                changeOrigin: true, //是否跨域
                pathRewrite: {
    
     // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
                    '^/api': "" 
                }
            }
        }
    }
}

20.如何处理Vue项目中的错误?

  • handleError在需要捕获异常的地方调用,首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法,在遍历调用完所有 errorCaptured 方法或 errorCaptured 方法有报错时,调用 globalHandleError 方法
  • globalHandleError 调用全局的 errorHandler 方法,再通过logError判断环境输出错误信息
  • invokeWithErrorHandling更好的处理异步错误信息
  • logError判断环境,选择不同的抛错方式。非生产环境下,调用warn方法处理错误

21.vue-router 动态路由是什么?有什么问题。

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个
User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用 “ 动态路径参数 ” ( dynamic segment )来达到这个效果:

 const User = {
    
    
  template: " User", 
  };
 const router = new VueRouter({
    
    
 routes: [
 	// 动态路径参数 以冒号开头
 	{
    
     path: "/user/:id", component: User },
 ],
 });

问题 : vue-router 组件复用导致路由参数失效怎么办?

解决方案 :
1 、通 过 watch 监听 路由参数再发请求

 watch:{
    
    
 "router":function(){
    
    
	this.getData(this.$router.params.xxx)
 }
}

2 、用 :key 来阻止复用

router-view :key=“$route.fullPath”

22. Vue3性能提升主要是通过哪几方面体现的?

Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:
(1)监测机制的改变

  • 3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制: 只能监测属性,不能监测对象

    检测属性的添加和删除;

    检测数组索引和长度的变更;

    支持 Map、Set、WeakMap 和 WeakSet。

    新的 observer 还提供了以下特性: 用于创建 observable 的公开
    API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。

    默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在
    3.x 中,只观察用于渲染应用程序最初可见部分的数据。

    更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x
    中,只有依赖于特定属性的 watcher 才会收到通知。

    不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结
    prop 传递或 Vuex 状态树以外的变化。

    更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered
    钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2)模板

  • 模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。 同时,对于 render 函数的方面,vue3.0
    也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3)对象式的组件声明方式

  • vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript
    的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
    3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。 此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。 现在 vue3.0 也全面改用
    TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4)其它方面的更改

  • vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改: 支持自定义渲染器,从而使得 weex
    可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。

    支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。

    基于 treeshaking 优化,提供了更多的内置功能。

23.Vuex 页面刷新数据丢失怎么解决?

需要做 vuex 数据持久化 ,一般使 用本地储存的方案 来保存数据,可以自己设计存储方案,也可以使用第三方插件。

推荐使用 vuex-persist 插件,它是为 Vuex 持久化储存而生的一个插件。

不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。

24.V3中Composition API与V2用的Options Api有什么区别?

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断。
  • Composition API 对 tree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的

25.$attrs是为了解决什么问题出现的?

主要作用是为了实现批量传递数据。

provide/inject更适合应用在插件中,主要实现跨级数据传递。

26.用Vue3设计一个Model?

27.Vuex是什么?有几种属性,它们的意义?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

28.使用过 Vue SSR 吗?说说 SSR?如何实现?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
即:SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。

服务端渲染 SSR 的优缺点如下:
(1)服务端渲染的优点

更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;

更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

(2) 服务端渲染的缺点

更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;

更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

29.Vuex辅助函数如何使用?

30. vue-loader做了哪些事情?

使用 vue-loader 的之前, 我们需要安装一些必要的 loader。。

必需的 loader 包括:vue-loader、vue-style-loader、vue-template-compiler、css-loader。 可能需要的 loader 包含:sass-loader、less-loader、url-loader 等。

vue-loader 工作原理

通过 vue-loader, webpack 可以将 .vue 文件 转化为 浏览器可识别的javascript。

vue-loader 的工作流程, 简单来说,分为以下几个步骤:

将一个 .vue 文件 切割成 template、script、styles 三个部分。

template 部分 通过 compile 生成 render、 staticRenderFns。

获取 script 部分 返回的配置项对象 scriptExports。

styles 部分,会通过 css-loader、vue-style-loader, 添加到 head 中, 或者通过 css-loader、MiniCssExtractPlugin 提取到一个 公共的css文件 中。

使用 vue-loader 提供的 normalizeComponent 方法, 合并 scriptExports、render、staticRenderFns, 返回 构建vue组件需要的配置项对象 - options, 即 **{data, props, methods, render, staticRenderFns...}**。

31.computed如何实现缓存?

  1. 初始化data和computed,分别代理其set以及get方法, 对data中的所有属性生成唯一的dep实例。
  2. 对computed中的sum生成唯一watcher,并保存在vm._computedWatchers中
  3. 执行render函数时会访问sum属性,从而执行initComputed时定义的getter方法,会将Dep.target指向sum的watcher,并调用该属性具体方法sum。
  4. sum方法中访问this.count,即会调用this.count代理的get方法,将this.count的dep加入sum的watcher,同时该dep中的subs添加这个watcher。
  5. 设置vm.count = 2,调用count代理的set方法触发dep的notify方法,因为是computed属性,只是将watcher中的dirty设置为true。
  6. 最后一步vm.sum,访问其get方法时,得知sum的watcher.dirty为true,调用其watcher.evaluate()方法获取新的值。

32. vue中computed和watch有什么区别?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

33.Vue页面渲染流程?

34.Vue2为什么不能检查数组的变化?如何解决?

35.Vnode有哪些属性?

Vue内部定义的Vnode对象包含了以下属性:

  • __v_isVNode: true,内部属性,有该属性表示为Vnode
  • __v_skip: true,内部属性,表示跳过响应式转换,reactive转换时会根据此属性进行判断
  • isCompatRoot?: true,用于是否做了兼容处理的判断
  • type: VNodeTypes,虚拟节点的类型
  • props: (VNodeProps &ExtraProps) | null,虚拟节点的props
  • key: string | number |null,虚拟阶段的key,可用于diff
  • ref: VNodeNormalizedRef | null,虚拟阶段的引用
  • scopeId:string | null,仅限于SFC(单文件组件),在设置currentRenderingInstance当前渲染实例时,一期设置
  • slotScopeIds: string[] | null,仅限于单文件组件,与单文件组件的插槽有关
  • children:VNodeNormalizedChildren,子节点
  • component: ComponentInternalInstance |null,组件实例
  • dirs: DirectiveBinding[] | null,当前Vnode绑定的指令
  • transition:TransitionHooks | null,TransitionHooks
  • DOM相关属性

el: HostNode | null,宿主阶段
anchor: HostNode | null // fragment anchor
target: HostElement | null ,teleport target 传送的目标
targetAnchor: HostNode | null // teleport target anchor
staticCount: number,包含的静态节点的数量

suspense 悬挂有关的属性
suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null

optimization only 用于优化的属性
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null

根节点会有的属性
appContext: AppContext | null,实例上下文
可以看到在Vue内部,对于一个Vnode描述对象的属性大概有二十多个。

Vue为了给用于减轻一定的负担,但又不至于太封闭,就创建了渲染h。可以在用户需要的时候,通过h函数创建对应的Vnode即可。

这样就给为一些高阶玩家保留了自由发挥的空间。

36.Vue路由中,hash 模式和history 模式有什么区别?

(1)hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 ‘#search’:

https://www.word.com#search

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

  • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;

  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;

  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。
唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

history 路由模式的实现主要基于存在下面几个特性:

  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;

  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

  • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

37.Vue 2/3响应式设计原理?

v2

整体思路是 数据劫持 + 观察者模式
Vue 在初始化数据时 ,会使用 Object.defineProperty 重新定义 data 中的所有属性 ,当页面 使用对 应 属性时,首先会进行 依赖收集 (收集当前组件的 watcher ),如果属性 发生变化 会通知相关 依赖进行 更新操作( 发布订阅 )

Vue2.x 采用 数据劫持结合发布订阅模式 (PubSub 模式)的方式,通过 Object.defineProperty 来劫 持 各个属性 的 setter、getter ,在 数据变动时 发 布消息给订阅者 , 触发相应的监听回 调。

当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用Object.defineProperty 将它们转为 getter/setter。用户看不到getter/setter,但是在内部它们让Vue 追踪依赖,在属性被访问和修改时 通知变化 。

Vue 的数据 双向绑定 整合了 Observer,Compile 和 Watcher 三者,通过 Observer 来监听 自己的model 的数据变化,通过 Compile 来解析编 译模板指令,最终 利用 Watcher 搭 起 Observer 和Compile 之间的 通信桥梁 ,达到数据变化->视图更新,视图交互变化(例如 input 操作)->数据model 变更的双向绑定效果。

v3

Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢 ?
监 测数组的时候可能触发多次 get/set ,那么如何 防止触发多 次呢?

参考答案: Vue3.x 响应式数据原理是什么? 在 Vue 2 中,响应式原理就是使用的 Object.defineProperty 来实现的。但是在 Vue 3.0 中采用了 Proxy,抛弃了Object.defineProperty 方法。
究其原因,主要是以下几点:
Object.defineProperty 无法监控 到数组 下标的变化 ,导致通过数组下标添加元素,不
能实时响应 Object.defineProperty 只能 劫持对象 的属性,从而需要对每个对象,每
个属性进行遍历,如果,属性值是对象,还需要深度遍历。 Proxy 可以劫持整个对象,并返回一个新的对象 。

Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。 Proxy 有多达 13 种拦截方法 Proxy作为新标准将受到浏览器厂商重点持续的性能优化 Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢? 判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。 监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢? 我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

38.Vue 3如何设置全局变量?

方法一 config.globalProperties
vue2.x挂载全局是使用 Vue.prototype.$xxxx=xxx 的形式来挂载,然后通过 this.$xxx来获取挂载到全局的变量或者方法。

这在 Vue 3 中,就等同于 config.globalProperties。这些 property 将被复制到应用中作为实例化组件的一部分。

// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}

// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}

方法二 Provide / Inject
vue3新的 provide/inject 功能可以穿透多层组件,实现数据从父组件传递到子组件。

可以将全局变量放在根组件的 provide 中,这样所有的组件都能使用到这个变量。

如果需要变量是响应式的,就需要在 provide 的时候使用 ref 或者 reactive 包装变量。

39.Vue中CSS scope的原理?

40.Vue模板如何编译?

这个问题的核心是如何将template转换成render函数。

将template模块转换成ast语法书 - parserHTML

对静态语法做标记(某些节点不改变)

重新生成代码 - codeGen,使用with语法包裹字符串

41.Vue中diff算法?

简单来说,diff 算法有以下过程
同级比较,再比较子节点

  • 先判断一方有子节点
  • 一方没有子节点的情况(如果新的 children 没有子节点,将旧的子 节点移除) 比较
  • 都有子节点的情况(核心diff) 递归比较子节点
  • 正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 Vue 将 Diff 进行了优化,从O(n^3) -> O(n),
  • 只有当新旧 children 都为多个子节点时才需要用核心的 Diff算法进行同层级比较。

Vue2 的核心 Diff 算法采用了双端比较的算法 ,同时从新旧 children 的两端开始进行比较, 借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 Diff 算法,同样情况 下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x 借鉴了 ivi 算法和 inferno 算法
在 创建 VNode 时 就确定其类型,以及在 mount/patch 的过程中 采用位运算来判断 一个 VNode 类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提 升 。该算法中还运用了动态规划的思想求解最长递归子序列。

42. Vue项目如何进行部署?是否有遇到部署服务器后刷新404问题?

一、如何部署

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可

我们知道vue项目在构建后,是生成一系列的静态文件

常规布署我们只需要将这个目录上传至目标服务器即可

// scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径
scp dist.zip user@host:/xx/xx/xx

让web容器跑起来,以nginx为例

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}
配置完成记得重启nginx

// 检查配置是否正确
nginx -t 

// 平滑重启
nginx -s reload
操作完后就可以在浏览器输入域名进行访问了

当然上面只是提到最简单也是最直接的一种布署方式

什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

二、404问题

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?

我们先还原一下场景:

vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误
先定位一下,HTTP 404 错误意味着链接指向的资源不存在

问题在于为什么不存在?且为什么只有history模式下会出现这个问题?

为什么history模式下有问题
Vue是属于单页应用(single-page application)

而SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html

现在,我们回头来看一下我们的nginx配置

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login

关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况

为什么hash模式下没有问题
router hash 模式我们都知道是用符号#表示的,如 website.com/#/login, hash 的值为 #/login

它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面

hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

三、 解决方案

看到这里我相信大部分同学都能想到怎么解决问题了,

产生问题的本质是因为我们的路由是通过JS来执行视图切换的,

当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404

所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理

对nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
    try_files $uri $uri/ /index.html;
  }
}
修改完配置文件后记得配置的更新

nginx -s reload

这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件

为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
})

关于后端配置方案还有:Apache、nodejs等,思想是一致的,这里就不展开述说了

43.v-if不建议与v-for一起使用?

v-for和v-if不要在同一标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。

永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)

如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环

44.Vue组件间通信方式有哪些?

Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

(1)props / $emit 适用 父子组件通信
这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

(2)ref 与 $parent / $children 适用 父子组件通信
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
$parent / $children:访问父 / 子实例

(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

(4) a t t r s / attrs/ attrs/listeners 适用于 隔代组件通信
$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

(5)provide / inject 适用于 隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

(6)Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

45.Vue中Tree shaking特性是什么?举例进行说明?

1.是什么

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去

而treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕

也就是说 ,tree shaking 其实是找出使用的代码

在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

import Vue from 'vue'

Vue.nextTick(() => {
    
    })

而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中

import {
    
     nextTick, observable } from 'vue'

nextTick(() => {
    
    })

2.如何做

Tree shaking是基于ES6模板语法(import与export),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking无非就是做了两件事:

编译阶段利用ES6 Module判断哪些模块已经加载
判断那些模块和变量未被使用或者引用,进而删除对应代码

3.作用

通过Tree shaking,Vue3给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

46.$route 和 $router 的区别?

在 Vue.js 中,$route 和 $router 都是与路由相关的对象,但它们之间有以下区别:

  1. $route$route 是一个当前路由信息的对象,包括当前 URL 路径、查询参数、路径参数等信息。$route 对象是只读的,不可以直接修改其属性值,而需要通过路由跳转来更新。

  2. $router$router 是 Vue Router 的实例对象,包括了许多用于导航控制和路由操作的 API,例如 push、replace、go、forward 等方法。$router 可以用来动态地改变 URL,从而实现页面间的无刷新跳转。

因此,$route$router在功能上有所不同,$route 主要用于获取当前路由信息,$router 则是用于进行路由操作,例如跳转到指定的路由、前进、后退等。通常来说,$route$router 是紧密关联的,并且常常一起使用。

47.V3和V2的区别?

以下是一些主要区别的总结:

  • 响应式系统:Vue 3 引入了 Composition API,这是一种新的响应式系统。Composition API 提供了更灵活和强大的组件状态和逻辑管理方式,使代码组织和重用更加方便。Composition API 使用函数而不是对象,可以提高摇树优化(Tree Shaking)并减小打包体积。

  • 更小的包体积:Vue 3 通过更好的 Tree Shaking 和更高效的运行时代码生成,相较于 Vue 2,打包体积更小。Vue 3 的响应式系统也经过优化,性能更好。

  • 性能改进:Vue 3 采用了更快、更高效的渲染机制,得益于新的编译器。虚拟 DOM 的差异化算法经过优化,减少不必要的更新,提升渲染性能。

  • 作用域插槽替代为 :在 Vue 3 中,作用域插槽的概念被更直观、更简化的 语法所取代,使得在组件组合中定义和使用插槽更加容易。

  • 引入 Teleport 组件:Vue 3 引入了 Teleport 组件,可以在 DOM 树中的不同位置渲染内容,用于创建模态框、工具提示和其他覆盖层效果。

  • 片段:Vue 3 引入了一个名为片段(Fragment)的内置组件,允许将多个元素进行分组,而无需添加额外的包装元素。

  • 更好的 TypeScript 支持:Vue 3 默认提供了更好的 TypeScript 支持,具有增强的类型推断和与 TypeScript 工具更好的集成。

  • 简化的 API:Vue 3 对许多 API 进行了简化和优化,使得学习和使用框架更加容易。新的 API 提供了更好的一致性,并与 JavaScript 标准更加对齐。

虽然 Vue 3 引入了这些变化,但它保持与 Vue 2 API 的向后兼容性,允许现有的 Vue 2 项目逐步升级。Vue 3 提供了一个迁移构建版本,与大多数 Vue 2 代码兼容,从而使开发者的过渡更加平滑。

总体而言,Vue 3 在性能、包体积和开发者体验方面带来了显著的改进,同时引入了 Composition API 作为管理组件状态和逻辑的更强大工具。

48.Vue 的响应式开发比命令式开发有什么优势?

Vue 的响应式开发相较于命令式开发有以下优势:

  1. 简化代码:在 Vue 中,通过将数据和模板绑定起来实现视图更新的自动化,从而避免了手动操作 DOM 的繁琐和容易出错的操作。因此,可以大幅减少编写样板代码和调试代码所需的时间。

  2. 提高可维护性:使用 Vue 的响应式开发可以帮助我们更方便地管理应用程序的状态,并对状态变化进行统一处理。这不仅可以提高代码的可读性和可维护性,还可以更方便地进行单元测试和集成测试。

  3. 增强用户体验:通过 Vue 的响应式开发,可以实现局部更新、异步加载等功能,从而提升用户体验。例如,在列表中添加或删除项目时,只需要更新相应的项目,而不是重新渲染整个列表。又比如,在加载大量图片时,可以通过异步加载和懒加载的方式,提高页面加载速度和用户体验。

  4. 支持复杂组件设计:Vue 的响应式开发支持组件化设计,它能够轻松地将一个大型应用程序拆分成多个小型、可重用的组件。这些组件可以根据需要进行嵌套和组合,形成更为复杂和丰富的 UI 界面,而且每个组件都具有独立的状态和生命周期。

总之,Vue 的响应式开发可以帮助我们更高效、更方便、更灵活地进行前端开发,从而提供更好的用户体验和更高的代码质量。

49.Vue常用的修饰符有哪些?分别用于什么应用场景?

一、修饰符是什么

在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符号

在Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理

vue中修饰符分为以下五种:

表单修饰符
事件修饰符
鼠标按键修饰符
键值修饰符
v-bind修饰符

二、修饰符的作用

表单修饰符
在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

lazy
trim
number
lazy

在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步

  • lazy
<input type="text" v-model.lazy="value">
<p>{
    
    {
    
    value}}</p>
  • trim
    自动过滤用户输入的首空格字符,而中间的空格不会过滤
<input type="text" v-model.trim="value">
  • number
    自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值
<input v-model.number="age" type="number">

事件修饰符
事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

stop
prevent
self
once
capture
passive
native
  • stop
    阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div @click="shout(2)">
  <button @click.stop="shout(1)">ok</button>
</div>
//只输出1
  • prevent
    阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>
  • self
    只当在 event.target 是当前元素自身时触发处理函数
<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

  • once
    绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
  • capture
    使事件触发从包含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
    obj1
<div @click.capture="shout(2)">
    obj2
<div @click="shout(3)">
    obj3
<div @click="shout(4)">
    obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3 
  • passive
    在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。

passive 会告诉浏览器你不想阻止事件的默认行为

  • native
    让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>

使用.native修饰符来操作普通HTML标签是会令事件失效的

鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

left 左键点击
right 右键点击
middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

键盘修饰符
键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的,有如下:

keyCode存在很多,但vue为我们提供了别名,分为以下两种:

普通键(enter、tab、delete、space、esc、up…)
系统修饰键(ctrl、alt、meta、shift…)
// 只有按键为keyCode的时候才触发

<input type="text" @keyup.keyCode="shout()">

还可以通过以下方式自定义一些全局的键盘码别名

Vue.config.keyCodes.f2 = 113

v-bind修饰符
v-bind修饰符主要是为属性进行操作,用来分别有如下:

sync
prop
camel
sync

能对props进行一个双向绑定

//父组件
<comp :myMessage.sync="bar"></comp> 
//子组件
this.$emit('update:myMessage',params);
以上这种方法相当于以下的简写

//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){
    
    
 this.bar = e;
}
//子组件js
func2(){
    
    
  this.$emit('update:myMessage',params);
}

使用sync需要注意以下两点:

使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用

将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的

  • props
    设置自定义标签属性,避免暴露数据,防止污染HTML结构
<input id="uid" title="title1" value="1" :index.prop="index">
  • camel
    将命名变为驼峰命名法,如将 view-Box属性名转换为 viewBox
<svg :viewBox="viewBox"></svg>

三、应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:

.stop:阻止事件冒泡
.native:绑定原生事件
.once:事件只执行一次
.self :将事件绑定在自身身上,相当于阻止事件冒泡
.prevent:阻止默认事件
.capture:用于事件捕获
.once:只触发一次
.keyCode:监听特定键盘按下
.right:右键

50.Vue 的父组件和子组件生命周期钩子函数执行顺序?

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

51.组件中 data 为什么是一个函数?

为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?

// data
data() {
    
    
  return {
    
    
    message: "子组件",
    childName:this.name
  }
}

// new Vue
new Vue({
    
    
  el: '#app',
  router,
  template: '<App/>',
  components: {
    
    App}
})

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,
如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

52.Class 与 Style 如何动态绑定?

Class 可以通过对象语法和数组语法进行动态绑定:
对象语法:

<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
    
    
  isActive: true,
  hasError: false
}

数组语法:

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
    
    
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也可以通过对象语法和数组语法进行动态绑定:
对象语法:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
    
    
  activeColor: 'red',
  fontSize: 30
}

数组语法:

<div v-bind:style="[styleColor, styleSize]"></div>

data: {
    
    
  styleColor: {
    
    
     color: 'red'
   },
  styleSize:{
    
    
     fontSize:'23px'
  }
}

53.怎样理解 Vue 的单向数据流?

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。
这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

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

有两种常见的试图改变一个 prop 的情形 :

  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
    
    
  return {
    
    
    counter: this.initialCounter
  }
}
  • 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
props: ['size'],
computed: {
    
    
  normalizedSize: function () {
    
    
    return this.size.trim().toLowerCase()
  }
}

54.直接给一个数组项赋值,Vue 能检测到变化吗?

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

当你修改数组的长度时,例如:vm.items.length = newLength

为了解决第一个问题,Vue 提供了以下操作方法:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

为了解决第二个问题,Vue 提供了以下操作方法:

// Array.prototype.splice
vm.items.splice(newLength)

55. 在哪个生命周期内调用异步请求?

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间;

ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

56.在什么阶段才能访问操作DOM?

在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。
vue 具体的生命周期示意图可以参见如下,理解了整个生命周期各个阶段的操作,关于生命周期相关的面试题就难不倒你了。
在这里插入图片描述

57.父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
    
    
  this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
    
    
   console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){
    
    
   console.log('子组件触发 mounted 钩子函数 ...');
},    

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

58.v-model 的原理?

我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;

checkbox 和 radio 使用 checked 属性和 change 事件;

select 字段将 value 作为 prop 并将 change 作为事件。

以 input 表单元素为例:

<input v-model='something'>

相当于

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

如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

父组件:
<ModelChild v-model="message"></ModelChild>

子组件:
<div>{
    
    {
    
    value}}</div>

props:{
    
    
    value: String
},
methods: {
    
    
  test1(){
    
    
     this.$emit('input', '小红')
  },
},

59.vue-router 路由模式有几种?

vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:

switch (mode) {
    
    
  case 'history':
    this.history = new HTML5History(this, options.base)
    break
  case 'hash':
    this.history = new HashHistory(this, options.base, this.fallback)
    break
  case 'abstract':
    this.history = new AbstractHistory(this, options.base)
    break
  default:
    if (process.env.NODE_ENV !== 'production') {
    
    
      assert(false, `invalid mode: ${
      
      mode}`)
    }
}

其中,3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

60.什么是 MVVM?

Model–View–ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。

MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。

如下图所示:
在这里插入图片描述

(1)View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。

(2)Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。

(3)ViewModel 层
ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。

需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。

MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。

这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:
(1)View 层

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

(2)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;
            }
        });
    }
})

(3) Model 层

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

61.Vue 框架怎么实现对象和数组的监听?

Vue2.x 中实现检测数组变化的方法,是将数组的常用方法进行了 重写 。Vue 将 data 中的数组进行了 原型链重写 ,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新 。如果数组中包含着引用类型,会对数组中的引用类型 再次递归遍历进行监控 。这样就实现了 监测数组变化 。

流程:

  1. 初始化传入 data 数据执行 initData
  2. 将数据进行观测 new Observer
  3. 将数组原型方法指向重写的原型
  4. 深度观察数组中的引用类型

有两种情况无法检测到数组的变化 。

  1. 当利用索引直接设置一个数组项时,例如 vm.items[indexOfItem] = newValue
  2. 当修改数组的长度时,例如 vm.items.length = newLength

不过这两种场景都有对应的解决方案。利用索引设置数组项的替代方案
//使用该方法进行更新视图

// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)

62.Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

63 .Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)来实现为对象添加响应式属性,那框架本身是如何实现的呢?

我们查看对应的 Vue 源码:

export function set (target: Array<any> | Object, key: any, val: any): any {
    
    
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    
    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val)
    return val
  }
  // key 已经存在,直接修改属性值
  if (key in target && !(key in Object.prototype)) {
    
    
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    
    
    target[key] = val
    return val
  }
  // 对属性进行响应式处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

我们阅读以上源码可知,vm.$set 的实现原理是:

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

64 自定义指令?应用场景?

在 Vue 中,自定义指令是一种用于扩展 Vue 的模板语法的机制。通过自定义指令,你可以在 DOM 元素上添加自定义行为,并在元素插入、更新和移除时进行相应的操作。

自定义指令由 Vue.directive 函数定义,它接收两个参数:指令名称和指令选项对象。指令选项对象包含一系列钩子函数,用于定义指令的行为。

以下是一些常见的自定义指令应用场景:

  • 操作 DOM:自定义指令可以用于直接操作 DOM 元素,例如修改元素的样式、属性、事件绑定等。你可以通过在指令的钩子函数中访问和操作
    DOM 元素。

  • 表单验证:你可以创建自定义指令来实现表单验证逻辑。通过自定义指令,你可以监听输入框的值变化,并根据自定义的验证规则进行验证,以便提供实时的反馈。

  • 权限控制:自定义指令可以用于权限控制场景,例如根据用户权限来隐藏或禁用某些元素。你可以在自定义指令中根据用户权限进行条件判断,并修改元素的显示或行为。

  • 第三方库集成:当你需要在 Vue 中使用第三方库或插件时,可以使用自定义指令来进行集成。你可以创建一个自定义指令,在其中初始化和配置第三方库,并在适当的时机调用库的方法。

  • 动画和过渡效果:自定义指令可以与 Vue的过渡系统一起使用,实现自定义的动画和过渡效果。你可以在自定义指令中监听过渡钩子函数,并根据需要操作元素的样式或类名来实现过渡效果。

这只是一些常见的应用场景,实际上自定义指令的应用范围非常广泛,可以根据具体需求进行灵活的使用。通过自定义指令,你可以扩展 Vue 的能力,实现更复杂和灵活的交互行为。

猜你喜欢

转载自blog.csdn.net/2201_75499330/article/details/131437792
今日推荐