从源码解读 - Vue常考面试题

420 阅读30分钟

* 回答面试题的套路

1、先说这个点的明确定义,或者是特性;

2、再说具体的应用场景;

3、说说自己的看法、观点;

4、可以稍微举一反三,说说同类特性,或者类似的框架,更好的方案。

正文从这里开始~~~

NO1.常考-基础点

1、MVVM结构比MVC好在哪里 (面试常问 )

MVC模式是MVVM模式的基础,mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。

mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到View 。

mvc特点,所有通信都是单向的。

  1. View 传送指令到 Controller;

  2. Controller 完成业务逻辑后,要求 Model 改变状态;

3)Model 将新的数据发送到 View,用户得到反馈。

MVVM 映射关系的简化。

1)各部分之间的通信,都是双向的;

2)采用双向绑定: View 的变动,自动反映在 ViewModel,反之亦然。

Vue核心特性

数据驱动(MVVM),组件化,指令系统。

库和框架的区别:库我们用户主动调用库中的方法,框架的特点我们被动的被别人调用。

2、对 SPA 单⻚⾯的理解,优缺点是什么?

*核心答案:

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

优点:

1)⽤户体验好、快,内容的改变不需要重新加载整个⻚⾯,避免了不必要的跳转和重复渲染;

2)SPA 相对对服务器压⼒⼩;

3)前后端职责分离,架构清晰,前端进⾏交互逻辑,后端负责数据处理;

缺点:

1)⾸屏(初次)加载慢:为实现单⻚ Web 应⽤功能及显示效果,需要在加载⻚⾯的时候将JavaScript、CSS 统⼀加载,部分⻚⾯按需加载;

2)不利于 SEO:由于所有的内容都在⼀个⻚⾯中动态替换显示,所以在 SEO 上其有着天然的弱势。

3、Vue 常用修饰符 (重要)(面试常问 )

  1. 组件在编译的时候会对一些修饰符做处理 (根据不同的修饰符 生成不同的代码)

  2. 真正运行的时候 也要去处理一些修饰符

  3. once,passive,capture 得在绑定事件的时候在进行特殊处理。 再创建的时候会依次调用对应的属性钩子来实现创建对应的功能 [create,update,destroy]

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

1)表单修饰符

  • lazy change事件之后再进行信息同步;

  • trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤;

  • number 自动将用户的输入值转为数值类型;

2)事件修饰符

  • stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法

  • prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法

  • self 只当在 event.target 是当前元素自身时触发处理函数

  • once 绑定了事件以后只能触发一次,第二次就不会触发

  • capture 用于事件捕获

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

  • native 绑定原生事件

3)鼠标按键修饰符

  • left 左键点击

  • right 右键点击

  • middle 中键点击

4)键值修饰符

  • 普通键(enter、tab、delete、space、esc、up...)
  • 系统修饰键(ctrl、alt、meta、shift...)

5)v-bind修饰符

  • async 能对props进行一个双向绑定

  • prop 设置自定义标签属性,避免暴露数据,防止污染HTML结构

  • camel 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox

给普通的HTML标签监听一个事件,之后添加 .native 修饰符是不会起作用的。 

<div id="app"> <a href="#" v-on:click.native="clickFun">click me</a> </div>

给某个组件的根元素上监听一个事件,之后添加 .native 修饰符就会起作用了。 

<div id="app"> <my-component v-on:click.native="clickFun"></my-component> </div>

4、Vue中.sync修饰符的作用,用法及实现原理

  • .sync 在vue3中被删除掉了, 类似于v-model的语法糖 可以解析出对应的结果

  • :xxx.sync="abc" :xxx="abc" :update:xxx="v=>abc=v" 可以实现.sync

  • v-model 默认传递的值叫value和input除非用户改写 如果传递多个数据

