阅读 8367

「源码级回答」大厂高频Vue面试题(上)

写在前面(不看错过一个亿)

最近一直在读Vue源码,也写了一系列的源码探秘文章。

但,收到很多朋友的反馈都是:源码晦涩难懂,时常看着看着就不知道我在看什么了,感觉缺乏一点动力,如果你可以出点面试中会问到的源码相关的面试题,通过面试题去看源码,那就很棒棒。

看到大家的反馈,我丝毫没有犹豫:安排!!

我通过三篇文章整理了大厂面试中会经常问到的一些Vue面试题,通过源码角度去回答,抛弃纯概念型回答,相信一定会让面试官对你刮目相看。

文中源码基于 Vue2.6.11版本

请说一下响应式数据的原理?

Vue实现响应式数据的核心APIObject.defineProperty

其实默认Vue在初始化数据时,会给data中的属性使用Object.defineProperty重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖进行更新操作。

这里,我用一张图来说明Vue实现响应式数据的流程:

  • 首先,第一步是初始化用户传入的data数据。这一步对应源码src/core/instance/state.js的 112 行
function initData (vm: Component{
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    // ...
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
   // ...
  }
  // observe data
  observe(data, true /* asRootData */)
}
复制代码
  • 第二步是将数据进行观测,也就是在第一步的initData的最后调用的observe函数。对应在源码的src/core/observer/index.js的 110 行
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

这里会通过new Observer(value)创建一个Observer实例,实现对数据的观测。

  • 第三步是实现对对象的处理。对应源码src/core/observer/index.js的 55 行。
/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

   /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // ...
}
复制代码
  • 第四步就是循环对象属性定义响应式变化了。对应源码src/core/observer/index.js的 135 行。
/**
 * Define a reactive property on an Object.
 */

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
{
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerabletrue,
    configurabletrue,
    getfunction reactiveGetter ({
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()  // 收集依赖
        // ...
      }
      return value
    },
    setfunction reactiveSetter (newVal{
      // ...
      dep.notify()  // 通知相关依赖进行更新
    }
  })
}
复制代码
  • 第五步其实就是使用defineReactive方法中的Object.defineProperty重新定义数据。在get中通过dep.depend()收集依赖。当数据改变时,拦截属性的更新操作,通过set中的dep.notify()通知相关依赖进行更新。

Vue 中是如何检测数组变化?

Vue中检测数组变化核心有两点:

  • 首先,使用函数劫持的方式,重写了数组的方法
  • Vuedata 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组 api 时,就可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次进行观测。

这里用一张流程图来说明:

这里第一步和第二步和上题请说一下响应式数据的原理?是相同的,就不展开说明了。

  • 第一步同样是初始化用户传入的 data 数据。对应源码src/core/instance/state.js的 112 行的initData函数。
  • 第二步是对数据进行观测。对应源码src/core/observer/index.js的 124 行。
  • 第三步是将数组的原型方法指向重写的原型。对应源码src/core/observer/index.js的 49 行。
if (hasProto) {
  protoAugment(value, arrayMethods)
else {
  // ...
}
复制代码

也就是protoAugment方法:

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */

function protoAugment (target, src: Object{
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
复制代码
  • 第四步进行了两步操作。首先是对数组的原型方法进行重写,对应源码src/core/observer/array.js
/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */


import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

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
  })
})

复制代码

第二步呢,是对数组调用observeArray方法:

// src/core/observer/index.js line:74
/**
 * Observe a list of Array items.
 */

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}
复制代码

其实就是遍历数组,对里面的每一项都调用observe方法,进行深度观测。

为什么Vue采用异步渲染?

我们先来想一个问题:如果Vue不采用异步更新,那么每次数据更新时是不是都会对当前组件进行重写渲染呢?

答案是肯定的,为了性能考虑,会在本轮数据更新后,再去异步更新视图。

通过一张图来说明Vue异步更新的流程:

  • 第一步调用dep.notify()通知watcher进行更新操作。对应源码src/core/observer/dep.js中的 37 行。
