Vue 经典面试题解析(一)

1. MVVM

MVC、MVVM 这类模式的目的都是为了职责划分和分层,如果代码都堆在一起,就很臃肿,也不利于维 护。

MVVM 借鉴了后端的 MVC 模式,比如 java 中比较原始的 servlet + jsp 技术。

mvc.png

但是对于前端来说,数据变化无法同步到视图中,还是需要将逻辑聚拢在 controller 层。

所以就产生了 MVVM 模式,MVVM 模式是对映射关系的简化,隐藏了 Controller 层。

Vue 就借鉴了 MVVM 的思想,但是它并没有完全遵循 MVVM 模型。

在 vue 中是可以使用 JS 操作视图的,MVVM 模式不推荐直接操作视图。

mvvm.png

2. vue2 响应式原理

首先响应式原理和双向绑定并不是一个概念。

响应式原理是指当数据变化时,会驱动视图变化。

vue2 的响应式原理是通过 Object.defineProperty 实现的。

处理对象

使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性)。多层对象是通过递归实现劫持。

src/core/observer/index.js

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

src/core/observer/index.js

// 定义响应式数据
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
 
  // 如果不可以配置直接 return
  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, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 依赖收集
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          // 对象本身进行依赖收集
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果是数组,让 arr 属性和外层数组进行收集
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
javascript

处理数组

数组则是通过重写数组方法来实现的。

数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择重写数组方法。

push、shift、pop、unshift、reverse、sort、splice

也正因如此,数组的索引和长度变化是无法监控到的。

src/core/observer/index.js

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

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
javascript

src/core/observer/array.js

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

对于数组中新增(splice、push、unshift )的数据也会进行观测。

3. Vue3 响应式原理

export function reactive (target) {
  // 创建响应式对象
  return createReactiveObject(target, mutableHandler); 
}

function createReactiveObject (target, baseHandler) {
  if (!isObject(target)) {
    return target;
  }
  const observed = new Proxy(target, baseHandler);
  return observed;
}
javascript
/**
 * @file 对象和数组相关的处理函数
 */
const get = createGetter(),
      set = cretaeSetter();


function createGetter () {
  return function get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    // 依赖收集
    track(target, TrackOpTypes.GET, key);
    // 如果是数组或对象
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  }
}

function cretaeSetter () {
  return function setter (target, key, value, receiver) {
    // 检查一个属性是否已经存在
    const hasKey = hasOwn(target, key);

    const oldValue = target[key];

    const res = Reflect.set(target, key, value, receiver); // target[key] = value

    if (!hasKey) {
      // 新增属性
      trigger(target, TriggerOpTypes.ADD, key, value);
    } else if (hasChanged(value, oldValue)) {
      // 设置属性值
      trigger(target, TriggerOpTypes.SET, key, value, oldValue);
    }

    return res;
  }
}

export const mutableHandler = {
  get,
  set
}
javascript

vue3 相对于 vue2 是懒代理,vue2 中会递归对属性进行定义。其次使用 Proxy 是可以代理数组的。

4. 依赖收集

每个属性都拥有自己的 dep 属性,存放所依赖的 watcher,当属性变化后会通知自己对应的 watcher 去更新。

默认在初始化时会调用 render 函数,此时会触发属性依赖收集 dep.depend。

当属性发生修改时会触发 watcher 更新 dep.notify()。

depend.png

depend02.png

src/core/observer/index.js

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  
  // ....
  
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
javascript

vue 初始化的时候,如果是 Runtime + Compiler 版本的,就会执行模板编译相关逻辑,生成 render 函数。

然后会调用 mountComponent 方法实例化渲染 Watcher,同时执行自身 get 方法。

这时会触发 Dep.pushTarget 方法,然后会执行传入的 getter,即 updateComponent 方法。

updateComponent 会触发 vm._update(vm._render()) 方法,这个过程就会触发 Object.definePropery 定义的 get 进行依赖收集。

执行完 updataComponnet 方法后会执行 Dep.popTarget 方法。接着处理后续逻辑。

5. 模板编译原理

编译入口

当使用 Runtime + Compiler 版本的 Vue.js 时,就会执行模板编译过程,最终生成 render 函数。

src/platforms/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
   
  /* istanbul ignore if */
  // el 元素不能是 body 和 documentElement 元素。
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    // 如果存在模板
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // 模板编译入口
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
javascript

compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns。

src/platforms/web/compiler/index.js

/* @flow */

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }
javascript

compileToFunctions 是 src/compiler/index.js 的返回值。

核心逻辑

template 模板编译核心逻辑就定义在 createCompilerCreator 传入的函数 baseCompile 中。

如果将 template 转换成 render 函数?

  1. 解析语法树。将 template 模板转换成 AST 语法树 (parseHTML);
  2. 优化语法树。比如对静态节点做静态标记,静态节点会跳过 diff 操作(从子到父);
  3. 生成代码。将优化后的 AST 树转换成可执行代码(codegen)。

