在《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
沿着主线将其实现逻辑整理成一张图,如下:
后续根据这张图一步一步地讲解其内部是如何实现的?
合并 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._isComponent 为 true ,则执行 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)
}
}
初始化 injections 在 data/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)
}
}
该函数所做的初始化操作都是我们平时经常用到的,props、methods 、data 、computed 、watch ;分别调用函数 initProps 、initMethods 、initData 、initComputed 、initWatch 函数对它们各自进行初始化。
initProvide
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
初始化 provide 在 data/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 内部实现逻辑就分析完了,其中涉及到的很多细节没展开,后续会单独以主题的形式来分析。