notify () {  // 通知依赖更新
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort((a, b) => a.id - b.id)
  }
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()  // 依赖中的update方法
  }
}
复制代码
  • 第二步其实就是在第一步的notify方法中,遍历subs,执行subs[i].update()方法,也就是依次调用watcherupdate方法。对应源码src/core/observer/watcher.js的 164 行
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */

update () {
  /* istanbul ignore else */
  if (this.lazy) {  // 计算属性
    this.dirty = true
  } else if (this.sync) {  // 同步watcher
    this.run()
  } else {
    queueWatcher(this)  // 当数据发生变化时会将watcher放到一个队列中批量更新
  }
}
复制代码
  • 第三步是执行update函数中的queueWatcher方法。对应源码src/core/observer/scheduler.js的 164 行。

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */

export function queueWatcher (watcher: Watcher{
  const id = watcher.id  // 过滤watcher,多个属性可能会依赖同一个watcher
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)  // 将watcher放到队列中
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 10, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)  // 调用nextTick方法,在下一个tick中刷新watcher队列
    }
  }
}
复制代码
  • 第四步就是执行nextTick(flushSchedulerQueue)方法,在下一个tick中刷新watcher队列

谈一下nextTick的实现原理?

Vue.js在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick 的时候将这个队列 queue 全部拿出来 runWatcher 对象的一个方法,用来触发 patch 操作) 一遍。

因为目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 PromisesetTimeoutsetImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。

nextTick方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。

所以这个 nextTick 方法是异步方法。

通过一张图来看下nextTick的实现:

  • 首先会调用nextTick并传入cb。对应源码src/core/util/next-tick.js的 87 行。
export function nextTick (cb?: Function, ctx?: Object{
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码
  • 接下来会定义一个callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,所有的 cb 都会被存在这个 callbacks 数组中。
  • 下一步会调用timerFunc函数。对应源码src/core/util/next-tick.js的 33 行。
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  timerFunc = () => {
    // ...
  }
  isUsingMicroTask = true
else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {

  timerFunc = () => {
    // ...
  }
  isUsingMicroTask = true
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
复制代码

来看下timerFunc的取值逻辑:

1、 我们知道异步任务有两种,其中 microtask 要优于 macrotask ,所以优先选择 Promise 。因此这里先判断浏览器是否支持 Promise

2、 如果不支持再考虑 macrotask 。对于 macrotask 会先后判断浏览器是否支持 MutationObserversetImmediate

3、 如果都不支持就只能使用 setTimeout 。这也从侧面展示出了 macrotasksetTimeout 的性能是最差的。

nextTickif (!pending) 语句中 pending 作用显然是让 if 语句的逻辑只执行一次,而它其实就代表 callbacks 中是否有事件在等待执行。

这里的flushCallbacks函数的主要逻辑就是将 pending 置为 false 以及清空 callbacks 数组,然后遍历 callbacks 数组,执行里面的每一个函数。

  • nextTick的最后一步对应:
if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
    _resolve = resolve
  })
}
复制代码

这里 if 对应的情况是我们调用 nextTick 函数时没有传入回调函数并且浏览器支持 Promise ,那么就会返回一个 Promise 实例,并且将 resolve 赋值给 _resolve。回到nextTick开头的一段代码:

let _resolve
callbacks.push(() => {
  if (cb) {
    try {
      cb.call(ctx)
    } catch (e) {
      handleError(e, ctx, 'nextTick')
    }
  } else if (_resolve) {
    _resolve(ctx)
  }
})
复制代码

当我们执行 callbacks 的函数时,发现没有 cb 而有 _resolve 时就会执行之前返回的 Promise 对象的 resolve 函数。

你知道Vuecomputed是怎么实现的吗?

这里先给一个结论:计算属性computed的本质是 computed Watcher,其具有缓存。

一张图了解下computed的实现:

  • 首先是在组件实例化时会执行initComputed方法。对应源码src/core/instance/state.js的 169 行。
