【全网最细】Vue3 逐行学习源码之路-01 - createApp

175 阅读9分钟

开始叠甲~

背景:闲来无事,需求不多,想着自己能不能看看Vue3的源码,尝试自己去看看能不能串起来Vue3的一个流程。也是自己学习的一个过程

不是教学,只是自己做一个记录,希望也能帮助到有需要的人,有错误之处,欢迎指正~

我也是自己一点点摸索的~ 菜勿喷~

叠甲完毕~

vue 版本 3.5.8

createApp想必每个开发Vue的程序员都不陌生,所以我们先从它入手,但是我一开始并没有找到createApp方法的一个清晰的引入路径,虽然我知道它在 @vue/runtime-dom 这个包下,所以第一步我想找到一个清晰的路径来展示createApp的来龙去脉!

1. 熟悉代码目录

已知vue3的代码都在packages目录下,我本来是想找一个入口的index的文件的,但是我在Vue文件中的找了半天,并没有发现一个直达createApp方法的目录(其实在vue的src/index中,是从runtime中都导入导出了,所以在index中也是可以直接用createApp方法的,但是后面我看了vue-compat这个目录,在这里目录中有更清晰的路径导入,所以我们直接从vue-compat目录的src的index开始本章教学

image.png

话不多话 直接上代码 (我们只关注核心代码,一些其他的警告以及环境判断就分析了)

2. 源码分析

packages/vue-compat/src/index.ts - compileToFunction

index中compileToFunction这个方法其实并没有真正的在这里执行,vue-compat这里的代码和vue中的有一点不同但是代码是99%相同的,所以我们从这开始问题不大, 也不影响我们先分析一波

import { createCompatVue } from './createCompatVue'
import {
  type CompilerError,
  type CompilerOptions,
  compile,
} from '@vue/compiler-dom'
import {
  type CompatVue,
  type RenderFunction,
  registerRuntimeCompiler,
  warn,
} from '@vue/runtime-dom'
import { NOOP, extend, generateCodeFrame, isString } from '@vue/shared'
import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
import * as runtimeDom from '@vue/runtime-dom'
import {
  DeprecationTypes,
  warnDeprecation,
} from '../../runtime-core/src/compat/compatConfig'

// 创建了一个对象 叫编译缓存
const compileCache: Record<string, RenderFunction> = Object.create(null)

// template是字符串或者HTML元素
// options的类型CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions,
): RenderFunction {
  // 模板不是字符串并且是HTML元素 取出innerHTML
  if (!isString(template)) {
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      // NOOP是函数返回一个空对象
      return NOOP
    }
  }

  const key = template
  // 从compileCache对象中查找模板
  const cached = compileCache[key]
  // 缓存中找到就直接返回模板,找不到就在下面赋值
  if (cached) {
    return cached
  }
  
  // 到这template是一个字符串,因为如果不是字符串在上面已经判断赋值了
  // 如果template[0]第一个字符是以#开头,就获取它的HTML结构,将template赋值
  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    template = el ? el.innerHTML : ``
  }
  
  // 这里是个警告,我就不深入去了解了
  if (__DEV__ && !__TEST__ && (!options || !options.whitespace)) {
    warnDeprecation(DeprecationTypes.CONFIG_WHITESPACE, null)
  }

  // TODO 这里是通过compile方法去编译返回一个虚拟DOM
  // compile这个方法里面有很多工作,但是不是本章重点,所以先不展开,知道是返回虚拟DOM对象即可
  const { code } = compile(
    template,
    extend(
      {
        hoistStatic: true,
        whitespace: 'preserve',
        onError: __DEV__ ? onError : undefined,
        onWarn: __DEV__ ? e => onError(e, true) : NOOP,
      } as CompilerOptions,
      options,
    ),
  )
  
  // 声明一个报错的方法
  function onError(err: CompilerError, asWarning = false) {
    const message = asWarning
      ? err.message
      : `Template compilation error: ${err.message}`
    const codeFrame =
      err.loc &&
      generateCodeFrame(
        template as string,
        err.loc.start.offset,
        err.loc.end.offset,
      )
    warn(codeFrame ? `${message}\n${codeFrame}` : message)
  }

  // 创建一个render函数,__GLOBAL__判断是否是在Node环境下,如果是在浏览器环境下,runtimeDom里面所有的方法会作为参数传进去
  const render = (
    __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
  ) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true
  
  // 给compileCache缓存对象设置一个key = render的缓存,方便后续直接获取而不用从新赋值了
  return (compileCache[key] = render)
}

