概述
為了實現響應式模式,Vue用render函式來生成vnode,並使用diff演算法對比新舊vnode,最後更新到真實DOM上。
由於是在編譯階段而不是在監聽階段,所以vnode沒有對比的物件,直接通過vnode生成真實DOM。
Vnode是Vdom上的一個節點,是對真實DOM的抽象,在Vue中,我們可以通過對比新舊Vnode和Vdom來得到需要更新真實DOM的操作,並通過Vue框架來執行這些操作。於是我們可以把更多的精力投放到業務邏輯上。
編譯階段
該階段會解析template,把template轉化為render函式會經過三個過程:
- parse,將 template 模板中進行字串解析,得到指令、class、style等資料,形成 AST
- optimize,這個階段用於優化patch階段,標記節點的 static 屬性是否是靜態的
- generate,將 AST 轉化成 render funtion 字串,最終得到 render 的字串以及 staticRenderFns 字串
如果使用vue-cli工具的話,藉助webpack可以在打包過程中把template轉化為render函式和staticRenderFns函式
render 的字串與render 函式的關係
render函式內部包含render字串:
function render(vm) {
with(vm) {
eval(render_string)
}
}
複製程式碼
掛載節點
Vue例項化的最後一步就是掛載節點。該階段會分為兩步:
- 通過render函式獲得vnode
- 通過傳入vnode給patch函式生成真實DOM並掛載到頁面上
render函式被執行時機
那麼render函式在什麼時候會被再次執行呢?
在解釋VueJS 響應式原理的時候有提到過,Render-Watcher例項的getter就是執行render函式的:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製程式碼
所以,render函式會被執行的時機有:
- Vue初始化的時候,會執行一次
- 當template(模板)中需要觀察的資料物件更新值的時候,也會觸發render函式(render-watcher)執行
render函式的關鍵是_createElement
,負責返回VNode,它會根據標籤名是否存在已註冊的元件中,返回普通VNode或是元件VNode:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// .......
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (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 = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
複製程式碼
patch函式執行時機
和render函式的一樣,因為patch函式就在vm._update(vm._render(), hydrating)
中的_update裡。
- 在Vue初始化的時候,會生成真實DOM並掛載到document上
- 當template(模板)中需要觀察的資料物件更新值的時候,會對比新舊vnode,並返回新vnode對應的真實DOM
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// 初始化渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
複製程式碼
更新的patch函式的核心是diff演算法,類似git的diff指令,大致邏輯如下:
通過對比新舊vnode,找到更新真實DOM需要的所有操作,比如新增、刪除、替換節點的操作。然後通過Vue框架來執行這些更新DOM的操作,最後返回更新的DOM。