30道常见Vue面试题

2022-06-16 / Development Vue

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

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

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

基于上面一点,SPA 相对对服务器压力小;前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

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

2. v-show与v-if有什么区别?

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

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

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

3. 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'
      }
    }
    

4. 怎样理解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()
      }
    }
    

5. computed和watch的区别和运用的场景?

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

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

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed, 因为可以利用computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch 选项允许我们执行异步操作(访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

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

由于JavaScript 的限制,vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfltem] = 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)

7. 谈谈你对Vue生命周期的理解?

(1) 生命周期是什么?

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

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

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

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

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

  • 加载渲染过程
  • 子组件更新过程
  • 父组件更新过程
  • 销毁过程

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

可以在钩子西数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 己经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在created 钩子西数中调用异步请求,因为在 created 钩子西数中调用异步请求有以下优点:

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

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

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

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

比如有父组件 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 等都可以监听。

12. 谈谈你对keep-alive的了解?

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

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和exclude 属性,两者都支持宇符串或正则表达式,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated,当组件被激活时,触发钩子函数activated, 当组件被移除时,触发钩子函数 deactivated.

13. 组件中 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 的实例,是不会被复用的,因此不存在引用对象的问题。

14. 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 = Sevent.target.value">

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

<ModelChild -model= "message"></ModelChild>

子组件:

<div>{{value}}</div>

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

15. vue 组件间通信有哪几种方式?

Vue组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 vue 掌握的越熟练。vue 组件问通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
(1) props / $emit 适用 父子组件通信

这种方法是 vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

(2) ref 与 $parent / $children 适用 父子组件通信

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

(3) EventBus($emit / $on)适用于 父子、隔代、兄弟组件通信

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

(4) $attrs/$listeners 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别(且获取)的特性绑定(class 和stvle除外)。当一个组件没有声明任何 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 是一个专为 vuejs 应用程序开发的状态管理模式。每一个vuex 应用的核心就是store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态( state )。

  • Vuex 的状态存储是响应式的。当 vue 组件从 store 中读取状态的时候,若 store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

16. 你使用过 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 且同时保存在单一的状态树中。

17. 使用过 Vue SSR 吗?说说SSR?

Vue.js是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue组件进行生成DOM 和操作DOM。然而,也可以将同一个组件渲染为服务端的 HTML字符串,将它们直接发送到浏览器,最后将这些静态标记”激活”为客户端上完全可交互的应用程序。

即:SSR大致的意思就是vue在客户端将标签渲染成的整个html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。

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

  • 更好的SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜素引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过Ajax 荻取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 更快的内容到达时间(首屏加载更快):SPA 会等待所有vue 编译后的 is 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页 面直接返回显示,无需等待下载 is 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

(2) 服务端渲染的缺点:

  • 更多的开发条件限制:例如服务端渲染只支持 beforcreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node js server 运行环境;
  • 更多的服务器负载:在Node js 中渲染完整的应用程序,显然会比仅仅提供静态文件的server 更加大量占用CPU 资源 (CPU-intensive-CPU 密集),因此如果你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。

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

19. 能说下 vue-router 中常用的 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 事件,这时我们需要手动触发页面跳转(渲染)。

20. 什么是 MVVM?

ModelView-ViewModel (MvvM)是一个软件架构设计模式,由微软 WPF 和Silverlight 的架构师 Ken Cooper 和Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表MVVM 源自于经典的 Model-View-Controller (MVC)模式,MvVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是ViewModel 层,它就像是一个中转站 (value converter),负责转换Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与Model 层通过接口请求进行数据交互,起呈上启下作用。

(1) View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和CSS 来构建。
(2) Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。

21. Vue 是如何实现数据双向绑定的?

Vue 数据双向鄉定主要是指:数据变化更新视图,视图变化更新数据。

即:

  • 输入框内容变化时,Data 中的数据同步 变化。即 View=>Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即Data => View 的变化。其中,View 变化更新 Data,可以通过事件监听的方式来实现,所以 vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。

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

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

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

如果被问到 vue 怎么实现数据双向绑定,大家肯定都会回答通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 vue框架中都知道,vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:

