经过模版编译后,会生成一个render函数, 生成的render函数会赋值给options.render
entry-runtime-with-compiler.js中的mount方法生成render函数之后调用了runtime.js中定义的mount方法,mount方法调用了mountComponent方法,此方法中执行了如下代码
updateComponent = () => {
// 先调用_render()方法生成vnode 然后调用_update方法,更新真实dom
vm._update(vm._render(), hydrating)
}
调用了vm._render()方法,_render方法是在renderMixin(Vue)方法中定义的,定义在render.js中,那么我们接下来看一下_render函数的逻辑
// 挂在_render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// 拿到render函数和_parentVnode
// _parentVnode就是类似 <comp1 prop1="123"></comp1> 转化而来的vnode,而非comp1组件内部的vnode,组件内部的vnode是vm._vnode
// render函数可以是从模板编译来的也可以是用户自定义的render函数
const { render, _parentVnode } = vm.$options
// 如果是非根组件,因为根实例没有_parentVnode
if (_parentVnode) {
// 处理slot相关数据
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
// $vnode表示父节点的vnode
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
// 全局变量,当前正在渲染的Vue实例
currentRenderingInstance = vm
// 执行render函数,参数是 $createElement方法,this = vm
// 如果是用户自定义render函数,那么会调用render(vm.$createElement)
// 如果是从模板编译来的,都是调用vm._c()
vnode = render.call(vm._renderProxy, vm.$createElement)
} 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' && 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
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// 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
}
_render()方法的主要逻辑在于如下这段代码
vnode = render.call(vm._renderProxy, vm.$createElement)
render就是生成的render函数,这里会执行render函数 模版编译生成的render函数类似如下,执行_c()方法
function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('comp1',{attrs:{"prop1":"hello"}})],1)}
}
_c()方法定义在vm._c()下,是在执行initRender的时候定义的,调用了createElement()方法
// 通过template编译生成的render函数会执行的_c方法
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.
// 用户自定义会执行的render方法
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
render.call(vm._renderProxy, vm.$createElement)传入的vm.$createElement其实是为了用户手写的render函数服务的,如下
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
接下来我们分析以下createElement方法
createElement方法中主要调用了_createElement方法 这个方法主要作用就是创建Vnode,即虚拟dom,如果是标签元素,就创建标签对应的Vnode,如果是组件那么创建组件的Vnode。
我们可以看一下Vnode的数据结构,其实本质上就是将dom节点抽象成对象
this.tag = tag // 当前节点标签名
this.data = data // 当前节点数据(VNodeData类型)
this.children = children // 当前节点子节点
this.text = text // 当前节点文本
this.elm = elm // 当前节点对应的真实DOM节点
this.ns = undefined // 当前节点命名空间
this.context = context // 当前节点上下文
this.fnContext = undefined // 函数化组件上下文
this.fnOptions = undefined // 函数化组件配置项
this.fnScopeId = undefined // 函数化组件ScopeId
this.key = data && data.key // 子节点key属性
this.componentOptions = componentOptions // 组件配置项
this.componentInstance = undefined // 组件实例
this.parent = undefined // 当前节点父节点
this.raw = false // 是否为原生HTML或只是普通文本
this.isStatic = false // 静态节点标志 keep-alive
this.isRootInsert = true // 是否作为根节点插入
this.isComment = false // 是否为注释节点
this.isCloned = false // 是否为克隆节点
this.isOnce = false // 是否为v-once节点
this.asyncFactory = asyncFactory // 异步工厂方法
this.asyncMeta = undefined // 异步Meta
this.isAsyncPlaceholder = false // 是否为异步占位
我们看到render方法中不仅有_c方法,也有很多其他方法,例如如下模版编译后的render函数
comp2: {
template: '<h1>comp2</h1>'
}
function anonymous(
) {
with(this){return _c('h1',[_v("comp2")])}
}
此render函数中先回执行_v方法,那么_v是什么呢, 其实还有很多生成Vnode的方法定义在Vue.prototype中,如下
export function installRenderHelpers (target: any) {
//
target._o = markOnce
target._n = toNumber
// 返回字符串
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
// 创建一个文本的vnode
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
installRenderHelpers是在renderMixin方法中调用的
// 原型挂在一些render需要用到的转换方法
installRenderHelpers(Vue.prototype)
我们可以看到_v方法的定义,其实也是最简单的文本节点的vnode生成,直接实例化了一个Vnode对象
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
其他诸如此类的方法有的是处理数据,有的是生成Vnode,大同小异
总结
其实虚拟dom就是将render函数执行,render函数中有一系列的方法可以会生成对应的Vnode,如文本节点等,Vnode就是对模版的抽象,将模版抽象成具有树状结构的数据。虚拟的节点生成好之后,下一步就是将虚拟的dom渲染成真实的dom节点。