系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
从上一章节中我们知道,在调用$mount进行挂载的过程中,会执行updateComponent方法:
/* core/instance/lifecycle.js */
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
可以看到,在updateComponent方法中,首先会调用_render方法,生成组件对应的VNode,然后调用_update方法,根据VNode渲染成真实的DOM。那么接下来,我们就来看看_render方法是如何生成VNode的。
_render
_render是在引入Vue时添加到Vue.prototype上的,代码如下所示:
/* core/instance/render.js */
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// 解析作用域插槽
if (_parentVnode) {
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.
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.
currentRenderingInstance = vm
// 根据渲染函数,生成VNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
} 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)) {
// ...
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
可以看到,在_render方法中,首先从$options上取出渲染函数render、父占位符节点_parentVnode,对于子组件来说,此时还会调用normalizeScopedSlots,用于解析作用域插槽,然后将_parentVnode赋值给实例的$vnode,从这里可以看出,由于根组件不存在父占位符节点,所以它的$vnode为undefined,当根组件完成挂载后,在$mount的最后会调用mounted钩子函数,而子组件则会跳过这段逻辑:
/* core/instance/lifecycle.js */
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
}
接着就会调用render渲染函数,它是_render的核心逻辑,除了生成VNode外,Vue还在此过程中完成了对依赖的收集。这里的render函数可以是经过vue-loader处理过的template模板,也可以是用户手写的render函数,总之,render的返回结果就是当前组件的渲染根VNode,然后通过parent属性,构建渲染VNode与占位符VNode之间的父子关系。
那么接下来,我们就来看看对于单个节点,Vue是如何生成VNode的。
createElement
Vue提供了两个方法生成VNode,一个是_c,一个是$createElement,通过vue-loader生成的渲染函数,其内部使用的是vm._c,手写渲染函数时,传入的第一个参数是vm.$createElement:
/* core/instance/render.js */
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)
}
可以看到,这两个方法内部都是调用createElement方法,只是最后一个参数不同。
其实简单点来说,createElement就是用来创建VNode,而对于每个VNode节点,最关键的无非是三样东西:
-
tag:标签名,表示当前VNode是何种标签元素,比如div、p,也可以表示组件。 -
data:节点数据,表示当前VNode上所有与之相关的数据,比如attrs、on等。 -
children:子节点,表示当前VNode的所有子节点。
那么接下来,我们就详细看看createElement方法的实现:
/* core/vdom/create-element.js */
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// ...
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 规范化子节点
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 根据tag,生成普通节点或组件节点
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && 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()
}
}
可以看到,createElement就是对_createElement方法的包装,在_createElement方法中,首先根据normalizationType,调用normalizeChildren或simpleNormalizeChildren方法,规范化子节点,其实内部就是用来合并相邻的文本节点,将嵌套子节点展平的逻辑,处理完成后,children是一个VNode类型的一维数组。
接下来,就是根据tag的类型生成不同种类的VNode节点,我们首先来看看对于普通元素节点,Vue是如何创建VNode的。对于普通元素节点来说,其tag肯定是一个字符串,所以typeof tag === 'string'为true,接着通过isReservedTag判断tag是否是平台内置的标签元素,如果是的话,则说明该节点是普通元素节点,就直接调用VNode构造函数,创建元素节点对应的VNode,isReservedTag方法的代码如下所示:
/* platforms/web/util/element.js */
export const isReservedTag = (tag: string): ?boolean => {
return isHTMLTag(tag) || isSVG(tag)
}
export const isHTMLTag = makeMap(
'html,body,base,head,link,meta,style,title,' +
'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
'embed,object,param,source,canvas,script,noscript,del,ins,' +
'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
'output,progress,select,textarea,' +
'details,dialog,menu,menuitem,summary,' +
'content,element,shadow,template,blockquote,iframe,tfoot'
)
最后,就可以通过tag、data、children,创建一个VNode,它所包含的信息就会告诉Vue页面上需要渲染什么样的节点以及其子节点。
通过上面的分析,我们已经知道tag和children分别代表标签名和子节点,那么data中又应该包含哪些信息呢?我们可以从VNodeData中找到:
export interface VNodeData {
key?: string | number;
// 普通命名插槽
slot?: string;
// 作用域插槽(在父组件占位符VNode中存在)
scopedSlots?: { [key: string]: ScopedSlot | undefined };
ref?: string;
refInFor?: boolean;
tag?: string;
staticClass?: string;
class?: any;
staticStyle?: { [key: string]: any };
style?: string | object[] | object;
// 组件的prop
props?: { [key: string]: any };
// html attribute 通过el.setAttribute设置
attrs?: { [key: string]: any };
// DOM property 通过el[prop]=xxx设置
domProps?: { [key: string]: any };
// VNode的hook
hook?: { [key: string]: Function };
// html原生事件 + 组件自定义事件
on?: { [key: string]: Function | Function[] };
// 定义在组件上的原生事件
nativeOn?: { [key: string]: Function | Function[] };
transition?: object;
show?: boolean;
inlineTemplate?: {
render: Function;
staticRenderFns: Function[];
};
// 自定义指令
directives?: VNodeDirective[];
keepAlive?: boolean;
}
总结
通过createElement方法,可以生成对应的VNode节点,对于一个普通的元素节点来说,只需要给定tag、data、children,就可以完整的描述对应的真实节点。