从 `render()` 函数到 VNode
上文介绍了在编译阶段,从 HTML 字符串到 render()
函数的生成过程。
让我们回到运行阶段的 $mount()
方法:
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
抛开依赖收集和生命钩子的执行,实际调用的是 _render()
而非我们之前挂的 vm.$options.render()
。
export function mountComponent(vm, el, hydrating) {
vm.$el = el;
callHook(vm, 'beforeMount');
let updateComponent = () => {
vm._update(vm._render(), hydrating);
};
// 依赖收集
vm._watcher = new Watcher(vm, updateComponent, noop);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm;
}
_render()
是在 Vue 初始化阶段挂在原型对象上的,可以看到内部调用了真正的 render()
,并传入了 $createElement
// src/core/instance/render.js
Vue.prototype._render = function () {
const vm = this;
const {render, $parentVnode} = vm.$options;
let vnode;
try {
vnode = render.call(vm._renderProxy, vm.$createElement);
}
catch (e) {
// 省略错误处理
}
// set parent
vnode.parent = $parentVnode;
return vnode;
};
还是在初始化阶段,_c
的来源也找到了:
// src/core/instance/render.js
import {createElement} from '../vdom/create-element';
export function initRender(vm) {
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
}
顺便介绍一下之前在 render 函数中使用到的其他快捷方法,例如 _v
和 _s
,其实就是创建文本类型 VNode 和 toString()
:
// src/core/instance/render-helpers/index.js
export function installRenderHelpers(target) {
target._s = toString;
target._v = createTextVNode;
}
已经出现了很多创建 VNode 的方法了,让我们看看吧。
创建 VNode
关于 VNode 的结构,这次我们看下 ts 中的定义
export interface VNode {
tag?: string;
data?: VNodeData;
children?: VNode[];
text?: string;
elm?: Node;
ns?: string;
context?: Vue;
key?: string | number;
componentOptions?: VNodeComponentOptions;
componentInstance?: Vue;
parent?: VNode;
raw?: boolean;
isStatic?: boolean;
isRootInsert: boolean;
isComment: boolean;
}
创建文本类型 VNode,也就是之前的 _v
:
export function createTextVNode(val) {
return new VNode(undefined, undefined, undefined, String(val));
}
在创建元素时,需要支持多种 参数类型。
首先第一个参数标签名可以是字符串或者异步函数。当标签名是字符串时,又分成 HTML 保留标签和自定义标签两种情况。对于前者,直接创建 VNode 就好,而对于后者,首先需要检查 components
属性是否定义了组件构造函数,如果有再调用 createComponent
:
export function $createElement(
context,
tag,
data,
children,
normalizationType
) {
// 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;
let ns;
// 处理标签名
if (typeof tag === 'string') {
let Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
// HTML 保留元素
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 (isDef(vnode)) {
if (ns) {
applyNS(vnode, ns);
}
return vnode;
}
return createEmptyVNode();
}
创建组件
这里省略了配置对象和异步函数这两种情况。其中主要处理了这么几件事:
v-model
的处理,转成data.on
data.on
调整到listeners
中- 生成
data.hook
对象,这涉及到后续的 patch 阶段,调用init
prepatch
等钩子
export function createComponent(
Ctor,
data,
context,
children,
tag
) {
data = data || {};
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag);
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
// merge component management hooks onto the placeholder node
mergeHooks(data);
// return a placeholder vnode
const name = Ctor.options.name || tag;
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{Ctor, propsData, listeners, tag, children},
asyncFactory
);
return vnode;
}
总结
生成 VNode 这种中间的表现形式,说到底还是为了后续操作方便。