likes
comments
collection
share

[Vue 源码] Vue 3.2 - 组件挂载原理

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

运行结果

[Vue 源码] Vue 3.2 - 组件挂载原理

代码示例

  <body>
    <script src="../dist/runtime-dom.global.js"></script>
    <div id="app"></div>
    <script>
      const { createApp, h } = VueRuntimeDOM;
      const App = {
        render() {
          return h("div", "hello world");
        },
      };
      let app = createApp(App, {});
      app.mount("#app"); 
    </script>
  </body>

组件挂载

第一:执行第一句代码 createApp 函数,传入 App 组件。实际上调用的是 createAppAPI 函数返回的 createApp,在 createApp 函数中中返回 app 对象,app对象上有 mount, use, component, directive, mixin…… 等等常用的方法。

export function createAppAPI<HostElement>(
    render: RootRenderFunction,
    hydrate?: RootHydrateFunction
  ): CreateAppFunction<HostElement> {
    return function createApp(rootComponent, rootProps = null) { // 组件、属性
      const context = createAppContext()// 创造应用的上下文
      const installedPlugins = new Set() //要安装的插件
      let isMounted = false 
  
      const app: App = (context.app = {  
        _uid: uid++,
        _component: rootComponent as ConcreteComponent,
        _props: rootProps,
        _container: null,
        _context: context,
        version,
        use(plugin: Plugin, ...options: any[]) { // use方法
        },
        mixin(mixin: ComponentOptions) { // mixin方法
        },
        directive(name: string, directive?: Directive) { // 对应的指令
        },
        mount(rootContainer: HostElement, isHydrate?: boolean): any { // 挂载方法
        },
        unmount() {
        },
        provide(key, value) {
        }
      })
      return app
    }
  }

第二:执行 app.mount("#app"), 通过 createVNode 递归创建虚拟 Dom, 我们的 Hello World 场景中是如下情况:

[Vue 源码] Vue 3.2 - 组件挂载原理

const app = {
  mount(rootContainer: HostElement, isHydrate?: boolean): any {
    // 挂载方法
    if (!isMounted) {
      const vnode = createVNode(
        // 创造vnode节点
        rootComponent as ConcreteComponent,
        rootProps
      );
      render(vnode, rootContainer); // 渲染vnode到容器中
      isMounted = true; // 挂载完毕
      app._container = rootContainer;

      return vnode.component!.proxy;
    }
  },
};

第三:执行 render 函数将 vnode 渲染到容器当中。在render 函数调用 patch 方法,初次挂载 patch 的第一参数为 null。进入 patch 函数。

  const render: RootRenderFunction = (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)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }

第四:在 patch 函数中根据就不同的标签,进行不同的 process。这里是 App 组件标签,会调用 processComponent 函数。每个 process 处理函数都会对应两个逻辑,第一个是挂载,第二个是更新。我们这里调用的是挂载函数 mountComponent。

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

第五:在 mountComponent 调用 setupRenderEffect 函数进行组件初始化。

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

第六:调用 setupRenderEffect 函数。

  • 创建 componentUpdateFn 函数,该函数 是 ReactiveEffct 对象的 fn 属性。不懂这句话的意思,可以看之前的文章。Vue3 Reactive原理

  • new ReactiveEffect 函数,创建 ReactiveEffect 对象,并且传入第二个参数 scheduler,进行组件更新的批处理。

  • 绑定 update 方法给 组件 instance。

  • 调用 update 方法,调用 effect.fn 属性,也就是调用 componentUpdateFn 函数。

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {}

  // create reactive effect for rendering
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

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

  update()
}

第七: componentUpdateFn 函数中,在 componentUpdateFn 中也有两个函数,一个是挂载,一个是更新,这里我们走到挂载的逻辑。

  • 调用 renderComponentRoot 函数,调用组件的 render 方法,得到子节点,放到 组件 instance.subTree 属性上。
  • 继续拿着子节点去 Patch。
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      let vnodeHook: VNodeHook | null | undefined
      const { el, props } = initialVNode
      const { bm, m, parent } = instance
      const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

      const subTree = (instance.subTree = renderComponentRoot(instance))

      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)

      initialVNode.el = subTree.el
      instance.isMounted = true
    }
  }

第八:patch 子节点,也就是 h1 元素,在 patch 函数中根据就不同的标签,进行不同的 process。这里是普通节点标签,会调用 processElement。调用 mountElement 函数。

  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

第九:在 mountElement 函数中:

  • hostCreateElement 函数,创建 H1 节点赋值给 vnode.el 属性。
  • 深度优先原则去 patch 子节点,这里没有我们默认跳过。
  • hostPatchProp 函数,挂载 props。
  • Object.defineProperty 方法将 vnode 定义为 el.__vnode 属性。
  • hostInsert 函数,传入容器和锚点 anchor 通过 parent.insertBeofore 函数插入节点。自此 h1 被插入到了 div#app 中。
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },
}
const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let el: RendererElement
  let vnodeHook: VNodeHook | undefined | null
  const { type, props, shapeFlag, transition, dirs } = vnode

  el = vnode.el = hostCreateElement(
    vnode.type as string,
    isSVG,
    props && props.is,
    props
  )

  // mount children first, since some props may rely on child content
  // being already rendered, e.g. `<select value>`
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      vnode.children as VNodeArrayChildren,
      el,
      null,
      parentComponent,
      parentSuspense,
      isSVG && type !== 'foreignObject',
      slotScopeIds,
      optimized
    )
  }

  // props
  if (props) {
    for (const key in props) {
      if (key !== 'value' && !isReservedProp(key)) {
        hostPatchProp(
          el,
          key,
          null,
          props[key],
          isSVG,
          vnode.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }

    if ('value' in props) {
      hostPatchProp(el, 'value', null, props.value)
    }
    if ((vnodeHook = props.onVnodeBeforeMount)) {
      invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }
  }

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    Object.defineProperty(el, '__vnode', {
      value: vnode,
      enumerable: false
    })
    Object.defineProperty(el, '__vueParentComponent', {
      value: parentComponent,
      enumerable: false
    })
  }

  hostInsert(el, container, anchor)
}

自此组件挂载完毕。