挑战一轮大厂后的面试总结 (含六个方向) - 框架(vue)和工程相关

在去年底开始换工作,直到现在算是告了一个段落,断断续续的也面试了不少公司,现在回想起来,那段时间经历了被面试官手撕,被笔试题狂怼,悲伤的时候差点留下没技术的泪水。

这篇文章我打算把我找工作遇到的各种面试题(每次面试完我都会总结)和我自己复习遇到比较有意思的题目,做一份汇总,年后是跳槽高峰期,也许能帮到一些小伙伴。

先说下这些题目难度,大部分都是基础题,因为这段经历给我的感觉就是,不管你面试的是高级还是初级,基础的知识一定会问到,甚至会有一定的深度,所以基础还是非常重要的。

我将根据类型分为几篇文章来写:

面试总结:javascript 面试点汇总(万字长文)(已完成) 强烈大家看看这篇,面试中 js 是大头

面试总结:nodejs 面试点汇总(已完成)

面试总结:浏览器相关 面试点汇总(已完成)

面试总结:css 面试点汇总(已完成)

面试总结:框架 vue 和工程相关的面试点汇总(已完成)

面试总结:面试技巧篇(已完成)

六篇文章都已经更新完啦~

这篇文章是对 框架 vue 和工程相关 相关的题目做总结,欢迎朋友们先收藏在看。

先看看目录

挑战一轮大厂后的面试总结 (含六个方向) - 框架(vue)和工程相关_第1张图片

VUE

这部分是 vue 相关的整理。

响应式原理

响应式原理是 vue 的核心思想之一,后续打算单独整理一篇,这里简单介绍响应式的三大件

Observer

观察者,使用 Object.defineProperty 方法对对象的每一个子属性进行数据劫持/监听,在 get 方法中进行依赖收集,添加订阅者 watcher 到订阅中心。
在 set 方法中,对新的值进行收集,同时订阅中心通知订阅者们。

/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      /*如果原本对象拥有getter方法则执行*/
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        /*进行依赖收集*/
        dep.depend()
        if (childOb) {
          /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        /*如果原本对象拥有setter方法则执行setter*/
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      /*新的值需要重新进行observe,保证数据响应式*/
      childOb = observe(newVal)
      /*dep对象通知所有的观察者*/
      dep.notify()
    }
  })

在setter中向Dep(调度中心)添加观察者,在getter中通知观察者更新。

watcher

扮演的角色是订阅者,他的主要作用是为观察属性提供通知函数,当被观察的值发生变化时,会接收到来自订阅中心 dep 的通知,从而触发依赖更新。

核心方法有:
get() 获得getter的值并且重新进行依赖收集
addDep(dep: Dep) 添加一个依赖关系到订阅中心 Dep 集合中
update() 提供给订阅中心的通知接口,如果不是同步的(sync),那么会放到队列中,异步执行,在下一个事件循环中执行(采用 Promise、MutationObserver以及setTimeout来异步执行)

Dep

扮演的角色是调度中心,主要的作用就是收集观察者 Watcher 和通知观察者目标更新。
每一个属性都有一个 Dep 对象,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的 watch,让订阅者执行自己的 update 逻辑。

computed 为什么比 watch method 性能要好

从编码上 computed 实现的功能也可以通过普通 method 实现,但与函数相比,计算属性是基于响应式依赖进行缓存的,只有在依赖的数据发生改变是,才重新进行计算,只要依赖项没有发生变化,多次访问都只是从缓存中获取。

计算属性是基于 watcher 实现,看看源码

/*初始化computed*/
// 核心是为每个计算属性创建一个 watcher 对象
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    /*
      计算属性可能是一个function,也有可能设置了get以及set的对象。
    */
    let getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production') {
      /*getter不存在的时候抛出warning并且给getter赋空函数*/
      if (getter === undefined) {
        warn(
          `No getter function has been defined for computed property "${key}".`,
          vm
        )
        getter = noop
      }
    }
    // create internal watcher for the computed property.
    /*
      为每个计算属性创建一个内部的监视器Watcher,保存在vm实例的_computedWatchers中
      这里的computedWatcherOptions参数传递了一个lazy为true,会使得watch实例的dirty为true
    */
    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    /*组件定义的计算属性不能与 data 和 property 重复定义*/
    if (!(key in vm)) {
      /*定义计算属性*/
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      /*如果计算属性与已定义的data或者props中的名称冲突则发出warning*/
      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)
      }
    }
  }
}

/*创建计算属性的getter*/
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      /*实际是脏检查,在计算属性中的依赖发生改变的时候dirty会变成true,在get的时候重新计算计算属性的输出值
       *若依赖没发生变化,直接读取 watcher.value
       */
      if (watcher.dirty) {
        watcher.evaluate()
      }
      /*依赖收集*/
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

computed 和 watch 主要区别在于使用场景,计算属性更适用于模板渲染,依赖其他对象值的变化,做重新计算在渲染,监听多个值来改变一个值。而监听属性 watch ,是用于监听某一个值的变化,进行一系列复杂的操作。监听属性可以支持异步,计算属性只能是同步。

