vue3- 实例挂载mount

先看一下vue-next官方文档的介绍:

每个 Vue 应用都是通过用 createApp 函数创建一个新的应用实例开始的

传递给 createApp 的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。

一个应用需要被挂载到一个 DOM 元素中。例如,如果我们想把一个 Vue 应用挂载到

,我们应该传递 #app

我们将分为两部分进行渲染过程的理解:

  • 创建应用实例,函数createApp的剖析
  • 应用实例挂载, 函数mount方法挂载过程

本篇详细讲述调用应用实例方法mount过程

下面是一个简单的demo


  

双向绑定:{{value}}

const { createApp } = Vue
const helloComp = {
      name: 'hello-comp',
      props: {
        personName: {
          type: String,
          default: 'wangcong'
        }
      },
      template: '

hello {{personName}}!

' } const app = { data() { return { value: '', info: { name: 'tom', age: 18 } } }, components: { 'hello-comp': helloComp }, mounted() { console.log(this.value, this.info) }, } createApp(app).mount('#app')

现在我们从mount函数为入口,去了解应用挂载的过程。


image.png

挂载 mount

现在回顾一下在demo中我们调用的方法createApp(app).mount('#app')

此时调用的这个mount方法是在runtime-dom模块重写之后的,在内部依然会执行应用实例的app.mount方法。

重写mount方法,在app._component引用的根组件对象上面添加了template属性,用来获取html中的模板字符串。

并返回了proxy,也就是根组件实例vm

  // runtime-dom模块重写了`app.mount`方法
  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // app._component引用的对象是根组件对象,就是我们传入createApp方法的根组件对象
    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')
    container.setAttribute('data-v-app', '')
    return proxy
  }

proxy是调用了mount方法返回的:const proxy = mount(container)

现在看一下在应用实例app中定义的mount方法,它做了两件事情:

  • 执行createVNode方法生产VNode
  • 执行render方法。(上一篇文中所提:父级作用域函数createAppAPI(render, hydrate)接受的第一个参数)
mount(rootContainer: HostElement, isHydrate?: boolean): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // store app context on the root VNode.
    // this will be set on the root instance on initial mount.
    vnode.appContext = context

    // HMR root reload
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer)
      }
    }

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode, rootContainer as any)
    } else {
      render(vnode, rootContainer)
    }
    isMounted = true
    app._container = rootContainer
    // for devtools and telemetry
    ;(rootContainer as any).__vue_app__ = app

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      devtoolsInitApp(app, version)
    }

    return vnode.component!.proxy
  } else if (__DEV__) {
    warn(
      `App has already been mounted.\n` +
        `If you want to remount the same app, move your app creation logic ` +
        `into a factory function and create fresh app instances for each ` +
        `mount - e.g. \`const createMyApp = () => createApp(App)\``
    )
  }
}

createVNode

先看一下VNode包含的类型

export type VNodeTypes =
  | string
  | VNode
  | Component
  | typeof Text // 文本
  | typeof Static // 静态组件 纯html
  | typeof Comment
  | typeof Fragment // 多根组件
  | typeof TeleportImpl // 内置组件传送
  | typeof SuspenseImpl // 内置组件悬念 一般配合异步组件

createVNode函数内部实际会调用_createVNode函数

export const createVNode = (__DEV__
  ? createVNodeWithArgsTransform
  : _createVNode) as typeof _createVNode

我们传入createApp方法的根组件对象会作为_createVNode方法接收的第一个参数type,并在执行后会返回一个VNode。VNode中包含一个shapeFlag标识类型,在后面patch过程中进入不同的处理逻辑。

此时传入的type参数类型符合定义的接口interface ClassComponent

  • data函数
  • mounted函数
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  ...
  return vnode
}

此时返回的VNode:


image.png

render

在得到了VNode后,执行了render(vnode, rootContainer)

render函数是在 packages/runtime-core/src/renderer.js 中函数baseCreateRenderer闭包内部声明的。

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方法完成对VNode的解析与渲染

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
 
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
     /*...*/
    default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        }
       /*...*/
    }
}

在这个switch语句中会根据vnodeshapeFlag字段对不同类型的vnode进行处理。

ShapeFlags

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
}

通过位运算符>>|定义了枚举类型 ShapeFlags; 在逻辑语句中使用& 位运算符进行匹配;

左移:a << b 将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充。

按位或:a | b 对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。

