vue3-創建應用createApp

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

每個 Vue 應用都是通過用 createApp 函數創建一個新的應用實例開始的

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

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

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

  • 創建應用實例,函數createApp的剖析
  • 應用實例掛載, 函數mount方法掛載過程

本篇詳細講述調用方法createApp過程

image.png

創建應用實例 createApp

下面是一個簡單的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')

現在我們從createApp函數為入口,去了解應用創建的過程。

查看官方文檔和上面的例子我們可以知道,createApp方法接收的是根組件對象作為參數,并返回了一個有mount方法的應用實例對象。

按照依賴關系可以找到createApp方法出自packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
  }

  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')
    container.setAttribute('data-v-app', '')
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

這里做了兩件事情:

  • 創建app應用實例: ensureRenderer().createApp(...args)
  • 重寫了app.mount方法。document.querySelector方法獲取HTMLElement對象作為參數傳入原mount方法。該部分會在mount段落詳細講解。
image.png

ensureRenderer

ensureRenderer函數的目的是惰性創建renderer對象,這樣做目的是在用戶只引入reactivity模塊時,對renderer核心邏輯部分可以進行tree-shake。

renderer對象包含三個屬性:
render方法、 hydrate( ssr客戶端激活相關)、createApp方法

renderer對象實際上是方法createRenderer函數返回的。

// nodeOps: dom節點增刪改查操作的原生api
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)

let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

createRenderer

這個方法在packages/runtime-core/src/renderer.ts中定義。

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

createRenderer方法接受兩個通用的類型參數HostNodeHostElement。其目的是在自定義渲染器中可以傳入特定于平臺的類型;

例如:
對于瀏覽器環境runtime-domHostNode將是DOM Node接口;HostElement將是DOM Element接口。

Element繼承了Node類,也就是說Element是Node多種類型中的一種,即當NodeType為1時Node即為ElementNode,另外Element擴展了Node,Element擁有id、class、children等屬性。

baseCreateRenderer

這個方法也在packages/runtime-core/src/renderer.ts中定義
該方法比較長,暫時忽略中間代碼。該函數執行返回了一個對象(上文中的renderer對象):

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  ...
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}
  • render:接受兩個參數VNodeElement
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
  }
  • hydrate:與ssr客戶端激活相關
  • createApp:接收方法createAppAPI(render, hydrate)的返回值

createAppAPI

這個方法在packages/runtime-core/src/apiCreateApp.ts中定義。

在這里createAppAPI的返回結果是一個函數createApp,這里終于找到了demo中調用的那個接受跟組件對象的createApp函數。

createApp返回了應用實例app對象,其中包含了我們比較熟悉的一些方法,例如:mixincomponentdirective等;

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      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,
      get config() {
        return context.config
      },

      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },
      // 插件注冊
      use(plugin: Plugin, ...options: any[]) {
        ...
        return app
      },

      mixin(mixin: ComponentOptions) {
        ...
        return app
      },

      // 組件注冊
      component(name: string, component?: Component): any {
        ...
        return app
      },

      // 指令注冊
      directive(name: string, directive?: Directive) {
        ...
        return app
      },

      // dom掛載
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        ...
        return app
      },

      // 卸載
      unmount() {
        if (isMounted) {
          render(null, app._container)
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsUnmountApp(app)
          }
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`)
        }
      },

      // 注入
      provide(key, value) {
        ...
        return app
      }
    }

    return app
  }
}

下面就是在應用實例app還沒有調用mount方法進行掛載前的屬性:

image.png

這里強調一下app._component引用的就是我們傳入createApp的根組件對象

對比

2.x global API:

import Vue from 'vue'
import App from './App.vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

Vue.prototype.customProperty = () => {}

new Vue({
  render: h => h(App)
}).$mount('#app')

從技術上講,Vue 2沒有“應用”的概念。我們定義為應用的只是通過創建的根Vue實例new Vue()。從同一Vue構造函數創建的每個根實例都共享相同的全局配置。

Vue當前的某些全局API和配置會永久更改全局狀態。這會導致一些問題:

  • 全局配置更容易使測試過程中意外污染其他測試案例
  • 影響每一個根實例
Vue.mixin({ /* ... */ })

const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

vue3 應用程序實例

createApp返回一個提供應用上下文的應用實例。應用實例掛載的整個組件樹共享同一個上下文。該上下文提供了先前在Vue 2.x中“全局”的配置。該實例不會被應用于其他實例的任何全局配置所污染。共享實例屬性應附加到應用程序實例的config.globalProperties

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.provide(/* ... */)

app.config.globalProperties.customProperty = () => {}

app.mount(App, '#app')

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容