前言
- vue2 面试题(针对一些常问的)
1.说一下computed 和 watch 区别是什么
# 主要考察
1. computed和watch都是基于Watcher来实现的
2. 分别是计算属性watcher和用户watcher。computed属性是具备缓存的, 依赖值不发生变化,
对齐取值时计算属性方法不会重新执行(可以模板渲染, 取值的过程中不支持异步方法)
watch是监控值发生变化, 当值发生变化时调用对应的函数
# 补充回答
1. computed不会立即重新计算生成新的值,而是先标记为脏数据,
当下次computed被获取时候,才会进行重新计算并返回
2. watch则是立即执行, 将老值保存在watcher上, 当数据更新时重新计算新值, 将新值与老值
传递回答函数中
2.说一下响应式数据的理解
# 主要考察:数组和对象是如何劫持到的
1. 对象内部通过defineReactive方法, 使用的是Object.defineProperty将属性进行劫持(只会劫持已经存在的属性, 后续增加的不劫持)
2. 对数组是通过重新写数组的方法实现的(push, pop, unshift, shfit, sort, reverse, splice)
* 可以顺便带出一些相关知识 如多层对象是通过递归实现的, 顺便提一嘴Vue3中是用proxy来实现数据响应的
# 主要考察答出了 也可进行补充回答
1. 内部依赖收集怎么回事 每个属性都拥有自己的dep属性 存放它所依赖的watcher 当属性变化后会通知watcher去更新
2. 每个对象本身也有个dep属性 为了$set
# 这里可以引出性能优化相关内容
1. 对象层级过深 性能就会差
2. 不需要响应的内容不要放到data中
3. Object.freeze() 进行数据冻结(vue将不能对数据进行getter和setter)
3.说一下Vue如何检测数组变化的
# 主要考察
1. 数组考虑性能原因没有用Object.defineProperty进行拦截,
2. 选择重新写数组的七个方法(push, pop, unshift, shfit, sort, reverse, splice)
# 补充回答
1. 所以在Vue中修改数组的索引和长度是无法监控到的, 需要通过以上的7个方法, 去触发数组对应的watcher, 实现更新
2. 如果数组中是对象 会进行递归劫持
# 引发问题
1. 想要通过索引更改数据, 可以通过`Vue.$set()`处理(内部用的splice方法)
4.说一下模板编译原理
# 主要考察
1. 如何将template转换成render函数(这里要说明的是我们在开发时尽量不要使用template, 因为将template转换成render方法, 需要在运行时进行编译操作会有性能消耗
同时引用带有compiler包vue的体积也会变大, 默认的.vue文件中的template处理是通过vue-loader进行处理, 并不是通过运行时编译)
* 1.将template模板转换成ast语法树 - parserHTML方法
* 2.对静态语法做静态标记 - markUp方法
* 3.重新生成代码 - codeGen方法
# 补充回答
1. 模板引擎的实现原理就是new Function + with
2. vue-loader中处理template属性主要考的是vue-template-compiler模块
* const VueTemplateCompiler = require('vue-template-compiler')
* const { render } = VueTemplateCompiler.compile('<div id="hello">{{msg}}</div>')
5.说一下生命周期钩子是如何实现的
vue2核心原理(简易) - 生命周期初次实现 + Vue.mixin笔记
# 主要考察
1. 生命周期钩子 就是回掉函数, 在创建组件实例的过程中去调用对应的钩子方法
# 补充回答
1. 内部主要使用callHook方法去调用对应的方法(核心是个发布订阅模式), 将钩子订阅好(内部采用数组的方式存储), 然后在对应得阶段进行发布
6.说一下Vue.mixin()的使用场景和原理
vue2核心原理(简易) - 生命周期初次实现 + Vue.mixin笔记
# 主要考察(原理)
1. Vue.mixin(options), 将Vue本身的options, 和后期用户传入的options, 递归混合
2. 针对不同类型的options(options.key不同), 采用不同的策略(列队存储, 递归继承等)
3. 这些策略放在一个存储空间 strats = {}
4. 如果strats中存在对应的key策略, 就直接执行该策略, 然后返回即可
如果不存在, 就父组件(Vue.options对应的key)与子组件(options)进行合并 都有的情况下, 子覆盖父
5. 最后将父子options对应的key都执行了一遍 实现了混合策略
# 主要考察(场景)
1. 父子组件的生命周期就是采用Vue.mixin()方法将每次混入的钩子函数按照顺序都放在一个队列当中, 当触发条件时 依次执行
2. 父子组件的混合(Vue.componet), 根据父对象构建一个新的对象(Object.create(parent.options.componet)), 新对象就继承了父对象的proto,
然后将新对象依次赋予options[key] = childOptins[key], 这样如果自身没有值 就可以在父组件找到对应的值
7.说一下nextTick使用场景和原理
vue2核心原理(简易)-异步更新(Vue.nextTick)笔记
# 主要考察(场景原理)
1. nextTick中的回调是下次DOM更新循环结束之后执行的延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM
2.原理就是异步方法(如promise mutationObserver,setimmediate,setTimeout)经常与事件一起来问(宏任务和微任务)
# 补充回答
1. vue多次更新数据 最终会进行批处理更新 内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用, 从而实现更新后的DOM
8.说一下为什么需要虚拟DOM
# 主要考察(场景原理)
1. Virtual DOM就是用js对象来描述真实DOM, 是对真实DOM的抽象,由于直接DOM的抽象, 由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作
2. 最终通过diff算法比对差异进行更新DOM, 减少了对真实DOM的操作, 虚拟DOM不依赖真实平台环境,虚拟DOM不依赖真实平台环境从而也可以实现跨平台
# 补充回答
1. 虚拟DOM的实现就是普通对象包含tag, attrs, children等属性对真实节点的描述,本质上就是在JS和DOM之间的缓存
9.说一下diff的原理
# 主要考察
1. Vue中的diff算法是平级比较, 不考虑跨级比较的情况, 内部采用深度递归的方式 + 双指针的方式进行比较
* 首先比较是相同的节点 不是直接替换掉
* 相同的节点比较属性 并复用老节点
* 比较儿子节点 考虑老节点和新节点儿子情况
* 优化比较: 头头比较 尾尾比较 头尾比较 尾头比较
* 比对查找进行复用
# 补充回答
1. Vue3中采用最长递增子序列实现diff算法
10.说一下既然Vue通过数据劫持可以精准探测数据变化 为什么还需要虚拟DOM进行diff检测差异
# 主要考察
1. 响应式数据变化 Vue确实可以在数据发生变化时 响应式系统可以立刻得知, 但如果给每个属性都添加watcher用于更新的话,
会产生大量的watcher从而降低性能,而且力度过细也会导致不精准的问题 所以Vue采用组件级的watcher配合diff来检测差异
2. 可以往diff算法说说
11.说一下Vue.$set方法怎么实现的
# 主要考察(为什么$set可以触发更新操作)
1. 其实在数据劫持的时候 对象和数组本身都添加了一个dep属性, 当给对象新增一个不存在的属性的时候, 触发对象收集的watcher去更新
2. 修改数组索引时候 调用的其实是数组本身的splice方法去更新数组(重新写的数组的七个方法)
12.说一下生命周期方法有哪些, 一般在那一步发起请求及原因
# 主要考察(组件的生命周期中各个钩子函数做了什么事情)
1. beforeCreate 在实例初始化之后, 数据观测(data observer) 和 event/watcher事件配置之前被调用
2. created 实例创建完成之后被调用 在这一步 实例已完成以下配置: 数据观测(data observer) 属性和方法的运算 watcher/event事件回调 还没有挂载$el
3. beforeMount 在挂载开始之前被调用: 相关的render函数首次被调用
4. mounted el 被新创建的vm.$el替换, 并挂载并挂载到实例上去之后调用该钩子
5. beforeUpdate 数据更新时调用, 发生在虚拟dom重新渲染和打补丁之前
6. updated 由于数据更改导致的虚拟dom重新渲染和打补丁 在这之后调用该钩子
7. beforeDestroy 实例销毁之前调用 在这一步 实例任然完全可用
8. destroyed Vue实例销毁后调用 调用后 vue实例指示的所有东西都会解除绑定, 所有的事件监听器会被移除, 所有的子实例也会被销毁, 该钩子在服务器端渲染期间 不会被调用
# 补充回答(钩子的作用)
1. created 实例已经创建完成 以为它是最早触发的原因 可以进行一些数据, 资源的请求 (vue ssr 支持created方法)
2. mounted 实例已经挂载完成 可以进行一些dom操作
3. beforeUpdate 可以进一步更改状态 不会触发重渲染过程
4. updated 可以执行依赖dom的操作 然而在大多数情况下 应该避免在此期间更改状态 因为可能会导致更新无限循环, 该钩子在服务器端渲染期间 不会被调用
5. destroyed 可以执行一些优化操作 清空定时器 解除绑定事件
* 在哪里发请求都可以 具体看做什么事
13.说一下vue组件间传值方式 以及之间的区别
# 主要考察(为什么$set可以触发更新操作)
1. props和$emit 父组件向子组件传递数据时通过props传递的
子组件传递的数据给父组件是通过$emit触发事件
2. $parent, $children 获取当前组件的父组件和当前组件的子组件
3. $attrs和$listeners A->B->C Vue2.4开始提供了$attrs 和 $listeners来解决这个问题
4. 父组件中通过provide来提供变量 然后在子组件中通过inject来注入变量
5. $refs 获取实例
6. eventBus 平级组件数据传统 这种情况下可以使用中央事件总线的方式
7. vuex状态管理
14.说一下$attrs是为了解决什么问题出现的 以及应用场景有哪些, provide/inject不能解决它能解决的问题么
# 主要考察
1. $attrs是当前许久组件为了批量获取父级传递的attribute, 除了当前组件已经接收的props和style, class
2. 应用场景有多层级嵌套组件传值, 减少层层使用prop
3. provide/inject实现了跨级组件数据传递, 在组件库插件中非常有用
# 补充回答
1. 一般$attrs和$listeners(vue2.4新增)可以解决A-> B-> C 这样的嵌套组件间传值, A传给B多个
参数再传给C, 可以使用$attrs搭配$listeners实现 A<=>C 交互
2. provide/inject 也可以实现组件通信, 但是对于数据来源不清晰 不建议使用
15.说一下Vue组件的渲染流程
vue2核心原理(简易) - 组件(Vue.componet)的实现笔记
# 主要考察
1. 父子组件渲染的先后顺序
2. 组件是怎么渲染到页面上的
`
<template>
<!--这个是$vnode占位符节点-->
<app></app>
</template>
`
# 流程说明
1. 在渲染父组件时会创建父组件的虚拟节点,其中可能包含子组件的标签
2. 在创建虚拟节点时,会获取组件定义使用的Vue.extend生成组件的构造函数
3. 将虚拟节点转化成真实节点时,会创建组件的实例并且调用组件的$mount方法
4. 所以组件的创建过程是先父后子
16.说一下Vue中组件data为什么是个函数
# 主要考察
1. 每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据
源,为了保证多个组件的数据相互不影响
class Vue {
constructor(options) {
this.data = options.data()
}
}
let data = () => ({a:1,b:2})
let test1 = new Vue({data})
let test2 = new Vue({data})
test1.data.a = 100
test1.data.b = 200
console.log(test2.data) // {a:1,b:2}
17.说一下v-if和v-show的区别
# 主要考察
1. v-if在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点
2. v-show会被编译成指令,条件不满足时控制样式将对应节点隐藏, (内部其他指令依旧会继续执行)
# 扩展回答
1. 频繁控制显示隐藏尽量不要使用v-if
2. v-if和v-for尽量不要连用
18.说一下Vue.use是做什么的,原理是什么
# 主要考察
1. 用来使用插件的, 我们可以在插件中扩展全局组件,指令,原型方法等
19.说一下Vue-router有几种钩子函数, 具体是什么, 及执行流程是什以
基于vue2手写一个简易的Vue-router(以beforeEach为例)
# 主要考察
1. 路由钩子的执行流程
2. 钩子函数种类有: 全局守卫,路由守卫,组件守卫
# 完整流程
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的回调函数,创建好的组件实例会作为回调函数的参数传入
20.说一下Vue-router两种模式区别
# 主要考察
1. hash模式,history模式
2. hash模式: hash + hashChange兼容性好但是不美观
3. history模式: historyApi + popState虽然美观,但是刷新会出现404需要后端进行配置
21.说一下函数式组件的优势和原理
# 主要考察
1. 函数式组件的特性,无状态,无生命周期,无this
if (isTrue(Ctor.options.functional)) { // functional=>true为函数性组件
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
const listeners = data.on
data.on = data.nativeOn
// 安装组件相关钩子(函数式组件没有调用此方法, 所以性能高于普通组件)
installComponentHooks(data)
22.说一下v-if和v-for的优先级
# 主要考察
1. v-for和v-if不要在同一个标签中使用,因为解析时先解析v-for在解析v-if
2. 如果遇到同时使用可以写成计算属性的方式
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) {
return genFor(el, state) // v-for
} else if (el.if && !el.ifProcessed) {
return genIf(el, state) // v-if
}
23.说一下组件中写name选项好处和作用
# 主要考察
1. 可以通过名字找到对应的组件(递归组件)
2. 可以通过name属性实现缓存功能(keep-alive)
3. 可以通过name来识别组件(跨级组件通信时非常重要)
24.说一下Vue.directive源码实现
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
// 看这里 如果是指令包装成对象
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 放到Vue.options上
this.options[type + 's'][id] = definition
return definition
}
}
})
}
25.说一下对自定义指令的理解
# 主要考察(实现的原理)
1. 在生成ast语法树时, 遇到指令会给当前元素添加directives属性
2. 通过genDiectives生成指令代码
3. 在patch前将指令的钩子提取到cbs中 在patch过程中调用对应的钩子
4. 当执行指令对应钩子函数时, 调用对应指令定义的方法