# 初次渲染

core 中返回的 app 上的 mount 是这样的

mount(rootContainer: HostElement, isHydrate?: boolean): any {
  if (!isMounted) {
    const vnode = createVNode(rootComponent as Component, rootProps)
    vnode.appContext = context

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer)
    }
    isMounted = true
    app._container = rootContainer
    return vnode.component!.proxy
  } else if (__DEV__) {
    // warn
  }
}

而在我们调用的 dom 的 createApp 里面:

const { mount } = app;
app.mount = (containerOrSelector: Element | string): any => {
  const container = normalizeContainer(containerOrSelector);
  if (!container) return;
  const component = app._component;
  if (!isFunction(component) && !component.render && !component.template) {
    component.template = container.innerHTML;
  }
  // clear content before mounting
  container.innerHTML = "";
  const proxy = mount(container);
  container.removeAttribute("v-cloak");
  return proxy;
};

这里 dom 并不是重新实现了 mount,而是提取了一些平台相关的逻辑出来,比如读取内部 html 作为模板,core 并不知道平台特性当然不能由他来做这个事情,但是最后肯定还是调用了 core 的 mount,所以我们的重心仍然在 core

core 里面的 mount 代码还是很简单的,也就两步:

  • 常见根组件的 vnode
  • 渲染这个 vnode

renderhydrate的区别就是是否需要复用 container 里面的 dom 节点,关于hydrate后面会单独开章节讲解。那么自然我们接下去要看的就是render方法,vnode 的结构可以参考vnode 章节

render 方法:

const render: RootRenderFunction = (vnode, container) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true);
    }
  } else {
    patch(container._vnode || null, vnode, container);
  }
  flushPostFlushCbs();
  container._vnode = vnode;
};

这里调用了patch,那么这个 patch 就是渲染组件的关键所在了,后面的组件更新最终走的也是这个方法。我们在baseCreateRenderer那里面 2000 来行的代码,其实主要就是为patch方法服务的,简单来说 patch 方法就是根据下面的条件:

  • 节点类型
  • 是否是初次渲染

来执行不同的操作,由于代码太长就不全部挂出来来,代码在runtime-core -> renderer.ts -> baseCreateRenderer里面,大家可以自己去看,我会贴一些关键代码出来。

// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
  anchor = getNextHostNode(n1);
  unmount(n1, parentComponent, parentSuspense, true);
  n1 = null;
}

一开始这段代码就明示了,对于前后节点类型不同的,vue 是直接卸载之前的然后重新渲染新的,不会考虑可能的子节点复用。

const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:

然后是根据节点的类型来执行不同的process方法,类型有如下:

  • Text 文本
  • Comment 注释
  • Static 静态
  • Fragment 片段
  • ShapeFlags.ELEMENT 原生节点(div 等)
  • ShapeFlags.COMPONENT 组件节点
  • ShapeFlags.TELEPORT 传送节点(比如你的 modal 内容并不是在当前节点树下的,而是会挂载到 body 下)
  • ShapeFlags.SUSPENSE 挂起节点(异步渲染)
export const Text = Symbol(__DEV__ ? "Text" : undefined);
export const Comment = Symbol(__DEV__ ? "Comment" : undefined);
export const Static = Symbol(__DEV__ ? "Static" : undefined);

// 这个类型编排方式也和react一样。。。
export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
}

这里可以看到这些类比的参数都是像 Symbol 或者一些特定标识符,就是专门用来做标示的。值得注意的是ShapeFlags是通过位运算得到的数字,那么他们有什么特点呢?翻译一下就很简单明了了:

export const enum ShapeFlags {
  ELEMENT = 00000000001,
  FUNCTIONAL_COMPONENT = 0000000010,
  STATEFUL_COMPONENT = 0000000100,
  // ...
  COMPONENT = 00000000110,
}

通过二进制数中 1 所在的位置来标示不同的含义。这么做有什么优势呢?很简单,对于复合类型的类比会很简单,比如这里的COMPONENTSTATEFUL_COMPONENTFUNCTIONAL_COMPONENT的复合类型,那么对于一个节点是否是STATEFUL_COMPONENT我们只需要执行elementType & STATEFUL_COMPONENT如果得到的结果大于 0 标示他就是(010 & 010第二位是 1,其他为 0,具体可以了解位运算)。

对于我们的 demo,渲染的肯定是组件,所以会走到:

else if (shapeFlag & ShapeFlags.COMPONENT) {
  processComponent(
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  )
}

关于 processComponent 做了什么,请看[这里],关于如何处理不同的组件类型我们集中放到了一起

处理完节点节点之后会处理ref,具体相关看ref 讲解

if (ref != null && parentComponent) {
  setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2);
}