68 Vue面试考点答题分析
请说一下响应式数据的理解
参考答案
- 核心点考察的是:数组和对象类型当值变化时如何劫持到。对象内部通过
defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。 这里在回答时可以带出一些相关知识点(比如多层对象是通过递归来实现劫持,顺带提出Vue3中是使用proxy来实现响应式数据) - 核心点答出来了也可以在进行补充回答,内部依赖收集是怎样做到的,每个属性都拥有自己的
dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新 - 这里可以引出性能优化相关的内容
- 对象层级过深,性能就会差
- 不需要响应数据的内容不要放到
data中 Object.freeze()可以冻结数据
Vue如何检测数组变化
- 核心点考察的是:数组考虑性能原因没有用
defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写 - 核心点答出来了也可以在进行补充回答,在
Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象数据类型也会进行递归劫持 - 引发出的问题,那如果想更改索引更新数据怎么办?可以通过
Vue.$set()来进行处理 => 核心内部用的是splice方法
Vue中模板编译原理
`
- 核心点考察的是:如何将
template转换成render函数(这里要注意的是我们在开发时尽量不要使用template,因为将template转化成render方法需要在运行时进行编译操作会有性能损耗,同时引用带有compiler包的vue体积也会变大。默认.vue文件中的template处理是通过vue-loader来进行处理的并不是通过运行时的编译 - 后面我们会说到默认vue项目中引入的vue.js是不带有compiler模块的) - 模板编译过程
- 将
template模板转换成ast语法树 -parserHTML - 对静态语法做静态标记 -
markUp - 重新生成代码 -
codeGen - 核心点答出来了也可以在进行补充回答 (模板引擎的实现原理就是
new Function + with来进行实现的)
- 将
function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点:对ast树进行标记,标记静态节点
if (options.optimize !== false) {
optimize(ast, options);
}
// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}
* `vue-loader`中处理`template`属性主要靠的是`vue-template-compiler`模块
const VueTemplateCompiler = require('vue-template-compiler');
const {render} = VueTemplateCompiler.compile("<div id="hello">{{msg}}</div>");
console.log(render.toString())
生命周期钩子是如何实现的
- 核心点考察的是:
Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法 - 核心点答出来了也可以在进行补充回答:内部主要是使用
callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布
export function callHook(vm, hook) {
// 依次执行生命周期对应的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向当前实例
}
}
}
// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); //初始化数据之后
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
// 销毁实例实现
Vue.prototype.$destory = function() {
// 触发钩子
callHook(vm, 'beforeDestory')
// 自身及子节点
remove()
// 删除依赖
watcher.teardown()
// 删除监听
vm.$off()
// 触发钩子
callHook(vm, 'destoryed')
}
Vue.mixin的使用场景和原理
- 核心点考察的是:
Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准 - 核心点答出来了也可以在进行补充回答:
mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题",这里强调一下mixin的数据是不会被共享的
nextTick在哪里使用?原理是
- 核心点考察的是:
nextTick中的回调是在下次DOM更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。原理就是异步方法(promise,mutationObserver,setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务) - 核心点答出来了也可以在进行补充回答:
vue多次更新数据,最终会进行批处理更新。内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用,从而可以获取更新后的DOM
Vue为什么需要虚拟DOM
- 核心点考察的是:
Virtual DOM就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。虚拟DOM不依赖真实平台环境从而也可以实现跨平台。 - 核心点答出来了也可以在进行补充回答:虚拟
DOM的实现就是普通对象包含tag、attrs、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存)
Vue中的diff原理
核心点考察的是:
Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较
比较过程
- 先比较是否是相同节点
- 相同节点比较属性,并复用老节点
- 比较儿子节点,考虑老节点和新节点儿子的情况
- 优化比较:
头头、尾尾、头尾、尾头 - 比对查找进行复用
Vue3中采用最长递增子序列实现diff算法
Vue中computed和watch的区别
- 核心点考察的是:
computed和watch都是基于Watcher来实现的,分别是计算属性watcher和用户watcher。computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行(可以用模板渲染,取值的过程中不支持异步方法)watch则是监控值的变化,当值发生变化时调用对应的回调函数 - 核心点答出来了也可以在进行补充回答:
computed不会立即执行,内部通过defineProperty进行定义。并且通过dirty属性来检测依赖的数据是否发生变化。watch则是立即执行将老值保存在watcher上,当数据更新时重新计算新值,将新值和老值传递到回调函数中
Vue.set方法是如何实现的
核心点考察的是:为什么
$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组
export function set (target: Array | Object, key: any, val: any): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// 4.如果是Vue实例 或 根数据data时 报错
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 7.通知视图更新
ob.dep.notify()
return val
}
Vue.use是干什么的?原理是什么
核心点考察的是:
Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等
Vue.use = function (plugin: Function | Object) {
// 插件不能重复的加载
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this) // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
if (typeof plugin.install === 'function') { // 调用插件的install方法
plugin.install.apply(plugin, args) Vue.install = function(Vue,args){}
} else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
plugin.apply(null, args)
}
installedPlugins.push(plugin) // 缓存插件
return this
}
vue-router有几种钩子函数?具体是什么及执行流程是怎样的
核心点考察的是:路由钩子的执行流程, 钩子函数种类有:
全局守卫、路由守卫、组件守卫
完整的导航解析流程
- 导航被触发
- 在失活的组件里调用
beforeRouteLeave守卫 - 调用全局的
beforeEach守卫 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+) - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve守卫 (2.5+) - 导航被确认
- 调用全局的
afterEach钩子 - 触发
DOM更新 - 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入
阅读全文