5、如何理解自定义指令

  • 1.在生成ast语法树时,遇到指令会给当前元素添加directives属性 '{directive:'v-for',name:for}'

  • 2.通过genDirectives生成指令代码

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

  • 4.当执行cbs对应的钩子时,调用对应指令定义的方法 (create/update/destroy) , 调用用户自定义指令的钩子函数 (inserted,bind,unbind, componentUpdate)

    directives: [{ name: "model", rawName: "v-model", value: (xxx), expression: "xxx" }]

6、new Vue() 发生了什么?

*核心答案:

1)结论:new Vue()是创建Vue实例,它内部执行了根实例的初始化过程。

2)具体包括以下操作:

  • 选项合并

  • childrenchildren,refs,slotsslots,createElement等实例属性的方法初始化

  • 自定义事件处理

  • 数据响应式处理

  • 生命周期钩子调用 (beforecreate created)

  • 可能的挂载

3)总结:new Vue()创建了根实例并准备好数据和方法,未来执行挂载时,此过程还会递归的应用于它的子组件上,最终形成一个有紧密关系的组件实例树。

源码地址:src/core/instance/init.js

7、Vue.use是干什么的?原理是什么?

*核心答案:

vue.use 是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。

1、检查插件是否注册,若已注册,则直接跳出;

2、处理入参,将第一个参数之后的参数归集,并在首部塞入 this 上下文;

3、执行注册方法,调用定义好的 install 方法,传入处理的参数,若没有 install 方法并且插件本身为 function 则直接进行注册;

*补充回答:

  1. 使用VUE的插件 都会 通过Vue.use(plugin) , 用户在使用这个插件的时候将VueRouter传入,从而解决版本依赖的问题。

  2. VueRouter库 依赖于vue的 我的vue版本是 2.6 , 分离插件和Vue的强依赖。

  3. 插件不能重复的加载

install 方法的第一个参数是vue的构造函数,其他参数是Vue.set中除了第一个参数的其他参数; 代码:args.unshift(this)

  1. 调用插件的install 方法 代码:typeof plugin.install === 'function'

  2. 插件本身是一个函数,直接让函数执行。 代码:plugin.apply(null, args)

  3. 缓存插件。 代码:installedPlugins.push(plugin)

源码地址:src/core/global-api/use.js

8、请说一下Vue2.X响应式数据的理解?

*核心答案:

根据数据类型来做不同处理,数组和对象类型当值变化时如何劫持。

  1. 对象内部通过defineReactive方法,使用 Object.defineProperty() 监听数据属性的 get 来进行数据依赖收集,再通过 set 来完成数据更新的派发;

**重要:**多层对象是通过递归来实现劫持。缺陷就是要递归构建,不存在的属性无法被监控到。

  1. 数组则通过重写数组方法来实现的。扩展它的 7 个变更⽅法,通过监听这些方法可以做到依赖收集和派发更新;( push/pop/shift/unshift/splice/reverse/sort )

*优化方案:

  • Vue2中减少层级数据嵌套不要过深;

  • 如果不需要响应式的数据就不要放在data中, 合理使用Object.freeze;

  • 我们尽量缓存使用过的变量;

这里在回答时可以带出一些相关知识点 (比如多层对象是通过递归来实现劫持,顺带提出vue3中是使用 proxy来实现响应式数据)

扩展:

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

*补充回答:

响应式流程:

1、defineReactive 把数据定义成响应式的;

2、给属性增加一个 dep,用来收集对应的那些watcher;

3、等数据变化进行更新

dep.depend() // get 取值:进行依赖收集

dep.notify() // set 设置时:通知视图更新

这里可以引出性能优化相关的内容:1)对象层级过深,性能就会差。2)不需要响应数据的内容不要放在data中。3)object.freeze() 可以冻结数据。

源码地址:src/core/observer/index.js 158

9、Vue3.x响应式数据原理

*核心答案:

vue3采用了proxy直接可以对对象拦截(监听对象和数组的变化),不用重写get和set方法,性能高,不需要直接递归。 对数组并没有采用defineProperty 因为数组中可能数组很长但是用户不会操作索引更改数据。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。

10、Vue如何检测数组变化?

*核心答案:

