【真香系列】Vue-Next 源码第一章

1,008 阅读3分钟

说明

本系列将以一个 demo 为基础,从源码执行流程的角度进行分析,涉及内容只包含 vue-next 新特性,如 proxysetup 等,预计共分为七篇:

第四章完成后会提供一个整体运行流程的脑图供大家参考

准备工作

  1. 克隆代码并安装依赖
git clone https://github.com/vuejs/vue-next.git
cd vue-next
yarn
  1. 修改 rollup.config.jstsconfig.json 配置,开启 sourcemap
function createConfig(format, output, plugins = []) {
  if (!output) {
    console.log(require('chalk').yellow(`invalid format: "${format}"`))
    process.exit(1)
  }

  // output.sourcemap = !!process.env.SOURCE_MAP
  output.sourcemap = true // 开启 sourcemap
  ...
}
{
  "compilerOptions": {
    "sourceMap": true,
  }
  ...
}
  1. 编译 dev dist
yarn dev
  1. 在项目根目录创建 demo,包含 demo.htmldemo.js
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="../packages/vue/dist/vue.global.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
<script src="./index.js"></script>
</html>
const { reactive } = Vue
const App = {
  template: `<h1>{{state.message}}</h1>`,
  setup() {
    const state = reactive({ message: 'World' })
    setTimeout(() => {
      state.message = 1
    }, 3000)
    return {
      state
    }
  }
}
const app = Vue.createApp(App)
app.mount('#app')
  1. 运行 demo
yarn serve

createApp

demo 中首先声明了一个 app 组件选项,然后通过 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>

首先 ensureRenderer() 调用 createRenderer 方法创建了一个平台相关的渲染器,在本例中就是浏览器环境。参数 rendererOptions 包含了浏览器的一些 dom 操作方法,如 insertremove 等。渲染器主要包括两个方法 rendercreateApp

// packages/runtime-dom/src/index.ts
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
// packages/runtime-core/src/renderer.ts
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  ...
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

ensureRenderer 之后调用了 createApp, 实际就是 createAppAPI 返回的函数,该函数返回了 Vue 实例,后续会使用实例的 mount 方法去挂载。

// packages/runtime-core/src/apiCreateApp.ts
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,

      get config() {},

      set config(v) {},

      use(plugin: Plugin, ...options: any[]) {},

      mixin(mixin: ComponentOptions) {},

      component(name: string, component?: Component): any {},

      directive(name: string, directive?: Directive) {},

      mount(rootContainer: HostElement, isHydrate?: boolean): any {},

      unmount() {},

      provide(key, value) {}
    })

    return app
  }
}

mount

例子中 app.mount('#app') 将组件挂载到了容器下,mount 其实是一个被包装过的方法。首先备份 app 上的 mount 方法,然后针对浏览器环境将其重写。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  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 如何被创建出来以及针对 runtime 的一些额外操作。后面我们从 mount 为入口点,开始介绍挂载流程,其中 app 上的一些被定义的方法也会涉及到。