likes
comments
collection
share

vue3组件渲染成DOM

作者站长头像
站长
· 阅读数 14

vue3组件渲染成DOM

项目入口 main.js

import { createApp } from 'vue'

import App from './App.vue'

createApp(App).mount('#app')

App.vue 编译后效果

看看App.vue使用不同API风格被编译后的结果

vue3组件渲染成DOM

可以看出,App.vue使用组合式 API (Composition API)编写风格,被编译后,默认导出一个对象,里面有一个setup函数,返回一个页面渲染的render函数

vue3组件渲染成DOM 可以看出,App.vue使用选项式 API (Options API)编写风格,被编译后,同样默认导出一个对象,并赋值一个render函数属性

从createApp进入源码

下载vue3代码,通过 pnpm i 安装依赖,通过pnpm dev 调试源码,createApp方法在packages/runtime-dom/src/index.ts中

vue3组件渲染成DOM

通过调试看vue是如何一步步进行组件渲染的

createApp

packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  //创建app对象
  const app = ensureRenderer().createApp(...args);
  // 重写mount
  const { mount } = app;
  app.mount = (containerOrSelector) => {

  };

  return app;
}) as CreateAppFunction<Element>;
// 如果 renderer 有值的话,那么以后都不会初始化了
function ensureRenderer() {
  return (
    renderer || 
    // rendererOptions: 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
    (renderer = createRenderer(rendererOptions))
  )
}

packages/runtime-core/src/renderer.ts

export function createRenderer(options) {
  return baseCreateRenderer(options)
}

function baseCreateRenderer(options) {
  const render = (vnode, container) => {
    // 组件渲染的核心逻辑
  }
  //...
  return {
    render,
    createApp: createAppAPI(render)
  }
}

baseCreateRenderer里面定义了非常多方法,接近2000行代码,里面有patch,processElement,mountElement,mountChildren,patchElement,processComponent,mountComponent,updateComponent...,有核心的渲染和更新方法,以及diff算法等

mount 挂载

packages/runtime-core/src/apiCreateApp.ts

export function createAppAPI(render){
  //接收了组件对象(有属性setup函数,返回render函数)作为根组件 rootComponent
  return function createApp(rootComponent, rootProps = null) {  
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent 
      _props: rootProps,

      use(plugin, ...options) {},
      mixin(mixin) { },
      component(name: string, component?: Component): any {},
      directive(name: string, directive?: Directive) {},
      unmount() { },
      provide(key, value) {},
      mount(rootContainer,isHydrate){
        if (!isMounted) {
          // 创建根组件的 vnode  rootComponent
          const vnode = createVNode(rootComponent, rootProps)  
          // 渲染根组件 ,这个render就是baseCreateRenderer里面定义传过来的
          render(vnode, rootContainer) 
          
          isMounted = true
          app._container = rootContainer
      }, 
    return app
  }
}

vue3组件渲染成DOM

app对象里面定义了很多重要的方法,这里主要分析mount

createVNode

packages/runtime-core/src/vnode.ts

function createVNode(
  type,   //第一个参数 type 是我们的 <App /> 组件对象
  props = null,
  children = null,
): VNode {

  // class & style normalization.
  if (props) { 
     //主要处理class和style
  }

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
    ? ShapeFlags.SUSPENSE
    : isTeleport(type)
    ? ShapeFlags.TELEPORT
    : isObject(type) 
    ? ShapeFlags.STATEFUL_COMPONENT
    : isFunction(type)
    ? ShapeFlags.FUNCTIONAL_COMPONENT
    : 0

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag, // <APp/>组件对象 是ShapeFlags.STATEFUL_COMPONENT
    isBlockNode,
    true
  )
}

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  const vnode = {
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null, //组件实例
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null,
    ctx: currentRenderingInstance
  } as VNode

  //...
  return vnode
}

主要任务就是处理props、对 vnode 的类型信息编码、创建 vnode 对象,标准化子节点 children

render 渲染

 mount(rootContainer,isHydrate){
        if (!isMounted) {
          // 创建根组件的 vnode  rootComponent
          const vnode = createVNode(rootComponent, rootProps)  
          // 渲染根组件 ,这个render就是baseCreateRenderer里面定义传过来的
          render(vnode, rootContainer) 
    }, 

packages/runtime-core/src/renderer.ts

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        // 如果 vnode 不存在,需要卸载组件
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 创建或者更新组件
      patch(container._vnode || null, vnode, container)
    }
    // 缓存 vnode
    container._vnode = vnode 
  }

patch

