核心渲染流程:创建vnode和渲染vnode
1.创建vnode
vnode本质上是用来描述DOM的JavaScript对象,它在Vue.js中可以描述不同类型的节点,比如普通元素节点、组件节点等。那么在Vue.js中,是如何创建这些vnode的呢? 在上一节讲的createApp函数里面,其中有一句:
const { mount } = app;
我们看会app.mount的方法,可以看到内部是通过createVNode函数创建了根组件的vnode:
const vnode = createVNode(rootComponent, rootProps);
我们查了一下源码,可以看到如下代码:
const createVNode = (createVNodeWithArgsTransform);
const createVNodeWithArgsTransform = (...args) => {
return _createVNode(...(vnodeArgsTransformer
? vnodeArgsTransformer(args, currentRenderingInstance)
: args));
};
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
...
if (props) {
// 处理 props相关逻辑,标准化class和style
}
// 对vnode类型信息编码
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0;
...
normalizeChildren(vnode, children);
...
}
其中的normalizeChildren函数作用是标准化子节点,把不同数据类型的children转成数组或者文本类型。 因此,createVNode函数主要做了几件事:对props做标准化处理、对vnode类型信息编码、创建标准子节点。
2.渲染vnode
看回app.mount这个函数,内部是通过执行:
render(vnode, rootContainer, isSVG);
去渲染创建好的vnode。我搜索了一下源码,发现有这个函数,估计应该就是调用这个吧。
const render = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
}
else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
flushPostFlushCbs();
container._vnode = vnode;
}
首先,先判断vnode是否为null, 如果null, 就销毁组件,如果不是就调用patch创建或者更新组件。最后,container._vnode = vnode;这是把已经渲染的vnode节点,缓存起来。那么接下来我们就得来看看这个patch函数到底干了啥?搜索源码,可以找到如下函数:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false) => {
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1);
unmount(n1, parentComponent, parentSuspense, true);
n1 = null;
}
const { type, ref, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container, anchor);
break;
case Comment:
...
}
这个函数:
n1 表示旧的vnode,当n1为null的时候,表示一次挂载的过程;
n2 表示新的vnode节点,后续会根据这个vnode类型执行不同的处理逻辑。
container:表示DOM容器,也就是渲染生成DOM后,会挂载到container下面。
第一段主要是判断:是否存在新旧节点,且新旧节点类型不同,如果有,则销毁旧节点。接下就是对vnode节点类型的不同,做不同的处理。其中对组件的处理函数是:processComponent。
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
if (n1 == null) {
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized);
}
else {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
}
else {
updateComponent(n1, n2, optimized);
}
}
如果n1==null,那么就挂载组件,否则就更新组件。挂载组件函数:mountComponent。
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
...
setupComponent(instance);
{
endMeasure(instance, `init`);
}
...
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
{
popWarningContext();
endMeasure(instance, `mount`);
}
}
首先先创建组件实例,然后设置组件实例,最后设置并运行带副作用的渲染函数setupRenderEffect。
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
...
// 渲染组件生成子树vnode:每个组件都会有对应的render函数,而renderComponentRoot函数就是去执行render函数创建整个组件树内部的vnode
const subTree = (instance.subTree = renderComponentRoot(instance));
...
}
}
这渲染函数大概的作用是:渲染组件生成subTree,然后把subTree挂载到container中。然后通过递归patch,就可以构造完整的DOM数,完成组件的渲染。