三、组件化
vue允许我们使用小型、独立和通常可复用的组件构建大型应用。这样在项目的代码复用性,解耦,维护性上都有很大的提升。
以vue-cli的初始化部分分析组件化的实现
new Vue({
el: '#app',
render: h => h(App)
})
1、createComponent 生成Vnode
简单回顾一下生成Vnode的过程
1.执行_render,vm._render()
2.通过$options拿到render const { render, _parentVnode } = vm.$options
3.执行render,把$createElement作为参数传入,vnode = render.call(vm._renderProxy, vm.$createElement)
4.$createElement函数返回createElementvm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
5.createElement返回_createElementcreateElement => _createElement
上一次分析中,传入的tag参数是'dev'符合typeof tag === 'string',本次传入的是App是一个组件,因此走到了else vnode = createComponent(tag, data, context, children)这条逻辑
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
if (typeof tag === 'string') {
...
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...
}
组件生成VnodecreateComponent函数主要做了
- 拿到Vue构造函数,通过
Ctor = baseCtor.extend(Ctor)生成当前组件的构造函数 - 初始化相应的钩子函数
installComponentHooks(data) - 通过
new VNode生成Vnode
// src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// 通过 context.$options._base 拿到Vue构造函数
const baseCtor = context.$options._base // Vue
// 把传入的Ctor调用Vue.extend方法,生成新的构造函数
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
...
// install component management hooks onto the placeholder node
// 绑定相应的钩子
installComponentHooks(data)
// return a placeholder vnode
// 创建组件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
}
1-1 baseCtor.extend(Ctor)
首先执行了const baseCtor = context.$options._base 来定义baseCtor,context是vm也就是当前的vue实例
$options._base,在initGlobalAPI的时候,给Vue.options._base赋值
// src/core/global-api/index.js
Vue.options._base = Vue
vue在执行_init的时候会初始化$options,通过mergeOptions方法,把Vue.options合并到vm.$options
// src/core/instance/init.js
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
所以baseCtor其实就是Vue构造函数,baseCtor.extend(Ctor) === Vue.extend(Ctor)
调用Ctor = baseCtor.extend(Ctor)后 Ctor其实就是一个继承于Vue的构造函数
src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {
...
Vue.extend
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this // Vue
const SuperId = Super.cid
...
const name = extendOptions.name || Super.options.name
...
const Sub = function VueComponent (options) {
this._init(options)
}
// Sub继承于Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并配置处理
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
...
return Sub
}
}
1-2 installComponentHooks(data)
installComponentHooks函数为data初始化了4个钩子
src/core/vdom/create-component.js
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
/**
* componentVNodeHooks 中有4个函数钩子
* const componentVNodeHooks = {
* init (vnode: VNodeWithData, hydrating: boolean): ?boolean {...},
* prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode){...},
* insert (vnode: MountedComponentVNode) {...},
* destroy (vnode: MountedComponentVNode) {...}
* }
* const hooksToMerge = Object.keys(componentVNodeHooks)
*/
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
/**
* 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
* }
*/
// 合并策略
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
1-3 new VNode 生成一个vnode对象
// src/core/vdom/create-component.js
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
)
2、patch
patch中会调用createElm函数,在vnode转换为真实dom的过程中组件的和标签有所区别,会进入createComponent这个函数
src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
在组件vnode的生成中,会调用installComponentHooks(data),为vnode的data挂载4个钩子
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
// 首先判断有没有vnode.data
if (isDef(i)) {
...
// 执行vnode.data.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
}
}
}
首先会执行data中的init函数,执行createComponentInstanceForVnode函数
// src/core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// keep-alive逻辑
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
...
} else {
// 执行createComponentInstanceForVnode函数,他返回的是 vnode.componentInstance
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
...
}
createComponentInstanceForVnode函数,执行new vnode.componentOptions.Ctor(options)
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
...
// 调用 new vnode.componentOptions.Ctor(options)
return new vnode.componentOptions.Ctor(options)
}
vnode.componentOptions.Ctor(options)函数中,vnode.componentOptions是在组件vnode创建中 { Ctor, propsData, listeners, tag, children }其中的Ctor是通过Ctor = baseCtor.extend(Ctor)赋值的,也就是说Ctor实际上是组件的构造函数,那么在new vnode.componentOptions.Ctor(options)的过程中,实际上是执行了Sub中的 this._init(options)也就是Vue的_init方法
那么创建组件在执行Vue._init方法中的区别,首先合并options的操作发生了改变
// src/core/instance/init.js
// options._isComponent在createComponentInstanceForVnode中被赋值为了true
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 {
...
}
initInternalComponent函数中parent实际上是第二个参数activeInstance,他定义在lifeccyle.js文件中的一个全局变量
// 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
}
}
_update会调用setActiveInstance(vm),对activeInstance赋值为当前的vm实例
// src/core/instance/lifecycle.js
export function setActiveInstance(vm: Component) {
// 把之前的vm实例保存
const prevActiveInstance = activeInstance
// 把当前activeInstance实例赋值为当前vm实例
activeInstance = vm
// 当再次调用时,进行还原
return () => {
activeInstance = prevActiveInstance
}
}
initLifecycle中因为当前的parent是Vue,当前的vm是组件app的vm,所以通过$parent和$children建立父子关系
// src/core/instance/lifecycle.js
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
...
}
_init中因为组件创建中没有vm.$options.el,最后的vm.$mount(vm.$options.el)是不会执行的,_init结束后,手动调用了child.$mount(hydrating ? vnode.elm : undefined, hydrating)也就是子组件的mountComponent
// src/core/instance/lifecycle.js
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
...
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} 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 */)
...
return vm
}
子组件的vm._render(),vm.$vnode赋值为vm.$options._parentVnode也就是父的vnode,然后给当前的vnode的parent也赋值为vm.$options._parentVnode
// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
...
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
// vm.$vnode是_parentVnode,也就是占位符vnode,即父的vnode
vm.$vnode = _parentVnode
...
let vnode
...
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
}
...
// set parent
vnode.parent = _parentVnode
return vnode
}
到了子组件的_update中,首先把activeInstance = vm把当前的vm作为了activeInstance,vm._vnode = vnode也就是说 vm._vnode和vm.$vnode是一个父子关系。 vm._vnode是一个渲染的vnode,vm.$vnode是占位符vnode,之后执行了 vm.__patch__
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
...
// setActiveInstance函数把activeInstance赋值为当前的vm
const restoreActiveInstance = setActiveInstance(vm)
// _vnode赋值为当前的渲染vnode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 执行__patch__
if (!prevVnode) {
// initial render
// 此时的vm.$el是undefined
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
...
}
...
}
执行patch的时候第一个参数为空,走到了createElm(vnode, insertedVnodeQueue),如果子组件中还有子组件,就会再次调用createComponent建立这样的组件树,在createElm中如果是组件则不会执行下边的dom插入操作,组件的插入会在createComponent中接着执行initComponent,vnode.elm = vnode.componentInstance.$el,$el为子组件的根元素,再执行insert(parentElm, vnode.elm, refElm)进行插入,插入的顺序一定是先子后父的,因为父组件的init最终会去先创建子组件,子组件会先执行insert
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 */)
}
// 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
}
}
}
总结
- 组件
patch的流程, createComponent -> 子组件init -> 子组件render -> 子组件patch activeInstance为当前激活的vm实例,vm.$vnode为组件的占位符vnode,vm._vnode是渲染vnode- 插入的顺序是先子后父