Vue2源码解析(组件化)
Vue主要的一个思想就是组件化,正常开发中我们的一个完整Vue项目一般都是又一个一个Vue组件模块拼装组成,我们先看看Vue如何初始化一个组件:
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(App)
})
这里组件渲染调用render方法,将App参数传入createElement函数中:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
createElement函数最终会调用_createElement,我们看看_createElement内部主要逻辑:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 获取component is属性获取对应的组件
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// 如果不纯在 tag 创建一个注释节点
return createEmptyVNode()
}
// 处理组件slot插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 组件children vnode数组扁平化,如果是模板生成的render 则根据参数选择扁平化方案,用户自建的render,则进行深度递归扁平化数组
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children) // 深度递归扁平化数组
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children) // 二维转一维
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断是否是平台标准标签
if (config.isReservedTag(tag)) {
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
// 创建标签vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建 component vnode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 创建 tag名的 Vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// 创建 component vnode
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
_createElement中tag参数就是我们传入的App对象,这个时候就会创建组件Vnode执行vnode = createComponent(tag, data, context, children)函数:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// 创建Vue子类构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
....
// 处理异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
....
// 安装组件钩子函数
installComponentHooks(data)
const name = Ctor.options.name || tag
// 创建组件Vnode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
....
return vnode
}
createComponent方法主要做了三件事:
- 将组件对象通过
Ctor = baseCtor.extend(Ctor)构建成Vue的子类构造函数 - 将组件的钩子函数通过
installComponentHooks(data)方法将componentVNodeHooks的钩子函数合并到data.hook中。 - 创建组件Vnode,并返回Vnode。
创建完组件Vnode之后vm._update函数中会执行vm.__patch__(prevVnode, vnode)方法将Vnode转换为真实的DOM,这个方法定义在src/core/vdom/patch.js:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 删除旧节点
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) { // 如果旧节点不存在 创建新节点
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
......
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
在这里因为我们是一个新的组件Vnode节点,所以执行createElm(vnode, insertedVnodeQueue)函数:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
在这里因为我们是组件Vnode,所以createComponent(vnode, insertedVnodeQueue, parentElm, refElm)会返回true:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 执行组件Vnode 钩子函数init
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
因为是组件Vnode所以满足条件执行init钩子函数,这个钩子函数定义在src/core/vdom/create-component.js:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// 如果组件是keepAlive
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
如果组件是keepAlive就执行componentVNodeHooks.prepatch(mountedNode, mountedNode)方法,如果不是则createComponentInstanceForVnode创建一个Vue实例,然后调用$mount方法挂载子组件:
export function createComponentInstanceForVnode (
vnode: any,
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
函数中先构建组件的参数,然后执行new vnode.componentOptions.Ctor(options),这里的vnode.componentOptions.Ctor就是之前继承了Vue的子构造函数,参数_isComponent为true表明是一个组件,parent表明当前的组件实例,就是指的父组件。所以子组件的实例化是在这时机执行的,然后会执行构造函数里的_init方法:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
let startTag, endTag
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
组件实例传入的_isComponent为true,执行initInternalComponent(vm, options)方法:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
这个函数主要是将我们传入的参数合并到选项$options中。
_init函数最后执行:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
因为组件实例初始化不传入el,所以组件自己接管的$mount方法,实例化组件Vue之后在钩子函数componentVNodeHooks的init中会执行child.$mount(hydrating ? vnode.elm : undefined, hydrating),这里hydrating为false所以,它最终会调用mountComponent方法,最后执行vm._render()方法:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
vm.$vnode = _parentVnode
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode
return vnode
}
这里的_parentVnode就是之前组件Vnode,它会被作为当前组件的父VNode,而render函数生成的渲染Vnode的parent指向_parentVnode,也就是vm.$vnode,他们是父子关系,在生成渲染vnode之后,执行vm._update去渲染对应vnode:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
首先将组件渲染vnode赋值给vm._vnode,这里的const restoreActiveInstance = setActiveInstance(vm)的作用是保存当前上下文的Vue实例:
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
这里的activeInstance是全局变量,prevActiveInstance保存当前vm实例的父Vue实例,activeInstance保存当前的Vue实例然后返回一个函数用于子组件patch完成后将activeInstance指回当前实例的父Vue实例,因为Vue初始化是深度遍历的过程,在实例化子组件的时候需要知道当前Vue实例,把它作为子组件的父Vue实例,在子组件$mount挂载之前会调用initLifecycle(vm)方法:
export function initLifecycle (vm: Component) {
const options = vm.$options
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存储到父实例的$children中,这样就保证了vm实例和它所有子树的父子关系。
然后执行了vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */),会调用开头的createElm渲染成DOM:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// ...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
// ...
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
这里和开始不同的是我们这里传入的vnode是组件渲染vnode,因为我们的渲染vnode根元素是一个普通元素,所以是普通的vnode,而不是之前的组件vnode,所以不会走之前创建组件实例的逻辑,而是走下面普通vnode的逻辑,先判断是否vnode中是否包含tag参数,包含的话就先校验合法性,然后去调用平台DOM操作去创建一个占位符元素:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
然后调用createChildren方法创建子元素,然后通过invokeCreateHooks(vnode, insertedVnodeQueue)触发所有的create钩子函数并把vnodepush到insertedVnodeQueue中:
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
最后调用insert(parentElm, vnode.elm, refElm),因为我们传入的parentElm是空,所以在这不会做插入操作,而是在createComponent函数中完成组件初始化之后执行initComponent函数将组件渲染vnode的$el赋值给组件vnode的elm:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// ....
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// ...
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
最后在执行insert(parentElm, vnode.elm, refElm)完成组件DOM的插入。
总结
Vue在根据vnode树渲染整个项目时,如果是普通vnode会直接创建元素插入,但如果是组件vnode会先初始化组件Vue实例并将其parent指向父实例,然后进行组件的patch将生成的真实DOM插入到组件所在位置中,这就是组件初始化渲染的大致过程。