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
:html
// 簡化版本
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
簡要歸納,vm._render
函數最後是經過render
執行了createElement
方法並返回vnode
;下面就來具體看下vm._renderProxy,vm.$createElement,vnode
vue
首先來看下vm._renderProxy
,vm._renderProxy
是在_init()中掛載的:node
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)
,找到定義:es6
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)
緩存
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
的特點屬性,在渲染過程也會用到.bash
vm.$createElement
vue文檔中介紹了render函數,第一個參數就是createElement,以前的例子轉換成render函數就是:app
<div id="app">
{{ message }}
</div>
// 轉換成render:
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
複製代碼
能夠看出,createElement
就是vm.$createElement
dom
找到vm.$createElement
定義,在initRender
方法中,ide
// 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
就都是空值
createEmptyVNode
直接返回註釋節點:export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true//註釋vnode
return node
}
複製代碼
children
中有function
類型做slot
處理,此處先不做分析children
作normalize
變成vnode
一維數組,有2種不一樣的方式:normalizeChildren
和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
}
複製代碼
若是chilren中有一個是數組則將整個children做爲參數組用concat
鏈接,能夠獲得每一個子元素都是vnode
的children
,這適用於只有一級嵌套數組的狀況
export function normalizeChildren (children: any): ?Array<VNode> {
// 判斷是否基礎類型,是:建立文本節點,否:判斷是否數組,是:做normalizeArrayChildren處理
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
複製代碼
普通的children處理:最後也是返回一組一維vnode的數組,當children是Array時,執行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 (){...}
複製代碼
} 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 {
複製代碼
} 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)
}
}
複製代碼
normalizeArrayChildren
的遞歸調用,二是文本節點的合併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
。