src/compiler/index.js

/* @flow */

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1. 解析 ast 语法树
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 2. 对 ast 树进行标记(根据配置项决定是否开启优化)
    optimize(ast, options)
  }
  // 3. 生成代码,返回 render 函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
javascript

ast 用来描述语法结构的,virtual dom 用来描述 DOM 节点,层面不同。vue 3 的模板编译流程与 vue2 基本类似。

vue 在执行上述核心编译逻辑之前,还有很多其他处理,过程比较复杂,先看下图。

compiler.png

你可能会问编译过程为啥会这么复杂?

因为 Vue.js 在不同的平台都会有编译的过程,因此编译过程中依赖的配置 baseOptions 会有所不同。

Vue.js 利用了柯里化的技巧把核心的编译函数抽出来,通过 createCompilerCreator(base)  的方式把真正编译的过程和其他逻辑(编译配置处理、缓存处理等)剥离开,这样的设计思路很值得我们学习。

6. 生命周期钩子函数

Vue 的生命周期就是回调函数,当创建组件实例的过程中会调用对应的钩子方法。

实现过程

Vue 内部会对钩子函数进行处理,将钩子函数维护成数组的形式(配置合并)。

**src/core/instance/init.js **

Vue.options 上包含所有全局属性,将全局属性和局部属性进行合并。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // 属性合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // ....
  }
}
javascript

src/core/util/options.js

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
javascript

mergeField 对不同的 key 有不同的合并策略,以达到处理不同的逻辑的功能。

