Vue2.0源码理解(3) - 组件的创建和patch过程

组件化

组件化是vue的另一个核心思想,所谓的组件化,就是说把页面拆分成多个组件(component),每个组件依赖的css、js、图片等资源放在一起开发和维护。组件是资源独立的,在内部系统中是可以多次复用的,组间之间也是可以互相嵌套的。
接下来我们用vue-cli为例,来分析一下Vue组件是如何工作的,还是它的创建及其工作原理。

 import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(App)
})

创建组件 - createComponent

在分析createComponent函数前,我们得先知道vue的源码执行过程中是怎么调用到createComponent的。其实我们在上一章就有所提及,具体流程如下:
①:Vue.prototype.$mount; (src\platforms\web\entry-runtime-with-compiler.js和src\platforms\web\runtime\index.js)
②:mountComponent; (src\core\instance\lifecycle.js)
③:vm._update(vm._render()); (src\core\instance\lifecycle.js)
④:render.call(vm._renderProxy, vm.$createElement); (src\core\instance\render.js)
⑤:vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true); (src\core\instance\render.js)
⑥:createElement; (src\core\vdom\create-element.js)
⑦:_createElement; (src\core\vdom\create-element.js)
⑧:createComponent;

 //src\core\vdom\create-element.js
// part 3
export function _createElement (
context: Component, //上下文环境,一般就是vm
tag?: string | Class<Component> | Function | Object, //标签(element)
data?: VNodeData, //VNode数据,VnodeData类型,详见flow\vnode.js
children?: any, //Vnode子节点
normalizationType?: number //子节点规范类型
): VNode | Array<VNode> {
...
//这次这个tag就是Class<Component>了
if (typeof tag === 'string') {
...
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...
}

这次我们进入到_createElement函数走的就是createComponent的流程了。

 // src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //当前vm实例
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) //即Vue.extend(src\core\global-api\extend.js)
}
...
// 钩子函数挂载到data对象,详情查阅源码
installComponentHooks(data)
}

①:baseCtor其实就是Vue,流程如下

 // src\core\global-api\index.js
// 初始化时候定义了Vue.options._base = Vue
Vue.options._base = Vue
 // src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
// 在这里吧Vue.options合并到vm.$options上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}

而context其实就是vm,所以baseCtor =context.$options._base = vm.$options._base = Vue.options._base = Vue;

②:分析完baseCtor的由来,那么baseCtor.extend显然就是Vue.extend了,把Ctor对象转换成新的构造器,我们下面来详细看看Vue.extend。

 Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this //vue
const SuperId = Super.cid
//添加了一个_Ctor空对象属性
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
//后面会缓存cachedCtors[SuperId],防止多次生成相同构造器
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}

const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
//名称校验,防止你们整些花里胡哨的关键字段。
validateComponentName(name)
}

const Sub = function VueComponent (options) {
this._init(options) //vue._init
}
Sub.prototype = Object.create(Super.prototype) //子构造器原型指向父构造器原型
Sub.prototype.constructor = Sub
Sub.cid = cid++
//入参配置和vue配置合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super

// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}

// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}

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

// cache constructor 缓存起来
cachedCtors[SuperId] = Sub
return Sub
}

Vue.extend的作用其实就是构建一个Vue的子类,把对象转换成继承于Vue的构造器Sub并返回,然后对Sub本身扩展了option、全局API等,并对配置中的props和computed做了初始化工作,最后对Sub做了缓存,防止多次生成相同构造器。

 // src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //当前vm实例
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// 钩子函数挂载到data对象
installComponentHooks(data)
...
}

我们继续看createComponent函数,中间忽略了一些代码块,后续涉及到的时候再分析,现在我们先看看installComponentHooks函数。

 // src\core\vdom\create-component.js
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}

const hooksToMerge = Object.keys(componentVNodeHooks)
//默认钩子
const componentVNodeHooks = {
init(){},
prepatch(){},
insert(){},
destroy(){},
}
//合并钩子
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}

installComponentHooks其实就遍历了hooksToMerge,其实就是遍历了componentVNodeHooks的钩子然后和data.hook合并。
接下来我们继续往下看createComponent函数

 // src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //当前vm实例
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
...
return vnode
}

组件的new VNode和之前入参不太一样,我们先回顾一下Vnode的入参分别是什么?

 // src\core\vdom\vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
){},
}

主要有三个需要关注的点:
tag:会有一个'vue-component-'标识这个是个组件;
children:入参是undefined,记住组件的children是空的,这个到时候再patch遍历时候会用到;
componentOptions:组件的很多数据都存放在这里虽然chilrend入参为空,但是这里有传入;

new Vnode之后把vnode return到createComponent,到此我们createComponent的流程就跑完了。接下来我们又回到了vm._update(vm._render(), hydrating)中,开始了vm._update之旅了,其实也就是回到了把vnode转换成真实dom的patch函数。

patch函数 - 组件处理

又回到最初的起点,呆呆的站在patch前。

 // src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}

patch的流程和配置el或者template一样,会走到createElm

 // src\core\vdom\patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}

与之前最大的不同一就是跑到了createComponent函数时候的处理了,这边需要注意的是这个createComponent函数是patch.js中的,而不是我们上文提及到的create-component.js中的,这里要区分开来,不要混淆了。
下面我们来看看我们调用到的patch.js中的pcreateComponent函数

 // src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
//keepalive逻辑,先不解读
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// i.hook = data.hook,再判断是否有init方法,都成立是运行init。
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}

