vue 中Virtual Dom被創(chuàng)建的方法
本文將通過解讀render函數(shù)的源碼,來分析vue中的vNode是如何創(chuàng)建的。在vue2.x的版本中,無論是直接書寫render函數(shù),還是使用template或el屬性,或是使用.vue單文件的形式,最終都需要編譯成render函數(shù)進行vnode的創(chuàng)建,最終再渲染成真實的DOM。 如果對vue源碼的目錄還不是很了解,推薦先閱讀下 深入vue -- 源碼目錄和編譯過程。
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的原型上,會返回vnode,vnode通過代碼render.call(vm._renderProxy, vm.$createElement)進行創(chuàng)建。
在創(chuàng)建vnode過程中,如果出現(xiàn)錯誤,就會執(zhí)行catch中代碼做降級處理。
_render中最核心的代碼就是:
vnode = render.call(vm._renderProxy, vm.$createElement)
接下來,分析下這里的render,vm._renderProxy,vm.$createElement分別是什么。
render函數(shù)
const { render, _parentVnode } = vm.$options
render方法是從$options中提取的。render方法有兩種途徑得來:
在組件中開發(fā)者直接手寫的render函數(shù)
通過編譯template屬性生成
參數(shù) vm._renderProxy
vm._renderProxy定義在 src/core/instance/init.js 中,是call的第一個參數(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,也就是說,在生產(chǎn)環(huán)境,render函數(shù)執(zhí)行的上下文就是當前vue實例,即當前組件的this。
開發(fā)環(huán)境:
開發(fā)環(huán)境會執(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)
用來判斷瀏覽器是否支持es6的Proxy。
Proxy作用是在訪問一個對象時,對其進行攔截,new Proxy的第一個參數(shù)表示所要攔截的對象,第二個參數(shù)是用來定制攔截行為的對象。
開發(fā)環(huán)境,如果支持Proxy就會對vm實例進行攔截,否則和生產(chǎn)環(huán)境相同,直接將vm賦值給vm._renderProxy。具體的攔截行為通過handlers對象指定。
當手寫render函數(shù)時,handlers = hasHandler,通過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分別是對vm對象的屬性的讀取和propKey in proxy的操作進行攔截,并對vm的參數(shù)進行校驗,再調(diào)用 warnNonPresent 和 warnReservedPrefix 進行Warn警告。
可見,initProxy方法的主要作用就是在開發(fā)時,對vm實例進行攔截發(fā)現(xiàn)問題并拋出錯誤,方便開發(fā)者及時修改問題。
參數(shù) vm.$createElement
vm.$createElement就是手寫render函數(shù)時傳入的createElement函數(shù),它定義在initRender方法中,initRender在new Vue初始化時執(zhí)行,參數(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是為開發(fā)者手寫render函數(shù)提供的方法,vm._c是為通過編譯template生成的render函數(shù)使用的方法。它們都會調(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方法主要是對參數(shù)做一些處理,再調(diào)用_createElement方法創(chuàng)建vnode。
下面看一下vue文檔中createElement能接收的參數(shù)。
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標簽字符串,組件選項對象,或者
// 解析上述任何一種的一個 async 異步函數(shù)。必需參數(shù)。
'div',
// {Object}
// 一個包含模板相關屬性的數(shù)據(jù)對象
// 你可以在 template 中使用這些特性。可選參數(shù)。
{
},
// {String | Array}
// 子虛擬節(jié)點 (VNodes),由 `createElement()` 構建而成,
// 也可以使用字符串來生成“文本虛擬節(jié)點”。可選參數(shù)。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
文檔中除了第一個參數(shù)是必選參數(shù),其他都是可選參數(shù)。也就是說使用createElement方法的時候,可以不傳第二個參數(shù),只傳第一個參數(shù)和第三個參數(shù)。剛剛說的參數(shù)處理就是對這種情況做處理。
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
通過判斷data是否是數(shù)組或者是基礎類型,如果滿足這個條件,說明這個位置傳的參數(shù)是children,然后對參數(shù)依次重新賦值。這種方式被稱為重載。
重載:函數(shù)名相同,函數(shù)的參數(shù)列表不同(包括參數(shù)個數(shù)和參數(shù)類型),至于返回類型可同可不同。
處理好參數(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()
}
}
方法開始會做判斷,如果data是響應式的數(shù)據(jù),component的is屬性不是真值的時候,都會去調(diào)用createEmptyVNode方法,創(chuàng)建一個空的vnode。 接下來,根據(jù)normalizationType的值,調(diào)用normalizeChildren或simpleNormalizeChildren方法對參數(shù)children進行處理。這兩個方法定義在 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ù)組扁平化處理,最終返回一個vnode的一維數(shù)組。
simpleNormalizeChildren是針對函數(shù)式組件做處理,所以只需要考慮children是二維數(shù)組的情況。 normalizeChildren方法會考慮children是多層嵌套的數(shù)組的情況。normalizeChildren開始會判斷children的類型,如果children是基礎類型,直接創(chuàng)建文本vnode,如果是數(shù)組,調(diào)用normalizeArrayChildren方法,并在normalizeArrayChildren方法里面進行遞歸調(diào)用,最終將children轉(zhuǎn)成一維數(shù)組。
接下來,繼續(xù)看_createElement方法,如果tag參數(shù)的類型不是String類型,是組件的話,調(diào)用createComponent創(chuàng)建vnode。如果tag是String類型,再去判斷tag是否是html的保留標簽,是否是不認識的節(jié)點,通過調(diào)用new VNode(),傳入不同的參數(shù)來創(chuàng)建vnode實例。
無論是哪種情況,最終都是通過VNode這個class來創(chuàng)建vnode,下面是類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 // 標簽名
this.data = data // 當前節(jié)點數(shù)據(jù)
this.children = children // 子節(jié)點
this.text = text // 文本
this.elm = elm // 對應的真實DOM節(jié)點
this.ns = undefined // 命名空間
this.context = context // 當前節(jié)點上下文
this.fnContext = undefined // 函數(shù)化組件上下文
this.fnOptions = undefined // 函數(shù)化組件配置參數(shù)
this.fnScopeId = undefined // 函數(shù)化組件ScopeId
this.key = data && data.key // 子節(jié)點key屬性
this.componentOptions = componentOptions // 組件配置項
this.componentInstance = undefined // 組件實例
this.parent = undefined // 父節(jié)點
this.raw = false // 是否是原生的HTML片段或只是普通文本
this.isStatic = false // 靜態(tài)節(jié)點標記
this.isRootInsert = true // 是否作為根節(jié)點插入
this.isComment = false // 是否為注釋節(jié)點
this.isCloned = false // 是否為克隆節(jié)點
this.isOnce = false // 是否有v-once指令
this.asyncFactory = asyncFactory // 異步工廠方法
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類定義的數(shù)據(jù),都是用來描述VNode的。
至此,render函數(shù)創(chuàng)建vdom的源碼就分析完了,我們簡單的總結(jié)梳理一下。
_render 定義在 Vue.prototype 上,_render函數(shù)執(zhí)行會調(diào)用方法render,在開發(fā)環(huán)境下,會對vm實例進行代理,校驗vm實例數(shù)據(jù)正確性。render函數(shù)內(nèi),會執(zhí)行render的參數(shù)createElement方法,createElement會對參數(shù)進行處理,處理參數(shù)后調(diào)用_createElement, _createElement方法內(nèi)部最終會直接或間接調(diào)用new VNode(), 創(chuàng)建vnode實例。
03 vnode && vdom
createElement 返回的vnode并不是真正的dom元素,VNode的全稱叫做“虛擬節(jié)點 (Virtual Node)”,它所包含的信息會告訴 Vue 頁面上需要渲染什么樣的節(jié)點,及其子節(jié)點。我們常說的“虛擬 DOM(Virtual Dom)”是對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。
04 心得
讀源碼切忌只看源碼,一定要結(jié)合具體的使用一起分析,這樣才能更清楚的了解某段代碼的意圖。就像本文render函數(shù),如果從來沒有使用過render函數(shù),直接就閱讀這塊源碼可能會比較吃力,不妨先看看文檔,寫個demo,看看具體的使用,再對照使用來分析源碼,這樣很多比較困惑的問題就迎刃而解了。
總結(jié)
以上所述是小編給大家介紹的vue 中Virtual Dom被創(chuàng)建的方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
相關文章
Flutter部件內(nèi)部狀態(tài)管理小結(jié)之實現(xiàn)Vue的v-model功能
本文是 Flutter 部件內(nèi)部狀態(tài)管理的小結(jié),從部件的基礎開始,到部件的狀態(tài)管理,并且在過程中實現(xiàn)一個類似 Vue 的 v-model 的功能,感興趣的朋友跟隨小編一起看看吧2019-06-06
Element中el-input密碼輸入框瀏覽器自動填充賬號密碼問題的解決方法
自己寫了一個管理系統(tǒng),登錄成功之后,瀏覽器提示我保存賬號密碼,每次登錄時就會自動回填記住的賬號密碼,方便用戶快速登錄,下面這篇文章主要給大家介紹了關于Element中el-input密碼輸入框瀏覽器自動填充賬號密碼問題的解決方法,需要的朋友可以參考下2022-09-09
vue項目中常見的三種文件類型在線預覽實現(xiàn)(pdf/word/excel表格)
最近在項目中要做一個pdf在線預覽的功能,索性給大家整理個全面的,這篇文章主要給大家介紹了關于vue項目中常見的三種文件類型在線預覽實現(xiàn)的相關資料,文件類型分別是pdf/word文件/excel表格,需要的朋友可以參考下2022-05-05

