您好,登錄后才能下訂單哦!
本篇內容介紹了“Vue.js模版和數據是怎么被渲染成DOM的”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
前言
Vue.js 一個核心思想是數據驅動。也就是說視圖是由數據驅動生成的,我們對視圖的修改,不會直接操作 DOM,而是通過修改數據。當交互復雜的時候,只關心數據的修改會讓代碼的邏輯變的非常清晰,因為 DOM 變成了數據的映射,我們所有的邏輯都是對數據的修改,而不用碰觸 DOM,這樣的代碼非常利于維護。
在 Vue.js 中我們可以采用簡潔的模板語法來聲明式的將數據渲染為 DOM:
<div id="app"> {{ msg }} </div>
var app = new Vue({ el: '#app', data: { msg: 'Hello world!' } })
結果頁面上會展示出Hello world!。這是入門vue.js的時候就知道的知識。那么現在要問vue.js的源碼到底做了什么,才能讓模版和數據最終被渲染成了DOM???
從 new Vue() 開始
在寫vue 項目的時候,會在項目的入口文件 main.js文件里實例化一個vue 。如下:
var app = new Vue({ el: '#app', data: { msg: 'Hello world!' }, })
Vue 就是一個用 Function 實現的類。源碼如下:在src/core/instance/index.js中
// _init 方法所在的位置 import { initMixin } from './init' // Vue就是一個用 Function 實現的類,所以才通過 new Vue 去實例化它。 function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
當我們在項目中 new Vue({})傳入一個對象的時候,其實就是執行的上面的方法,并傳入參數為 options ,然后調用了this._init(options)方法。該方法在src/core/instance/init.js文件中。代碼如下:
import { initState } from './state' Vue.prototype._init = function (options?: Object) { const vm: Component = this // 定義了uid vm._uid = uid++ let startTag, endTag if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } vm._isVue = true // 合并options if (options && options._isComponent) { initInternalComponent(vm, options) } else { // 這里將傳入的options全部合并在$options上。 // 因此我們可以通過$el訪問到 vue 項目中new Vue 中的el // 通過$options.data 訪問到 vue 項目中new Vue 中的data vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // 初始化函數 vm._self = vm initLifecycle(vm) // 生命周期函數 initEvents(vm) // 初始化事件鏈 initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } // 判斷當前的$options.el是否有el 也就是說是否傳入掛載的DOM對象 if (vm.$options.el) { vm.$mount(vm.$options.el) } }
由以上代碼可知 this._init(options)主要是合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。重要的部分在代碼里做里注釋。
那么接下來依然從其中一個功能為例進行分析:以initState(vm)為例:
為什么在鉤子函數里可以訪問到 data 里定義的數據?
vue 項目中,當定義了 data 就可以在組件的鉤子函數 或者 在 methods 函數里都可以訪問到data 里定義的屬性。這是為什么??
var app = new Vue({ el: '#app', data:(){ return{ msg: 'Hello world!' } }, mounted(){ console.log(this.msg) // logs 'Hello world!' },
分析源碼:可以看到this._init(options)方法,在初始化函數部分有一個 initState(vm)函數。該方法實在./state.js中:具體代碼如下:
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options // 如果定義了 props 就初始化props; if (opts.props) initProps(vm, opts.props) // 如果定義了methods 就初始化methods; if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { // 如果定義了data,就初始化data;(要分析的內容從這里開始) initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
在initState方法中判斷:如果定義了data,就初始化data;繼續看初始化data 的函數:initData(vm)。代碼如下:
function initData (vm: Component) { /* 這個data 就是 我們vue 項目中定義的data。也就是上面例子中的 data(){ return { msg: 'Hello world!' } } */ let data = vm.$options.data // 拿到data 后,做了判斷,判斷它是不是一個function data = vm._data = typeof data === 'function' ? getData(data, vm) // 如果是 執行了getData()方法 ,這個方法就是返回data : data || {} // 如果不是一個對象則在開發環境報出一個警告 if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // 拿到data 定義的屬性 const keys = Object.keys(data) // 拿到props const props = vm.$options.props // 拿到 methods const methods = vm.$options.methods let i = keys.length // 做了一個循環對比,如果在data 上定義的屬性,就不能在props與methods在定義該屬性。因為不管是data里定義的,在props里定義的,還是在medthods里定義的,最終都掛載在vm實例上了。見proxy(vm, `_data`, key) while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { proxy(vm, `_data`, key) // 代理 定義了Getter 和 Setter } } // observe data observe(data, true /* asRootData */) }
// proxy 代理 const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { // 通過對象 sharedPropertyDefinition 定義了Getter 和 Setter sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] // 當訪問vm.key 的時候其實訪問的是 vm[sourceKey][key] // 以上述開始的問題,當訪問this.msg 實際是訪問 this._data.msg } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } // 對vm 的 key 做了一次Getter 和 Setter Object.defineProperty(target, key, sharedPropertyDefinition) }
綜上:初始化 data 實在./state.js文件里。執行initState() 方法,該方法判斷如果定義了data,就初始化data。
如果data 是一個function,就執行了getData()方法return data.call(vm, vm)。然后對 vm 上的 data 里定義的屬性、vm上的 props 、vm上的methods里的屬性進行循環比對,如果在data 上定義的屬性,就不能在props與methods在定義該屬性。因為不管是data里定義的,在props里定義的,還是在medthods里定義的,最終都掛載在vm實例上了。見proxy(vm, _data, key)。
然后通過proxy 方法給vm 上的屬性做了Getter 和 Setter 方法的綁定。回到上述的問題,當訪問this.msg 實際是訪問 vm._data.msg。因此在鉤子函數里確實可以訪問到 data 里定義的數據了。
不得不在說一遍,Vue 的初始化邏輯寫的非常清楚,把不同的功能邏輯拆成一些單獨的函數執行,讓主線邏輯一目了然,這樣的編程思想是非常值得借鑒和學習的。
其它初始化的內容大家可以自己補充,接下來看掛載vm。在初始化的最后,檢測到如果有 el 屬性,則調用 vm.$mount 方法掛載 vm,掛載的目標就是把模板渲染成最終的 DOM,那么接下來探究 Vue 的掛載過程吧
Vue 實例掛載的實現
Vue 中我們是通過 $mount 實例方法去掛載 vm 的。接下來要探究執行$mount('#app')的時候,源碼都干了什么???
new Vue({ render: h => h(App), }).$mount('#app')
$mount 方法在多個文件中都有定義,如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因為 $mount 這個方法的實現是和平臺、構建方式都有關系。
就選取 compiler 版本的 $mount 分析吧,文件地址在src/platform/web/entry-runtime-with-compiler.js,代碼如下:
// 獲取vue 原型上的 $mount 方法, 存在變量 mount 上。 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // query 定義在 './util/index'文件中 // 調用原生的DOM api querySelector() 方法。最后將el轉化為一個DOM 對象。 el = el && query(el) ... return mount.call(this, el, hydrating) }
讀代碼可知,代碼首先獲取了 vue 原型上的 $mount 方法,將其存在變量mount中,然后重新定義了該方法。該方法對傳入的el做了處理,el 可以是個字符串,也可以是DOM 對象。然后調用了 query()方法,該方法在./util/index文件中。主要是調用原生的DOM api querySelector() 方法。最后將el轉化為一個DOM 對象返回。上述只貼出了主要的代碼部分。
源碼了還對el進行了判斷,判斷傳入的el 是否為body 或者 html ,如果是,就會在開發環境報一個警告。vue 不可以直接掛載到body 和html上 ,因為會被覆蓋,當覆蓋了 html 或 body 整個文檔就會報錯。
源碼還獲取到 $options 判斷是否定義render方法。如果沒有定義 render 方法,則會把 el 或者 template 字符串最終將編譯為render()函數。
最后 return mount.call(this, el, hydrating)。此處的mount是vue 原型上的 $mount 方法。在文件./runtime/index。代碼如下:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
其中參數 el 表示掛載的元素,它可以是字符串,也可以是一個DOM 對象。如果是字符串在瀏覽器環境下會調用 query() 方法轉換成 DOM 對象。第二個參數是和服務端渲染相關,在瀏覽器環境下我們不需要傳第二個參數。最后return 的時候調用了mountComponent()方法。該方法定義在src/core/instance/lifecycle.js,代碼如下:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el ... let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const 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 = () => { vm._update(vm._render(), hydrating) } } new Watcher(vm, updateComponent, noop, { 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 }
讀代碼可知,該方法首先實例化一個渲染Watcher,在它的回調函數中會調用 updateComponent 方法,在此方法中調用 vm._render() 方法先生成虛擬DOM節點,最終調用 vm._update 更新 DOM。
最后判斷為根節點的時候設置 vm._isMounted 為 true, 表示這個實例已經掛載了,同時執行 mounted 鉤子函數。 vm.$vnode 表示 Vue 實例的父虛擬節點,所以它為 Null 則表示當前是根 Vue 的實例。
那么vm._render()是怎樣生成虛擬DOM節點的呢?
_render()渲染虛擬DOM 節點
在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render()。Vue 的 _render() 是實例的一個私有方法,它用來把實例渲染成一個虛擬DOM節點。它的定義在 src/core/instance/render.js 文件中,代碼如下:
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options ... let vnode try { currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } }
上述代碼 從vue實例的 $options 上獲取到 render 函數。通過call()調用了_renderProxy和 createElement()方法,先來探索createElement()方法。
createElement()
createElement()是在initRender()中。如下:
// 該函數是在 _init() 過程中執行 initRender() // 見 './init.js' 文件中的 initRender(vm) 傳入vm。就執行到下面的方法。 export function initRender (vm: Component) { // 被編譯后生成的render函數 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // 手寫render函數 創建 vnode 的方法。 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) }
initRender()是在 _init過程中執行了initRender()見 ./init.js 文件中的 initRender(vm)傳入vm。
在 vue 項目實際開發中,手寫 render 函數 案例如下:
new Vue({ render(createElement){ return createElement('div',{ style:{color:'red'} },this.msg) }, data(){ return{ msg:"hello world" } } }).$mount('#app')
因為是手寫的render函數省去了將 template 編譯為 render函數的過程,因此性能更好。
接下來看_renderProxy方法:
_renderProxy
_renderProxy方法,也是在 init 過程中執行的。見文件./init.js中,代碼如下:
import { initProxy } from './proxy' if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm }
如果當前環境為生產環境 就將 vm 直接賦值給 vm._renderProxy;
如果當前環境為開發環境,則執行initProxy()。
該函數在./proxy.js文件中,代碼如下:
initProxy = function initProxy (vm) { // 判斷瀏覽器是否支持 proxy 。 if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } }
首先判斷瀏覽器是否支持 proxy。它是ES6 新增的,用于給目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
如果瀏覽器不支持 proxy, 就將 vm 直接賦值給 vm._renderProxy;
如果瀏覽器支持 proxy,就執行new Proxy()。
綜上所述:vm._render 是通過執行 createElement 方法并返回虛擬的DOM 節點。那么什么是虛擬的DOM呢???
虛擬的DOM
在探究vue 的虛擬DOM 之前,先推薦一個虛擬DOM開源庫。有時間,有興趣的朋友可以去深入了解。它是用一個函數去表示一個應用程序的視圖層。view.js 是借鑒它實現了虛擬DOM。從而大大的提升了程序的性能。接下來我們就來看vue.js是怎么做的。
vnode 的定義在 src/core/vdom/vnode.js文件中,如下:
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ... }
虛擬DOM 是個js對象,是對真實DOM 的一種抽象描述,比如標簽名、數據、子節點名等。因為虛擬DOM只是用來映射真實DOM的渲染,所以不包含操作DOM的方法操作DOM的方法。因此更加的輕量,更加的簡單。因為虛擬DOM 的創建是通過createElement方法,那這個環節又是如何實現的呢???
createElement
Vue.js 利用 createElement 方法創建 DOM節點,它定義在 src/core/vdom/create-elemenet.js文件中,代碼如下:
export function createElement ( context: Component, // vm 實例 tag: any, // 標簽 data: any, // 數據 children: any,// 子節點 可以構造DOM 樹 normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { // 對參數不一致的處理 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } // 處理好參數,則調用 _createElement() 去真正的創建節點。 return _createElement(context, tag, data, children, normalizationType) }
createElement 方法是對 _createElement 方法的封裝,它允許傳入的參數更加靈活,在處理這些參數后,調用真正創建 DOM 節點的函數_createElement,代碼如下:
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { ... if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } ... }
_createElement 方法提供 5 個參數如下:
context 表示DOM節點的上下文環境,它是 Component 類型;
tag 表示標簽,它可以是一個字符串,也可以是一個 Component;
data 表示 DOM節點上的數據,它是一個 VNodeData 類型,可以在 flow/vnode.js 中找到它的定義;
children 表示當前DOM節點的子節點,它是任意類型的,它接下來需要被規范為標準的 VNode 數組;
normalizationType 表示子節點規范的類型,類型不同規范的方法也就不一樣,它主要是參考 render 函數是編譯生成的還是手寫的 render 函數。
createElement 函數的流程略微有點多,本文將重點探究 children 的規范化以及 VNode 的創建。
children 的規范化
虛擬DOM(Virtual DOM)實際上是一個樹狀結構,每一個DOM 節點都可能會有若干個子節點,這些子節點應該也是 VNode 的類型。
_createElement 接收的第 4 個參數 children 是任意類型的,因此我們需要把它們規范成 VNode 類型。
它是根據 normalizationType 的不同,調用了 normalizeChildren(children) 和 simpleNormalizeChildren(children) 方法,它們的定義都在 src/core/vdom/helpers/normalzie-children.js文件 中,代碼如下:
// render 函數是編譯生成的時候調用 // 拍平數組為一維數組 export function simpleNormalizeChildren (children: any) { for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 返回一維數組 export function normalizeChildren (children: any): ?Array<VNode> { return isPrimitive(children) ? [createTextVNode(children)] : Array.isArray(children) ? normalizeArrayChildren(children) : undefined }
simpleNormalizeChildren 方法調用場景是 render 函數是編譯生成的。但是當子節點為一個組件的時候,函數式組件返回的是一個數組而不是一個根節點,所以會通過 Array.prototype.concat 方法把整個 children 數組拍平,讓它的深度只有一層。
normalizeChildren 方法的調用場景有 2 種,一個場景是手寫 render 函數,當 children 只有一個節點的時候,Vue.js 從接口層面允許用戶把 children 寫成基礎類型用來創建單個簡單的文本節點,這種情況會調用 createTextVNode 創建一個文本節點的DOM 節點;另一個場景是當編譯 slot、v-for 的時候會產生嵌套數組的情況,會調用 normalizeArrayChildren 方法,代碼如下:
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> { const res = [] let i, c, lastIndex, last for (i = 0; i < children.length; i++) { c = children[i] if (isUndef(c) || typeof c === 'boolean') continue lastIndex = res.length - 1 last = res[lastIndex] // nested if (Array.isArray(c)) { if (c.length > 0) { c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`) // merge adjacent text nodes if (isTextNode(c[0]) && isTextNode(last)) { res[lastIndex] = createTextVNode(last.text + (c[0]: any).text) c.shift() } res.push.apply(res, c) } } else if (isPrimitive(c)) { if (isTextNode(last)) { res[lastIndex] = createTextVNode(last.text + c) } else if (c !== '') { res.push(createTextVNode(c)) } } else { // 如果兩個節點都為文本節點,則合并他們。 if (isTextNode(c) && isTextNode(last)) { res[lastIndex] = createTextVNode(last.text + c.text) } else { if (isTrue(children._isVList) && isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) { c.key = `__vlist${nestedIndex}_${i}__` } res.push(c) } } } return res }
normalizeArrayChildren 接收 2 個參數。
children 表示要規范的子節點;
nestedIndex 表示嵌套的索引; 因為單個 child可能是一個數組類型。 normalizeArrayChildren 主要是遍歷 children,獲得單個節點 c,然后對 c 的類型判斷,如果是一個數組類型,則遞歸調用 normalizeArrayChildren; 如果是基礎類型,則通過 createTextVNode 方法轉換成 VNode 類型;否則就已經是 VNode 類型了,如果 children 是一個列表并且列表還存在嵌套的情況,則根據 nestedIndex 去更新它的 key。
在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個連續的 text 節點,會把它們合并成一個 text 節點。
到此,children 變成了一個類型為 VNode 的 Array。這就是children 的規范化。
虛擬的DOM節點的創建
回到 createElement 函數,規范化 children 后,接下來就要創建一個DOM實例,代碼如下:
let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // 不認識的節點的處理 vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) }
這里先對 tag 做判斷,如果是 string 類型,則接著判斷如果是內置的一些節點,則直接創建一個普通 VNode,如果是為已注冊的組件名,則通過 createComponent 創建一個組件類型的 VNode,否則創建一個未知的標簽的 VNode。 如果 tag是一個 Component 類型,則直接調用 createComponent 創建一個組件類型的 VNode 節點。
到這一步,createElement方法就創建好了一個虛擬DOM樹的實例,它用來描述了真實DOM 樹,那么如何渲染為真實的DOM 樹呢???其實它是由 vm._update 完成的。
update把虛擬DOM 渲染為真實DOM
_update 方法是如何把虛擬DOM 渲染為真實DOM 的。這部分代碼在 src/core/instance/lifecycle.js文件中,代碼如下:
_update 方法是如何把虛擬DOM 渲染為真實DOM 的。這部分代碼在 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 restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode if (!prevVnode) { // 數據的首次渲染時候執行 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } ... }
讀代碼可知,當數據首次渲染的時候,調用了vm.__patch__()的方法,他接收了四個參數,結合我們實際vue項目的開發過程。vm.$el就是 id 為 app 的 DOM 對象,即:
;vnode 對應的是調用 render 函數的返回值;hydrating 在非服務端渲染情況下為 false,removeOnly 為 false。
vm.__patch__ 方法在不同的平臺的定義是不一樣的,對 web 平臺的定義在 src/platforms/web/runtime/index.js 中,代碼如下:
// 是否在瀏覽器環境 Vue.prototype.__patch__ = inBrowser ? patch : noop
在 web 平臺上,是否是服務端渲染也會對這個方法產生影響。因為在服務端渲染中,沒有真實的瀏覽器 DOM 環境,所以不需要把 VNode 最終轉換成 DOM,因此是一個空函數,而在瀏覽器端渲染中,它指向了 patch 方法,它的定義在 src/platforms/web/runtime/patch.js文件中,代碼如下:
import * as nodeOps from 'web/runtime/node-ops' import { createPatchFunction } from 'core/vdom/patch' import baseModules from 'core/vdom/modules/index' import platformModules from 'web/runtime/modules/index' const modules = platformModules.concat(baseModules) export const patch: Function = createPatchFunction({ nodeOps, modules })
讀代碼可知 createPatchFunction 方法的返回值被傳入了一個對象,其中,
nodeOps 封裝了一系列 DOM 操作的方法;
modules 定義了模塊的鉤子函數的實現; createPatchFunction方法的定義在 src/core/vdom/patch.js文件中,代碼如下:
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } // ... // 定義了一些輔助函數 // 當調用 vm.__dispatch__時,其實就是調用下面的 patch 方法 // 這塊應用了函數柯理化的技巧 return function patch (oldVnode, vnode, hydrating, removeOnly) { // ... return vnode.elm } }
createPatchFunction 內部定義了一系列的輔助方法,最終返回了一個 patch 方法,這個方法就賦值給了 vm._update函數里調用的 vm.__patch__。也就是說當調用 vm.__dispatch__時,其實就是調用patch (oldVnode, vnode, hydrating, removeOnly) 方法,這塊其實是應用了函數柯理化的技巧。
patch 方法接收 4個參數,如下:
oldVnode 表示舊的 VNode 節點,它也可以不存在或者是一個 DOM 對象;
vnode 表示執行 _render 后返回的 VNode 的節點;
hydrating 表示是否是服務端渲染;
removeOnly 是給 transition-group 用的。
分析patch方法,因為傳入的oldVnode實際上是一個 DOM container,所以 isRealElement 為 true,然后調用 emptyNodeAt 方法把 oldVnode 轉換成 虛擬DOM節點(一個js對象),然后再調用 createElm 方法。代碼如下:
if (isRealElement) { oldVnode = emptyNodeAt(oldVnode) }
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check const data = vnode.data const children = vnode.children const tag = vnode.tag // 接下來判斷 vnode 是否包含 tag, // 如果包含,先對tag的合法性在非生產環境下做校驗,看是否是一個合法標簽; // 然后再去調用平臺 DOM 的操作去創建一個占位符元素。 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 ) } } // 調用 createChildren 方法去創建子元素: vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) /* istanbul ignore if */ if (__WEEX__) { // ... } else { // 調用 createChildren 方法去創建子元素 // 用 createChildren 方法遍歷子虛擬節點,遞歸調用 createElm // 在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節點占位符傳入。 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) } }
createElm方法的作用是通過虛擬節點創建真實的 DOM 并插入到它的父節點中。判斷 vnode 是否包含 tag,如果包含,先對 tag 的合法性在非生產環境下做驗證,看是否是一個合法標簽;然后再去調用平臺 DOM 的操作去創建一個占位符元素。然后調用 createChildren 方法去創建子元素,createChildren方法代碼如下:
createChildren(vnode, children, insertedVnodeQueue) function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }
createChildren方法遍歷子虛擬節點,遞歸調用 createElm,在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節點占位符傳入。然后調用 invokeCreateHooks方法執行所有的 create 的鉤子并把 vnode push 到 insertedVnodeQueue 中。最后調用 insert 方法把 DOM 插入到父節點中,因為是遞歸調用,子元素會優先調用 insert,所以整個 vnode 樹節點的插入順序是先子后父。insert 方法定義在 src/core/vdom/patch.js 文件中,代碼如下:
insert(parentElm, vnode.elm, refElm) function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (ref.parentNode === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } }
“Vue.js模版和數據是怎么被渲染成DOM的”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。