数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组 方法以进行重写。当数组调用到这 7 个方法的时候,执行 ob.dep.notify() 进行派发通知 Watcher 更新;

* 重写数组方法:push/pop/shift/unshift/splice/reverse/sort (7个变异的方法 能改变原数组的方法)

*补充回答:

vue2中并没有使用defineProperty来检测我们的数组(性能差),vue2里面就采用重写数组的方法来实现 (7个变异的方法 能改变原数组的方法) 通过原型链 + 函数劫持的方式实现的 (缺陷是不能检测到索引更改和数组长度的更改) 数组中的元素也会被再次观测。

  1. 在Vue中修改数组的索引和长度是无法监控到的。

  2. 数据中如果是对象数据类型也会进行递归劫持。

需要通过以下7种变异方法修改数组才会触发数组对应的wacther进行更新。数组中如果是对象数据类型也会进行递归劫持。

说明:那如果想要改索引更新数据怎么办?

可以通过Vue.set()来进行处理 =》 核心内部用的是 splice 方法。

// 取出原型方法;
const arrayProto = Array.prototype  
// 拷贝原型方法;
export const arrayMethods = Object.create(arrayProto)  
// 重写数组方法;
def(arrayMethods, method, function mutator (...args) { }
ob.dep.notify()  // 调用方法时更新视图;

源码地址:src/core/observer/array.js 15

11、Vue中如何进行依赖收集? (观察者模式)

内部依赖收集是怎么做到的?

  1. 每个属性都拥有自己的dep属性,存放他所依赖的 watcher,当属性变化后会通知自己对应的 watcher去更新。

  2. 默认在初始化时会调用render函数,些时会触发属性依赖收集 dep.depend();

  3. 当属性发生修改时会触发 watcher 更新 dep.notify();

依赖收集的目的是,等会数据变化了可以更新视图, 如何收集的 每个属性都有一个dep属性、每个对象也都有一个dep属性。 每个组件在渲染的过程中都会创建一个渲染watcher (watcher有三种,渲染watcher,计算属性watcher,用户watcher) , 一个属性可能会有多个watcher, 反过来一个watcher有多个dep。

当调用取值方法的时候如果有watcher就会将watcher收集起来, 等会数据变化后会通知自己对应的dep触发更新调用watcher.update方法。

12、Vue.set 方法是如何实现的?

*核心答案:

为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性,当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组。

重要:

  1. 如果对象是响应式的那么对象本身就会有一个dep属性,新增属性后触发dep对应的watcher去更新;

  2. 针对数组内部会调用splice方法,针对对象会调用defineReactive方法并且手动notify;

*补充回答:

官方定义 Vue.set(object, key, value)

  1. 如果是数组,调用重写的splice方法 (这样可以更新视图 )

代码:target.splice(key, 1, val)

  1. 如果不是响应式的也不需要将其定义成响应式属性。

  2. 如果是对象,将属性定义成响应式的 defineReactive(ob.value, key, val)

通知视图更新 ob.dep.notify()

源码地址:src/core/observer/index.js 202

13、Vue中模板编译原理?

*核心答案:

模板编译原理的核心就是 ast -> 生成代码。

如何将template转换成render函数(这里要注意的是我们在开发时尽量不要使用template,因为将template转化成render方法需要在运行时进行编译操作会有性能损耗,同时引用带有complier包的vue体积也会变大) 默认.vue文件中的 template处理是通过vue-loader 来进行处理的并不是通过运行时的编译。

  1. 将 template 模板转换成 ast 语法树 - parserHTML

  2. 对静态语法做静态标记 - markUp (vue3中模板编译做了哪些优化 patchFlag, blockTree,事件缓存,节点缓存。。。)

  3. 重新生成代码 - codeGen 拼接render函数字符串 + new Function + with

*补充回答:

模板引擎的实现原理就是new Function + with来进行实现的。

vue-loader中处理template属性主要靠的是 vue-template-compiler

vue-loader

// template => ast => codegen => with+function 实现生成render方法 
let {ast, render } = VueTemplateCompiler.compile(`<div>{{aaa}}</div>`)
console.log(ast, render)
// 模板引擎的实现原理 with + new Function
console.log(new Function(render).tostring())
// render方法执行完毕后生成的是虚拟 dom
// with(this){return _c('div',[_s(aaa)])}
// 代码生成

源码设置:

const ast = parse(template.trim(), options) // 将代码解析成ast语法树
  if (options.optimize !== false) {
    optimize(ast, options) // 优化代码 标记静态点 标记树
  }
  const code = generate(ast, options) // 生成代码

源码地址:src/compiler/index.js

14、Proxy 与 Object.defineProperty 优劣对比

*核心答案:

Proxy 的优势如下:

1)Proxy 可以直接监听对象而非属性;

