Vue 实例化其内部是如何实现的

415 阅读3分钟

《Vue2 初始化入口》一文中,详细地讲解了 import Vue from 'vue' 这行代码背后究竟发生了什么。而我们经常使用到 new Vue ,其背后又是怎么回事的呢?这将是本文将探究的主题。

Vue 是如何定义的

在 Vue 源码中,使用 Function 来实现类 Vue,文件路径位于:src/core/instance/index,具体实现如下:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

从代码中可以看出,其核心就一行代码:

this._init(options)

函数 _init 又是在哪里定义的呢?其实就在函数 initMixin 里定义,直接挂载在 Vue 原型上。所以,要知道 new Vue 发生了什么,就得来探究 _init 函数究竟是如何定义的?

_init

沿着主线将其实现逻辑整理成一张图,如下:

_init.png

后续根据这张图一步一步地讲解其内部是如何实现的?

合并 options

// merge options
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
  )
}

合并 options 有两个分支,如果 options._isComponenttrue ,则执行 if;否则执行 else 分支。最终会将合并结果挂载到 Vue 属性 $options 上,即 vm.$options = 合并 options

设置全局属性 _renderProxy

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

对于 _renderProxy 的设置,需要区分环境的。如果是生产环境,则设置为 Vue 实例;如果是开发环境,则调用函数 initProxy(vm) 来对其进行设置。initProxy 代码实现如下:

initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}

initLifecycle

export function initLifecycle (vm: Component) {
  const options = vm.$options
​
  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
​
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
​
  vm.$children = []
  vm.$refs = {}
​
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

该函数的作用是在 Vue 实例上设置属性,比如 $parent$root$children$refs_watcher_inactive_directInactive_isMounted_isDestroyed_isBeingDestroyed

initEvents

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

同样也是在 Vue 实例上设置属性,比如 _events_hasHookEvent

initRender

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

函数 initRender 做的事情相对多点,同样也在 Vue 实例上定义属性:_vnode_staticTrees$slots$scopedSlots_c$createElement$attrs$listeners'

其中需要注意的是 _c$createElement ,它们两个都是函数,只不过 _c 是内部版本,在将模板编译成 render 函数时调用,其最终还是调用函数 $createElement;而 $createElement 是一个公共 API,当使用手写 render 函数时调用。

callHook(vm, 'beforeCreate')

该函数的作用是调用 Vue 生命周期函数,我们平时所写的生命周期函数 beforeCreate 就在此刻被调用,也是 Vue 第一个生命周期函数被执行。

initInjections

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

初始化 injectionsdata/props 之前,调用函数 defineReactive 将其设置为 Vue 实例上的响应式属性。

initState

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

该函数所做的初始化操作都是我们平时经常用到的,propsmethodsdatacomputedwatch ;分别调用函数 initPropsinitMethodsinitDatainitComputedinitWatch 函数对它们各自进行初始化。

initProvide

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

初始化 providedata/props 之后,其实现逻辑挺简单的,如果 provide 数据类型是 Function,则执行调用操作,将其返回结果赋值给 vm._provide;否则直接赋值给 vm._provide 。即最终挂载在 Vue 实例属性 _provide 上。

callHook(vm, 'created')

调用 Vue 生命周期函数 created,即执行 Vue 第二个生命周期函数。

调用 $mount

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

对于挂载 Vue 实例,有两种方式:手动挂载和自动挂载。手动挂载是用户自己调用 $mount 进行挂载;自动挂载则是传入元素 el ,Vue 内部自行调用函数 $mount 进行挂载。这两种方式的挂载逻辑是一样的,只不过是方式不同而已。

至此,对于 new vue 内部实现逻辑就分析完了,其中涉及到的很多细节没展开,后续会单独以主题的形式来分析。

参考链接