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 排版

相關文章
相關標籤/搜索