组件化-patch
在 08.组件化-createComponent 中,我们通过 createComponent 创建了组件的 VNode,然后执行 _update 函数,也就是 patch 函数,把 VNode 转换为真正的 浏览器DOM 节点。patch 函数在之前分析普通对象时已经大概了解 【Vue 源码】数据驱动-7._update,接下来我们具体分析一下当组件的 VNode 是如何被解析的。
patch 会调用 createElm 生成真实的 浏览器DOM 并插入到对应的节点,createElm 定义在 src/core/vdom/patch.js 中:
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
// ...
}
createComponent
这里对判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的结果,如果为 true 直接结束。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;
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 做判断,如果是 component 组件那么这些条件就会满足,执行 i.init 方法。
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 */);
}
// ...
}
i.init 出现在上一节中,就是在创建组件 VNode 时,执行的合并钩子函数 installComponentHooks。
const componentVNodeHooks = {
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);
}
},
// ...
};
init 钩子除去 keepAlive 的情况下,会走到下面逻辑:
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
));
createComponentInstanceForVnode
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);
}
createComponentInstanceForVnode 方法会创建一个 创建组件的 options,然后调用 new vnode.componentOptions.Ctor(options),
这里的 vnode.componentOptions.Ctor 就是子组件对应的构造函数,
我们在 08.组件化-createComponent 中已经知道它实际上是继承于 Vue 的一个构造器 Sub。
Vue.extend = function (extendOptions: Object): Function {
// ...
// Vue实例的初始化逻辑,初始化的同时添加全局api
const Sub = function VueComponent(options) {
this._init(options);
};
// ...
return Sub;
};
所以 子组件 的 实例化时机 就是在这里执行的,并且执行 _init 方法,在执行 _init 方法时,和之前有点不同:
// 给Vue的原型prototype添加 _init方法,在new Vue初始化实例时调用
Vue.prototype._init = function (options?: Object) {
const vm: Component = this;
// ...
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
);
}
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
在这次的流程中,因为 options._isComponent 为 true,所以执行 initInternalComponent(vm, options) 进行合并配置。
initInternalComponent
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;
// 父 VNode
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 流程中,只有几个关键的点需要记住:
opts.parent = options.parentopts._parentVnode = parentVnode
他是把之前我们通过 createComponentInstanceForVnode 函数传入的几个参数合并到组件内部的 $options 选项里了。
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
回到 _init,因为在组件初始化的时候,没有 el,所以在 _init 方法中不执行 mount。
组件的 mount 时机是在 i.init,即上面写到的这里执行的
const componentVNodeHooks = {
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);
}
},
// ...
};
在这里完成实例化 _init 之后执行 child.$mount(hydrating ? vnode.elm : undefined, hydrating)。hydrating 为服务端渲染标识,所以这里为 child.$mount(undefined, 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,而 render 函数生成的 vnode 当前组件的渲染 vnode,vnode 的 parent 指向了 _parentVnode,也就是 vm.$vnode,它们是一种父子的关系。
我们知道在 _render 执行完生成 VNode 之后又会去执行 _update 去渲染 VNode,来看一下在渲染的过程中,组件VNode 是什么流程。
vm._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 有几个关键的点:
vm._vnode = vnode,vnode是通过_render生成的vm._vnode和vm.$vnode是一种父子关系,可以表示为vm._vnode.parent === vm.$vnodeactiveInstance的作用是保持当前上下文中Vue的实例,它定义在src/core/instance/lifecycle.js中,是模块全局变量。我们之前调用createComponentInstanceForVnode时传入的activeInstance参数,就是从lifecycle导入的。因为JavaScript是单线程的,所以在父子、不同组件进行不同操作时,锁定当前组件实例,并把它作为子组件的父 Vue 实例。
之前我们提到过对子组件的实例化过程先会调用 initInternalComponent(vm, options) 合并 options,把 parent 存储在 vm.$options 中,在 $mount 之前会调用 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.$parent 就是用来保留当前 vm 的父实例,并且通过 parent.$children.push(vm) 来把当前的 vm 存储到父实例的 $children 中。
在 vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const prevActiveInstance = activeInstance 用 prevActiveInstance 保留上一次的 activeInstance。实际上,prevActiveInstance 和当前的 vm 是一个父子关系,当一个 vm 实例完成它的所有子树的 patch 或者 update 过程后,activeInstance 会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的 父Vue 实例,并在 _init 的过程中,通过 vm.$parent 把这个父子关系保留。
最后回到 patch,就是调用 __patch__ 来渲染 VNode。
function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
let isInitialPatch = false;
const insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// ...
}
// ...
}
这里又回到了文章的开始,在这里我们只传了 2 个参数,所以 parentElm 是 undefined。我们来看 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)) {
// ...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
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);
}
}
我们在这里传入的 vnode 是组件渲染的 vnode,也就是之前说的 vm._vnode。如果组件的根节点是普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的值是 false,接下来就和我们之前的分析一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。
由于我们传入的 parentElm 为 undefined,所以对组件的插入,在 createComponent 中有那么一段逻辑:
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;
}
}
}
在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么 DOM 的插入顺序是先子后父。
总结
目前我们已经知道,一个组件 VNode 是如何创建、初始化、渲染的过程了,在这个过程中先渲染 <App/> 这样的占位符节点,然后渲染 App.vue 里面的内容,如果碰到里面还有组件的话再进行递归。我们知道组件的编写其实是返回一个 JavaScript 对象,那么下一节将深入了解组件的合并配置。
参考:Vue.js 技术揭秘