// 执行registerRuntimeCompiler这个函数
registerRuntimeCompiler(compileToFunction)

// 关键方法 createCompatVue 
const Vue: CompatVue = createCompatVue()
Vue.compile = compileToFunction

export default Vue

packages/vue-compat/src/createCompatVue.ts - createCompatVue()

我们从上面的代码知道了vue = createCompatVue()的返回值,所以我们看看createCompatVue()执行了哪些,返回值具体是什么

import { initDev } from './dev'
import {
  type CompatVue,
  DeprecationTypes,
  KeepAlive,
  Transition,
  TransitionGroup,
  compatUtils,
  createApp,
  vModelDynamic,
  vShow,
} from '@vue/runtime-dom'
import { extend } from '@vue/shared'

// 如果是dev环境执行initDev(),我们暂时不考虑深入环境相关内容
if (__DEV__) {
  initDev()
}

// 从runtime-dom中导入所有方法命名空间名称为runtimeDom
import * as runtimeDom from '@vue/runtime-dom'

// 看起来像是处理兼容V2和V3的一些内容,我们暂时先不去管它,继续看createCompatVue
function wrappedCreateApp(...args: any[]) {
  // @ts-expect-error
  const app = createApp(...args)
  if (compatUtils.isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, null)) {
    // register built-in components so that they can be resolved via strings
    // in the legacy h() call. The __compat__ prefix is to ensure that v3 h()
    // doesn't get affected.
    app.component('__compat__transition', Transition)
    app.component('__compat__transition-group', TransitionGroup)
    app.component('__compat__keep-alive', KeepAlive)
    // built-in directives. No need for prefix since there's no render fn API
    // for resolving directives via string in v3.
    app._context.directives.show = vShow
    app._context.directives.model = vModelDynamic
  }
  return app
}

// 调用了compatUtils.createCompatVue传入了createApp和处理兼容的wrappedCreateApp方法
export function createCompatVue(): CompatVue {
  // compatUtils.createCompatVue这个方法深入到里面最终还是执行createApp这个方法
  // 只不过包含了很多兼容v2的方法
  const Vue = compatUtils.createCompatVue(createApp, wrappedCreateApp)
  extend(Vue, runtimeDom)
  return Vue
}

所以到这我们算是发现了createApp方法的来源是来自 @vue/runtime-dom 这个包,那我们就去这个包里看看createApp方法具体做了哪些吧。

由于我们是分析V3的代码,compatUtils.createCompatVue这个里包含了很多兼容V2的处理,最终也是会调用createApp这个方法,所以我这里就不深入去分析和学习了,有兴趣的伙伴可以自己去了解了解。

packages/runtime-dom/src/index.ts - createApp()

到了这一步,createApp只是一个入口,要分析的方法就会很多,让我们一步一步拆解吧

createApp

export const createApp = ((...args) => {
  // 这一步执行 相当于下面两步的执行结果调用createApp,关键在于baseCreateRenderer的执行
  // createRenderer(rendererOptions) => 
  // baseCreateRenderer(options).createApp(...args)
  const app = ensureRenderer().createApp(...args)
  
  // 环境判断 先跳过
  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }
  // 从app中获取mount方法
  const { mount } = app
  // TODO  暂时没有继续分析
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
     
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__ && container.nodeType === 1) {
        for (let i = 0; i < (container as Element).attributes.length; i++) {
          const attr = (container as Element).attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null,
            )
            break
          }
        }
      }
    }

    // clear content before mounting
    if (container.nodeType === 1) {
      container.textContent = ''
    }
    const proxy = mount(container, false, resolveRootNamespace(container))
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

