初次探索
什麼是虛擬dom
Vue
通過建立一個虛擬 DOM
對真實 DOM
發生的變化保持追蹤。請仔細看這行程式碼:
return createElement('h1', this.blogTitle)
複製程式碼
createElement
到底會返回什麼呢?其實不是一個實際的 DOM
元素。它更準確的名字可能是 createNodeDescription
,因為它所包含的資訊會告訴 Vue 頁面上需要渲染什麼樣的節點,及其子節點。我們把這樣的節點描述為“虛擬節點 (Virtual Node
)”,也常簡寫它為“VNode
”。“虛擬 DOM
”是我們對由 Vue
元件樹建立起來的整個 VNode
樹的稱呼。
以上這段對虛擬Dom
的簡短介紹來自Vue
的官網
第一個斷點
我們一開始的斷點先打在app.vue
的兩個hook
上:
export default {
name: 'app',
created () {
debugger
},
mounted () {
debugger
}
}
複製程式碼
重新整理頁面,此時呼叫棧中顯示的函式跟預想中的不太一樣:
在created
這個hook
執行之前,多出了一些比較奇怪的函式:
createComponentInstanceForVnode
Vue._update
mountComponent
?看完以後我心中出現了一個疑問:
為什麼在
created
鉤子執行之前就出現了mountComponent
這個方法,到底是文件出問題了,還是文件出問題了呢?帶著這個疑惑我們接著往下看
mountComponent
做了什麼?
通過上面打第一個斷點,其實不難看出這樣的執行順序(從上往下):
(annoymous)
Vue.$mount
mountComponent
(annoymous)
這步其實就是在執行我們的main.js
,程式碼很短:
...
new Vue({
render: h => h(App)
}).$mount('#app')
複製程式碼
Vue.$mount
Vue.prototype.$mount = function (
el,
hydrating
) {
// 判斷是否處於瀏覽器的環境
el = el && inBrowser ? query(el) : undefined;
// 執行mountComponent
return mountComponent(this, el, hydrating)
};
複製程式碼
mountComponent
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
// 開發環境下給出警告提示
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
);
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
);
}
}
}
callHook(vm, 'beforeMount');
var updateComponent;
/* istanbul ignore if */
// 這裡對測試環境跟正式環境的updateComponent 做了實現上的一個區分
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var vnode = vm._render();
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
複製程式碼
簡單羅列下上面這兩段程式碼的邏輯?:
- 呼叫
beforeMount
鉤子函式 - 封裝一個
updateComponent
函式 - 執行
new Watcher
並將updateComponent
當做引數傳入 - 呼叫
vm._update
方法
_update
方法是如何被觸發的?
Watcher
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
...
// 將函式賦值給this.getter,這裡是updateComponent函式
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== 'production' && warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
// 根據this.lazy決定是否觸發get方法
this.value = this.lazy
? undefined
: this.get();
};
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
// 這裡呼叫getter方法,實際上也就是呼叫updateComponent方法並拿到返回值
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
// 返回函式(updateComponent)執行結果
return value
};
複製程式碼
簡單梳理下上面這段程式碼的邏輯:
- 新建
Watcher
例項時,將updateComponent
賦值給getter
屬性 - 通過
this.get
方法,觸發updateComponent
函式 - 最終拿到函式的執行結果
小結
通過上面的分析我們可以初步得出一個結論:
元件的渲染跟
Watcher
離不開關係,父元件在執行完created
鉤子函式之後,會呼叫updateComponent
函式對子元件進行處理
深入研究
如果前面你動手跟著斷點一直走,那麼不難得知存在這樣的呼叫關係(從上往下):
- ...
mountComponent
Watcher
get
updateComponent
Vue._update
patch
createElm
createComponent
init
createComponentInstanceForVnode
VueComponent
Vue._init
callHook
invokeWithErrorHandling
created
Vue.prototype._update
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
// 重儲存當前父例項
var restoreActiveInstance = setActiveInstance(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 {
// 執行patch函式
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
...
};
複製程式碼
當然,我們通過全域性檢索可以得知_patch
函式相關的程式碼?:
// 只在瀏覽器環境下patch函式有效
Vue.prototype.__patch__ = inBrowser ? patch : noop;
複製程式碼
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
}
}
複製程式碼
這裡先不深究patch
的實現,我們只要知道patch
是使用createPatchFunction
來生成的一個閉包函式即可。
子元件的渲染
我們注意到,在子元件created
鉤子執行之前存在一個init
方法?:
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 建立子元件例項
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
// 對子元件執行$mount方法
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
...
複製程式碼
相關程式碼:
createComponentInstanceForVnode
function createComponentInstanceForVnode (
vnode, // we know it's MountedComponentVNode but flow doesn't
parent // activeInstance in lifecycle state
) {
// 初始化一個子元件的vnode配置
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// 檢查render函式內是否有template模板
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
// 返回子元件例項
return new vnode.componentOptions.Ctor(options)
}
複製程式碼
總結
- 存在子元件時,先初始化父元件,在
created
鉤子執行之後,生成子元件的vnode
例項 - 子元件的
created
鉤子執行完,檢查子元件是否也有子元件 - 子元件也存在子元件時,則重複1,否則直接執行
$mount
函式,渲染子元件