vue中常问面试题

一、vue2.x中响应式原理

1)原理

采用数据劫持 + 订阅-发布模式的方式,通过Object.defineProperty来劫各个属性的setter  getter,数据变化时,发布消息给订阅者,触发响应的回调。

即在创建Vue实例的时候,会遍历选项data的属性,用Object.defineProperty,为属性添加getter  setter 对数据的读取进行劫持,在内部追踪依赖,在属性被访问或修改时,通知变化;如果属性是Object则会循环递归的处理

2)在实现响应式原理中有三大核心实现类

Observer:给对象属性添加getter   setter,用于依赖收集、派发更新

Dep:用于收集当前响应式对象的依赖关系。每一个响应式对象及其子对象都有自己的Dep实例(里面的subs是Watcher实例数组),当数据有变更时,会通过dep.notify()通知各个Watcher

Watcher:观察者对象。Watcher实例分为3种:渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)

3)Watcher和Dep的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新

4)依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
  3. render()的过程,触发 render watcher 依赖收集
  4. re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值

5)派发更新

  1. 组件中对响应的数据进行了修改,触发 setter 的逻辑
  2. 调用 dep.notify()
  3. 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法

二、computed实现原理

computed 本质是一个惰性求值的观察者,同时持有一个 dep 实例;

内部通过watcher的 this.dirty 属性标记计算属性是否需要重新求值;

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。);

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

三、computed与watch的区别,运用场景

区别

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存。只有它依赖的属性值发生改变时,才会重新计算computed的值

watch 侦听器 : 无缓存性,当监听的数据变化时,执行回调,进行处理

运用场景

computed:当我们需要进行数值计算,并且依赖于其它数据时,可以使用 computed。这样可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算

watch:当我们需要在数据变化时执行异步或开销较大的操作时,可以使用 watch。 watch 允许我们执行异步操作 ( 访问一个 API )、限制我们执行该操作的频率、并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的

四、vue3中实现响应式原理,为什么从Vue2中的Object.defineProperty改成了Proxy

从数组层面:

在Vue2中,通过索引修改数组的值,这时候是不能监听到数组变化的。虽然Object.defineProperty可以监控到数组下标变化的能力,但是从性能、体验的性价比出发,就放弃了该特性。为了解决该问题,Vue内部重写了push. pop.  unshift.  shift.  reverse.  sort. splice方法,来监听数组修改。由于只是对以上7种方法进行hack,数组的其他属性还是监听不到,有一定的局限性

从Object层面

Object.defineProperty只能劫持对象的属性,因此需要对每个对象的每个属性进行遍历,如果属性是一个Object,那么还需要深度遍历。如果能劫持整个对象才是最佳选择

Proxy可以劫持整个对象,并返回一个新的对象;可以代理数组、代理动态增加的属性

五、Vue中的key作用

key 是给每一个 vnode 的唯一 id,依靠 key,diff 操作可以更准确、更快速 

diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)

六、nextTick的实现原理

1)vue 用异步队列的方式控制 DOM 更新、nextTick 回调先后执行

2)microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

3)考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,才会采用 setTimeout(fn, 0) 代替

七、Vue中是如何实现对Array的方法重写

Vue通过对Array的原型拦截的方式,重写了Array的7个方法:push、pop、unshift、shift、sort、reverse、splice

1)首先获取到数组的:ob = this.__ob__(也就是它的Observer对象)

2)如果有新的值,就调用ob.observeArray,对新的值进行监听

3)接着手动调用ob.dep.notify(),通知render watch执行update

八、Vue中的组件data选项为什么必须是函数

在new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中data 必须是一个函数呢?

1)组件是可以复用的

2)js中对象是引用关系,如果组件中的data是对象,那么子组件中的data属性值会相互影响,产生副作用

因此,Vue中data选项必须是function,每个实例可以维护一份一份被返回对象的独立拷贝,而new Vue()实例是不会被复用的,因此不会哟以上问题

九、谈谈Vue中的事件机制,手写$on     $off      $once     $emit实现代码