2)Proxy 可以直接监听数组的变化;

3)Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;

4)Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;

5)Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

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

NO2.常考-生命周期

1、Vue的生命周期方法有哪些?一般在哪一步发起请求及原因

*核心答案:

总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。

1、创建前/后:

  1. beforeCreate阶段:vue实例的挂载元素el和数据对象data都为undefined,还未初始化。

说明:在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。

  1. created阶段:vue实例的数据对象data有了,el还没有。

说明:可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。

2、载入前/后:

  1. beforeMount阶段:vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点。

说明:当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

  1. mounted阶段:vue实例挂载完成,data.message成功渲染。

说明:在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

3、更新前/后

  1. beforeUpdate阶段:响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器。

说明:可以在当前阶段进行更改数据,不会造成重渲染。

  1. updated阶段:虚拟DOM重新渲染和打补丁之后调用,组成新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环。

说明:当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

4、销毁前/后

  1. beforeDestroy阶段:实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件。

说明:在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

  1. destroyed阶段:实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。

说明:当前阶段组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

*补充回答:

第一次页面加载时会触发:beforeCreate, created, beforeMount, mounted。

  1. created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。(服务器渲染支持created方法)

  2. mounted 实例已经挂载完成,可以进行一些DOM操作。(接口请求)

源码地址:src/core/instance/lifecycle.js

2、生命周期钩子是如何实现的?

*核心答案:

  1. Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。

  2. 内部会对钩子函数进行处理,将钩子函数维护成数组的形式。

*补充回答:

生命周期钩子在内部会被vue维护成一个数组 (vue内部有一个方法 mergeOptions) 和全局的生命周期合并最终转换成数组,当执行到具体流程时会执行钩子 (发布订阅模式) callHook 来实现调用。

源码地址:src/core/util/options.js 146 core/instance/lifecycle.js 336

3、Vue 的父组件和子组件生命周期钩子执行顺序

*核心答案:

第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子。

渲染过程:

父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件mouted之后

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

子组件更新过程:

影响到父组件:父beforeUpdate -> 子beforeUpdate->子updated -> 父updted

不影响父组件:子beforeUpdate -> 子updated

父组件更新过程:

影响到子组件:父beforeUpdate -> 子beforeUpdate->子updated -> 父updted

不影响子组件:父beforeUpdate -> 父updated

销毁过程:

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

重要:父组件等待子组件完成后,才会执行自己对应完成的钩子。

NO3.常考-组件通信

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

*核心答案:

因为内部会调用Vue.extend 会将组件的定义传入,此时会将用户的参数进行合并检测data属性,如果data不是函数会报警告,会将当前定义的data合并到组件的内部,如果data是一个对象就存在数据被共享的可能。

const Sub = Vue.extend({ data(){ return {a:1} } }) new Sub({props,slot}); new Sub({props,slot});

每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。

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

源码地址:src/core/util/options 121

2、Vue 组件间通信有哪几种方式?

*核心答案:

Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。1、props / $emit 适用 父子组件通信这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

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

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

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

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

6、Vuex 适用于 父子、隔代、兄弟组件通信Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

3、组件中写 name选项有哪些好处及作用?

