render的作用
render函式可以作為一道分割線,render函式的左邊可以稱之為編譯期,將Vue的模板轉換為渲染函式。render函式的右邊是Vue的執行時,主要是基於渲染函式生成Virtual DOM樹,Diff和Patch。
- render渲染函式將結合資料生成Virtual DOM的。
- 有了虛擬的DOM樹後,再交給Patch函式,負責把這些虛擬DOM真正施加到真實的DOM上。在這個過程中,Vue有自身的響應式系統來偵測在渲染過程中所依賴到的資料來源。在渲染過程中,偵測到資料來源之後就可以精確感知資料來源的變動。
- 根據需要重新進行渲染。當重新進行渲染之後,會生成一個新的樹,將新的樹與舊的樹進行diff對比,就可以最終落實到真實DOM上的改動。
一個簡單的例項,看render:
vue的渲染機制可以總結如下圖:
render前置操作
- 為何要用with(this){}包裹?
- 何時將render函式的字串轉成函式?
第1個問題?:
with通常被當做重複引用同一個物件中的多個屬性的快捷方式,可以不需要重複引用物件本身。引自《你不知道的JavaScript》上2.2.2節with
with(this)會形成塊級作用域this,render函式裡面的變數都會指向Vue例項(this)
第2個問題?:
經過parse生成AST和optimize對AST樹的優化,會生成render函式的字串。在下圖框出的createCompilerCreator(在src\compiler\create-compiler.js),呼叫createCompileToFunctionFn將字串轉成函式。
createCompileToFunctionFn(在src\compiler\to-function.js)中,會優先讀快取資訊,若沒有才會執行編譯方法,同時將render字串通過createFunction(在src\compiler\to-function.js中)呼叫New Function()的方法,創造render函式,並且快取資訊。
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}複製程式碼
render詳解
render初始化入口:src\core\instance\index.js
renderMixin(在src\core\instance\render.js)主要做了三件事情
- 執行installRenderHelpers(Vue.prototype),在原型上擴充套件如下,生成vnode節點的幾個函式解析,在執行render函式時,會呼叫,這裡先簡單的看一個。
export function renderSlot ( name: string, fallback: ?Array<VNode>, props: ?Object, bindObject: ?Object): ?Array<VNode> { const scopedSlotFn = this.$scopedSlots[name] let nodes if (scopedSlotFn) { props = props || {} if (bindObject) { props = extend(extend({}, bindObject), props) } nodes = scopedSlotFn(props) || fallback } else { nodes = this.$slots[name] || fallback } const target = props && props.slot if (target) { //呼叫createElemnet return this.$createElement('template', { slot: target }, nodes) } else { return nodes } } //有作用域插槽,會合並props和需要繫結的物件,不然直接去$solt陣列裡取,最後會呼叫creatElement()複製程式碼
- 在原型上擴充套件Vue.prototype.$nextTick方法,在watch監聽資料變化時,不會立馬更新檢視,會推到一個佇列裡,nextTick會觸發檢視的更新
- 在原型上擴充套件Vue.prototype._render。
//核心程式碼
vnode = render.call(vm._renderProxy, vm.$createElement)複製程式碼
以上是render函式執行的核心程式碼,render歸根結底,是呼叫createElement建立vnode節點。下面會詳細分析createElement到底做了哪些事情,首先我們先通過一個例子,來看下,createElement方法的入參,主要為3個入參,可通過一個例子呈現:
- 第一個引數是HTML標籤字元 “必選”
- 第一個引數是HTML標籤字元 “必選”
- 第三個引數是傳涵蓋子元素的一個陣列 “可選”
<div id="app">
<render-element></render-element>
</div>
Vue.component('render-element', {
render: function (createElement) {
var self = this
return createElement(
'div', // 第一個引數是HTML標籤字元 “必選”
{
class: {
title: true
},
style: {
border: '1px solid',
padding: '10px'
}
}, // 第二個引數是包含模板相關屬性的資料物件 “可選”
[
createElement('h1', 'Hello Vue!'),
createElement('p', '開始學習Vue!')
] // 第三個引數是傳涵蓋子元素的一個陣列 “可選”
)
}
})
let app = new Vue({
el: '#app'
})複製程式碼
createElement的詳細解析過程
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
// 相容不傳data的情況
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
// 呼叫_createElement建立虛擬節點
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context, tag, data, children, normalizationType) {
// 如果存在data.__ob__,說明data是被Observer觀察的資料,不能用作虛擬節點的data,返回一個空節點
if (data && data.__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()
}
// 當元件的tag未空,渲染一個空節點
if (!tag) {
return createEmptyVNode()
}
// 作用域插槽
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根據normalizationType的值,選擇不同的處理方法
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 = config.getTagNamespace(tag)
// 判斷是否為保留標籤
if (config.isReservedTag(tag)) {
// 如果是保留標籤,就建立一個這樣的vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// vm的components上查詢是否有這個標籤的定義
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果找到了這個標籤的定義,就以此建立虛擬元件節點
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 兜底方案,正常建立一個vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
// 當tag不是字串的時候,我們認為tag是元件的構造類,直接建立
} else {
vnode = createComponent(tag, data, context, children)
}
// 如果有vnode
if (vnode) {
// 應用namespace,繫結data,然後返回vnode
if (ns) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
}複製程式碼
梳理成流程圖如下:
特別注意當通過tag判斷為元件時,會執行createcompontent()
總結
最後總結下render函式的編譯的主要幾個步驟:
- 將template字串解析成ast
- 優化:將那些不會被改變的節點(statics)打上標記
- 生成render函式字串,並用with包裹(最新版本有改為buble)
- 通過new Function的方式生成render函式並快取
另外本文的重點是createElement如何生成一個vnode,接下來vnode如何對映到正式的dom上,是通過資料變化,通知vm.watcher,最終呼叫vm.update,最後呼叫patch方法對映到真實的dom節點中,這裡涉及到資料雙向繫結相關,請詳見[vue]雙向資料繫結。