createRenderer

let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer;

// patchProp 用于更新 DOM 属性和事件的一个函数,处理的内容包括class style等
// nodeOps 是一个对象,包括很多方法,插入 移除 创建Element节点等
const rendererOptions = /*@__PURE__*/ extend({ patchProp }, nodeOps);

// 返回了createRenderer()执行之后的返回值,参数是rendererOptions
function ensureRenderer() {
// 初始renderer是undefine,所以renderer = createRenderer(rendererOptions))
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

createRenderer

packages/runtime-core/src/renderer.ts

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

baseCreateRenderer

packages/runtime-core/src/renderer.ts

重头戏来了 这里面有2000多行代码还有和SSR相关的一些代码。本期我们只考虑和createApp相关代码 其余暂时用不到的代码我就先删除,不然2000多行代码 太长了, 影响阅读,后面到创建DOM的时候再分析相应

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions,
): any {
  
  ...内容暂时省略
  因为代码很多都是render渲染相关的,创建DOM以及DOM的增删改查,我觉得应该放到render那一期
  我们只关心返回了createApp方法,createApp方法是createAppAPI执行的结果
  
  // render函数,参数有虚拟DOM,容器和命名空间
  const render: RootRenderFunction = (vnode, container, namespace) => {
    // 如果没有传入虚拟DOM,那会执行卸载逻辑
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 执行比较方法,放到render渲染和DOM DIFF中讲
      patch(
        container._vnode || null,
        vnode,
        container,
        null,
        null,
        null,
        namespace,
      )
    }
    
    container._vnode = vnode
    // 判断是否刷新逻辑
    if (!isFlushing) {
      isFlushing = true
      flushPreFlushCbs()
      flushPostFlushCbs()
      isFlushing = false
    }
  }
  
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate), // hydrate SSR方法暂时不考虑
  }
}

createAppAPI

packages/runtime-core/src/apiCreateApp.ts 终于到了createApp这个方法的尽头

createApp 官网的API

image.png