*核心答案:

  1. 可以通过名字找到对应的组件 ( 递归组件 )

  2. 可以通过name属性实现缓存功能 (keep-alive)

  3. 可以通过name来识别组件 (跨级组件通信时非常重要)

好处1: 就是可以在自己组件中,循环使用自己的组件;

好处2: 有了名字以后可以定位到具体的组件 不停的向上找到某个组件,给这个组件派发事件;

Vue.extend = function () {
    if(name) {
        Sub.options.componentd[name] = Sub
    }
}

源码地址:src/core/vdom/create-element.js 111

4、keep-alive平时在哪里使用?原理是?

*核心答案:

  1. 缓存是什么? 缓存组件的实例,组件的实例上 vm.el(缓存了实例就是缓存了dom元素)。组件在切换的时候如果有缓存,直接可以复用上次渲染出的vm.el(缓存了实例就是缓存了dom元素)。组件在切换的时候如果有缓存,直接可以复用上次渲染出的vm.el结果。

  2. keep-alive 不用做任何渲染操作 , 内部使用了一个LRU算法来管理缓存( 抽象组件)。

  3. keep-alive中组件切换的时候 插槽会触发更新,如果插槽的内容变化了 会重新渲染$forceupdate, 重新进行渲染。

keep-alive 主要是组件缓存,采用的是LRU算法。最近最久未使用法。

常用的两个属性include/exclude,允许组件有条件的进行缓存。

两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。

abstract: true, // 抽象组件 
props:{
    include: patternTypes,  // 要缓存的有哪些
    exclude: patternTypes, // 要排除的有哪些
    max: [String, Number] //最大缓存数量 
}
if(cache[key]) { // 通过key 找到缓存,获取实例
    vnode.componentInstance = cache[key].componentInstance
    remove(keys, key) //将key删除掉 
    keys.push(key) // 放到末尾 
} else {
    cache[key] = vnode // 没有缓存过 
    keys.push(key) //存储key
    if(this.max && keys.length > parseInt(this.max)) { // 如果超过最大缓存数 
    // 删除最早缓存的 
    pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true // 标记走了缓存

5、Vue.minxin的使用场景和原理?

*核心答案:

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

  1. Vue.mixin 价值在哪里主要解决的问题就是公共逻辑,抽离可用采用mixin (比较常见的 react高阶函数, hooks,compositionApi)。

  2. 缺陷数据来源不明确,命名冲突问题 》 vue3 采用了组合式api更加方便。

  3. 内部实现原理 主要采用的就是mergeOptions 把数据合并到全局的options中,每个组件初始化的时候会将选项进行合并。

*补充回答:

mixin中有很多缺陷“命名冲突问题”,“依赖问题”,“数据来源问题”,这里强调一下mixin的数据是不会被共享的。

源码地址:src/core/util/options.js

6、谈一谈对Vue组件化的理解

组件化最早出现在webComponent浏览器可以实现自定义标签兼容性差。 组件化的好处就是 (为了能实现组件级更新,合理规划代码,复用性强, 单向数据流)

组件化会常用到一些技术 属性、事件、插槽 (我封装过哪些组件,自己是怎样封装组件的,如何基于原有组件进行二次开发)

7、Vue的组件渲染流程

编写的组件(用的时候都是标签的方式来使用 ) -> AST语法树,识别的时候会根据组件创建一个虚拟节点 -> 组件变成真实节点 -> 插入到页面中

  1. 注册组件,在当前实例中可以获取到组件

  2. Vue.extend 根据组件对象创造一个组件类 Sub

  3. 创建一个组件的虚拟节点, 虚拟节点上组件会增加生命周期钩子 init方法

  4. 组件的虚拟节点上会包含一个componentOptions(Sub,children...)

  5. 组件的初始化, 就会调用组件的init钩子 ( new Sub ($mount) )

  6. 根据组件的内容生成一个虚拟节点,创建节点, 插入到页面上

8、Vue组件更新流程

什么情况会导致组件更新? 1) 组件自己的状态发生了变化 2) props变化也会导致更新 父组件更新了同时导致了子组件更新。

