面试官:看到你简历上写着会Vue?

26,359 阅读4分钟
分享在面试过程中,被问到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.$elthis.$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.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 nextTick(cb){
    
    if(cb){
        
        callback.push(()=>{
            cb()
        })
        
    }
    
    if(!pendding){
        
        pendding = true
        
        promise.resolve().then(()=>{
    
            padding =false
 
            const copies = callback.slice(0)
 
            callback.length = 0
 
            for(let i = 0;i < copies.length;i++){
     
                copies[i]()
     
            }
        })
    }
}

指令的执行原理

在模版阶段,会将节点上的指令解析处理并添加到AST的directives属性中。随后directives数据会传到Vnode中,接着就可以通过vnode.data.directives获取一个节点所绑定的指令。最后,当VDom进行修补时,会根据节点的对比结果触发一些钩子函数。更新指令的程序会监听create,update,destory钩子函数,并在这三个钩子函数出发时对VNode和oldVNode进行对比,最终根据对比触发指令的钩子函数。