本文將通過(guò)解讀render函數(shù)的源碼,來(lái)分析vue中的vNode是如何創(chuàng)建的。在vue2.x的版本中,無(wú)論是直接書(shū)寫(xiě)render函數(shù),還是使用template或el屬性,或是使用.vue單文件的形式,最終都需要編譯成render函數(shù)進(jìn)行vnode的創(chuàng)建,最終再渲染成真實(shí)的DOM。 如果對(duì)vue源碼的目錄還不是很了解,推薦先閱讀下 深入vue -- 源碼目錄和編譯過(guò)程。
01 render函數(shù)
render方法定義在文件 src/core/instance/render.js 中
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options // ... // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { 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 } } // 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定義在vue的原型上,會(huì)返回vnode,vnode通過(guò)代碼render.call(vm._renderProxy, vm.$createElement)進(jìn)行創(chuàng)建。
在創(chuàng)建vnode過(guò)程中,如果出現(xiàn)錯(cuò)誤,就會(huì)執(zhí)行catch中代碼做降級(jí)處理。
_render中最核心的代碼就是:
vnode = render.call(vm._renderProxy, vm.$createElement)
接下來(lái),分析下這里的render,vm._renderProxy,vm.$createElement分別是什么。
render函數(shù)
const { render, _parentVnode } = vm.$options
render方法是從$options中提取的。render方法有兩種途徑得來(lái):
在組件中開(kāi)發(fā)者直接手寫(xiě)的render函數(shù)
通過(guò)編譯template屬性生成
參數(shù) vm._renderProxy
vm._renderProxy定義在 src/core/instance/init.js 中,是call的第一個(gè)參數(shù),指定render函數(shù)執(zhí)行的上下文。
/* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm }
生產(chǎn)環(huán)境:
vm._renderProxy = vm,也就是說(shuō),在生產(chǎn)環(huán)境,render函數(shù)執(zhí)行的上下文就是當(dāng)前vue實(shí)例,即當(dāng)前組件的this。
開(kāi)發(fā)環(huán)境:
開(kāi)發(fā)環(huán)境會(huì)執(zhí)行initProxy(vm),initProxy定義在文件 src/core/instance/proxy.js 中。
let initProxy // ... initProxy = function initProxy (vm) { 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 } }
hasProxy的定義如下
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
用來(lái)判斷瀏覽器是否支持es6的Proxy。
Proxy作用是在訪(fǎng)問(wèn)一個(gè)對(duì)象時(shí),對(duì)其進(jìn)行攔截,new Proxy的第一個(gè)參數(shù)表示所要攔截的對(duì)象,第二個(gè)參數(shù)是用來(lái)定制攔截行為的對(duì)象。
開(kāi)發(fā)環(huán)境,如果支持Proxy就會(huì)對(duì)vm實(shí)例進(jìn)行攔截,否則和生產(chǎn)環(huán)境相同,直接將vm賦值給vm._renderProxy
。具體的攔截行為通過(guò)handlers對(duì)象指定。
當(dāng)手寫(xiě)render函數(shù)時(shí),handlers = hasHandler
,通過(guò)template生成的render函數(shù),handlers = getHandler。 hasHandler代碼:
const hasHandler = { has (target, key) { const has = key in target const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)) if (!has && !isAllowed) { if (key in target.$data) warnReservedPrefix(target, key) else warnNonPresent(target, key) } return has || !isAllowed } }
getHandler代碼
const getHandler = { get (target, key) { if (typeof key === 'string' && !(key in target)) { if (key in target.$data) warnReservedPrefix(target, key) else warnNonPresent(target, key) } return target[key] } }
hasHandler,getHandler分別是對(duì)vm對(duì)象的屬性的讀取和propKey in proxy的操作進(jìn)行攔截,并對(duì)vm的參數(shù)進(jìn)行校驗(yàn),再調(diào)用 warnNonPresent 和 warnReservedPrefix 進(jìn)行Warn警告。
可見(jiàn),initProxy方法的主要作用就是在開(kāi)發(fā)時(shí),對(duì)vm實(shí)例進(jìn)行攔截發(fā)現(xiàn)問(wèn)題并拋出錯(cuò)誤,方便開(kāi)發(fā)者及時(shí)修改問(wèn)題。
參數(shù) vm.$createElement
vm.$createElement就是手寫(xiě)render函數(shù)時(shí)傳入的createElement函數(shù),它定義在initRender方法中,initRender在new Vue初始化時(shí)執(zhí)行,參數(shù)是實(shí)例vm。
export function initRender (vm: Component) { // ... // bind the createElement fn to this instance // so that we get proper render context inside it. // args order: tag, data, children, normalizationType, alwaysNormalize // internal version is used by render functions compiled from templates vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // ... }
從代碼的注釋可以看出: vm.$createElement
是為開(kāi)發(fā)者手寫(xiě)render函數(shù)提供的方法,vm._c是為通過(guò)編譯template生成的render函數(shù)使用的方法。它們都會(huì)調(diào)用createElement方法。
02 createElement方法
createElement方法定義在 src/core/vdom/create-element.js 文件中
const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 // wrapper function for providing a more flexible interface // without getting yelled at by flow export function createElement ( context: Component, tag: any, data: any, children: any, 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 } return _createElement(context, tag, data, children, normalizationType) }
createElement方法主要是對(duì)參數(shù)做一些處理,再調(diào)用_createElement方法創(chuàng)建vnode。
下面看一下vue文檔中createElement能接收的參數(shù)。
// @returns {VNode} createElement( // {String | Object | Function} // 一個(gè) HTML 標(biāo)簽字符串,組件選項(xiàng)對(duì)象,或者 // 解析上述任何一種的一個(gè) async 異步函數(shù)。必需參數(shù)。 'div', // {Object} // 一個(gè)包含模板相關(guān)屬性的數(shù)據(jù)對(duì)象 // 你可以在 template 中使用這些特性。可選參數(shù)。 { }, // {String | Array} // 子虛擬節(jié)點(diǎn) (VNodes),由 `createElement()` 構(gòu)建而成, // 也可以使用字符串來(lái)生成“文本虛擬節(jié)點(diǎn)”。可選參數(shù)。 [ '先寫(xiě)一些文字', createElement('h1', '一則頭條'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] )
文檔中除了第一個(gè)參數(shù)是必選參數(shù),其他都是可選參數(shù)。也就是說(shuō)使用createElement方法的時(shí)候,可以不傳第二個(gè)參數(shù),只傳第一個(gè)參數(shù)和第三個(gè)參數(shù)。剛剛說(shuō)的參數(shù)處理就是對(duì)這種情況做處理。
if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined }
通過(guò)判斷data是否是數(shù)組或者是基礎(chǔ)類(lèi)型,如果滿(mǎn)足這個(gè)條件,說(shuō)明這個(gè)位置傳的參數(shù)是children,然后對(duì)參數(shù)依次重新賦值。這種方式被稱(chēng)為重載。
重載:函數(shù)名相同,函數(shù)的參數(shù)列表不同(包括參數(shù)個(gè)數(shù)和參數(shù)類(lèi)型),至于返回類(lèi)型可同可不同。
處理好參數(shù)后調(diào)用_createElement方法創(chuàng)建vnode。下面是_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) } let vnode, ns if (typeof tag === 'string') { let Ctor // ... if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }
方法開(kāi)始會(huì)做判斷,如果data是響應(yīng)式的數(shù)據(jù),component的is屬性不是真值的時(shí)候,都會(huì)去調(diào)用createEmptyVNode方法,創(chuàng)建一個(gè)空的vnode。 接下來(lái),根據(jù)normalizationType的值,調(diào)用normalizeChildren或simpleNormalizeChildren方法對(duì)參數(shù)children進(jìn)行處理。這兩個(gè)方法定義在 src/core/vdom/helpers/normalize-children.js 文件下。
// 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. 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 } // 2. When the children contains constructs that always generated nested Arrays, // e.g. <template>, <slot>, v-for, or when the children is provided by user // with hand-written render functions / JSX. In such cases a full normalization // is needed to cater to all possible types of children values. export function normalizeChildren (children: any): ?Array<VNode> { return isPrimitive(children) ? [createTextVNode(children)] : Array.isArray(children) ? normalizeArrayChildren(children) : undefined }
normalizeChildren和simpleNormalizeChildren的目的都是將children數(shù)組扁平化處理,最終返回一個(gè)vnode的一維數(shù)組。
simpleNormalizeChildren是針對(duì)函數(shù)式組件做處理,所以只需要考慮children是二維數(shù)組的情況。 normalizeChildren方法會(huì)考慮children是多層嵌套的數(shù)組的情況。normalizeChildren開(kāi)始會(huì)判斷children的類(lèi)型,如果children是基礎(chǔ)類(lèi)型,直接創(chuàng)建文本vnode,如果是數(shù)組,調(diào)用normalizeArrayChildren方法,并在normalizeArrayChildren方法里面進(jìn)行遞歸調(diào)用,最終將children轉(zhuǎn)成一維數(shù)組。
接下來(lái),繼續(xù)看_createElement方法,如果tag參數(shù)的類(lèi)型不是String類(lèi)型,是組件的話(huà),調(diào)用createComponent創(chuàng)建vnode。如果tag是String類(lèi)型,再去判斷tag是否是html的保留標(biāo)簽,是否是不認(rèn)識(shí)的節(jié)點(diǎn),通過(guò)調(diào)用new VNode(),傳入不同的參數(shù)來(lái)創(chuàng)建vnode實(shí)例。
無(wú)論是哪種情況,最終都是通過(guò)VNode這個(gè)class來(lái)創(chuàng)建vnode,下面是類(lèi)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; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag // 標(biāo)簽名 this.data = data // 當(dāng)前節(jié)點(diǎn)數(shù)據(jù) this.children = children // 子節(jié)點(diǎn) this.text = text // 文本 this.elm = elm // 對(duì)應(yīng)的真實(shí)DOM節(jié)點(diǎn) this.ns = undefined // 命名空間 this.context = context // 當(dāng)前節(jié)點(diǎn)上下文 this.fnContext = undefined // 函數(shù)化組件上下文 this.fnOptions = undefined // 函數(shù)化組件配置參數(shù) this.fnScopeId = undefined // 函數(shù)化組件ScopeId this.key = data && data.key // 子節(jié)點(diǎn)key屬性 this.componentOptions = componentOptions // 組件配置項(xiàng) this.componentInstance = undefined // 組件實(shí)例 this.parent = undefined // 父節(jié)點(diǎn) this.raw = false // 是否是原生的HTML片段或只是普通文本 this.isStatic = false // 靜態(tài)節(jié)點(diǎn)標(biāo)記 this.isRootInsert = true // 是否作為根節(jié)點(diǎn)插入 this.isComment = false // 是否為注釋節(jié)點(diǎn) this.isCloned = false // 是否為克隆節(jié)點(diǎn) this.isOnce = false // 是否有v-once指令 this.asyncFactory = asyncFactory // 異步工廠(chǎng)方法 this.asyncMeta = undefined // 異步Meta this.isAsyncPlaceholder = false // 是否異步占位 } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
VNode類(lèi)定義的數(shù)據(jù),都是用來(lái)描述VNode的。
至此,render函數(shù)創(chuàng)建vdom的源碼就分析完了,我們簡(jiǎn)單的總結(jié)梳理一下。
_render 定義在 Vue.prototype 上,_render函數(shù)執(zhí)行會(huì)調(diào)用方法render,在開(kāi)發(fā)環(huán)境下,會(huì)對(duì)vm實(shí)例進(jìn)行代理,校驗(yàn)vm實(shí)例數(shù)據(jù)正確性。render函數(shù)內(nèi),會(huì)執(zhí)行render的參數(shù)createElement方法,createElement會(huì)對(duì)參數(shù)進(jìn)行處理,處理參數(shù)后調(diào)用_createElement, _createElement方法內(nèi)部最終會(huì)直接或間接調(diào)用new VNode(), 創(chuàng)建vnode實(shí)例。
03 vnode && vdom
createElement 返回的vnode并不是真正的dom元素,VNode的全稱(chēng)叫做“虛擬節(jié)點(diǎn) (Virtual Node)”,它所包含的信息會(huì)告訴 Vue 頁(yè)面上需要渲染什么樣的節(jié)點(diǎn),及其子節(jié)點(diǎn)。我們常說(shuō)的“虛擬 DOM(Virtual Dom)”是對(duì)由 Vue 組件樹(shù)建立起來(lái)的整個(gè) VNode 樹(shù)的稱(chēng)呼。
04 心得
讀源碼切忌只看源碼,一定要結(jié)合具體的使用一起分析,這樣才能更清楚的了解某段代碼的意圖。就像本文render函數(shù),如果從來(lái)沒(méi)有使用過(guò)render函數(shù),直接就閱讀這塊源碼可能會(huì)比較吃力,不妨先看看文檔,寫(xiě)個(gè)demo,看看具體的使用,再對(duì)照使用來(lái)分析源碼,這樣很多比較困惑的問(wèn)題就迎刃而解了。
總結(jié)
以上所述是小編給大家介紹的vue 中Virtual Dom被創(chuàng)建的方法,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com