跳至主要內容

面试题-Vue

望间面试Vue大约 18 分钟

面试题-Vue

简述 MVVM

什么是 MVVM

MVVM视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成`ViewModel

  • Model 层代表数据模型
  • View 代表 UI 组件
  • ViewModel 是ViewModel层的桥梁,数据会绑定到ViewModel层并自动将数据渲染到页面中,视图变化的时候会通知ViewModel层更新数据

以前是操作 DOM 结构更新视图,现在是数据驱动视图

MVVM 的优点

  1. 低耦合:视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变化,当 Model 变化的时候 View 也可以不变
  2. 可重用性:你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑
  3. 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计
  4. 可测试

Vue 底层实现原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调

将 Vue 作为例子,模型(Model)只是普通的 javascript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观

Observer(数据监听器) : Observer 的核心是通过 Object.defineProprtty() 来监听数据的变动,这个函数内部可以定义 settergetter,每当数据发生变化,就会触发 setter,这时候 Observer 就要通知订阅者,订阅者就是 Watcher

Watcher(订阅者) : Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

  1. 在自身实例化时往属性订阅器(dep)里面添加自己
  2. 自身必须有一个 update()方法
  3. 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调

Compile(指令解析器) : Compile 主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新试图

传送门:☞ 20 分钟吃透 Diff 算法核心原理open in new window

谈谈对 vue 生命周期的理解?

每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

  • create阶段:vue 实例被创建
    • beforeCreate: 最初调用触发,创建前,此时 data 和 methods 中的数据都还没有初始化,data 和 events 都不能用
    • created: 创建完毕,data 中有值,未挂载,data 和 events 已经初始化好,data 已经具有响应式;在这里可以发送请求
  • mount阶段: vue 实例被挂载到真实 DOM 节点
    • beforeMount:在模版编译之后,渲染之前触发,可以发起服务端请求数据,SSR 中不可用;基本用不上这个 hook
    • mounted: 在渲染之后触发,此时可以操作 DOM,并能访问组件中的 DOM 以及 $ref,SSR 中不可用
  • update阶段:当 vue 实例里面的 data 数据变化时,触发组件的重新渲染
    • beforeUpdate:更新前,在数据变化后,模版改变前触发,切勿使用它监听数据变化
    • updated:更新后,在数据改变后,模版改变后触发,常用于重渲染后的打点,性能检测或触发 vue 组件中非 vue 组件的更新
  • destroy阶段:vue 实例被销毁
    • beforeDestroy:实例被销毁前,组件卸载前触发,此时可以手动销毁一些方法,可以在此时清理事件、计时器或者取消订阅操作
    • destroyed:卸载完毕后触发,销毁后,可以做最后的打点或事件触发操作

组件生命周期

生命周期(父子组件) 父组件 beforeCreate --> 父组件 created --> 父组件 beforeMount --> 子组件 beforeCreate --> 子组件 created --> 子组件 beforeMount --> 子组件 mounted --> 父组件 mounted

加载渲染过程  父 beforeCreate --> 父 created --> 父 beforeMount --> 子 beforeCreate --> 子 created --> 子 beforeMount --> 子 mounted --> 父 mounted

挂载阶段  父 created --> 子 created --> 子 mounted --> 父 mounted

更新阶段(父子组件会互相影响)  父 beforeUpdate --> 子 beforeUpdate --> 子 updated --> 父 updated

销毁阶段  父 beforeDestroy --> 子 beforeDestroy --> 子 destroyed --> 父 destroyed

computed & watch

通俗来讲,既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed

  • computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量改变时,计算属性也会跟着改变,它可以设置 getter 和 setter

  • watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法。

watch(属性监听):是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用

computed(计算属性):属性的结果会被缓存,当 computed 中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取,除非依赖的响应式属性变化时才会重新计算;主要当做属性来使用, computed 中的函数必须用 return 返回最终的结果;data 不改变,computed 不更新

使用场景

  • computed:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能
  • watch:当一条数据影响多条数据的时候使用,例:搜索数据

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

一个组件被复用多次的话,就会创建多个实例;如果 data 是对象的话,对象属于引用类型,就会影响到所有的实例;所以为了保证组件之间 data 不冲突,data 必须是一个函数

本质上,这些实例用的都是同一个构造函数

为什么 v-for 和 v-if 不建议用在一起

  1. v-forv-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中,如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费
  2. 这种场景建议使用 computed,先对数据进行过滤

注意

3.x 版本中  v-if  总是优先于  v-for  生效;由于语法上存在歧义,建议避免在同一元素上同时使用两者

v-if 和 v-show 的区别

  • v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点
  • v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)

使用场景

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

如何实现 v-model

v-model 是 Vue 提供的一个语法糖,用于动态绑定

<input v-model="value" />

// 等价于

<input :value="value" @input="value = $event.target.value" />

Vue 内置指令

Vue 内置指令
Vue 内置指令

Vue 自定义指令

指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能

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

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

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

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

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

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

Vue 修饰符有哪些

事件修饰符

  • .stop:阻止事件继续传播
  • .prevent:阻止标签默认行为
  • .capture:使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
  • .self:只当在 event.target 是当前元素自身时触发处理函数
  • .once:事件将只会触发一次
  • .passive:告诉浏览器你不想阻止事件的默认行为

v-model 的修饰符

  • .lazy:通过这个修饰符,转变为在 change 事件再同步
  • .number:自动将用户的输入值转化为数值类型
  • .trim:自动过滤用户输入的首尾空格

键盘事件的修饰符

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

系统修饰键

  • .ctrl
  • .alt
  • .shift
  • .meta

鼠标按钮修饰符

  • .left
  • .right
  • .middle

v-for 中 key 的作用

  • key 的作用是为了在 diff 算法执行时更快的找到对应的节点,提高diff速度,更高效的更新虚拟DOM;

  • 为了在数据变化时强制更新组件,以避免 “就地复用” 带来的副作用。

    当 Vue.js 用  v-for  更新已渲染过的元素列表时,它默认用 “就地复用” 策略;如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素;重复的 key 会造成渲染错误

vue 组件的通信方式

  • 父子组件通信

    • 父 -> 子props,子 -> 父 $emit
    • 获取父子组件实例 $parent&$children,通过实例的方式调用组件的属性或者方法
  • 跨级组件通信

    • Vuex
    • $ref
    • $attrs&$listeners
      • $attrs:在组件上使用 v-bind="$attrs", 将父组件中不被认为 props 特性绑定的属性传入子组件中,通常配合 interitAttrs 选项一起使用
      • $listeners:它是一个对象,里面包含了作用在这个组件上的所有监听器,你就可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素
    • Provide&inject 官方不推荐使用,但是写组件库时很常用
  • 兄弟组件通信 Event Bus

    // 事件总线
    class Bus {
      constructor() {
        // 存放事件的名字
        this.callbacks = {};
      }
    
      $on(name, fn) {
        //事件名字,事件函数体
        this.callbacks[name] = this.callbacks[name] || [];
        this.callbacks[name].push(fn);
      }
    
      $emit(name, args) {
        //事件名字,事件参数
        if (this.callbacks[name]) {
          this.callbacks[name].forEach((cb) => cb(args));
        }
      }
    }
    
    // main.js
    Vue.prototype.$bus = new Bus();
    
    // bus1.vue
    this.$bus.emit("test", "");
    
    // bus2.vue
    this.$bus.on("test", this.handle);
    

$set 方法

了解 Vue 响应式原理都知道在两种情况下修改数据 Vue 是不会触发视图更新的

  1. 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
  2. 直接更改数组下标来修改数组的值

原理

因为响应式数据 我们给对象和数组本身都增加了__ob__属性,代表的是 Observer 实例

当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪,然后会触发对象__ob__的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组

nextTick

作用

  1. nextTick 是 Vue 提供的一个全局 API,是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM

实现原理

  1. 在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM
  2. nextTick 主要使用了宏任务和微任务;根据执行环境分别尝试采用 Promise、MutationObserver、setImmediate,如果以上都不行则采用 setTimeout 定义了一个异步方法,多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。

插槽

slot 说白了就是一个占位的

插槽类型

  • 匿名插槽:没有名字的默认都填到这里

  • 具名插槽:有名字的,根据名字填充到规定的位置

  • 作用域插槽:以让父组件使用插槽时插槽内容能够访问子组件中的数据

    <!-- 父组件 -->
    <template slot-scope="{ user }">
      <div>user.name</div>
      <div>user.age</div>
      <template>
        <!-- 子组件 -->
        <slot :user="user"></slot
      ></template>
    </template>
    

keep-alive

作用

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载

  • 常用的两个属性:include / exclude,允许组件有条件的进行缓存
  • 两个生命周期:activated / deactivated,用来得知当前组件是否处于活跃状态

场景:tabs、标签页、后台导航、vue 性能优化

原理

Vue.js 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构

它将满足条件 pruneCache&pruneCache 的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将vnode 节点从 cache 对象中取出并渲染。

在动态组件中的应用

<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <component :is="currentComponent"></component>
</keep-alive>

在 vue-router 中的应用

<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <router-view></router-view>
</keep-alive>

参考: Vue 官网 - keep-aliveopen in new window

mixin

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到 mixin,多个组件有相同的逻辑,抽离出来

mixin 并不是完美的解决方案,会有一些问题 vue3 提出的 Composition API 旨在解决这些问题

场景:PC 端新闻列表和详情页一样的右侧栏目,可以使用 mixin 进行混合

劣势:

  1. 变量来源不明确,不利于阅读
  2. 多 mixin 可能会造成命名冲突
  3. mixin 和组件可能出现多对多的关系,使得项目复杂度变高

Vue Router 相关

完整的导航解析流程:

  1. 导航被触发
  2. 在失活的组件里调用 beforeRouteLeave 守卫
  3. 调用全局的 beforeEach 守卫
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)
  9. 导航被确认
  10. 调用全局的 afterEach 钩子
  11. 触发 DOM 更新
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

路由模式

hash 模式

兼容性好但是不美观

location.hash 的值实际就是 URL 中 # 后面的东西

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

可以为 hash 的改变添加监听事件 window.addEventListener("hashchange", funcRef, false)

每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

history 模式

虽然美观,但是刷新会出现 404 需要后端进行配置

利用了 HTML5 History Interface 中新增的 pushState()replaceState() 方法

这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能

这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础

路由传参

编程式的导航

  1. 通过 params 属性传值

    params 必须搭配 name 使用

    // 路由配置文件
    {
      path: '/test',
      name: 'test',
      component: Test
    }
    
    // 跳转
    this.$router.push({
      name: 'test',
      params: {
        id: 1
      }
    })
    
    // 获取参数
    this.$route.params
    
  2. 通过 query 属性传值

    // 路由配置文件
    {
      path: '/test',
      name: 'test',
      component: Test
    }
    
    // 跳转
    this.$router.push({
      path: 'test',
      query: {
        id: 1
      }
    })
    
    // 获取参数
    this.$route.query
    
  3. 动态路由

    // 路由配置文件
    {
      path: '/test/:id',
      name: 'test',
      component: Test
    }
    
    // 跳转
    this.$router.push('/test/' + id)
    
    // 获取参数
    this.$route.params.id
    

声明式的导航

let path1 = {
  name: 'test',
  params: {
    id: 1
  }
}

let path2 = {
  path: 'test',
  query: {
    id: 1
  }
}

let path3 = '/test/' + id

<router-link :to="path1">target</router-link>
<router-link :to="path2">target</router-link>
<router-link :to="path3">target</router-link>

Vuex 的理解及使用场景

vuex
vuex

Vuex 是一个专为 Vue 应用程序开发的状态管理模式

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

改变 store 中的状态的唯一途径就是显式地提交 (commit)mutation,这样使得我们可以方便地跟踪每一个状态的变化 Vuex 主要包括以下几个核心模块:

  1. State:定义了应用的状态数据
  2. Getter:像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
  3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
  4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
  5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

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

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

export default function (app) {
  // 离开页面刷新前,将store中的数据存到session
  window.addEventListener("beforeunload", () => {
    sessionStorage.setItem("storeCache", JSON.stringify(app.store.state));
  });
  // 页面加载完成,将session中的store数据
  window.addEventListener("load", () => {
    const storeCache = sessionStorage.getItem("storeCache");
    if (storeCache) {
      // 将session中的store数据替换到store中
      app.store.replaceState(JSON.parse(storeCache));
    }
    // 获取数据后,清除
    sessionStorage.removeItem("storeCache");
  });
}

Vue 项目的性能优化

  • 对象层级不要过深,否则性能就会差
  • 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
  • v-ifv-show 区分使用场景
  • computedwatch 区分使用场景
  • v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
  • 大数据列表和表格性能优化-虚拟列表/虚拟表格
  • 防止内部泄漏,组件销毁后把全局变量和事件销毁
  • 图片懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 适当采用 keep-alive 缓存组件
  • 防抖、节流运用
  • 服务端渲染 SSR or 预渲染

虚拟 DOM 是什么 有什么优缺点

由于在浏览器中操作 DOM 是很昂贵的,频繁的操作 DOM,会产生一定的性能问题,这就是虚拟 Dom 的产生原因,Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现

Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

优点

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;

  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点

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

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

参考

面试题open in new window

上次编辑于:
贡献者: ViewRoom