createComponent一次赋值且判断vnode.data、vnode.data.hook、vnode.data.init是否为空,都成立是则运行init方法。那么这个init方法又是哪个呢?我们还记得上文生成vnode时候在create-component.js中的调用createComponent函数的时候有个installComponentHooks方法吗?在那里我们插入了init钩子,忘了的可以回顾一下前文,下面我们看看init的代码。

 // src\core\vdom\create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// componentInstance是undefined,进入else逻辑
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
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)
}
},
}

在vnode.componentInstance和vnode.data.keepAlive都是undefined的情况下我们进入了else逻辑。
其中执行了createComponentInstanceForVnode函数,它返回的其实就是一个vm实例,下面我们具体看看createComponentInstanceForVnode函数。

 // src\core\vdom\create-component.js
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any //vm实例
): Component {
const options: InternalComponentOptions = {
_isComponent: true, //重新进入vue._init时候做判断用到
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate //undefined
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}

上述函数一开始配置了options,inlineTemplate是undefined,直接忽略,然后到了return new vnode.componentOptions.Ctor(options);
vnode.componentOptions.Ctor究竟是什么?这个我们得回到上诉的componentOptions,还记得那里new vnode传入的参数吗?其实倒数第二个就是componentOptions,入参是{ Ctor, propsData, listeners, tag, children},
所以知道这个怎么来了吧,vnode.componentOptions.Ctor其实就是入参的Ctor,而Ctor忘记了的,自己回顾一下Ctor。
所以其实它运行的就是extend中的Sub构造器:

 // src\core\global-api\extend.js
const Sub = function VueComponent (options) {
this._init(options)
}

因为Sub构造器继承的是Vue,因此this._init又回来Vue._init这个初始化操作。
那么组件进入_init和普通节点有什么不一样呢?我们来重新进入vue._init,下面只选择性展示不一样的地方。

 // src\core\instance\init.js
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)
}
...
}

_isComponent在createComponentInstanceForVnode函数是已经配置好是true了,所以会进入到initInternalComponent方法,下面看看它的定义:

 // src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
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的配置给到vm.$options上。
然后接下来看其他的差异:

 // src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
initLifecycle(vm)
...
}
 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
}

这里其实就是建立一个父子vm的关联,下面主要分析一下parent是什么?看字面意思也知道这是父级的东西,没错就是父级vm实例,他是在哪里定义的呢,这时候我们的回顾一下createComponentInstanceForVnode的方法,其中有配置options.parent = parent,而这个入参parent再往上追溯,其实就在componentVNodeHooks中的init方法中传递过来的,它传递的activeInstance其实是一个全局变量,那么这个又是在什么时候定义的?其实它在src\core\instance\lifecycle.js中做了定义,运行Vue._update的时候已经做了赋值,下面我们看一下代码。

 // src\core\instance\lifecycle.js
//全局定义
export let activeInstance: any = null
// activeInstance的赋值
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// 调用activeInstance赋值方法
const restoreActiveInstance = setActiveInstance(vm)
}

因此在每次运行到_update的时候,在进入下个阶段patch函数之前,它都会缓存住生成真实dom之前的vm,因此在patch做递归进入一下个阶段的时候,activeInstance就是它的父vm实例。知道了parent的来源,那么这父子关联的代码块的逻辑也就明了了。
这时候patch也进入了递归阶段,递归方法还是和之前相似,在回顾一下流程:
①:Vue.prototype._init; (src\core\instance\init.js)
②:Vue.prototype.$mount; (src\platforms\web\entry-runtime-with-compiler.js)
②:Vue.prototype.$mount; (src\platforms\web\runtime\index.js)
③:mountComponent; (src\core\instance\lifecycle.js)
④:Vue.prototype._render; (src\core\instance\render.js)
⑤:Vue.prototype._update; (src\core\instance\lifecycle.js)
⑥:vm.__patch__; (src\platforms\web\runtime\index.js)
⑦:createPatchFunction; (src\core\vdom\patch.js)
⑧:patch; (src\core\vdom\patch.js)
⑨:createElm; (src\core\vdom\patch.js)
⑩:createComponent; (src\core\vdom\patch.js)
在createComponent中的i.init开始了新一轮的初始化,当递归结束后我们还有接下来的流程,我们继续看createComponent方法。

 // src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
//这里会完成整个patch的递归流程
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it.
// 在初始化hook之后,如果vnode是一个子组件,那么它应该创建一个子实例并挂载它。
// the child component also has set the placeholder vnode's elm.
// 子组件还设置了占位符vnode的elm
// in that case we can just return the element and be done.
// 在这种情况下,我们只需返回element就可以了

// 只有在patch结束后才进入了这里
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}

递归调用i.init结束后,我们进入下一个逻辑。vnode.componentInstance,在i.int中得到了赋值,就是一个vm实例,详情回顾componentVNodeHooks,因此我们进入了initComponent函数,下面看看initComponent是做什么的。

 // src\core\vdom\patch.js
function initComponent (vnode, insertedVnodeQueue) {
...
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 创建一些钩子,后续再分析
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
...
}
}

initComponent主要是给vnode.elm赋值,vnode.componentInstance.$el即vm.$el,__patch__方法有做返回。我们继续回到initComponent的下一步insert,insert其作用就是根据判断插入dom(insertBefore/appendChild),之前有讲述过,至此,子组件的真实dom就生成了。由于这是递归插入的模式,因此dom的插入顺序是先子后父。

到此组件的patch的过程就到此结束了,建议配合代码运行调试,反复几次理解运行逻辑,及参数传递与缓存。

标签: Javascript

添加新评论