组件属性发生变化会执行patchVnode (比对属性,用新的属性覆盖掉来的属性)vm.$options.propsData = propsData 如果响应式数据变化了 那么页面会更新

9、Vue中异步组件原理

对标图片的懒加载,流程就是先渲染一个空节点, 之后组件加载完毕了,需要重新强制渲染 重新进行组件的加载。

10、函数组件的优势及原理

缺点就是无状态、无生命周期,无this 没有自己的数据源,可以接受props ,单纯的页面渲染可以采用函数组件 (正常组件是一个类 _init(),但是函数组件就是一个普通的函数) 函数式组件 没watcher , 父亲重新渲染,那么函数式组件就重新渲染了。

11、Vue组件间传值的方式及之间区别

交互方式有多少种 “好多”

  • props,emit 典型的父子通信

  • 兄弟之间用什么传值? 可以借助共同的父级来通信, 或者采用eventBus

  • 跨组件通信 eventBus,vuex可以实现 vue.observable

  • inject provide 开发组件库可以使用其他场景不建议使用,因为导致数据来源不明确

  • parentparent children 获取父子组件的实例

  • $refs 父拿到子的实例

  • attrs listeners 也可以通信 父组件传递给子组件的所有属性和方法

*props和emit实现

  1. props 在创建虚拟节点的时候 会被抽离到componentOptions中的propsData中 ,就是在初始化的时候将propsData定义在了组件的 _props上,最后代理到实例上

  2. emit 默认给组件绑定的事件 会定义在组件的实例上 核心就是发布订阅 onon emit 解析时会将事件全部放在vm.$options._parentListeners

*parent children

在组件初始化的时候可以拿到父组件,构造父子关系

*inject provide如何实现跨级通信 父 {a:1}-》 孙子-》 曾孙 -》 玄孙

  1. 父组件将数据定义在vm._provide上

  2. 子组件通过vm.$parent 向上找,最后将找到的属性定义在自己的上

attrs listeners

  • 获取所有的事件和属性直接定义在实例上即可

  • defineReactive(vm, '$attrs', parentData && parentData.attrs)

  • defineReactive(vm, '$listeners', options._parentListeners)

refs

  • 给组件添加后可以获取组件实例

  • $refs = vnode.componentInstance || vnode.elm;

$attrs是为了解决什么问题出现的,provide和inject不能解决它能解决的问题吗?

  • v-bind="attrs"v−on="attrs"v−on="listeners" 可以直接快速的将属性和事件向下传递。 一层层的传递,不能实现跨级传递
  • provide和inject 主要是跨级通信,不用在进行传递了 可以在父组件中提供出来,子组件直接消费

12、Vue中slot是如何实现的?什么时候使用它?

1)具名插槽 构建一个映射表

2)普通插槽 是在父组件中渲染vnode的 (只能用父组件的数据,渲染后传递给儿子)

3)作用域插槽 在子组件中渲染vnode的 (可以使用子组件的数据来继续渲染) 表格组件 自定义列中的内容 (如果更新的话 插槽也会被更新 前后插槽不一致会强制重新渲染的)

NO4.常考-路由

1、Vue-router有几种钩子函数?具体是什么及执行流程是怎样的?

*核心答案:

路由钩子的执行流程,钩子函数种类有:全局守卫、路由守卫、组件守卫。

*完整的导航解析流程

1.导航被触发;

2.在失活的组件里调用beforeRouteLeave守卫;

3.调用全局beforeEach守卫;

4.在复用组件里调用beforeRouteUpdate守卫;

5.调用路由配置里的beforeEnter守卫;

6.解析异步路由组件;

7.在被激活的组件里调用beforeRouteEnter守卫;

8.调用全局beforeResolve守卫;

9.导航被确认;

10.调用全局的afterEach钩子;

11.DOM更新;

12.用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数。

2、vue-router 两种模式的区别?

*核心答案:

vue-router 有 3 种路由模式:hash、history、abstract。

  1. hash模式:hash + hashChange

