在官方文檔中對render的解釋是:
類型
(createElement: () => VNode) => VNode
詳細:
字符串模板的代替方案纸泡,允許你發(fā)揮 JavaScript 最大的編程能力锹安。該渲染函數接收一個 createElement 方法作為第一個參數用來創(chuàng)建 VNode。
如果組件是一個函數組件,渲染函數還會接收一個額外的 context 參數无埃,為沒有實例的函數組件提供上下文信息赴背。
上面文檔翻譯成代碼:
<div id="app">
{{ message }}
</div>
// 等同于
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
在Vue源碼里可以找到_render 方法,它是實例的一個私有方法逊朽,它用來把實例渲染成一個虛擬Node罕伯。
定義在 src/core/instance/render.js 文件中:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots
)
}
// 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
}
vnode = render.call(vm._renderProxy, vm.$createElement)
可以看到,render函數中的createElement 方法就是 vm.$createElement 方法:
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 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)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
vm.$createElement
方法定義是在執(zhí)行initRender
方法的時候,除了vm.$createElement
方法叽讳,還有一個vm._c
方法追他,它是被模板編譯成的render
函數使用,而vm.$createElement
是用戶手寫render
方法使用的岛蚤, 這倆個方法支持的參數相同邑狸,并且內部都調用了createElement
方法。
vm._render
最終是通過執(zhí)行createElement
方法并返回的是vnode
涤妒,它是一個虛擬 Node单雾。接下來分析vm.$createElement
利用 createElement 方法創(chuàng)建 VNode,它定義在 src/core/vdom/create-elemenet.js 中:
// 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 方法實際上是對 _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 (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 ((!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()
}
}
_createElement 方法有 5 個參數:
- context 表示 VNode 的上下文環(huán)境,它是 Component 類型渐逃;
- tag 表示標簽够掠,它可以是一個字符串,也可以是一個 Component朴乖;
- data 表示 VNode 的數據祖屏,它是一個 VNodeData 類型,可以在 flow/vnode.js 中找到它的定義买羞;
- children 表示當前 VNode 的子節(jié)點袁勺,它是任意類型的,它接下來需要被規(guī)范為標準的 VNode 數組畜普;
- normalizationType 表示子節(jié)點規(guī)范的類型期丰,類型不同規(guī)范的方法也就不一樣,它主要是參考 render 函數是編譯生成的還是用戶手寫的吃挑。
createElement 函數的流程略微有點多钝荡,接下來主要分析 2 個重點的流程 —— children 的規(guī)范化以及 VNode 的創(chuàng)建。
children 的規(guī)范化
從代碼:
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
這里根據normalizationType 的不同舶衬,調用了 normalizeChildren(children) 和 simpleNormalizeChildren(children) 方法埠通。
定義在src/core/vdom/helpers/normalize-children.js :
// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:
// 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
}
simpleNormalizeChildren 方法調用場景是 render 函數是編譯生成的。理論上編譯生成的 children 都已經是 VNode 類型的逛犹,但這里有一個例外端辱,就是 functional component 函數式組件返回的是一個數組而不是一個根節(jié)點,所以會通過 Array.prototype.concat 方法把整個 children 數組打平虽画,讓它的深度只有一層舞蔽。
normalizeChildren 方法的調用場景有 2 種,一個場景是 render 函數是用戶手寫的码撰,當 children 只有一個節(jié)點的時候渗柿,Vue.js 從接口層面允許用戶把 children 寫成基礎類型用來創(chuàng)建單個簡單的文本節(jié)點,這種情況會調用 createTextVNode 創(chuàng)建一個文本節(jié)點的 VNode脖岛;另一個場景是當編譯 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)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
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 表示要規(guī)范的子節(jié)點
- nestedIndex 表示嵌套的索引
因為單個 child 可能是一個數組類型混槐。 normalizeArrayChildren 主要的邏輯就是遍歷 children,獲得單個節(jié)點 c轩性,然后對 c 的類型判斷,如果是一個數組類型,則遞歸調用 normalizeArrayChildren;
- 如果是基礎類型揣苏,則通過 createTextVNode 方法轉換成 VNode 類型悯嗓;
- 否則就已經是 VNode 類型了,如果 children 是一個列表并且列表還存在嵌套的情況,則根據 nestedIndex 去更新它的 key卸察。
這里需要注意一點脯厨,在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個連續(xù)的 text 節(jié)點坑质,會把它們合并成一個 text 節(jié)點合武。
經過對 children 的規(guī)范化,children 變成了一個類型為 VNode 的 Array涡扼。
VNode 的創(chuàng)建
回到 createElement 函數稼跳,規(guī)范化 children 后,接下來會去創(chuàng)建一個 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 ((!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)
}
這里先對 tag 做判斷
- 如果是 string 類型吃沪,則接著判斷如果是內置的一些節(jié)點汤善,則直接創(chuàng)建一個普通 VNode
- 如果是為已注冊的組件名,則通過 createComponent 創(chuàng)建一個組件類型的 VNode票彪,否則創(chuàng)建一個未知的標簽的 VNode
- 如果是 tag 一個 Component 類型红淡,則直接調用 createComponent 創(chuàng)建一個組件類型的 VNode 節(jié)點。
對于 createComponent 創(chuàng)建組件類型的 VNode 的過程降铸,略過需要補充在旱,本質上它還是返回了一個 VNode。
總結
那么至此推掸,我們大致了解了 createElement 創(chuàng)建 VNode 的過程桶蝎,每個 VNode 有 children,children 每個元素也是一個 VNode终佛,這樣就形成了一個 VNode Tree俊嗽,它很好的描述了我們的 DOM Tree。
回到 mountComponent 函數的過程铃彰,我們已經知道 vm._render 是如何創(chuàng)建了一個 VNode绍豁,接下來就是要把這個 VNode 渲染成一個真實的 DOM 并渲染出來,這個過程是通過 vm._update 完成的牙捉,后續(xù)接著講竹揍。