Vue源码探秘(十)(组件的patch过程)

引言

经过Vue 源码探秘(九)(createComponent)的分析咱们知道,当咱们经过 createComponent 建立了组件 VNode,接下来会走到 vm._update,执行 vm.__patch__VNode 转换成真正的 DOM 节点。以前分析的是针对一个普通的 VNode 节点,接下来咱们来看看组件的 VNode 会有哪些不同的地方。vue

patch 函数的核心步骤是调用 createElm 函数来建立节点,这一节咱们再次回顾这个函数,看它是怎么处理组件的 VNode 的。node

createComponent

这一节咱们依然围绕上一节的例子来分析: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;
}
}
}
复制代码

函数一开始的 isReactivatedKeep-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实例,传入 vnodeactiveInstance 两个参数。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 上添加属性,这里要重点关注的属性是 _parentVnodeparent ,这两个分别对应一开始在 init 钩子函数传入的 VNodeactiveInstance参数。

回到 _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() 返回的组件渲染 VNodevm._vnodevm.$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 的,所以这里的 parentElmundefined ,再来看 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 排版

相关文章
相关标签/搜索