vue的原始碼學習之五——6.資料驅動(createElement)
1. 介紹
版本:2.5.17。
我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。
學習文件:https://ustbhuangyi.github.io/vue-analysis/data-driven/create-element.html
2. createElement
Vue.js 利用 createElement 方法建立 VNode,它定義在 src/core/vdom/create-elemenet.js
export function createElement ( context: Component, // vm例項 tag: any, //標籤 data: any, // vnode的資料 children: any, // vnode的子節點,進而可以構建vnode樹進而對映DOM樹 normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { //如果傳入引數時,沒有傳data這個引數,那麼實參和形參改變對應順序 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
方法的封裝,它允許傳入的引數更加靈活,在處理這些引數後,呼叫真正建立 VNode 的函式 _createElement
:
// _createElement 方法有 5 個引數, // context 表示 VNode 的上下文環境,它是 Component 型別; // tag 表示標籤,它可以是一個字串,也可以是一個 Component; // data 表示 VNode 的資料,它是一個 VNodeData 型別,可以在 flow/vnode.js 中找到它的定義; // children 表示當前 VNode 的子節點,它是任意型別的,它接下來需要被規範為標準的 VNode 陣列; // normalizationType 表示子節點規範的型別,型別不同規範的方法也就不一樣,它主要是參考 render 函式是編譯生成的還是使用者手寫的。 export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // warn against non-primitive key if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { if (!__WEEX__ || !('@binding' in data.key)) { warn( 'Avoid using non-primitive value as key, ' + 'use string/number value instead.', context ) } } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } 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 { // 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() } }
_createElement
方法有 5 個引數:
1.context
表示 VNode 的上下文環境,它是 Component
型別;
2.tag
表示標籤,它可以是一個字串,也可以是一個 Component
;
3.data
表示 VNode 的資料,它是一個 VNodeData
型別,可以在 flow/vnode.js
中找到它的定義
4.children
表示當前 VNode 的子節點,它是任意型別的,它接下來需要被規範為標準的 VNode 陣列;
5.normalizationType
表示子節點規範的型別,型別不同規範的方法也就不一樣,它主要是參考 render
函式是編譯生成的還是使用者手寫的。
3.children 的規範化
由於 Virtual DOM 實際上是一個樹狀結構,每一個 VNode 可能會有若干個子節點,這些子節點應該也是 VNode 的型別。_createElement 接收的第 4 個引數 children 是任意型別的,因此我們需要把它們規範成 VNode 型別。
_createElement方法會根據normalizationType不同調用不同方法
SIMPLE_NORMALIZE = 1,ALWAYS_NORMALIZE = 2
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
3.1 方法
以下方法都定義在們的定義都在 src/core/vdom/helpers/normalzie-children.js 中
3.1.1 simpleNormalizeChildren
// 對children進行遍歷,(只會有一層深度)
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
// 如果是二維陣列,就將其concat為一維陣列
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
// 返回一維陣列,陣列中每一個元素都是一個vnode
return children
}
simpleNormalizeChildren 方法呼叫場景是—–render 函式當函式是編譯生成的。
理論上編譯生成的 children 都已經是 VNode 型別的,但這裡有一個例外,就是 functional component 函式式元件返回的是一個數組而不是一個根節點,所以會通過 Array.prototype.concat 方法把整個 children 陣列打平,讓它的深度只有一層。
3.1.2 normalizeChildren
export function normalizeChildren (children: any): ?Array<VNode> {
//如果傳入的是基本資料型別,例如this.message代表的字串,那麼就建立一個文字結點
return isPrimitive(children)
//呼叫createTextVNode函式,其實就是將其tostring,返回一個文字結點vnode
? [createTextVNode(children)]
// 如果是 isArray,就呼叫normalizeArrayChildren方法
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
normalizeChildren 方法的呼叫場景有 2 種,一個場景是 render 函式是使用者手寫的,當 children 只有一個節點的時候,Vue.js 從介面層面允許使用者把 children 寫成基礎型別用來建立單個簡單的文字節點,這種情況會呼叫 createTextVNode 建立一個文字節點的VNode;另一個場景是當編譯 slot、v-for 的時候會產生巢狀陣列的情況,會呼叫 normalizeArrayChildren 方法
3.1.3 normalizeArrayChildren
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
// 遍歷children
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// 如果是陣列,遞迴children
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// 合併相連的兩個文字節點
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
// 如果children是基礎型別
} else if (isPrimitive(c)) {
// 合併相鄰TextNode
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 節點。
3.1.4 總結
children 的規範化,children 變成了一個型別為 VNode 的 Array。也就是說Array中每一個元素都是VNode(虛擬DOM)。
simpleNormalizeChildren(children): 遍歷最多二維,輸出元素都是VNode的一維array
normalizeChildren : 可遍歷多層,合併兩個連續的 text 節點,輸出元素都是VNode的一維array
4. VNode 的建立
回到 createElement 函式,規範化 children 後,接下來會去建立一個 VNode 的例項:
et vnode, ns
//對 tag 做判斷
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
//如果是內建的節點,例如我們常用的<div id="app"></div>
if (config.isReservedTag(tag)) {
// 建立 vnode,config.parsePlatformTagName(tag)為平臺的保留標籤
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 對元件進行解析
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 節點。對於 createComponent 建立元件型別的 VNode 的過程,我們之後會去介紹,本質上它還是返回了一個 VNode。
5. 總結
那麼至此,我們大致瞭解了 createElement
建立 VNode 的過程,每個 VNode 有 children
,children
每個元素也是一個 VNode,這樣就形成了一個 VNode Tree,它很好的描述了我們的 DOM Tree。
回到 mountComponent
函式的過程,我們已經知道 vm._render
是如何建立了一個 VNode,接下來就是要把這個 VNode 渲染成一個真實的 DOM 並渲染出來,這個過程是通過 vm._update
完成的,接下來分析一下這個過程。