前言
上回我們提到,在子元件存在的情況下,父元件在執行完
created
鉤子函式之後生成子元件的例項,子元件執行created
鉤子函式,同時也檢查是否也有子元件,有則重複父元件的步驟,否則子元件的dom
元素渲染
深入瞭解vnode
在上一篇文章中其實我們提到一個函式 —— createComponentInstanceForVnode
?
function createComponentInstanceForVnode (
vnode, // we know it's MountedComponentVNode but flow doesn't
parent // activeInstance in lifecycle state
) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
複製程式碼
與之相關的程式碼?
...
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
複製程式碼
從中我們可以得知:
- 子元件的例項是由
createComponentInstanceForVnode
生成的 - 上面的結論與
vnode.componentOptions.Ctor(options)
有關
VNode
通過全域性檢索componentOptions
,可知存在如下程式碼?
var VNode = function VNode (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
...
}
複製程式碼
實際上,在beforeMount
鉤子和mounted
鉤子之間,有段奇怪的程式碼?
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
複製程式碼
看過前面的文章的你其實已經知道Watcher
的執行邏輯:
- 初始化相關屬性,其中包括
getter
屬性 - 對
value
賦值的同時執行getter
附updateComponent
實現:
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
複製程式碼
這意味著函式 updateComponent
將被執行,同時存在這樣的呼叫順序(從上往下執行):
vm._render
vm_update
同時dom
元素肯定也是在這兩個函式呼叫時渲染
vm._render
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
var vnode;
try {
// There's no need to maintain a stack becaues 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);
} catch (e) {
handleError(e, vm, "render");
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
} catch (e) {
handleError(e, vm, "renderError");
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
);
}
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode
};
複製程式碼
簡單梳理下函式 _render
的執行過程(從上往下):
- 將
_parentVnode
(父元件的vnode
) 賦值給vm.$vnode
- 執行
normallizeScopedSlots
,將父子元件的$slots
跟$scopedSlots
合併 - 執行
render
函式並賦值給vnode
(即得到現有的vnode
) - 如果
vnode
為空則執行createEmptyVNode
函式 - 返回
vnode
這裡我們優先把斷點打入 render
函式,理所當然的會得到以下執行過程:
render
vm.$createElement
由於最先執行的是 new Vue({...})
,所以看上去斷點好像停在了「奇怪的地方」?
new Vue({
render: h => h(App),
}).$mount('#app')
複製程式碼
render
函式
細心的同學會注意到這樣一行程式碼?
vnode = render.call(vm._renderProxy, vm.$createElement);
複製程式碼
步進之後斷點馬上跳到了這裡?
new Vue({
render: h => h(App),
...
}).$mount('#app')
複製程式碼
其實,這裡將 vm._renderProxy
作為了 render
函式的上下文物件,而 vm.$createElement
返回一個閉包函式作為 render
函式的引數傳入
相關程式碼:
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
複製程式碼
總結
總結下生成 vnode
的完整邏輯:
- 執行
$mount
函式 - 判斷是否在瀏覽器環境下,是則獲取
dom
元素並賦值給el
變數,否則el
變數取值undefined
- 執行
mountComponent
函式 - 執行
new Watcher(vm, updateComponent, noop, ...)
- 由於
Watcher
的「特性」(傳入的updateComponent
賦值給getter
之後執行),_render
函式在這之後會被觸發 - 執行
$createElement
- 執行
createElement
- 執行
_createElement
- 判斷引數
data
及data.is
是否不為空,是則將data.is
賦值給tag
- 如果
tag
為空,那麼認為這是一個空白節點,此時呼叫createEmptyVNode
建立一個「空白節點」,並把isComment
標記為true
- 判斷
tag
是否是「保留字」,是則屬於HTML
標籤,生成對應的vnode
,否則呼叫createComponent
函式生成對應的vnode
- 最後返回
vnode
相關函式
createEmptyVNode
var createEmptyVNode = function (text) {
if ( text === void 0 ) text = '';
var node = new VNode();
node.text = text;
node.isComment = true;
return node
};
複製程式碼