Vue.js 3.0源码解读:组件的实现-组件渲染(2)

431 阅读3分钟

核心渲染流程:创建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数,完成组件的渲染。