/**
 * Hooks and props are merged as arrays.
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal // 子节点
    ? parentVal
      ? parentVal.concat(childVal) // 父节点
      : Array.isArray(childVal) // 子节点是数组
        ? childVal
        : [childVal] // 不是数组包装成数组
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}
javascript

组件也会执行 mergeOptions,是在执行 Vue.entend 方法执行的。

src/core/global-api/extend.js

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
    
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
   
  // ...
  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use
  
  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)
  
  // ...
  return Sub
}
javascript

上面的 extendOptions 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.options 中。

调用 Vue.mixin 方法时,也会进行合并配置的操作。

src/core/global-api/mixin.js

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
javascript

执行阶段

源码中执行生命周期的函数都是调用 callhook 方法。

src/core/instance/lifecycle.js

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
javascript

callhook 函数会根据传入的字符串,获取对应的回调函数数组,然后遍历执行。

beforeCreate、created

beforeCreate 和 created 都是定义在实例化 Vue 的阶段,在 init 方法中执行。

src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    
    // ...
  }
}
javascript

beforeCreate 和 created 钩子调用在 initState 的前后。

所以 beforeCreate 的钩子函数中并不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。

执行 created 钩子函数时,虽然已经初始化数据,但是并没有渲染 DOM,也不能访问 DOM,即 $el 对象。

之前我们说过的响应式原理就是发生在执行 initState 方法的阶段。

beforeMount、mounted

beforeMount 即在 DOM 挂载之前,在 mountComponent 中执行。

src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
 
  // ...
    
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
    
  return vm
}
javascript

beforeMount 钩子函数在渲染 VNode 之前(vm._render())调用。

首次渲染时,mounted 钩子函数会在将 VNode patch 到真实 DOM ( vm._update() )之后调用。

上面调用前 callHook(vm,  ‘mounted’) 有个判断逻辑,如果 vm.$node 为空才会执行,说明这是首次 Vue 的初始化过程。

组件的 mouted 时机发生在组件的 VNode patch 到真实 DOM 之后,这时会调用 invokeInsertHook 函数,把 insertedVnodeQueue 里保存的钩子函数执行一遍,继而执行组件的 mouted 过程。

src/core/vdom/patch.js


function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}
javascript

这时会调用 insert 或者钩子函数,它定义在 componentVNodeHooks 中。

src/core/vdom/create-component.js

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) { }
}
javascript

每个子组件都是在这个钩子函数中执行 mouted 钩子函数。mouted 钩子函数的执行顺序是先子后父。

beforeUpdate、updated

beforeUpdate 和 updated 的钩子函数都是在数据更新的时候才会执行。

breforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中,只有当组件已经 mouted 之后,才回去调用这个钩子函数。

src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
}
javascript

updated 在 flushScheduerQueue 函数调用的时候执行。

src/core/observer/scheduler.js

function flushSchedulerQueue () {
  // ...
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // beforeUpdate
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // ...
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
  
  // ...
}
javascript
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}
javascript

updatedQueue 是执行更新操作的 watcher 数组,在 callUpdatedHooks 函数中,会对数组进行遍历,只有满足当前 watcher 是 vm._watcher 并且组件已经 mouted 且没有销毁时,才会执行。

beforeDestroy、destroyed

beforeDestroy 和 destroyed 钩子函数执行发生在组件销毁的阶段。组件销毁最终会调用 $destroy方法。

src/core/instance/lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  // ...
  Vue.prototype.$destroy = function () {
    // ...
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
javascript

beforeDestory  在 $destroy 函数执行最开始的地方。这一步,vm 实例仍然是可用的。

$destroy 的执行过程中,会执行 vm.__patch__ 触发组件销毁,destoryed 钩子函数执行顺序也是先子后父,与 mounted 过程一致。

activated、deactiveted

activated 和 deactiveted 钩子函数专门用来处理 keep-alive 组件中。后面会分析 keep-alive 组件。

最后附一张 Vue 官网的生命周期图。

lifecycle.png

7. Vue.mixin 原理及使用场景

Vue.mixin 主要作用就是抽离公共的业务逻辑,原理类似 ”对象的继承“,当组件初始化时会调用 mergeOptions 方法进行合并,

采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据有冲突,会采用 ”就近原则“ 以组件的数据为准。

如果使用 mixin 不当  ,可能会产生很多问题,比如 ”命名冲突问题“、”依赖问题“、”数据来源问题“。

src/core/global-api/mixin.js

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

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
javascript

8. Vue 组件 data 为什么必须是个函数

每次使用组件时都会对组件进行实例化操作,只有调用 data 函数返回一个对象作为组件的数据,这样才能保证多个组件间的数据互不影响。Vue 组件实例化会使用 Vue.extend 方法构造一个 Vue 的子类,转换成一个继承于 Vue 的构造器 Sub 并返回。

src/core/global-api/extend.js

/**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
      
    // ....

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    
    // ...
    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
    // ...
}
javascript

组件会调用 mergeOptions 方法将全局属性和自身属性合并,合并到 Sub.options 上。

因为组件使用过程中会创建多次,如果不使用函数的方式,组件的属性还是引用类型,就会产生影响。

9. nextTick 原理及使用场景

nextTick 用来获取更新后的 DOM。

Vue 中数据更新是异步的(同样使用 nextTick 方法),使用 nextTick 方法可以保证用户自定义的逻辑在更新之后执行。

Vue 提供了两种方法调用 nextTick。一种是全局 API 方法 Vue.nextTick,一种是实例上的 vm.$nextTick。

src/core/util/next-tick.js

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

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

nextTick 是用来维护一个 callbacks 数组,以异步的方式去同步执行任务数组中的任务。

如果调用 nextTick 先于修改数据,同样也获取不到最终的数据。比如下面这个例子。

vm.$nextTick(() => {
   console.log(vm.a);
});

vm.a = 'yueluo';
javascript

10. computed 和 watch 区别

computed 和 watch 都是基于 Watcher 实现的。

computed 通常叫做计算属性 watcher(lazy:true),watch 叫做用户自定义 watcher(user:true)。

computed 属性是具备缓存的,依赖的值不发生变化,对其取值时的计算属性不会重新执行。

computed 只有取值时才执行。

watch 则是监控值的变化,当值发生变化时调用对应的回调函数。

src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
javascript

src/core/instance/state.js

计算属性的实现。

// 设置此属性后,实例化 watcher 时,首次就不会加载。
const computedWatcherOptions = { lazy: true }

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)) {
      // 定义 computed 属性
      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)
      }
    }
  }
}

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

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 如果数据存在变化,才会重新求值
      // watcher 调用 update 方法时,会把 dirty 属性设置为 true。
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 让计算属性所依赖的属性,收集计算属性 watcher
      if (Dep.target) {
        watcher.depend()
      }
      // 返回数据
      return watcher.value
    }
  }
}
javascript

src/core/instance/state.js

watch 的实现。

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

export function stateMixin (Vue: Class<Component>) {
  // ...
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 用户 watcher
    options.user = true
    // 实例化 watcher
    const 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}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
javascript

11. Vue.set 方法是如何实现的

Vue 初始化时给对象和数组本身都增加了 dep 属性,用来收集 watcher。

当给对象新增不存在的属性则主动触发对象依赖的 watcher 去更新。

当修改数组索引时调用数组本身的 splice 方法去更新数组,这时依赖的 watcher 会自动更新。

Vue.set、Vue.$set 都可以使用,这两个方法是同一个方法。只是初始化时机不同,一个是全局方法,一个是原型方法。

src/core/observer/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 1. 如果是开发环境,并且 target 没有定义或者 target 是基础类型报错
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 2. 如果是数组,调用重写的 splice 方法触发视图更新
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 3. 如果是对象本身的属性,直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 4. 如果是 Vue 实例或者根数据 data 时,直接报错
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 5. 如果不是响应式数据也不需要将其定义成响应式数据,直接添加数据即可
  if (!ob) {
    target[key] = val
    return val
  }
  // 6. 如果是响应式数据,将属性定义成响应式数据
  defineReactive(ob.value, key, val)
  // 7. 通知视图更新
  ob.dep.notify()
  return val
}
javascript