vue3- 實例掛載mount

先看一下vue-next官方文檔的介紹:

每個 Vue 應(yīng)用都是通過用 createApp 函數(shù)創(chuàng)建一個新的應(yīng)用實例開始的

傳遞給 createApp 的選項用于配置根組件。當我們掛載應(yīng)用時,該組件被用作渲染的起點。

一個應(yīng)用需要被掛載到一個 DOM 元素中。例如,如果我們想把一個 Vue 應(yīng)用掛載到<div id="app"></div>,我們應(yīng)該傳遞 #app

我們將分為兩部分進行渲染過程的理解:

  • 創(chuàng)建應(yīng)用實例,函數(shù)createApp的剖析
  • 應(yīng)用實例掛載, 函數(shù)mount方法掛載過程

本篇詳細講述調(diào)用應(yīng)用實例方法mount過程

下面是一個簡單的demo

<!-- template -->
  <div id="app">
    <input v-model="value"/>
    <p>雙向綁定:{{value}}</p>
    <hello-comp person-name="zhangsan"/>
  </div>
const { createApp } = Vue
const helloComp = {
      name: 'hello-comp',
      props: {
        personName: {
          type: String,
          default: 'wangcong'
        }
      },
      template: '<p>hello {{personName}}!</p>'
    }
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')

現(xiàn)在我們從mount函數(shù)為入口,去了解應(yīng)用掛載的過程。


image.png

掛載 mount

現(xiàn)在回顧一下在demo中我們調(diào)用的方法createApp(app).mount('#app')

此時調(diào)用的這個mount方法是在runtime-dom模塊重寫之后的,在內(nèi)部依然會執(zhí)行應(yīng)用實例的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是調(diào)用了mount方法返回的:const proxy = mount(container)

現(xiàn)在看一下在應(yīng)用實例app中定義的mount方法,它做了兩件事情:

  • 執(zhí)行createVNode方法生產(chǎn)VNode
  • 執(zhí)行render方法。(上一篇文中所提:父級作用域函數(shù)createAppAPI(render, hydrate)接受的第一個參數(shù))
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<Node, Element>, 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 // 靜態(tài)組件 純html
  | typeof Comment
  | typeof Fragment // 多根組件
  | typeof TeleportImpl // 內(nèi)置組件傳送
  | typeof SuspenseImpl // 內(nèi)置組件懸念 一般配合異步組件

createVNode函數(shù)內(nèi)部實際會調(diào)用_createVNode函數(shù)

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

我們傳入createApp方法的根組件對象會作為_createVNode方法接收的第一個參數(shù)type,并在執(zhí)行后會返回一個VNode。VNode中包含一個shapeFlag標識類型,在后面patch過程中進入不同的處理邏輯。

此時傳入的type參數(shù)類型符合定義的接口interface ClassComponent

  • data函數(shù)
  • mounted函數(shù)
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后,執(zhí)行了render(vnode, rootContainer)

render函數(shù)是在 packages/runtime-core/src/renderer.js 中函數(shù)baseCreateRenderer閉包內(nèi)部聲明的。

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
  }

內(nèi)部調(diào)用了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語句中會根據(jù)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 對于每一個比特位,當兩個操作數(shù)相應(yīng)的比特位至少有一個1時,結(jié)果為1,否則為0。

按位與:a & b 對于每一個比特位,只有兩個操作數(shù)相應(yīng)的比特位都是1時,結(jié)果才為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

并執(zhí)行了方法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類型后繼續(xù)執(zhí)行了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)
  }

創(chuàng)建組件實例

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

創(chuàng)建instance后,然后執(zhí)行了setupComponent(instance)

setupComponent核心代碼

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

initProps:初始化props、將其處理為響應(yīng)式的
initSlots: 處理插槽
setupStatefulComponent: 處理setup配置選項

解析模板、初始化選項

setupStatefulComponent簡略后的邏輯:

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // 創(chuàng)建渲染代理屬性訪問緩存
  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,則會調(diào)用handleSetupResult對返回值setupResult進行處理;

setupStatefulComponent內(nèi)最終邏輯都會調(diào)用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模板轉(zhuǎn)換為render函數(shù);
    備注:compile函數(shù)是在packages/vue/src/index.js中調(diào)用registerRuntimeCompiler(compileToFunction)完成的編譯器注冊

  2. 執(zhí)行applyOptions方法。并在初始化選項前調(diào)用beforeCreate鉤子:

選項初始化順序保持了與Vue 2的一致:

  • props (上一步initProps中已經(jīng)完成了初始化)
  • inject
  • methods
  • data (由于它依賴于this訪問而推遲)
  • computed
  • watch (由于它依賴于this訪問而推遲)

完成上面初始化后調(diào)用了 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函數(shù)結(jié)束,下面繼續(xù)執(zhí)行setupRenderEffect

setupRenderEffect:為渲染創(chuàng)建響應(yīng)性效果

下面是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)
}

調(diào)用了命名為effect的函數(shù),第一個參數(shù)接收一個函數(shù),第二個參數(shù)是配置選項;并將函數(shù)執(zhí)行結(jié)果賦值給instance.update;
componentEffect回調(diào)中得到子節(jié)點的VNode,并遞歸調(diào)用了patch方法。
通過initialVNode.el = subTree.el實現(xiàn)dom的掛載

何時創(chuàng)建的dom?

通過遞歸執(zhí)行patch方法,會遍歷整個vnode樹;
這個過程中非dom節(jié)點類類型的Vnode會重復上面的過程,繼續(xù)調(diào)用patch

遞歸函數(shù)最終會結(jié)束,那么這個函數(shù)內(nèi)一定有一個分支不再調(diào)用自己。這個就是普通vnode節(jié)點

舉例:當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創(chuàng)建了Text dom。該方法定義在packages/runtime-dom/src/nodeOps.ts

const createText = document.createTextNode(text)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容