vm._render
生成虛擬dom
我們知道在掛載過程中, $mount
會調用 vm._update和vm._render
方法,vm._updata
是負責把VNode渲染成真正的DOM,vm._render
方法是用來把實例渲染成VNode,這里的_render
是實例的私有方法,和前面我們說的vm.render
不是同一個,先來看下vm._render
定義,vm._render
是通過renderMixin(Vue)
掛載的,定義在src/core/instance/render.js
:
// 簡化版本
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
...
// render self
let vnode
try {
// _renderProxy生產環境下是vm
// 開發環境可能是proxy對象
vnode = render.call(vm._renderProxy, vm.$createElement) // 近似vm.render(createElement)
} catch (e) {...}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {...}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
- 先緩存
vm.$options.render
和vm.$options._parentVnode
,vm.$options.render
是在上節的$mount
中通過comileToFunctions
方法將template/el
編譯來的。 vnode = render.call(vm._renderProxy, vm.$createElement)
調用了render
方法,參數是vm._renderProxy,vm.$createElement
- 拿到
vnode
后,判斷類型是否為VNode
,如果有多個vnode
,則是模板上有多個根節點,觸發告警。 - 掛載
vnode
父節點,最后返回vnode
簡要概括,vm._render
函數最后是通過render
執行了createElement
方法並返回vnode
;下面就來具體看下vm._renderProxy,vm.$createElement,vnode
vm._renderProxy
首先來看下vm._renderProxy
,vm._renderProxy
是在_init()
中掛載的:
Vue.prototype._init = function (options?: Object) {
...
if (process.env.NODE_ENV !== 'production') {
// 對vm對一層攔截處理,當使用vm上沒有的屬性時將告警
initProxy(vm)
} else {
vm._renderProxy = vm
}
...
}
如果是生產環境,vm._renderProxy
直接就是vm
;開發環境下,執行initProxy(vm)
,找到定義:
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對一層攔截處理
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
先判斷當前是否支持Proxy
(ES6新語法),支持的話會實例化一個Proxy, 當前例子用的是hasHandler(只要判斷是否vm上有無屬性即可),這樣每次通過vm._renderProxy訪問vm時,都必須經過這層代理:
// 判斷對象是否有某個屬性
const hasHandler = {
has (target, key) {
// vm中是否有key屬性
const has = key in target
// 當key是全局變量或者key是私有屬性且key沒有在$data中,允許訪問該key
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
}
}
所以,_render
中的vnode = render.call(vm._renderProxy, vm.$createElement)
,實際上是執行vm._renderProxy.render(vm.$createElement)
Virtual DOM 虛擬dom
vue.2.0
中引入了virtual dom
,大大提升了代碼的性能。所謂virtual dom
,就是用js對象去描述一個dom
節點,這比真實創建dom快很多。在vue中,Virtual dom是用類vnode
來表示,vnode
在src/core/vdom/vnode.js
中定義,有真實dom
上也有的屬性,像tag/text/key/data/children
等,還有些是vue
的特色屬性,在渲染過程也會用到.
vm.$createElement
vue文檔中介紹了render函數,第一個參數就是createElement
,之前的例子轉換成render
函數就是:
<div id="app">
{{ message }}
</div>
// 轉換成render:
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
可以看出,createElement
就是vm.$createElement
找到vm.$createElement
定義,在initRender
方法中,
// 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)
看到這里定義了2個實例方法都是調用的createElement
,一個是用於編譯生成的render
方法,一個是用於手寫render
方法,createElement
最后會返回Vnode
,來看下createElement
的定義:
export function createElement (
context: Component, //vm實例
tag: any,
data: any, //可以不傳
children: any,// 子節點
normalizationType: any,
alwaysNormalize: boolean
) {
// 參數判斷,不傳data時,要把children,normalizationType參數往前移
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)
}
先經過參數重載,根據alwaysNormalize
傳不同的normalizationType
,調用_createElement()
,實際上createElement
是提前對參數做了一層處理
這里的參數重載有個小點值得注意,normalizationType
是關系到后面children
的扁平處理,沒有children
則不需要對normalizationType
賦值,children
和normalizationType
就都是空值
_createElement()
- 首先校驗
data
,data
是響應式的,調用createEmptyVNode
直接返回注釋節點:
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true//注釋vnode
return node
}
- 處理
tag
,沒有tag
時也返回注釋節點 key
做基礎類型校驗- 當
children
中有function
類型作slot
處理,此處先不作分析 - 對
children
做normalize
變成vnode
一維數組,有2種不同的方式:normalizeChildren
和simpleNormalizeChildren
- 創建
vnode
simpleNormalizeChildren
normalizeChildren
和simpleNormalizeChildren
是2種對children
扁平化處理的方法,先來看下simpleNormalizeChildren
定義:
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
}
如果children
中有一個是數組則將整個children
作為參數組用concat
連接,可以得到每個子元素都是vnode
的children
,這適用於只有一級嵌套數組的情況
normalizeChildren
export function normalizeChildren (children: any): ?Array<VNode> {
// 判斷是否基礎類型,是:創建文本節點,否:判斷是否數組,是:作normalizeArrayChildren處理
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
普通的children
處理:最后也是返回一組一維vnode
的數組,當children
是Array時,執行normalizeArrayChildren
normalizeArrayChildren
代碼較長,此處就不貼了,可以自己對照源碼來分析:
- 定義res
- 遍歷children,當
children[i]
是空或者是布爾值,跳過該次循環 - 如果
children[i]
還是個數組,再對children[i]
作normalizeArrayChildren
處理if (Array.isArray(c)) { if (c.length > 0) { c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)// 返回vnode數組 // merge adjacent text nodes // 優化:如果c的第一個vnode和children上一次處理的vnode都是文本節點可以合並成一個vnode if (isTextNode(c[0]) && isTextNode(last)) { res[lastIndex] = createTextVNode(last.text + (c[0]: any).text) c.shift() } res.push.apply(res, c) } } else if (){...}
- children[i]是基礎類型時
} else if (isPrimitive(c)) { // 當c是基礎類型時 // children上一次處理的vnode是文本節點,則合並成一個文本節點 if (isTextNode(last)) { // merge adjacent text nodes // this is necessary for SSR hydration because text nodes are // essentially merged when rendered to HTML strings // 這是SSR hydration所必需的,因為文本節點渲染成html時基本上都是合並的 res[lastIndex] = createTextVNode(last.text + c) } else if (c !== '') { // convert primitive to vnode res.push(createTextVNode(c))// c不為空直接創建文本節點 } } else {
- 其它情況,
children[i]
是vnode
時,} else {// 當c是vnode時 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}__` } // push到res上 res.push(c) } }
- 最后返回一組vnode
主要有2個點,一是normalizeArrayChildren
的遞歸調用,二是文本節點的合並
創建vnode
- 創建
vnode
,並返回
- 判斷
tag
類型,為字符串時:let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) // 判斷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 vnode = new VNode( tag, data, children, undefined, undefined, context ) }
tag
不是字符串類型時,vnode = createComponent(tag, data, context, children)
,先略過- 最后再對生成的
vnode
作校驗,返回vnode
小結
到此為止,我們分析了vm._render
方法和_createElement
方法,知道了創建vnode
的整個過程,在$mount中的 vm._update(vm._render(), hydrating)
,vm._render
返回了vnode,再傳入vm._update
中,由vm._update
渲染成真實dom
。