实现 createApp、mount 和 unmount

2025-10-27 12:41:48

前面已经实现了 render

render 是比较底层的 API,使用时需要先手动创建 vnode,再把它渲染到容器中:

const vnode = h('div', 'hello')

render(vnode, document.querySelector('#app'))

但平时写 vue 代码,常见的是这样:

import { createApp, h } from '../dist/vue.esm.js'

const App = {
  render() {
    return h('div', 'hello')
  }
}

createApp(App, { msg: 'hello' }).mount('#app')

所以在 render 的基础上补上 createApp

可以看出,createApp 的核心就是将组件挂载到 DOM 上,这和 render 的职责类似。

render 负责将虚拟节点渲染到容器中,因此只需要先把组件转换成虚拟节点,再交给 render 处理:

createApp(App, props).mount(container)

//
const vnode = h(App, props)
render(vnode, container)

所以,createApp 本质上只是对 render 的一层应用级封装。

基础签名

function createRenderer(options) {
  const render = (vnode, container) => {
  }

  return {
    render,
    createApp(rootComponent, rootProps) {
      function mount(container) {
      }

      function unmount() {
      }

      return {
        mount,
        unmount
      }
    }
  }
}

将代码拆分一下,把 createApp 抽离到 apiCreateApp.ts 中:

import { createAppAPI } from './apiCreateApp'

export function createRenderer(options) {
  const render = (vnode, container) => {
    // ...
  }

  return {
    render,
    createApp: createAppAPI(render)
  }
}

renderer.ts 主要负责渲染流程,createApp 则负责对外暴露应用入口。

createAppAPI

// packages/runtime-core/src/apiCreateApp.ts
import { h } from './h'

export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps) {
    const app = {
      _container: null,
      mount(container) {
        const vnode = h(rootComponent, rootProps)
        render(vnode, container)
        app._container = container
      },
      unmount() {
        if (app._container) {
          render(null, app._container)
        }
      }
    }

    return app
  }
}

选择器支持

现在 runtime-core 里的 mount 接收的是容器节点,但平时使用 vue 时,也可以直接传入选择器:

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

runtime-core 是平台无关的,选择器属于浏览器平台能力,所以应该放到 runtime-dom 中处理。

runtime-dom 包装 mount

runtime-dom 中,可以先拿到 runtime-core 创建出来的 app,再重写它的 mount 方法:

// packages/runtime-dom/src/index.ts
import { createRenderer } from '@vue/runtime-core'
import { isString } from '@vue/shared'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'

export * from '@vue/runtime-core'

const rendererOptions = {
  patchProp,
  ...nodeOps
}

const renderer = createRenderer(rendererOptions)

export function render(vnode, container) {
  renderer.render(vnode, container)
}

export function createApp(rootComponent, rootProps = null) {
  const app = renderer.createApp(rootComponent, rootProps)
  const mount = app.mount.bind(app)

  app.mount = function (containerOrSelector) {
    let container = containerOrSelector

    if (isString(containerOrSelector)) {
      container = document.querySelector(containerOrSelector)
    }

    mount(container)
  }

  return app
}

这样传入 '#app' 时,会先把它转换成真实 DOM 节点,之后再调用原来的 mount,把根组件渲染进去。

这样,createApp 的基础流程就打通了:

小结

  1. runtime-dom 处理选择器,拿到真实容器
  2. runtime-core 创建根组件 vnode
  3. 调用 render(vnode, container) 挂载应用
  4. 调用 render(null, container) 卸载应用