Vue源碼翻譯之渲染邏輯鏈
本篇文章主要要記錄說明的是,Vue在Vdom的創建上的相關細節。這也是描繪了Vue在界面的創建上的一個邏輯順序,同時我也非常拜服作者編碼的邏輯性,當然或許這麽龐大復雜的編碼不是一次性鑄就的,我想應該也是基於多次的需求變動而不斷完善至現在如此龐大的結構和復雜度。
首先我們回顧 上一篇文章 中,講到了Vue實例initMixin,就是實例初始化,但是,我們在看Vue的源碼時,經常會遇到某個變量或方法,好像還沒定義,怎麽就用上了。那是因為,其實我們在使用Vue,即 new Vue(options) 的時候,其實Vue的整個類,已經在我們js的頭部import時,就已經完全定義好了,Vue的類因為過於龐大,內部復雜,並且還有抽象分層,所以類的整個寫法,會比較分散,但是當你在用它的時候(new Vue()),其實它已經完全初始化完畢,整個類的裝配已經齊全,所以我們在看源碼時,是根據工程目錄來看,但Vue是建立在文本pack上,所以最終這些工程目錄是會整合到一個文件裏,所以我們遇到沒看到的變量,不要感到困惑,你只要知道,它一定是在其他的某個地方初始化過。
So,我們這次要說的,是整個Vue再界面的繪制邏輯。
整個Vue組件的繪制過程,是這樣一個方法鏈條:
vm.$mount() -> mountComponent -> new Watcher()的構造函數 -> watcher.get() -> vm._update -> vm.__patch__()-> patch.createElm -> patch.createComponent -> componentVNodeHooks.init() -> createComponentInstanceForVnode -> child.$mount
好了,從vm.$mount() -----> child.$mount,我相信大家應該看出個名堂來了,其實就是遞歸調用。在執行createComponentInstanceForVnode的時候,就把創建好的Vnode與父級Vnode進行關聯,通過這麽一長串的遞歸調用去創建整個Vnode Tree,然後在整個樹創建完了以後呢,在patch那部分的代碼,會繼續後續邏輯,後續邏輯自然就是把這個創建好的局部Vnode樹,替換掉對應的舊的Vnode節點,相當於更新了局部的頁面內容。但這只是執行界面繪制的動作鏈條,要理解整個過程,要區分一下,區分成執行,和初始化兩個步驟。我們來看看定義是從哪裏開始的。
首先要看的肯定是上一篇文章中講到的 vm._c 以及 vm.$createElement ,這個函數的定義,是整個界面繪制邏輯的入口,但是並不是動作觸發的入口,就像這個函數的名字一樣,initRender,初始化繪制方法,實際上,就是對繪制動作進行了定義,但是並不是從這裏執行。
InitRender
path:src/core/instance/render.js
1 export function initRender (vm: Component) { 2 vm._vnode = null // the root of the child tree 3 vm._staticTrees = null // v-once cached trees 4 const options = vm.$options 5 const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 6 const renderContext = parentVnode && parentVnode.context 7 vm.$slots = resolveSlots(options._renderChildren, renderContext) 8 vm.$scopedSlots = emptyObject 9 // bind the createElement fn to this instance 10 // so that we get proper render context inside it. 11 // args order: tag, data, children, normalizationType, alwaysNormalize 12 // internal version is used by render functions compiled from templates 13 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 14 // normalization is always applied for the public version, used in 15 // user-written render functions. 16 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 17 18 // $attrs & $listeners are exposed for easier HOC creation. 19 // they need to be reactive so that HOCs using them are always updated 20 const parentData = parentVnode && parentVnode.data 21 22 /* istanbul ignore else */ 23 if (process.env.NODE_ENV !== ‘production‘) { 24 defineReactive(vm, ‘$attrs‘, parentData && parentData.attrs || emptyObject, () => { 25 !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) 26 }, true) 27 defineReactive(vm, ‘$listeners‘, options._parentListeners || emptyObject, () => { 28 !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) 29 }, true) 30 } else { 31 defineReactive(vm, ‘$attrs‘, parentData && parentData.attrs || emptyObject, null, true) 32 defineReactive(vm, ‘$listeners‘, options._parentListeners || emptyObject, null, true) 33 } 34 }
由此,我們再去查看createElement,這是一個又一個代碼的封裝,整個方法鏈的調用是這樣子:
createElement -> _createElement -> createComponent
最終返回Vnode對象或Vnode對象數組(應該是在v-for的情況下返回數組)。中間的片段包含著一些校驗邏輯,我就不說了,不是什麽特別難理解的地方,我們直接看createComponent的方法
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { // Vue.extend(Component) Ctor = baseCtor.extend(Ctor) } // if at this stage it‘s not a constructor or an async component factory, // reject. if (typeof Ctor !== ‘function‘) { if (process.env.NODE_ENV !== ‘production‘) { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // merge component management hooks onto the placeholder node mergeHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ‘‘}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode }
首先,說明一下,入參Ctor是什麽。其實這個Ctor,就是你平時寫Vue文件時,components 對象裏的那些東西,就是你寫的單個Component對象。
這個可以從上層_createElement方法中得知,如下圖:
其中調用的resolveAsset方法,就是從你的options,即你寫的Component中,獲取components屬性,並且同時驗證一下,與對應的tag是否存在於你定義的文件中,這個tag,是標簽,是html標簽,我們在使用自定義Vue組件的時候,都是自定義標簽或<div is=‘componentName‘></div> 這樣的方式。而這個tag就是要嗎是is的值,要嗎是你使用的html標簽。
再來。回到createComponent方法中,可以看到,代碼一開始會去判斷你這個組件對象是否是undefind,如果是undefind,那就直接退出。再往下看,有一行其實我們很熟悉,但可能有點懵逼的代碼,就是 Ctor = baseCtor.extend(Ctor) ,這裏怎麽感覺有點熟悉,是的,這裏其實就是我們經常在文檔中看到的 Vue.extend(Component) 這麽一個方法。這個baseCtor可以看到是從contentx.$options._base來的,這個contex 上級方法追溯就可以知道是一個vm對象,但是這個_base從何而來?不要著急,前面說了,遇到這種好像沒看過的,它一定是在某處已經初始化過了,我們不用懷疑它,只需要找到他。
其實它在 src/core/global-api/index.js文件中,initGlobalAPI方法中就定義了,並且他指的就是Vue對象。
然後我們再回到 createComponent 方法這個主線任務中,繼續往下打怪,我們會發現遇到一個函數是mergeHooks,
1 function mergeHooks (data: VNodeData) { 2 if (!data.hook) { 3 data.hook = {} 4 } 5 for (let i = 0; i < hooksToMerge.length; i++) { 6 const key = hooksToMerge[i] 7 const fromParent = data.hook[key] 8 const ours = componentVNodeHooks[key] 9 data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours 10 } 11 }
所謂hook,就是鉤子,那再Vue中,這個鉤子自然就是在代碼中的某處可能會執行的方法,類似Vue實例的生命周期鉤子一樣。細看這個方法,它涉及到了一個對象,就是componentVNodeHooks對象,這個方法其實就是把這個對象裏的init、prepath、insert、destory方法存進data.hook這個對象中罷了,那你回頭要問,這個data又是從哪裏來?一直追溯你會發現,這個是$createElement函數上的參數,咦?好像線索就斷了= =?這個時候如果想要簡單理解,只需要查找 Vue文檔——深入data對象 你大概就知道這個data是神馬了。
而此處正定義了,最開頭說的界面渲染的執行動作鏈條中的遞歸調用創建子節點的部分。但是大家可能會覺得,奇怪,這個函數最終是走到了$createElement,可是跟先前提到的那個動作鏈條似乎沒有相關,就算定義了data.hook,讓動作鏈條就有componentVNodeHooks.init() 這個方法,可是什麽地方觸發這個定義呢?最開始的動作鏈條似乎沒有涉及定義這部分呀?沒地方觸發這些定義的方法呀?
大家稍安勿躁,所以我說真的是很繞,不可能沒定義,否則到執行data.hook.init的時候就undefind了。
我們要回頭看一下,在Vue進行初始化裝配的時候,有執行這麽一個方法 renderMixin(Vue) :
1 export function renderMixin (Vue: Class<Component>) { 2 // install runtime convenience helpers 3 installRenderHelpers(Vue.prototype) 4 5 Vue.prototype.$nextTick = function (fn: Function) { 6 return nextTick(fn, this) 7 } 8 9 Vue.prototype._render = function (): VNode { 10 const vm: Component = this 11 const { render, _parentVnode } = vm.$options 12 13 // reset _rendered flag on slots for duplicate slot check 14 if (process.env.NODE_ENV !== ‘production‘) { 15 for (const key in vm.$slots) { 16 // $flow-disable-line 17 vm.$slots[key]._rendered = false 18 } 19 } 20 21 if (_parentVnode) { 22 vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject 23 } 24 25 // set parent vnode. this allows render functions to have access 26 // to the data on the placeholder node. 27 vm.$vnode = _parentVnode 28 // render self 29 let vnode 30 try { 31 vnode = render.call(vm._renderProxy, vm.$createElement) 32 } catch (e) { 33 handleError(e, vm, `render`) 34 // return error render result, 35 // or previous vnode to prevent render error causing blank component 36 /* istanbul ignore else */ 37 if (process.env.NODE_ENV !== ‘production‘) { 38 if (vm.$options.renderError) { 39 try { 40 vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) 41 } catch (e) { 42 handleError(e, vm, `renderError`) 43 vnode = vm._vnode 44 } 45 } else { 46 vnode = vm._vnode 47 } 48 } else { 49 vnode = vm._vnode 50 } 51 } 52 // return empty vnode in case the render function errored out 53 if (!(vnode instanceof VNode)) { 54 if (process.env.NODE_ENV !== ‘production‘ && Array.isArray(vnode)) { 55 warn( 56 ‘Multiple root nodes returned from render function. Render function ‘ + 57 ‘should return a single root node.‘, 58 vm 59 ) 60 } 61 vnode = createEmptyVNode() 62 } 63 // set parent 64 vnode.parent = _parentVnode 65 return vnode 66 } 67 }
可能有的人就看明白了,我們看看,我們平時寫組件的時候,如果你有用到render的方式來寫組件樣式,那是如何工作的。在Vue.prototype._render這個方法體內,你會看到render從vm.$options中取出(vm.$options就是你寫的Component內的那些data、props等等的屬性),然後再看上面截出的代碼的第31行,render.call(vm._renderProxy,vm.$createElement),然後返回一個vnode,所以說,$createElemment在此處就會被調用,然後進行上面說的那些亂七八糟的代碼。但是你可能又會問:render.call?我平時寫Component的時候從來沒用render函數來做界面繪制呀!這個render又是在什麽時候被定義在$options的呢?否則直接從$options中取出肯定是會報錯的呀。還是我剛才那句話,不是沒定義,只是沒找到,實際上是定義了,定義在哪兒了?定義在mountComonent的最開始的部分了。
然後你可能又會想,那按照代碼的執行順序,能確保在使用前就定義了嗎?答案自然是肯定的。我們剛才看到$createElement這個方法,是被定義在vm._render當中,別忘了我們還有一個很重要的任務,就是找到$createElement是在哪裏被執行的,那也就是說,vm._render()是在哪裏被執行的。其實它就在mountComponent當中執行的,而且還一定是在render被定義之後才執行的。
其實這段代碼不是簡單地從上至下執行那麽容易理解,你可以看到updateComponent的寫法,其實它只是被定義了,而且在定義的時候,vm._update實際上是沒有執行的,並且vm._render()也是沒有被執行的,他們實際上是到了下面new Watcher()的構造函數當中才被執行,同時我們也可以看到,整個定義和動作執行兩個過程中,在watcher的構造函數裏,執行updateComponent方法時,vm._render()一定先執行然後返回一個vnode,然後才是到了vm._update開始執行,也就是說,此時data.hook已經被裝填了init等函數,所以在最開始的執行鏈不會因為屬性尚未定義而報出undefind被打斷。
哈哈,真的很繞。說實在話,看了良久才看明白這繞來繞去的邏輯。
另外,在我研讀這份源碼時,我才發現(額,我並木有什麽偏見),src/platforms 包下,除了web,多了一個weex。然後我就又回過頭理解了一圈,發現vue是把vm.$mount以及相關界面的模塊整個都抽出來單獨寫,然後在不同的平臺,就可以使用不同的渲染方式,然後我們在使用webpack打包時,只修要針對自己想要的平臺打包對應的模塊。如此將界面渲染層分開寫,真的是增加了Vue的擴展性,整個工程就很好擴展和管理。拜服大神的設計。
Vue源碼翻譯之渲染邏輯鏈