vue 对数组的处理

在官方文档上关于数组的注意事项有这么一段

由于 JavaScript 的限制,Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

先看第二点,这是因为 Object.defineProperty 不能监听数组的长度,所以直接修改数组长度是没法被监听到的。

关于第一点,我们看看源码的实现

// vue\src\core\observer\index.js
constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 在 Observer 构造函数中,对数组类型进行特殊处理
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
// observeArray 的实现
/**
 * Observe a list of Array items.
 */
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i]) // 对数组的值在进行 observe
  }
}

observeArray 是对数组中的值进行监听,并不是数组下标,所以通过索引来修改值是监听不到了,假如是通过监听索引的话,那是可以实现的。那为啥不监听下标呢?在 vue issue 中好像记得尤大说是性能考虑。

因为是对数组元素做的监听,那么数组 api 造成的修改自然就没法监听到了,所以 vue 对数组的方法进行了变异,包裹了一层,本质还是执行数组的 api

// vue\src\core\observer\array.js
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
/**
 * Intercept mutating methods and emit events
 */
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
  })
})

vue 中 key 的作用

有两点用处:快速节点比对和节点唯一标识

利用快速节点比对

用作于 vnode 的唯一标识,便于更快更准确的在旧节点列表中查找节点

在内部对两个节点进行比较的时候,会优先判断 key 是否一致,如下,如果 key 不一致,立马就可以得出结果

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}

列表节点唯一标识

列表循环 v-for="i in dataList" 会有提示我们需要加上 key ,因为循环后的 dom 节点的结构没特殊处理的话是相同的, key 的默认值是 undefined ,那么按照上面 sameVnode 的算法,新生成的 Vnode 与 旧的节点的比较结果就是相同的,vue会对这些节点尝试就地修改/复用相同类型元素的,这种模式是高效,但是这种模式会有副作用,比如节点是带有状态的,那么就会出现异常的bug,所以这种不写 key 的默认处理只适用于不依赖其他状态的列表。

注意:在不知道哪个版本,vue 对 for 遍历中未设置 key 的情况,内部做了处理,默认生成一个 key , 所以今后就算不设置 key 也是允许的了。

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, last
  for (i = 0; i < children.length; i++) {
        // .... 忽略其他代码
        // default key for nested array children (likely generated by v-for)
        if (isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__` // set key 设置默认 key
        }
        res.push(c)
      }
    }
  }
  return res
}

利于节点高效查找

同一层vnode节点是以数组的方式存储,那么如果节点非常多,通过遍历查找就稍微有点慢,因此,内部将 vnode 列表转换成对象,代码如下:

/*
  生成一个key与旧VNode的key对应的哈希表
  比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
  结果生成{key0: 0, key1: 1, key2: 2}
*/
function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

这样一来,就可以直接通过 key 查找到数组下标,利于加快查找时间。

参考文档:
https://muyiy.cn/question/frame/1.html

虚拟dom 与直接操作 dom 相比哪个更快?

以下是根据尤大在知乎的回答,做的总结:

  1. 首先是没有任何一个框架可以比纯手动优化操作 dom 快,因为框架的 dom 操作层需要应对上层API可能发生的操作,所以它的实现是普适性的,所以不可能对每个场景做优化,这就是个性能和可维护性的取舍。各大框架可以给到即使不需要手动优化,也可以提供较优秀的性能。
  2. 我们看看两者的重绘性能消耗:
  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
    render Virtual DOM + diff O 显然比渲染 html string 要慢,但我们知道,这是纯 js层面的计算相比, 与 DOM 层面的操作的开销相比要小很多。
    所以直接操作dom的开销就和整个页面相关,而虚拟dom的开销就是 js层面的计算和计算后的 dom 的开销,所以虚拟dom就可以保证,不管页面数据变化多少,每次计算后的重绘的性能都在可接受范围内。
  1. 因为机制不一样,那么比较的时候就要看场合,比如是大量数据的更新还是小量数据的更新。举个例子,如果一个非常大的列表,数据全都发生了变化,那么直接操作dom肯定是更快的,那如果只是其中的几行发生了变化,直接全量替换dom的开销可就大了,而虚拟dom在计算后,只需要替换个别dom即可
  2. 虚拟dom提供给开发者的价值不是性能,而是 1.为函数式的UI编程打开大门 2.扩展性强,可以渲染到 DOM 意外的其他平台
  3. 那如果开发中遇到特殊的情况导致虚拟dom的更新效率不满足,那么可以牺牲一定的维护性来自己手动进行优化

参考文档:
https://www.zhihu.com/question/31809713/answer/53544875

vue 可以定义函数式组件么

函数式组件:没有状态(data),没有生命周期,只接受传递的 props ,常用于纯 UI 组件
定义:

  • 通过 Vue.component 构建组件时,添加 functional: true; 需要通过调用 render 函数来渲染,常用包裹组建或者构建高阶组件
  • 对于单文件组件,在 template 上添加 functional