此处patch显然是创建组件

packages/runtime-core/src/renderer.ts

 export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
      return n1.type === n2.type && n1.key === n2.key
}

  const patch: PatchFn = (
    n1, //旧node,第一次挂载为null
    n2, //新node
    container, //container 表示需要挂载的 dom 容器;
    anchor = null,
    parentComponent = null,
  ) => {
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      //isSameVNodeType: 新老节点的 type 和 key 都相同
      // 类型不同,直接进行卸载旧节点
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text: 
        //文本节点
        break
      case Comment: 
        //注释节点
        break
      case Static: 
        //静态节点
        break
      case Fragment:
        // 处理 Fragment 元素
        break
      default:
          // 进行按位与运算
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // 处理普通 DOM 元素
          processElement(n1,n2,container,anchor,parentComponent )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
         // 处理 component
         processComponent(n1,n2,container,anchor,parentComponent)
        } else if () {
         // ....
        } 
    }
  }

此时是初次挂载App组件vnode,所以,n1为null,n2为App组件vnode,这个vnode有什么属性呢?

根据文章前面看到的App.vue被编译后是一个对象,里面有一个setup函数,setup函数返回一个render函数,所以const vnode = createVNode(rootComponent, rootProps)传入的rootComponent就是这个被编译后的对象,

所以在createVNode函数中第一个参数type是对象:isObject(type) ,shapeFlag = ShapeFlags.STATEFUL_COMPONENT

回到patch函数中来,通过按位与运算(运算符在两个操作数对应的二进位都为 1 时,该位的结果值才为 1),

// 左移操作符 (`<<`) 将第一个操作数向左移动指定位数,
// 左边超出的位数将会被清除,右边将会补零

//按位或(`|`)运算符在其中一个或两个操作数对应的二进制位为 `1` 时,
//该位的结果值为 `1`

export const enum ShapeFlags {
  ELEMENT = 1,                                      // 0b0000000001  1
  FUNCTIONAL_COMPONENT = 1 << 1,                    // 0b0000000010  2
  STATEFUL_COMPONENT = 1 << 2,                      // 0b0000000100  4
  TEXT_CHILDREN = 1 << 3,                           // 0b0000001000  8
  ARRAY_CHILDREN = 1 << 4,                          // 0b0000010000  16
  SLOTS_CHILDREN = 1 << 5,                          // 0b0000100000  32
  TELEPORT = 1 << 6,                                // 0b0001000000  64
  SUSPENSE = 1 << 7,                                // 0b0010000000  128
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,             // 0b0100000000  256
  COMPONENT_KEPT_ALIVE = 1 << 9,                    // 0b1000000000  512
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | 
             ShapeFlags.FUNCTIONAL_COMPONENT        // 0b0000000110  6           
}

所以shapeFlag & ShapeFlags.COMPONENT = 1,所以进入processComponent

processComponent

packages/runtime-core/src/renderer.ts

const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
  ) => {
    if (n1 == null) {
      //挂载 此时 n1=null
      mountComponent(n2,container,anchor,parentComponent)
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

mountComponent 挂载组件

packages/runtime-core/src/renderer.ts

const mountComponent = (initialVNode, container, anchor, parentComponent) => {
  // 1. 先创建一个 component instance ,同时放一份在vnode的component属性上
  const instance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ));

  // 2. 设置组件实例
  setupComponent(instance);

  // 3. 设置并运行带副作用的渲染函数 组件的更新逻辑
  setupRenderEffect(instance, initialVNode, container, anchor);
};

createComponentInstance 创建组件实例

export function createComponentInstance(vnode, parent) {
  const type = vnode.type; //<App/>组件对象
  // 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext;

  const instance = {
    uid: uid++, // 组件唯一 id
    vnode, // 组件 vnode
    type, // 这里是组件对象
    parent, // 指向父组件实例
    appContext, // app 上下文
    root: null!, // 根组件实例
    next: null, // 新的组件 vnode
    subTree: null!, // 子节点 vnode // will be set synchronously right after creation
    effect: null!,
    update: null!, // 带副作用更新函数 // will be set synchronously right after creation
    render: null, // 渲染函数
    proxy: null, // 渲染上下文代理
    provides: parent ? parent.provides : Object.create(appContext.provides),
    accessCache: null!, // 渲染代理的属性访问缓存
    isMounted: false, // 标记是否被挂载
    isUnmounted: false,
  };
  // 初始化渲染上下文
  instance.ctx = { _: instance };
  // 初始化根组件实例指针
  instance.root = parent ? parent.root : instance;

  return instance;
}