Vue中的事件机制本身就是一个订阅-发布模式的实现

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) { // event为数组,则循环遍历调用$on
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else { // event为string,则将监听事件和回调函数添加到事件处理中心_events对象中
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    // 定义监听事件的回调函数
    function on () {
      vm.$off(event, on)      // 从事件中心移除监听事件的回调函数
      fn.apply(vm, arguments) // 执行回调函数
    }
    on.fn = fn
    vm.$on(event, on) // 通过$on方法注册事件
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all,调用this.$off()没有传参数,则清空事件处理中心缓存的事件及其回调
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events,events为array,则循环遍历调用$off
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event,从事件处理中心取出缓存的事件
    const cbs = vm._events[event]
    if (!cbs) { // 如果事件中心没有缓存该事件,直接返回
      return vm
    }
    if (!fn) { // 如果调用$off时,没有传回调函数fn,则直接清空监听该事件的所有回调函数
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) { // 对监听的事件的回调函数进行循环遍历
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) { // 如果入参fn === 缓存的回调函数,或者入参fn === 缓存的cb.fn,则剔除该缓存的回调函数
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

// 实际是执行回调函数
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

十、说说Vue的渲染过程

1、new Vue()实例的时候,主要做了以下工作:

调用了内部方法_init()

1)给私有属性_uid自增1,每个组件每一次初始化时做的一个唯一的私有属性标识

2)合并options,并赋值给实例属性$options

3)定义实例私有属性vm._self = vm ,用于访问实例的数据和方法

4)调用initLifecycle(vm) :确认组件的父子关系;初始化实例属性vm.$parent、 vm.$root 、vm.$children、 vm.$refs;初识化内部相关属性

5)调用initEvents(vm) :将父组件的自定义事件传递给子组件;初始化实例内部属性_events(事件中心)、_hasHookEvent;

6)调用initRender(vm) :提供将render函数转为vnode的方法;初始化实例属性$slots、$scopedSlots;定义实例方法$createElement;定义响应式属性:$attrs、$listeners;

7)调用callHook(vm, 'beforeCreate') , 执行组件的beforeCreate钩子

8)调用initInjections(vm) ,resolve injections before data/props

9)调用initState(vm) ,对实例的选项props、data、computed、watch、methods初始化

10)调用initProvide(vm) , resolve provide after data/props

11)调用callHook(vm, 'created')

12)如果选项有提供挂载钩子,则执行挂载;$options.el:vm.$mount(vm.$options.el)
----------待完善----------

十一、keep-alive的实现原理、缓存机制

1、获取 keep-alive 包裹着的第一个子组件对象及其组件名

2、如果有 include/exclude,根据设定的 include/exclude进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

3、根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果缓存过,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

4、如果没有缓存过,则在 this.cache 对象中存储该组件实例并保存 key 值;之后检查如果设置缓了max,并且缓存的实例数量超过 max 的设置值,则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)

5、最后将组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到

LRU缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高。

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

即最新访问的放到缓存的末尾,淘汰的永远是下标为0的对象

十二、vm.$set的实现原理

 Vue 会在初始化实例的时候给data的属性添加 getter/setter ,因此属性在 data 对象上存在才能让 Vue 将它转换为响应式的;

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(target, propertyName, value) 方法向嵌套对象添加响应式属性

1、如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式

2、如果目标是对象,判断属性存在,即为响应式,直接赋值

3、如果 target 本身就不是响应式,直接赋值

4、如果属性不是响应式,则调用 defineReactive 方法进行响应式处理,并调用ob.dep.notify()通知相应的watcher

十三、vm.$delete的实现原理

1、如果target是数组,使用 vue 实现的变异方法 splice 实现响应式,并删除元素

2、如果是对象,属性不是自身属性,直接返回

3、使员工delete语法删除对象属性

4、如果target是响应式对象,则调用ob.debp.notify()通知watcher做更新

参考文章:

computed源码解析:https://blog.csdn.net/qq_27460969/article/details/94873042

computed源码解析:https://www.cnblogs.com/vickylinj/p/14034645.html

Vue源码面试题:https://zhuanlan.zhihu.com/p/101330697

おすすめ

転載: blog.csdn.net/tangxiujiang/article/details/116564771