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
的特點屬性,在渲染過程也會用到.app
vm.$createElement
vue文檔中介紹了render函數,第一個參數就是createElement,以前的例子轉換成render函數就是:dom
<div id="app"> {{ message }} </div> // 轉換成render: render: function (createElement) { return createElement('div', { attrs: { id: 'app' }, }, this.message) }
能夠看出,createElement
就是vm.$createElement
ide
找到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
就都是空值
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
代碼較長,此處就不貼了,能夠本身對照源碼來分析:
若是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) } }
主要有2個點,一是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
。