render
手寫 render
函數,仔細觀察下面這段代碼,試想一下這裏的 createElement 參數是什麼 。
new Vue({
el: '#application',
render(createElement) {
return createElement('div', {
attrs: {
id: 'app1' //注意這裏的id是app1了不是index.html中的application了
}
}, this.value)
},
data() {
return {
value: 'render function'
}
}
})
頁面效果:
我們可以看到app1已經替換掉了application,所以我們爲什麼不能將元素綁定在body/html這些元素上就知道了吧,因爲它會替換掉頁面上的元素。
源碼分析:
Vue 的 _render
方法是實例的一個私有方法,它用來把實例渲染成一個虛擬 Node。它的定義在 src/core/instance/render.js 文件中:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// 下面這兩個 if 先不用看
// reset _rendered flag on slots for duplicate slot check
if (process.env.NODE_ENV !== 'production') {
for (const key in vm.$slots) {
// $flow-disable-line
vm.$slots[key]._rendered = false
}
}
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
// 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 {
// 分析主線
// 從這裏我們可以看出,手寫render方法中的createElement參數就是 vm.$createElement方法
vnode = render.call(vm._renderProxy, vm.$createElement)
// 這個render 是 vm.$options.render
// vm._renderProxy 如果在生產環境下,其實就是 vm
// 如果在開發環境下,就是 Proxy 對象(ES6中的API,不瞭解的話可以去看看)
// vm.$createElement 定義在 initRender 函數中,初始化的時候定義的
// 手寫 render 函數創建 vnode 的方法
// vm.$createElement = function (a, b, c, d) {
// return createElement(vm, a, b, c, d, true);
// };
// 如果是編譯生成的render函數,創建vnode的方法則是下面這個方法
// vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
} 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') {
if (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
}
} else {
vnode = vm._vnode
}
}
// 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
}
}
Flow
是 facebook
出品的 JavaScript 靜態類型檢查工具。Vue.js
的源碼利用了 Flow 做了靜態類型檢查,function (): VNode
表示這個方法的返回值是一個 vnode
。
再回到 _render
函數中的 render
方法的調用:
vnode = render.call(vm._renderProxy, vm.$createElement)
可以看到,render
函數中的 createElement
方法就是 vm.$createElement
方法:
export function initRender (vm: Component) {
// ...
// 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)
}
實際上,vm.$createElement
方法定義是在執行 initRender
方法的時候,可以看到除了 vm.$createElement
方法,還有一個 vm._c
方法,它是被模板編譯成的 render
函數使用,而 vm.$createElement
是用戶手寫 render
方法使用的, 這倆個方法支持的參數相同,並且內部都調用了 createElement
方法。
vm._render
最終是通過執行 createElement
方法並返回的是 vnode
,它是一個虛擬 Node
。Vue 2.0 相比 Vue 1.0 最大的升級就是利用了 Virtual DOM
。因此在分析 createElement
的實現前,我們先了解一下 Virtual DOM
的概念。
Virtual DOM
Virtual DOM 這個概念相信大部分人都不會陌生,它產生的前提是瀏覽器中的 DOM 是很“昂貴"的,爲了更直觀的感受,我們可以簡單的把一個簡單的 div 元素的屬性都打印出來,如圖所示:
可以看到,真正的 DOM
元素是非常龐大的,因爲瀏覽器的標準就把 DOM
設計的非常複雜。當我們頻繁的去做 DOM 更新,會產生一定的性能問題。
而 Virtual DOM
就是用一個原生的 JS 對象去描述一個 DOM
節點,所以它比創建一個 DOM
的代價要小很多。在 Vue.js 中,Virtual DOM
是用 VNode
這麼一個 Class
去描述,它是定義在 src/core/vdom/vnode.js
中的。
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
可以看到 Vue.js
中的 Virtual DOM
的定義還是略微複雜一些的,因爲它這裏包含了很多 Vue.js 的特性。這裏千萬不要被這些茫茫多的屬性嚇到,實際上 Vue.js
中 Virtual DOM
是借鑑了一個開源庫 snabbdom 的實現,然後加入了一些 Vue.js 特色的東西。我建議大家如果想深入瞭解 Vue.js
的 Virtual DOM
前不妨先閱讀這個庫的源碼,因爲它更加簡單和純粹。
總結
其實 VNode
是對真實 DOM
的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、數據、子節點、鍵值等,其它屬性都是都是用來擴展 VNode
的靈活性以及實現一些特殊 feature
的。由於 VNode
只是用來映射到真實 DOM
的渲染,不需要包含操作 DOM
的方法,因此它是非常輕量和簡單的。
Virtual DOM 除了它的數據結構的定義,映射到真實的 DOM
實際上要經歷 VNode
的 create、diff、patch
等過程。那麼在 Vue.js
中,VNode
的 create
是通過之前提到的 createElement
方法創建的,