createElement
Vue.js 利用 createElement 方法創(chuàng)建 VNode叉存,它定義在 src/core/vdom/create-elemenet.js 中
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 方法實際上是對 _createElement 方法的封裝姐直,它允許傳入的參數(shù)更加靈活,在處理這些參數(shù)后,調(diào)用真正創(chuàng)建 VNode 的函數(shù) _createElement
export function _createElement (
// _createElement 方法有 5 個參數(shù)腾窝,context 表示 VNode 的上下文環(huán)境,
//它是 Component 類型瞒滴;tag 表示標簽鹤耍,它可以是一個字符串,也可以是一個 Component苟穆;
//data 表示 VNode 的數(shù)據(jù)抄课,它是一個 VNodeData 類型,
//可以在 flow/vnode.js 中找到它的定義雳旅,這里先不展開說跟磨;
//children 表示當前 VNode 的子節(jié)點,它是任意類型的攒盈,它接下來需要被規(guī)范為標準的 VNode 數(shù)組;
//normalizationType 表示子節(jié)點規(guī)范的類型抵拘,
//類型不同規(guī)范的方法也就不一樣,它主要是參考 render 函數(shù)是編譯生成的還是用戶手寫的型豁。
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 函數(shù)的流程略微有點多僵蛛,主要分析 2 個重點的流程 —— children 的規(guī)范化以及 VNode 的創(chuàng)建尚蝌。
children 的規(guī)范化
由于 Virtual DOM 實際上是一個樹狀結(jié)構(gòu),每一個 VNode 可能會有若干個子節(jié)點充尉,這些子節(jié)點應該也是 VNode 的類型飘言。_createElement 接收的第 4 個參數(shù) children 是任意類型的,因此我們需要把它們規(guī)范成 VNode 類型驼侠。
經(jīng)過對 children 的規(guī)范化姿鸿,children 變成了一個類型為 VNode 的 Array。
VNode 的創(chuàng)建
回到 createElement 函數(shù)泪电,規(guī)范化 children 后般妙,接下來會去創(chuàng)建一個 VNode 的
這里先對 tag 做判斷,如果是 string 類型相速,則接著判斷如果是內(nèi)置的一些節(jié)點碟渺,則直接創(chuàng)建一個普通 VNode,如果是為已注冊的組件名突诬,則通過 createComponent 創(chuàng)建一個組件類型的 VNode苫拍,否則創(chuàng)建一個未知的標簽的 VNode。 如果是 tag 一個 Component 類型旺隙,則直接調(diào)用 createComponent 創(chuàng)建一個組件類型的 VNode 節(jié)點绒极。對于 createComponent 創(chuàng)建組件類型的 VNode 的過程,之后再去寫蔬捷,本質(zhì)上它還是返回了一個 VNode垄提。
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)
}
那么至此,我們大致了解了 createElement 創(chuàng)建 VNode 的過程周拐,每個 VNode 有 children铡俐,children 每個元素也是一個 VNode,這樣就形成了一個 VNode Tree妥粟,它很好的描述了我們的 DOM Tree审丘。
回到 mountComponent 函數(shù)的過程,我們已經(jīng)知道 vm._render 是如何創(chuàng)建了一個 VNode勾给,接下來就是要把這個 VNode 渲染成一個真實的 DOM 并渲染出來滩报,這個過程是通過 vm._update 完成的
update
使用DOM最基本的 操作方式來實現(xiàn) DOM更新
_update 調(diào)用時機有兩個 首次渲染 和 改變數(shù)據(jù)時
_update 方法的作用是把 VNode 渲染成真實的 DOM
調(diào)用 insert 方法把 DOM 插入到父節(jié)點中,因為是遞歸調(diào)用播急,子元素會優(yōu)先調(diào)用 insert脓钾,所以整個 vnode 樹節(jié)點的插入順序是先子后父。來看一下 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)
}
}
}
insert 邏輯很簡單,調(diào)用一些 nodeOps 把子節(jié)點插入到父節(jié)點中
在 createElm 過程中,如果 vnode 節(jié)點如果不包含 tag沉噩,則它有可能是一個注釋或者純文本節(jié)點捺宗,可以直接插入到父元素中。在我們這個例子中川蒙,最內(nèi)層就是一個文本 vnode蚜厉,它的 text 值取的就是之前的 this.message 的值 Hello Vue!。
再回到 patch 方法畜眨,首次渲染我們調(diào)用了 createElm 方法昼牛,這里傳入的 parentElm 是 oldVnode.elm 的父元素, 在我們的例子是 id 為 #app div 的父元素康聂,也就是 Body贰健;實際上整個過程就是遞歸創(chuàng)建了一個完整的 DOM 樹并插入到 Body 上。