Vue中的一些面试题

255 阅读11分钟

面试当中肯定会被问到有关Vue的一些问题,在这里我收集了一些题目,写下自己的回答,进行一次自我面试。【如果涉及到Vue3.0会进行标注,否则默认2.X】

  • Vue中为什么采用异步渲染?
  1. Vue中的响应式数据发生变化后,会触发dep.notify()方法通知渲染watcher进行更新操作;watcher 中的update方法会将当前的渲染watcher放入到一个队列中;这里Vue会给渲染watcher标记一个uid,如果是相同的则只要添加一次即可;

  2. 设想一下,如果Vue并没有采取异步渲染,那么多次更新数据都要进行了一次渲染,这样性能就会很差。出于性能考虑,Vue采取了异步渲染方式;顺便说下,因为是异步渲染,所以我们更新数据后不能马上获取到dom上最新的值,如果有需求可用nextTick;

  • 说一下nextTick的原理?
  1. 涉及的知识点有宏任务微任务;浏览器事件循环中遇到同步任务直接执行,异步任务分为宏任务微任务,执行顺序是首先执行同步任务然后将宏任务添加到宏任务队列,在执行所有的微任务,完毕后又会执行新的宏任务,依次类推下去。
// 这是一个同步任务
console.log('1')         
// 这是一个宏任务
setTimeout(function () {   
  console.log('2')                   
});
new Promise(function (resolve) {
  // 这里是同步任务
  console.log('3');        
  resolve();                        
  // then是一个微任务
}).then(function () {      
  console.log('4')                   
  setTimeout(function () {
    console.log('5')
  });
});
  1. nextTick就是把我们的视图更新的操作塞到一个微任务或者宏任务中,来进行异步执行;Vue中首先检测是否支持promise,支持则进行promise.then(flushCallbacks)执行回调,否则看是否支持MutationObserver,支持则进行 new MutationObserver(flushCallbacks),否则看是否支持setImmediate支持则进行setImmediate(flushCallbacks),否则进行setTimeout(flushCallbacks, 0);
  • 谈一谈Vue中的响应式原理
  1. Vue中的响应式是其一大特色,何为响应式?就是我们由数据来驱动视图的更新。一旦发现我们所依赖的数据发生了变化,我们的视图就会执行更新;这带来的好处就是我们只要关心数据这一层就好,不用过多的关心视图层的变化。

  2. Vue中的响应式核心方法就是使用ES5中Object.defineProperty对数据操作进行‘劫持’,在读取数据的时候会执行get方法,然后进行依赖收集操作,每个依赖收集当前的渲染watcher或者计算watcher;收集的过程会形成一个相互映射关系,每个watcher会收集对应的依赖,每个依赖会收集当前的watcher;当我们进行数据修改操作的时候会执行set方法,这个方法将会通知该依赖的所有watcher触发update方法进行视图更新;当然Vue2.X中的响应式还是存在一些不可为的东西,比如数据的索引直接赋值,就不会进行响应式;当然Vue2.X中也提供了如Vue.$setVue.$delete方法进行间接的进行响应式;在Vue3.X中使用了Proxy则解决了这个问题;

  • 你知道Vue中是如何检测数组响应式变化?
  1. Vue中初始化的时候会对数据进行了递归遍历,让每个属性值进行响应式,如果是数组则遍历每一项进行响应式处理;

  2. 对于数据的新增、删除等原生方法操作,我觉得Vue比较很巧妙的进行了处理;利用改写数组的原型链的方法,从而拦截了pushdeletesplice原生方法;这是怎么实现的呢?伪代码如下:

// array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
...
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
// observe.js
...
if (hasProto) { // 判断浏览器是否兼容 __prop__ 
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src // 完成数组的原型链修改, 从而使得数组变成响应式的 ( pop, push, shift, unshift, ... )
  /* eslint-enable no-proto */
}
...

通过进行原型链的拦截,我们给每次添加的值进行响应式然后手动触发更新;Vue中的Vue.$setVue.$delete实现的原理也就是通过数组的splice进行实现;

  • 说一说Vue中的watch
  1. 首先说下什么是watch,就是一个响应式数据的监听器,一旦监听的数据发生变化,就会触发回调函数进而做一些复杂的逻辑处理;

  2. 具体的用法如下:

...
let app = new Vue({
    el: '#root',
    data() {
        return {
            msg: 'hello world'
        }
    },
    created() {

    },
    methods: {
        changeMsg() {
            this.msg = 'hello world!'
        }
    },
    watch: {
        msg(value) {
            console.log('value change', value)
        }
    }
})
...

还有一些高级点的用法:比如是否立即执行immediate:true,是否进行深度监听deep:true; 这里就不赘述,可以去官网查看;

  1. 实现原理
    首先Vue 在初始化initState方法的时候进行检测是否有用户写的watch,如果有则进入initWatch方法;
function initState(vm){
  ...
   if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  ...
}

initWatch方法中,对opts.watch进行遍历创建watcher;调用了createWatcher(vm, key, handler)

function createWatcher(vm, key, handler){
  ...
  return vm.$watch(expOrFn, handler, options)
}

好了最终我们通过vm.$watch揭开了watch的神秘面纱;其实就是一个watcher.不过这里的watcher被标记为用户watcher,user:true;这里还有个逻辑就是options.immediate=true,这就是上述高级用法中立即执行的实现逻辑;

Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
    ...
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
        }
      }
      ...
    };

提到watcher我们很容易想到的就是进行依赖收集,形成依赖跟watcher相互绑定关系;那么如何进行依赖收集呢?让我们再次回到watcher.js中看看Vuewatch的处理.
watcher初始化中,有一段处理watch表达式的逻辑:

if (typeof expOrFn === 'function') { // 就是 render 函数
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
}

有前面可知watch传过来的expOrFn是一个字符串key值,所以会走parsePath(expOrFn)方法,这里利用了函数柯里化的技巧返回了一个函数;接着初始化做重要的一件事调用this.get()方法;这个方法就是会进行this.getter方法触发,如果是渲染watcher则进行视图更新,如果是用户watcher则进行取值依赖收集;然后一旦依赖的值发生了变化就会触发各个watcherupdate方法;接下来的流程就跟上述响应式流程一样了。
好了,以上便是对watch的介绍

  • 说一说Vue中的计算属性computed
  1. 什么是computed计算属性,计算属性是基于响应式数据进行缓存的,只有响应式数据发生了变化,才会进行重新计算。这样的好处是只要依赖的响应式数据不发生变化,我们就不会多次的触发所依赖的响应式数据的get方法;

  2. 具体的用法如下:

...
let app = new Vue({
        el: '#root',
        data() {
            return {
                msg: 'hello world'
            }
        },
        computed:{
            computedMsg(){
                return this.msg+'!'
            }
        },
        created() {

        },
        methods: {
            changeMsg() {
                this.msg = 'hello world!'
            }
        }
    })
...
  1. 实现原理 首先Vue初始化initState方法的时候进行检测时候有computed属性;
function initState(vm){
  ...
   if (opts.computed) { initComputed(vm, opts.computed); }
  ...
}