setupComponent 设置组件实例

export function setupComponent(instance) {
  // 判断是否是一个有状态的组件
  const isStateful = isStatefulComponent(instance);
  // 设置有状态的组件实例
  const setupResult = isStateful
    ? setupStatefulComponent(instance) // 调用 setup 并处理 setupResult
    : undefined;
  return setupResult;
}
function setupStatefulComponent(instance) {
  // 定义 Component 变量 instance.type就是编译好的组件对象,里面有setup函数
  const Component = instance.type;

  const { setup } = Component;
  if (setup) {
    // 执行 setup 函数,获取结果 : return setup({attrs、slots、emit、expose})
    const setupResult = callWithErrorHandling(setup, instance);

    if (isPromise(setupResult)) {
    } else {
      // 处理 setup 执行结果
      handleSetupResult(instance, setupResult);
    }
  } 
}
export function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    //将setup运行结果放在组件实例的render属性上
    instance.render = setupResult;
  }
}

setupRenderEffect 设置并运行带副作用的渲染函数

const setupRenderEffect = (instance, initialVNode, container, anchor) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      const subTree = (instance.subTree = renderComponentRoot(instance));

      // 挂载子树 vnode 到 container 中
      // 当传入的 vnode 的 shapeFlags 是个 ELEMENT 时,会调用 processElement
      patch(null, subTree, container, anchor, instance);

      // 把渲染生成的子树根 DOM 节点存储到 el 属性上
      initialVNode.el = subTree.el;

      instance.isMounted = true;
    } else {
      // 更新
    }
  };

  // 创建响应式的副作用渲染函数
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope
  ));

  const update: SchedulerJob = (instance.update = () => effect.run());

  update.id = instance.uid; //为了父组件比子组件先更新

  update();
};

subTree是什么,和vnode有什么区别

const subTree = (instance.subTree = renderComponentRoot(instance)),renderComponentRoot,是执行instance.render获取的子vnode,那么subTree是什么样的vnode呢

通过断点调试一下

// main.js
import { createApp } from '@vue/runtime-dom'
import App from './App.vue'
createApp(App).mount('#app')

// App.vue
<script setup>
import Child from './Child.vue'
</script>

<template>
  <div class="App">
    <h1 class="App-text">App组件</h1>
    <Child />
  </div>
</template>

// Child.vue

<template>
  <div class="child">
    <span>child组件</span>
  </div>
</template>

vue3组件渲染成DOM

vue3组件渲染成DOM

接下来执行patch(null, subTree, container, anchor, instance) 回到patch方法中,

if (shapeFlag & ShapeFlags.ELEMENT) {
  // 处理普通 DOM 元素
   processElement(n1,n2,container,anchor,parentComponent )
 }

从调试中可以看到,此时shapeFlag为17,ShapeFlags.ELEMENT为1,所以进入processElement处理普通DOM元素

processElement

const processElement = (n1, n2, container, anchor, parentComponent) => {
  if (n1 == null) {
    mountElement(n2, container, anchor, parentComponent);
  } else {
    patchElement(n1, n2, parentComponent);
  }
};

mountElement

const mountElement = (vnode, container, anchor, parentComponent) => {
  let el: RendererElement;
  const { type, props, shapeFlag, transition, dirs } = vnode;

  // 根据 vnode 创建 DOM 节点
  el = vnode.el = hostCreateElement(
    vnode.type as string,
    isSVG,
    props && props.is,
    props
  );

  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 文本节点处理
    hostSetElementText(el, vnode.children as string);
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 处理子节点是数组
    mountChildren(
      vnode.children,
      el,
      null,
      parentComponent,
      parentSuspense,
      isSVG && type !== "foreignObject",
      slotScopeIds,
      optimized
    );
  }
  if (props) {
    for (const key in props) {
      // 处理 props 属性
    }
  }
  hostInsert(el, container, anchor); // 把创建好的 el 元素挂载到容器中
};

创建DOM元素:createElement,处理文本节点:setElementText,挂载Dom元素:hostInsert

\packages\runtime-dom\src\nodeOps.ts

  createElement: (tag, isSVG, is, props): Element => {
    const el = isSVG
      ? doc.createElementNS(svgNS, tag)
      : doc.createElement(tag, is ? { is } : undefined)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }
    return el
  },
  
 setElementText: (el, text) => {
    el.textContent = text
 },
 
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

mountChildren 处理数组子节点

遍历每一个child,递归调用patch,继续处理子节点

const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized,
    start = 0
  ) => {
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }