概述
为了实现响应式模式,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。