按位与:a & b 对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。

例如当前的demo:_createVNode方法在生成根组件VNode时,
属性赋值操作:shapeFlag = ShapeFlags.STATEFUL_COMPONENT
因此会匹配到ShapeFlags.COMPONENT

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

并执行了方法processComponent;

const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    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)
    }
  }

在判断当前组件不是keep-alive类型后继续执行了mountComponent方法

mountComponent

下面详细看mountComponent方法中的省略后的核心逻辑:

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

创建组件实例

createComponentInstance

初始化并returninstance对象,

export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  // inherit parent app context - or - if root, adopt from root vnode
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext
  const instance = {/*...*/}
  if (__DEV__) {
    instance.ctx = createRenderContext(instance)
  } else {
    instance.ctx = { _: instance }
  }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    devtoolsComponentAdded(instance)
  }

  return instance
  }

其中instance.ctx 做了一层引用instance.ctx = { _: instance }

setupComponent: 解析props、slots、setup

创建instance后,然后执行了setupComponent(instance)

setupComponent核心代码

function setupComponent (instance) {
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)
  const setupResult = setupStatefulComponent(instance, isSSR)
  return setupResult
}

initProps:初始化props、将其处理为响应式的
initSlots: 处理插槽
setupStatefulComponent: 处理setup配置选项

解析模板、初始化选项

setupStatefulComponent简略后的逻辑:

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // 创建渲染代理属性访问缓存
  instance.accessCache = {}
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupResult = setup()
    if (isPromise(setupResult)) {
      return setupResult.then((resolvedResult: unknown) => {
        handleSetupResult(instance, resolvedResult, isSSR)
      })
    } else {
      handleSetupResult(instance, setupContext)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

如果组件选项里配置了setup,则会调用handleSetupResult对返回值setupResult进行处理;

setupStatefulComponent内最终逻辑都会调用finishComponentSetup方法:

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  Component.render = compile(Component.template, {
    isCustomElement: instance.appContext.config.isCustomElement,
    delimiters: Component.delimiters
  })
  applyOptions(instance, Component)
}
  1. 解析模板: 将template模板转换为render函数;
    备注:compile函数是在packages/vue/src/index.js中调用registerRuntimeCompiler(compileToFunction)完成的编译器注册

  2. 执行applyOptions方法。并在初始化选项前调用beforeCreate钩子:

选项初始化顺序保持了与Vue 2的一致:

  • props (上一步initProps中已经完成了初始化)
  • inject
  • methods
  • data (由于它依赖于this访问而推迟)
  • computed
  • watch (由于它依赖于this访问而推迟)

完成上面初始化后调用了 created钩子,然后注册其余声明周期钩子:

  beforeMount?(): void
  mounted?(): void
  beforeUpdate?(): void
  updated?(): void
  activated?(): void
  deactivated?(): void
  /** @deprecated use `beforeUnmount` instead */
  beforeDestroy?(): void
  beforeUnmount?(): void
  /** @deprecated use `unmounted` instead */
  destroyed?(): void
  unmounted?(): void
  renderTracked?: DebuggerHook
  renderTriggered?: DebuggerHook
  errorCaptured?: ErrorCapturedHook

到此setupComponent函数结束,下面继续执行setupRenderEffect

setupRenderEffect:为渲染创建响应性效果

下面是setupRenderEffect的伪代码:

const setupRenderEffect = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) {
      instance.update = effect(function componentEffect() {
      const subTree = (instance.subTree = renderComponentRoot(instance))
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
      initialVNode.el = subTree.el
  },  EffectOptions)
}

调用了命名为effect的函数,第一个参数接收一个函数,第二个参数是配置选项;并将函数执行结果赋值给instance.update;
componentEffect回调中得到子节点的VNode,并递归调用了patch方法。
通过initialVNode.el = subTree.el实现dom的挂载

何时创建的dom?

通过递归执行patch方法,会遍历整个vnode树;
这个过程中非dom节点类类型的Vnode会重复上面的过程,继续调用patch

递归函数最终会结束,那么这个函数内一定有一个分支不再调用自己。这个就是普通vnode节点

举例:当Vnode类型是Text,会走到处理逻辑processText方法中

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }

hostCreateText创建了Text dom。该方法定义在packages/runtime-dom/src/nodeOps.ts

const createText = document.createTextNode(text)

你可能感兴趣的:(vue3- 实例挂载mount)