特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。通过监听 hash(#)的变化来执行js代码 从而实现 页面的改变。

核心代码:

window.addEventListener(‘hashchange‘,function(){

self.urlChange()

})

  1. history模式:historyApi + popState

HTML5推出的history API,由pushState()记录操作历史,监听popstate事件来监听到状态变更;

因为 只要刷新 这个url(www.ff.ff/jjkj/fdfd/f… 配置一下服务器端。

说明:

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

NO5.常考-属性作用与对比

1、nextTick在哪里使用?原理是?

*核心答案:

nextTick的回调是在下次DOM更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。nextTick主要使用了宏任务和微任务。原理就是异步方法(promise, mutationObserver, setImmediate, setTimeout)经常与事件循环一起来问。

  1. nextTick功能是批处理,多次调用默认会将逻辑暂存到队列中,稍后同步代码执行完毕后会采用,同步的方式依次刷新队列 (nextTick本身内部采用了异步方法,但是执行逻辑的时候采用的是同步)。

  2. 内部实现原理(异步的实现原理 先采用promise.then 不行在采用 mutationObserver 不行在采用 setImmediate 不行在采用 setTimeout 优雅降级。

*补充回答:

vue多次更新数据,最终会进行批处理更新。内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用,从而可以获取更新后的DOM。

源码地址:src/core/util/next-tick.js 42

2、Vue 为什么需要虚拟DOM? 虚拟DOM的优劣如何?

*核心答案:

Virtual DOM 就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM (减少了对真实DOM的操作)。虚拟DOM不依赖真实平台环境从而也可以实现跨平台。

*重要:

  • 最核心的是跨端,不同的平台实现方案不同。 内部实现可以不局限于针对浏览器平台;

  • 如果开发者频繁操作dom可能会浪费性能,虚拟dom你可以认为增加了一层缓存,我们会先更新虚拟dom,在更新到页面上;

  • 因为dom diff比较的是前后的虚拟dom 比较差异更新页面 (也可以真实domdiff性能差);

  • 多次dom操作浏览器会进行合并的;

*补充回答:

虚拟DOM的实现就是普通对象包含tag、data、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存)

Vue2的 Virtual DOM 借鉴了开源库snabbdom的实现。

VirtualDOM映射到真实DOM要经历VNode的create、diff、patch等阶段。

源码地址:src/core/vdom/vnode: 3

3、Vue中key的作用和工作原理,说说你对它的理解

*核心答案:

例如:v-for="(item, itemIndex) in tabs" :key="itemIndex"

key的作用主要是为了高效的更新虚拟DOM,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。

*补充回答:

key的作用是为了标识唯一性, 在diff算的时候可以进行复用。 判断是否是相同节点 (tag,key)。 key尽量在动态列表中不要使用索引(如果使用的是索引,相当于就没有写key), 可能会导致更新出问题。(如果是死的列表可以使用索引作为key)

源码地址:src\core\vdom\patch.js - updateChildren()

4、Vue 中的diff原理

*核心答案:

vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。

*重要:

  1. diff算法是O(n)级别的,采用的是同级比较, 内部是深度优先遍历的方式遍历节点;

  2. 节点判断是否是同一个元素,如果是同一个元素,则比对属性比对孩子,如果不是则直接删除老的换成新的

  3. Vue2中采用了双指针对一些场景做了优化策略 (如果是静态节点可以跳过diff算法)

  4. 头头,尾尾,尾头,头尾进行优化

  5. 最后乱序比较就是根据老节点创造一个映射表,用新的去里边找能复用的就复用节点 (乱序的时候可能中间的顺序是固定的但是都会做一次移动)

vue3 里面还有一个blockTree概念,如果是通过模板编译的,会把dymanicChildren组成数组直接数组比对,性能更好,如果不能使用这种方式才采用全量比对 (v-for) ;

vue3优化移动节点的时候采用了最长递增子序列来实现 贪心+二分查找+前驱节点实现的 O(nlogn);

*补充回答:

  1. 先比较是否是相同节点

  2. 相同节点比较属性,并复用老节点

  3. 比较儿子节点,考虑老节点和新节点儿子的情况

  4. 优化比较:头头、尾尾、头尾、尾头

  5. 比对查找进行复用

Vue2 与 Vue3.x 的diff算法:

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。

Vue3.x借鉴了ivi算法和 inferno算法,该算法中还运用了动态规划的思想求解最长递归子序列。(实际的实现可以结合Vue3.x源码看。)

源码地址:src/core/vdom/patch.js 501

5、v-if 与 v-for的优先级

*核心答案:

1、v-for优先于v-if被解析

2、如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能

3、要避免出现这种情况,则在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环

4、如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项

源码地址:compiler/codegen/index.js

if (el.staticRoot && !el.staticProcessed) {
   return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
   return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // 处理v-for
   return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // 处理v-if
   return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
   return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
   return genSlot(el, state)
}

6、v-if,v-model,v-for的实现原理

1)v-for 原理就是拼接一个循环函数 内部用了一个方法 _l

2)v-if 自动会被转义成三元表达式 (v-for和v-if) 并不会编译出directive来,在生成代码的时候就将这两个东西进行了转义