// 已知参数render是渲染方法,第二个参数不考虑
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {

  // 是不是不知道rootComponent 和 rootProps是什么了?让我们看看官网API
  // rootComponent是根组件, rootProps它是要传递给根组件的 props。
  return function createApp(rootComponent, rootProps = null) {
    // 如果不是一个函数,那就是一个对象, 就创建一个新的组件构造函数
    // 目的是为了方便扩展,且继承原有的rootComponent
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }
    
    // 判断rootProps如果有有值 并且 不是对象 抛出警告错误 赋值为null
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    
    // 创建一个context上下文,方法具体返回值放到了最下面
    // 创建上下文,为了方便阅读,我把这个方法复制到这里了
    export function createAppContext(): AppContext {
      return {
        app: null as any,
        config: {
          isNativeTag: NO,
          performance: false,
          globalProperties: {},
          optionMergeStrategies: {},
          errorHandler: undefined,
          warnHandler: undefined,
          compilerOptions: {},
        },
        mixins: [],
        components: {},
        directives: {},
        provides: Object.create(null),
        optionsCache: new WeakMap(),
        propsCache: new WeakMap(),
        emitsCache: new WeakMap(),
      }
    }
    
    const context = createAppContext()
    const installedPlugins = new WeakSet()
    const pluginCleanupFns: Array<() => any> = []

    let isMounted = false
    
    // 创建一个vue对象
    const app: App = (context.app = {
      // 唯一ID
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,
      // 获取上下文配置
      get config() {
        return context.config
      },
      // 禁止修改上下文配置
      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`,
          )
        }
      },
     
      // 提供一个use方法 第一个参数应是插件本身,可选的第二个参数是要传递给插件的选项。
      use(plugin: Plugin, ...options: any[]) {
        // 如果有 并且是测试环境 就警告,反正我理解就是执行一次 重复安装也就一次
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          // 如果没有 并且有插件和install方法,就加到weakset中。执行install方法
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          // plugin是函数,执行plugin,传入app和options
          installedPlugins.add(plugin)
          plugin(app, ...options)
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function or an object with an "install" ` +
              `function.`,
          )
        }
        return app
      },

      mixin(mixin: ComponentOptions) {
        if (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin)
          } else if (__DEV__) {
            warn(
              'Mixin has already been applied to target app' +
                (mixin.name ? `: ${mixin.name}` : ''),
            )
          }
        } else if (__DEV__) {
          warn('Mixins are only available in builds supporting Options API')
        }
        return app
      },

      component(name: string, component?: Component): any {
        if (__DEV__) {
          validateComponentName(name, context.config)
        }
        if (!component) {
          return context.components[name]
        }
        if (__DEV__ && context.components[name]) {
          warn(`Component "${name}" has already been registered in target app.`)
        }
        context.components[name] = component
        return app
      },

      directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name)
        }

        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && context.directives[name]) {
          warn(`Directive "${name}" has already been registered in target app.`)
        }
        context.directives[name] = directive
        return app
      },
      
      // 关键mount挂载了
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        namespace?: boolean | ElementNamespace,
      ): any {
        // 初始默认是是false
        if (!isMounted) {
          // 跳过
          if (__DEV__ && (rootContainer as any).__vue_app__) {
            warn(
              `There is already an app instance mounted on the host container.\n` +
                ` If you want to mount another app on the same host container,` +
                ` you need to unmount the previous app by calling \`app.unmount()\` first.`,
            )
          }
          // 执行createVNode 创建虚拟DOM 并且返回虚拟DOM
          const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
          // 挂载上下文
          vnode.appContext = context

          if (namespace === true) {
            namespace = 'svg'
          } else if (namespace === false) {
            namespace = undefined
          }

          // 跳过
          if (__DEV__) {
            context.reload = () => {
              // casting to ElementNamespace because TS doesn't guarantee type narrowing
              // over function boundaries
              render(
                cloneVNode(vnode),
                rootContainer,
                namespace as ElementNamespace,
              )
            }
          }
       
          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // 执行render函数,渲染DOM
            render(vnode, rootContainer, namespace)
          }
          // 改成true 下次就不进来了
          isMounted = true
          
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }
          // 用公共实例
          return getComponentPublicInstance(vnode.component!)
        } 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)\``,
          )
        }
      },

      onUnmount(cleanupFn: () => void) {
        if (__DEV__ && typeof cleanupFn !== 'function') {
          warn(
            `Expected function as first argument to app.onUnmount(), ` +
              `but got ${typeof cleanupFn}`,
          )
        }
        pluginCleanupFns.push(cleanupFn)
      },

      unmount() {
        if (isMounted) {
          callWithAsyncErrorHandling(
            pluginCleanupFns,
            app._instance,
            ErrorCodes.APP_UNMOUNT_CLEANUP,
          )
          render(null, app._container)
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = null
            devtoolsUnmountApp(app)
          }
          delete app._container.__vue_app__
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`)
        }
      },

      provide(key, value) {
        if (__DEV__ && (key as string | symbol) in context.provides) {
          warn(
            `App already provides property with key "${String(key)}". ` +
              `It will be overwritten with the new value.`,
          )
        }

        context.provides[key as string | symbol] = value

        return app
      },

      runWithContext(fn) {
        const lastApp = currentApp
        currentApp = app
        try {
          return fn()
        } finally {
          currentApp = lastApp
        }
      },
    })

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }
    
    // 返回实例
    return app
  }
}

总结

其实createApp这个方法核心功能就是创建了一个app实例(也叫上下文),并且提供了以下方法

  • use
  • mixin
  • component
  • directive
  • mount
  • onUnmount
  • unmount
  • runWithContext

最重要的方法就是mount,负责创建虚拟DOM,然后调用render函数进行渲染。

createApp就分析到这里了。下一章 我们去分析 createVNode 看看创建虚拟DOM以及如何渲染DOM到页面。欢迎点赞收藏 ~