initComputed中做了以下几件事;1:在Vue实例上挂了一个_computedWatchers对象。2:通过遍历computed属性分别往_computedWatchers对象上添加添加计算watcher;并进行了计算属性的计算defineComputed;这里面有个小逻辑,因为我们用户手写计算属性的时候,是可以写get的,所以Vue中进行了处理;还有就是检测我们的computed属性是否已经存在vm中,然后抛出错误信息。这也是我们经常会出现的一些错误;最终得出一个小结论computed也是一个watcher;

 function initComputed (vm, computed) {
    // $flow-disable-line
    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    var isSSR = isServerRendering();

    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }

      if (!isSSR) {
        // create internal watcher for the computed property.
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if (!(key in vm)) {
        defineComputed(vm, key, userDef);
      } else {
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
  }

既然也是一个watcher,那么我们还是进入watcher.js中看看对computed具体做了哪些处理;注意下在创建一个计算watcher的时候传入了一个computedWatcherOptions参数,这也是计算属性的一个关键。下面来看一下:

 this.value = this.lazy // computedWatcherOptions 传入的
      ? undefined
      : this.get()

在计算watcher初始化中,并没有进行this.get()调用;然后我们进入defineComputed这个逻辑中;

// 删除掉服务端渲染逻辑
function defineComputed (
    target,
    key,
    userDef
  ) {
    if (typeof userDef === 'function') {
      createComputedGetter(key)
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? createComputedGetter(key)
        : noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }
    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {
        warn(
          ("Computed property \"" + key + "\" was assigned to but it has no setter."),
          this
        );
      };
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

这里的主要做了一件事就是要对每个要计算的属性进行响应式处理;然后手写了getset方法,那么我们看下getcreateComputedGetter:

function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }

这里的几个东西需要我们回忆下,_computedWatchersdirty以及watcher.evaluate();首先是_computedWatchers就是我们的计算属性watcher,我们这里根据key拿到之前存储的watcher,然后因为dirty=true,所以会进行一次计算,这个时候就会触发watcher中的get方法:

 evaluate () {
    this.value = this.get()
    this.dirty = false
  }

这就会读取依赖this.msg,这里就会触发msg对计算watcher的收集;仅仅是收集计算watcher是不够的,我们肯定还要收集渲染watcher,于是Vue又进行了一次watcher收集:

 if (Dep.target) {
      watcher.depend();
  }

为什么这样可以收集到渲染watcher呢?
watcherget方法会对watcher进行压栈出栈的操作,如何理解呢?首先在执行render函数时候会对计算属性computedMsg(上述例子)进行读取,调用了get方法,执行了pushTarget;此时的targetStack[渲染watcher],然后再进行msg(上述例子)收集计算watcher又调用了get方法,再次进行pushTarget,这时候的targetStack应该是[渲染watcher,计算watcher],执行完get方法后就会调用popTarget;此时的targetStack就变成了[渲染watcher];这样就能很巧妙的完成了msg(上述例子)对渲染watcher收集了。
以上就是对computed计算属性的初始化分析,那又是如何根据依赖的变化而进行重新渲染的呢?
我们已经完成了计算属性的相关依赖的收集,所以只要依赖发生了变化就会触发计算wacther渲染watcher的更新;那问题来了,我们如何进行重新计算呢?对的,就是改掉这个dirty标识;我们在计算watcher进行update时候设置这个dirty=true:然后执行渲染watcher的时候进行值的更新以及视图的更新;

update () {
    /* istanbul ignore else */
    if (this.lazy) {     
      this.dirty = true
    } else if (this.sync) {   
      this.run()
    } else {
      queueWatcher(this)      
    }
  }

我们都知道computed具有缓存数据作用,主要是靠什么进行缓存的呢?
就是靠我们的dirty标识,dirty的改变是依赖于响应数据的改变的,只要响应式数据不改变我们就不会重新计算,而是返回之前的计算值;

  • watch跟computed的区别

首先两者都是依赖于响应式数据的,computed具有缓存作用,只有依赖的数据发生改变才会进行重新计算,watch则只要监听数据发生了变化就会触发回调函数;watch更加偏向于一些异步以及复杂的逻辑处理;正如官网给出的比较demo,在计算fullName=firstName+lastname时候,如果使用watch进行侦听则比较繁琐,建议还是使用computed;

  • v-forv-if的优先级以及如何进行性能提升? 有过Vue开发经验的小伙伴都会知道,如果你的编辑器里有这样一段代码:
<ul>
    <li v-for="(item,index) in 5" v-if="isExpand">
        {{item}}
    </li>
</ul>

这样控制台会有一个警告,官网也有给过一个说明永远不要把 v-if 和 v-for 同时用在同一个元素上。;那具体的原因,官网是没有给出的,我们来看一看两种编译后的代码块,对比一下就能一目了然。

  1. 优先级比较
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)
  } else if (el.if && !el.ifProcessed) {
    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)
  } else {
    ...
  }

从这段逻辑看for的优先级是在if前面的; 2. 同时使用后的代码块:

 with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('div', {
      staticClass: "lists"
    }, [_c('ul', _l((5), function (item, index) {
      return (isExpand) ? _c('li', [_v("\n " + _s(item) + "\n")]) :
        _e()
    }), 0)])])
  }

我们先忽略里面的_c_l,_l逻辑就是渲染了一个列表,注意这里的逻辑是先遍历一遍然后处理v-ifisExpand三元表达式逻辑。在看一下我们另一段写发代码块;

 <ul v-if="isExpand">
     <li v-for="(item,index) in 5">
          {{item}}
     </li>
</ul>

编译后的代码如下:

 with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('div', {
      staticClass: "lists"
    }, [(isExpand) ? _c('ul', _l((5), function (item, index) {
      return _c('li', [_v("\n                    " + _s(item) + "\n                ")])
    }), 0) : _e()])])
  }

从这段逻辑可以看出,我们是先进行三元表达式的计算。当结果是true的时候就进行了列表的渲染;所以把v-if写在v-for上层能够带来一定的性能优化。