const computedWatcherOptions = { lazytrue }

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

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && 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 (process.env.NODE_ENV !== 'production') {
      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)
      }
    }
  }
}
复制代码

initComputed 函数拿到 computed 对象然后遍历每一个计算属性。判断如果不是服务端渲染就会给计算属性创建一个 computed Watcher 实例赋值给watchers[key](对应就是vm._computedWatchers[key])。然后遍历每一个计算属性调用 defineComputed 方法,将组件原型,计算属性和对应的值传入。

  • defineComputed定义在源码src/core/instance/state.js210 行。
// src/core/instance/state.js
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
{
  const shouldCache = !isServerRendering();
  if (typeof userDef === "function") {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (
    process.env.NODE_ENV !== "production" &&
    sharedPropertyDefinition.set === noop
  ) {
    sharedPropertyDefinition.set = function ({
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

首先定义了 shouldCache 表示是否需要缓存值。接着对 userDef 是函数或者对象分别处理。这里有一个 sharedPropertyDefinition ,我们来看它的定义:

// src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerabletrue,
  configurabletrue,
  get: noop,
  set: noop,
};
复制代码

sharedPropertyDefinition其实就是一个属性描述符。

回到 defineComputed 函数。如果 userDef 是函数的话,就会定义 getter 为调用 createComputedGetter(key) 的返回值。

因为 shouldCachetrue

userDef 是对象的话,非服务端渲染并且没有指定 cachefalse 的话,getter 也是调用 createComputedGetter(key) 的返回值,setter 则为 userDef.set 或者为空。

所以 defineComputed 函数的作用就是定义 gettersetter ,并且在最后调用 Object.defineProperty 给计算属性添加 getter/setter ,当我们访问计算属性时就会触发这个 getter

对于计算属性的 setter 来说,实际上是很少用到的,除非我们在使用 computed 的时候指定了 set 函数。

  • 无论是userDef是函数还是对象,最终都会调用createComputedGetter函数,我们来看createComputedGetter的定义:
function createComputedGetter(key{
  return function computedGetter({
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value;
    }
  };
}
复制代码

我们知道访问计算属性时才会触发这个 getter,对应就是computedGetter函数被执行。

computedGetter 函数首先通过 this._computedWatchers[key] 拿到前面实例化组件时创建的 computed Watcher 并赋值给 watcher

new Watcher时传入的第四个参数computedWatcherOptionslazytrue,对应就是watcher的构造函数中的dirtytrue。在computedGetter中,如果dirtyfalse(即依赖的值没有发生变化),就不会重新求值。相当于computed被缓存了。

接着有两个 if 判断,首先调用 evaluate 函数:

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */

evaluate () {
  this.value = this.get()
  this.dirty = false
}
复制代码

首先调用 this.get() 将它的返回值赋值给 this.value ,来看 get 函数:

// src/core/observer/watcher.js
/**
 * Evaluate the getter, and re-collect dependencies.
 */

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
复制代码

get 函数第一步是调用 pushTargetcomputed Watcher 传入:

// src/core/observer/dep.js
export function pushTarget(target: ?Watcher{
  targetStack.push(target);
  Dep.target = target;
}
复制代码

可以看到 computed Watcher 被 push 到 targetStack 同时将 Dep.target 置为 computed Watcher 。而 Dep.target 原来的值是渲染 Watcher ,因为正处于渲染阶段。回到 get 函数,接着就调用了 this.getter

回到 evaluate 函数:

evaluate () {
  this.value = this.get()
  this.dirty = false
}
复制代码

执行完get函数,将dirty置为false

回到computedGetter函数,接着往下进入另一个if判断,执行了depend函数:

// src/core/observer/watcher.js
/**
 * Depend on all deps collected by this watcher.
 */

depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
复制代码

这里的逻辑就是让 Dep.target 也就是渲染 Watcher 订阅了 this.dep 也就是前面实例化 computed Watcher 时候创建的 dep 实例,渲染 Watcher 就被保存到 this.depsubs 中。

在执行完 evaluatedepend 函数后,computedGetter 函数最后将 evaluate 的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。