dom
Vue
經過創建一個虛擬 DOM
對真實 DOM
發生的變化保持追蹤。請仔細看這行代碼:javascript
return createElement('h1', this.blogTitle)
createElement
到底會返回什麼呢?其實不是一個實際的 DOM
元素。它更準確的名字多是 createNodeDescription
,由於它所包含的信息會告訴 Vue 頁面上須要渲染什麼樣的節點,及其子節點。咱們把這樣的節點描述爲「虛擬節點 (Virtual Node
)」,也常簡寫它爲「VNode
」。「虛擬 DOM
」是咱們對由 Vue
組件樹創建起來的整個 VNode
樹的稱呼。html
以上這段對虛擬Dom
的簡短介紹來自Vue
的官網前端
咱們一開始的斷點先打在app.vue
的兩個hook
上:vue
export default { name: 'app', created () { debugger }, mounted () { debugger } }
刷新頁面,此時調用棧中顯示的函數跟預想中的不太同樣: java
在created
這個hook
執行以前,多出了一些比較奇怪的函數:node
createComponentInstanceForVnode
Vue._update
mountComponent
🤔看完之後我心中出現了一個疑問:express
爲何在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
鉤子執行完,檢查子組件是否也有子組件$mount
函數,渲染子組件掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。