经过Vue 源码探秘(九)(createComponent)的分析咱们知道,当咱们经过 createComponent
建立了组件 VNode
,接下来会走到 vm._update
,执行 vm.__patch__
把 VNode
转换成真正的 DOM
节点。以前分析的是针对一个普通的 VNode
节点,接下来咱们来看看组件的 VNode
会有哪些不同的地方。vue
patch
函数的核心步骤是调用 createElm
函数来建立节点,这一节咱们再次回顾这个函数,看它是怎么处理组件的 VNode
的。node
这一节咱们依然围绕上一节的例子来分析:react
import Vue from "vue";
import App from "./App.vue";
var app = new Vue({
el: "#app",
// 这里的 h 是 createElement 方法
render: h => h(App)
});
复制代码
回顾一下 createElm
的实现,它的定义在 src/core/vdom/patch.js
中,在函数的一开始有这么一段代码:web
// src/core/vdom/patch.js
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
// ..
}
复制代码
这里判断若是调用 createComponent
函数返回 true
,则结束执行 createElm
函数。传给 createComponent
函数的 vnode
参数是组件 VNode
,所以 createComponent
函数返回 true
,不会再往下执行。来看 createComponent
函数的定义:app
// 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;
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;
}
}
}
复制代码
函数一开始的 isReactivated
和 Keep-alive
相关,暂时不展开讲。而后接下来的 if
语句的意思是判断 vnode.data.hook.init
是否存在,这里vnode
是一个组件 VNode
,那么条件知足,而且获得 i
就是 init
钩子函数。dom
回顾Vue 源码探秘(九)(createComponent),在执行 createComponent
函数的时候会调用 installComponentHooks
函数给 vnode.data.hook
安装四个钩子函数。回顾 init
钩子函数的代码,它被定义在 src/core/vdom/create-component.js
文件中:编辑器
// src/core/vdom/create-component.js
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
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)
}
}
复制代码
if
语句依然是 Keep-alive
相关,咱们先跳过。else
逻辑调用了 createComponentInstanceForVnode
函数建立一个Vue
实例,传入 vnode
、activeInstance
两个参数。activeInstance
是指什么呢,在 src/core/instance/lifecycle.js
文件中有这么几行代码:ide
// src/core/instance/lifecycle.js
export let activeInstance: any = null;
export function lifecycleMixin(Vue: Class<Component>) {
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
// ...
const restoreActiveInstance = setActiveInstance(vm);
// setActiveInstance内: const prevActiveInstance = activeInstance
// activeInstance = vm
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// setActiveInstance返回:activeInstance = prevActiveInstance
// ...
};
// ...
}
复制代码
这里面在先后分别调用了
setActiveInstance(vm)
和他的返回值,我把函数内部的逻辑揉进了总体代码逻辑中。函数
能够看到,activeInstance
是一个全局变量,在调用 __patch__
前先用 prevActiveInstance
保存 activeInstance
,而后将当前实例 vm
赋给 activeInstance
,在执行完 __patch__
后再恢复 activeInstance
原来的值。那为何要这样作呢,咱们带着这个疑问继续往下看。组件化
咱们回过来继续看 createComponentInstanceForVnode
函数的代码:
// src/core/vdom/create-component.js
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
};
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options);
}
复制代码
这里定义了一个options
,将 vnode
做为 _parentVnode
,将 activeInstance
做为 parent
。后面和 inline-template
相关的先略过。最后经过构造函数 vnode.componentOptions.Ctor
建立一个对象并返回,并传入 options
做为参数。
这里的 vnode.componentOptions.Ctor
对应的就是子组件的构造函数,回顾上一节,咱们知道它其实是继承于 Vue
的一个构造器 Sub
,至关于 new Sub(options)
。
回顾 Vue.extend
函数是怎么定义子构造函数的:
const Sub = function VueComponent(options) {
this._init(options);
};
复制代码
这里子构造函数继承了 Vue.prototype
上的 _init
函数,因此 createComponentInstanceForVnode
函数最后就是将 options
传给了 Vue.prototype._init
函数并执行。
来看下init
方法:
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
// ...
// 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
逻辑调用 initInternalComponent
函数。简单看下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;
}
}
复制代码
initInternalComponent
函数的做用就是往 vm.$options
上添加属性,这里要重点关注的属性是 _parentVnode
和 parent
,这两个分别对应一开始在 init
钩子函数传入的 VNode
和 activeInstance
参数。
回到 _init
函数,来看最后一段代码:
Vue.prototype._init = function(options?: Object) {
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
复制代码
这一段代码也是组件实例与普通实例不一样之处,因为组件没有 el
,因此不会执行 if
语句中的逻辑。从新回到钩子函数 init
:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if ( /* ... */ ) { // ...
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
复制代码
在调用 createComponentInstanceForVnode
函数建立了组件实例后,调用了组件实例的 $mount
方法。
回顾Vue 源码探秘(四)(实例挂载 $mount),$mount
方法分为原型定义和重写两部分,重写部分就是多了将 template
转换为 render
函数的步骤,而组件在编译时就生成了 render
函数,不会执行重写部分,只执行了原型定义的 $mount
函数。回顾原型上定义的 $mount
函数:
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function(
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
复制代码
$mount
函数最终调用了 mountComponent
函数。咱们知道 mountComponent
函数会建立一个 Watcher
对象并调用 updateComponent
函数,进而执行vm._render()
方法:
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;
// render self
let vnode;
try {
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode;
return vnode;
};
复制代码
能够看到 _render
函数拿到 vm.$options._parentVNode
,也就是占位符 VNode
,对应例子里面的 App
组件的 VNode
,将它赋值给 vm.$vnode
。以后经过组件的渲染函数 render
建立渲染 vnode
,并把 _parentVnode
赋给了 vnode.parent
。
以后建立出来的渲染 vnode
传给了 _update
函数:
// src/core/instance/lifecycle.js
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
const prevEl = vm.$el;
const prevVnode = vm._vnode;
const prevActiveInstance = activeInstance;
activeInstance = vm;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
activeInstance = prevActiveInstance;
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
复制代码
也就是说咱们绕了一大圈又回到了 _update
函数。这是由于 Vue
的初始化是深度优先搜索
的过程,Vue
实例引用了组件,若是组件中又引用了组件,那么它就会一直执行上述流程直到一个 vm
实例完成它的全部子树的 patch
或者 update
过程。
_update
过程当中有几个关键的代码,首先 vm._vnode = vnode
的逻辑,这个 vnode
是经过 vm._render()
返回的组件渲染 VNode
,vm._vnode
和 vm.$vnode
的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode
。
回顾这一节的内容,在实例化子组件的时候,咱们须要知道这个子组件的父实例是谁,把它存入 vm.$options
中,后面调用 initLifecycle
函数的时候再把它的父实例保存到 vm.$parent
,同时经过 parent.$children.push(vm)
来把子组件的 vm
存储到父实例的 $children
中,经过这些操做创建父子关系。
activeInstance
的做用就体如今这里,在 vm._update
的过程当中,把当前的 vm
赋值给 activeInstance
,同时 prevActiveInstance
保留上一次的 activeInstance
。当一个 vm
实例完成它的全部子树的 patch
或者 update
过程后,activeInstance
经过 prevActiveInstance
又回到它的父实例,这样就完美地保证了在整个深度遍历过程当中,在实例化子组件的时候能传入当前子组件的父 Vue
实例,并在 initLifecycle
的过程当中,经过 vm.$parent
把这个父子关系保留。
回到 _update
函数,这里又再次调用了 __patch__
方法,而后又再次执行 patch
方法当中的 createElm
方法,就又回到本节开头提到的判断:
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)) {
if (process.env.NODE_ENV !== "production") {
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
"Unknown custom element: <" +
tag +
"> - did you " +
"register the component correctly? For recursive components, " +
'make sure to provide the "name" option.',
vnode.context
);
}
}
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);
}
if (process.env.NODE_ENV !== "production" && data && data.pre) {
creatingElmInVPre--;
}
} 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);
}
}
复制代码
这一次传给 createComponent
函数的 vnode
是渲染 VNode
而不是组件 VNode
,所以会继续往下执行。
往下的逻辑以前在分析元素节点 VNode
时已经分析过了,而其中有一点区别就是执行 insert(parentElm, vnode.elm, refElm)
这条语句时,因为 parentElm
参数对应的是 vm.$el
,而组件实例是没有 $el
的,所以这里的 parentElm
是 undefined
,再来看 insert
函数的定义:
// src/core/vdom/patch.js
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
复制代码
能够看到,没有 parentElm
的话是不会执行插入操做的,那插入操做是在哪里执行的呢,咱们回顾 createComponent
函数:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
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;
}
}
}
复制代码
执行完 initComponent
函数后又执行了 insert
函数,插入过程就是在这里执行的,由于这里的 parentElm
参数是有值的。
因为整个过程是一个深度遍历的过程,若是组件中又嵌套子组件,那么它会递归调用 createComponent
函数完成子组件的一系列过程,所以整个 DOM
的插入顺序是先子后父
。
本节带你们梳理了一个组件的 VNode
建立、初始化、渲染的完整流程。组件的 patch
过程相对于普通元素的 patch
来讲复杂了许多,这部分须要反复翻看,经过断点调试等方法来加深理解。
在对组件化的实现有一个大概了解后,接下来咱们来介绍一下这其中的一些细节。咱们知道编写一个组件其实是编写一个 JavaScript
对象,对象的描述就是各类配置,以前咱们提到在 _init
的最初阶段执行的就是 merge 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
);
}
复制代码
那么下一节咱们从源码角度来分析合并配置的过程。
本文使用 mdnice 排版