在面试过程中,被问到Vue知识点的一些碎事。 来源:掘金推荐
面试官:vue是什么?
vue是一门渐进式的javascript框架。所谓的渐进式就是:从中心的的视图层渲染开始向外扩散的构建工具层。这过程会经历:视图层渲染->组件机制->路由机制->状态管理->构建工具;五个层级。
特点:易用,灵活,高效,入门门槛低。
图来自百度👆面试官:v-if和v-show的区别?
前者是将DOM创建和删除后者则是改变display的值来控制DOM的显示和隐藏。
面试官:vue有什么生命周期?在new Vue 到 vm.$destory的过程经历了什么?
初始化阶段
beforeCreate和create
挂载阶段
beforeMount和mounted
更新阶段
beforeUpdate和update
卸载阶段
beforeDestory和destory
当new Vue()
后,首先会初始化事件
和生命周期
,接着会执行beforeCreate生命周期钩子
,在这个钩子里面还拿不到this.$el
和this.$data
;接着往下走会初始化inject
和将data的数据进行侦测也就是进行双向绑定
;接着会执行create钩子函数
,在这个钩子里面能够拿到this.$data
还拿不到this.$el
;到这里初始化阶段就走完了。然后会进入一个模版编译阶段,在这个阶段首先会判断有没有el
选项如果有的话就继续往下走,如果没有的话会调用vm.$mount(el)
;接着继续判断有没有template
选项,如果有的话,会将template
提供的模版编译到render函数中
;如果没有的话,会通过el选项
选择模版;到这个编译阶段就结束了。(温馨提示:这个阶段只有完整版的Vue.js才会经历,也是就是通过cmd引入的方式;在单页面应用中,没有这个编译阶段,因为vue-loader已经提前帮编译好,因此,单页面使用的vue.js是运行时的版本)。模版编译完之后(这里说的是完整版,如果是运行时的版本会在初始化阶段结束后直接就到挂载阶段),然后进入挂载阶段,在挂在阶段首先或触发beforeMount钩子
,在这个钩子里面只能拿到this.$data
还是拿不到this.$el
;接着会执行mounted钩子
,在这个钩子里面就既能够拿到this.$el
也能拿到this.$data
;到这个挂载阶段就已经走完了,整个实例也已经挂载好了。当数据发生变更的时候,就会进入更新阶段,首先会触发beforeUpdate钩子
,然后触发updated钩子
,这个阶段会重新计算生成新的Vnode,然后通过patch函数里面的diff算法,将新生成的Vnode和缓存中的旧Vnode进行一个比对,最后将差异部分更新到视图中。当vm.$destory
被调用的时候,就会进入卸载阶段,在这个阶段,首先触发beforeDestory钩子
接着触发destoryed钩子
,在这个阶段Vue会将自身从父组件中删除,取消实例上的所有追踪并且移除所有的事件监听。到这里Vue整个生命周期就结束了。
图来自vue官网👆
面试官:vue的模版编译过程是怎么样的?
首先会先将模版通过解析器,解析成AST(抽象语法树),然后再通过优化器,遍历AST树,将里面的所有静态节点找出来,并打上标志,这样可以避免在数据更新进行重新生成新的Vnode的时候做一些无用的功夫,和diff算法对比时进行一些无用的对比,因为静态节点这辈子是什么样就是什么样的了,不会变化。接着,代码生成器会将这颗AST编译成代码字符串,这段字符串会别Vdom里面的createElement函数调用,最后生成Vnode。
面试官:vue是怎么实现数据侦测的?
vue主要是通过Object.defineProperty
进行数据的侦测;vue的数据侦测有两种:1.Object类型的数据侦测。2.Array类型的数据侦测。Object类型的数据侦测比较容易直接通过Object.defineProperty
结合递归就能实现,但是Array的类型侦测就比较麻烦一些,需要通过劫持Array原型上的push,pop,shift,unshift,splice,`sort,reverse
的方法来实现侦测,因为这几个方法都会改变自身的数据。导致Array类型侦测比较麻烦还是因为Object.defineProperty
对数组的支持比较差。(到Vue.3,vue的数据侦测会通过proxy进行重写)
具体代码实现如下:
const arrProto = Array.prototype
const arrayMethods = Object.create(arrProto)
const m = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
m.forEach(function (method) {
const original = arrProto[method]
Object.defineProperty(arrayMethods, method, {
value: function v(...args) {
return original.apply(this, args)
}
})
})
function defineReactive(data) {
if (data && typeof data !== 'object') return
if (Array.isArray(data)) {
data.__proto__ = arrayMethods
} else {
Object.keys(data).forEach(function (val) {
observer(data, val, data[val])
})
}
}
function observer(data, key, value) {
defineReactive(data)
Object.defineProperty(data, key, {
get() {
return value
},
set(newVal) {
if (newVal === value) return
value = newVal
}
})
}
复制代码
面试官:我这样this.xxx[xxx] = xxx,在data里添加一个数据,vue能不能侦测到?为什么?
不能,在new Vue()初始化的时候,在实例的data初始化的数据,才能被侦测到;因为在生命周期beforeCreate到create钩子之间会进行将data中的数据进去双向绑定的侦测;实例初始化完之后再添加的数据,无办法完成侦测初始化。
面试官:那有没有办法解决这个问题?
通过vm.$set()可以解决这个问题。
面试官:vm.$set()具体是怎么做的?
vm.$set(target,key,val)
1.target如果是数组的话,先判断key是不是合法的下标,如果这两个条件都通过.那就将target.length和传进来的key取一个最大值赋值给target.length,然后调用splice去修改数组
2.key已经存在target里面并且不是存在target原型上的,那就是只改变值
3.target如果不是响应式数据,那么也只是改变数据,不需要通知watcher
4.如果target是vue实例,或者target是this.$data,那么直接退出程序
5.如果上面的条件都不满足,那么就是新添加的响应数据,那就直接调用defineReactive()去侦测该数据,然后去通知watcher
具体实现代码如下:
function set(target, key, val) {
const ob = target.__ob__
if (Array.isArray(target) && key >= 0) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if ((key in target && !(key in Object.prototype)) || !ob) {
target[key] = val
return val
}
if (target._isVue || (ob && ob.vmCount)) {
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
复制代码
提示:这里解释一下target.__ob__
,target._isVue
,ob.vmCount
是什么,如果target是一个双向绑定的是数据,它的原型上就会有一个__ob__
属性,如果有原型上有_isVue
属性,证明它是Vue的实例,如果__ob__.vmCount
大于0就证明该target是根数据this.$data
面试官:那vm.$delete又是怎么做的?
vm.delete(target,key)
1.target如果是数组的话并且key是合法的,那就通过splice去改变数组
2.target如果是vue实例.或者是this.$data,那就直接退出程序
3.target如果不是双向绑定数据,那就直接delete就行不需要,通知watcher
4.以上条件都不满足,那么target就是双向绑定数据,delete之后通知watcher
具体实现代码如下:
function del(target, key) {
if (Array.isArray(target) && key > 0) {
target.splice(key, 1)
return
}
const ob = target.__ob__
if ((target._isVue && (ob && ob.vmCount)) || !target.hasOwnProperty(key)) return
delete target[key]
if (!ob) return
ob.dep.notify()
}
复制代码
面试官:有没有用过vm.$on,vm.$off,vm.$once,vm.$emit
?实现原理是什么?能不能手写一下?
$on,$off,$once,$emit
,这四个实现方法是一个很标准的订阅发布模式
具体实现代码如下:
vm.$on
Vue.prototype.$on = function (event, fn) {
const vm = this
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push[fn]
}
return vm
}
复制代码
温馨提示:_events是实现初始化的时候定义的,this.events = Object.create(null),所以不要困惑_events哪里来的。
vm.$off
Vue.prototype.$off = function (event, fn) {
const vm = this
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
vm.$off(event[i], fn)
}
return vm
}
const cbs = vm._events[event]
if (!cbs) return vm
if (arguments.length === 1) {
vm._events[event] = null
return vm
}
if (fn) {
let len = cbs.length
while (len--) {
let cb = cbs[len]
if (cb === fn || cb[len].fn === fn) {
cbs.splice(len, 1)
break
}
}
}
return vm
}
复制代码
vm.$once
Vue.prototype.$once = function (event,fn){
const vm = this
function on(){
vm.$off(event,on)
fn.apply(vm,arguments)
}
on.fn = fn
vm.$on(event,on)
return vm
}
复制代码
vm.$emit
Vue.prototype.$emit = function (event, ...params) {
const vm = this
let cbs = vm._events[event]
if (cbs) {
for (let i = 0; i < cbs.length; i++) {
cbs[i].apply(vm, params)
}
}
return vm
}
复制代码
还有问到vm.$nextTick的实现原理和指令的执行原理,因为太菜了没答上来😭。
回来查了一下上面两个问题实现👇
vm.$nextTick
nextTick主要是通过js eventLoop的执行机制原理,将回调通过(promise)添加到microTask上面,来实现,在下一次DOM周期后执行回调函数。
具体实现代码如下:
const callback = []
let pendding = false
function flushCallbacks(){
padding =false
const copies = callback.slice(0)
callback.length = 0
for(let i = 0;i < copies.length;i++){
copies[i]()
}
}
lei microTimeFunc
const p = promise.resolve()
microTimeFunc = ()=>{
p.then(flushCallbacks)
}
function nextTick(cb,ctx){
if(cb){
callback.push(()=>{
cb.call(ctx)
})
}
if(!pendding){
pendding = true
microTimeFunc()
}
}
复制代码
指令的执行原理
在模版阶段,会将节点上的指令解析处理并添加到AST的directives属性中。随后directives数据会传到Vnode中,接着就可以通过vnode.data.directives获取一个节点所绑定的指令。最后,当VDom进行修补时,会根据节点的对比结果触发一些钩子函数。更新指令的程序会监听create,update,destory钩子函数,并在这三个钩子函数出发时对VNode和oldVNode进行对比,最终根据对比触发指令的钩子函数。
来源:oschina
链接:https://my.oschina.net/jamesview/blog/3217619