/**
* Observe a list of Array items.
*/
observeArray(items: Array<any>) {
  for (let i = 0, | = items.length; i < I; i++) {
    observe(items[i]) // observe 功能为监测数据的变化
  }
}

/**
* 对属性进行递归遍历
*/
let childob = !shallow && observe(val) // observe 功能为监测数据的变化

通过以上 vue 源码部分 查看,我们就能知道 vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty( 也能对对象和数组(部分方法的操作)进行监听。

23. Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:

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

Object.defineProperty 的优势如下:

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

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

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

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 方法触发相应式;
  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 define Reactive 方法进行响应式处理。

25. 虛拟 DOM 的优缺点?

优点:

  • 保证性能下限:框架的虛拟 DOM 需要适配任何上层 API 可能产生的操作,它的些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情況下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM:我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虛拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台:虚拟 DOM 本质上是Javascript 对象,而 DOM 与平台强相关,相比之下虛拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

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

26. 虚拟 DOM 实现原理?

虛拟 DOM 的实现原理主要包括以下3部分:

  • 用Javascript 对象模拟真实 DOM 树,对真实DOM 进行抽象;
  • diff 算法一比较两棵虚拟 DOM树的差异;
  • pach 算法 一将两个虚拟 DOM 对象的差异应用到真正的 DOM树。

27. Vue中的key有什么作用?

key 是为 Vue 中 Vnode 的唯一标记,通过这个key,我们的diff 操作可以更准确、更快速。vue 的 diff 过程可以概括为:oldCh 和newCh 各有两个头尾的变量 oldStartindex、oldEndindex 和 newStartindex、newEndlindex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartindex 和 oldStartindex 、newEndIndex 和 oldEndindex、newStartIndex 和 oldEndIndex , newEndIndex 和oldStartindex, 如果以上 4种比较都没匹配,如果设置了key,就会用key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIndex>EndIndex 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。

所以 vue 中 key 的作用是:key是为vue 中 vnode 的唯一标记,通过这个key,我们的 diff 操作可以更准确、更快速。

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

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

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

28. 你有对Vue项目进行哪些优化?

(1) 代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

(2) Webpack 层面的优化

  • webpack 对图片进行压缩
  • 减少ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 cSS
  • 优化 SourceMap
  • 构建结果输出分析
  • vue 项目的编译优化

(3) 基础的Web技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN的使用
  • 使用 Chrome Performance 查找性能瓶颈

29. 对于的Vue3.0特性你有什么了解的吗?

Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:

(1) 监测机制的改变

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

  • 只能监测属性,不能监测对象
  • 检测属性的添加和删除;
  • 检测数组索引和长度的变更;
  • 支持 Map、Set、WeakMap 和 Weak Seto

新的 observer 还提供了以下特性:

  • 用于创建observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
  • 默认采用惰性观察。在2.x 中,不管反应式数据有多大,都会在启动时被观察到,如果你的数据集很大,这可能会在应用启动时带来明显的开销。在3.x中,只观察用于渲染应用程序最初可见部分的数据。
  • 更精确的变更通知。在2.x中,通过 vue.set 强制添加新属性将导致依赖于该对象的watcher 收到变更通知。在3.x中,只有依赖于特定属性的watcher 才会收到通知。
  • 不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 vuex 状态树以外的变化。
  • 更好的调试功能:我们可以使用新的renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2) 模板

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

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

Vue2.x 中的组件是通过声明的方式传入一系列 option,和Type Script 的结合需要通过一 些装饰器的方式来做,量然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 Type Script 的结合变得很容易。

此外,Vue 的源码也改用了 Type Script 来写。其实当代码的功能复杂之后,必须有。个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4) 其它方面的更改

Vue 3.0 的改变是全面的,上面只涉及到主要的 3个方面,还有一些其他的更改:

  • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接fork 源码来改的方式。
  • 支持 Fragment (多个根节点) 和Protal(在dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
  • 基于 treeshaking 优化,提供了更多的内置功能。

30. 说说你使用 vue 框架踩过最大的坑是什么?怎么解决的?