3)v-model 可以用在组件(可以实现组件数据的同步) (还可以用在元素上)添加指令

4)v-model在组件中就是value + input的语法糖

5)如果放到表单元素上v-model是有一些差异的 会被解析成一个指令 (在编译的时候会将v-model解析成一个指令) 默认会给input事件拼接一个处理中文输入法的问题。 在运行的时候需要调用指令(会对不同的类型做不同的处理) 指令执行的时候还会去处理修饰符 v-model.lazy

7、v-if与v-show的区别

*核心答案:

v-if 是真正的条件渲染,直到条件第一次变为真时,才会开始渲染。

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

注意:v-if 适用于不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

8、computed 和 watch 的区别和运用的场景?

*核心答案:

  1. computed和watch基于watcher来实现的;

  2. computed: 计算属性。具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行。

  3. watch: 监听数据的变化。监控值的变化,当值发生变化时调用对应的回调函数。

*补充回答:

  1. 区别是computed数据可以用于页面渲染,watch不行;

  2. computed只有在取值时才会执行对应的回调(lazy为true所以不会立即执行),watch默认会执行一次(拿到老的值)。 computed用了一个dirty属性实现了缓存机制,多次取值如果依赖的值不发生变化不会更改dirty的结果,拿到的就是老的值。

运用场景:

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

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

9、如何理解自定义指令?

*核心答案:

指令的实现原理,可以从编译原理 =>代码生成=> 指令钩子实现进行概述

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

2、通过 genDirectives 生成指令代码

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

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

10、V-model的原理是什么?

*核心答案:

v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。可以通过model属性的prop和event属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性。

v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

1)text 和 textarea 元素使用 value 属性和 input 事件;

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

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

NO6.常考-性能优化

*Vue性能优化

1、你都做过哪些Vue的性能优化?( 统计后的结果 )

1)编码阶段

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher;

  • 如果需要使用v-for给每项元素绑定事件时使用事件代理;

  • SPA 页面采用keep-alive缓存组件;

  • 在更多的情况下,使用v-if替代v-show;

  • key保证唯一;

  • 使用路由懒加载、异步组件;

  • 防抖、节流;

  • 第三方模块按需导入;

  • 长列表滚动到可视区域动态加载;

  • 图片懒加载;

2)用户体验:

  • 骨架屏;

  • PWA;

  • 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

3)SEO优化

  • 预渲染;

  • 服务端渲染SSR;

4)打包优化

  • 压缩代码;

  • Tree Shaking/Scope Hoisting;

  • 使用cdn加载第三方模块;

  • 多线程打包happypack;

  • splitChunks抽离公共文件;

  • sourceMap优化;

参与资料:

Vue.js 技术揭秘:

ustbhuangyi.github.io/vue-analysi…

Vue 源码地址 :

github.com/vuejs/vue