vue3源码之createApp

一、前言

Vue3从2020年9月发布至今已有三年半的时间了,经过大大小小的迭代和优化,目前已成为Vue开发的首选版本。Vue2也在去年年底停止维护,所以现在学习Vue3已成为Vue技术栈的一门必备功课。

二、项目入手

初始化Vue3项目:

vue create my-vue3-project

此时可得到这样一个项目:
Snipaste_2024-04-28_11-03-27.png
根据经验,我们很容易知道main.js就是真实的入口文件:

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

const app = createApp(App)

app.mount('#app')

#app是html页面上一个元素的id,这段代码逻辑为:

  • 引入createApp
  • 引入样式文件style.css
  • 引入单文件组件App.vue
  • 调用createApp传入组件App,并执行mount将其挂载到#app元素上

所以,createApp是一个接受Vue组件并返回一个带有amount方法的对象。

1、createApp

在main.js中,通过Ctrl+右键,可以自动跳转到node_modules下找到createApp对应类型声明文件,但这不是源码,源码的话还是得去官方仓库查看,对应的源码目录在core/packages/runtime-dom/src/index.ts(vue2的源码项目名为vue,vue3的项目名为core)。
createApp的源码如下:

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

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  const { mount } = app
  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) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null,
            )
            break
          }
        }
      }
    }

    // clear content before mounting
    container.innerHTML = ''
    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>

可以看出createApp是对ensureRenderer().createApp(…args)返回的app实例的amount方法进行重写增加了对传入参数(需要挂载的节点,上述说到的App.vue)的校验(是否能找到指定元素)和元素清空的方法。但核心仍是ensureRenderer()和他返回的createApp()

2、ensureRenderer

进入ensureRenderer方法,发现其也只是调用了createRenderer方法:

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

当我看到这里时,会有疑问:
问题①:ensureRenderer方法作用是什么,为什么需要他,直接返回createRenderer方法不行吗?
我们先了解一下vue3的机制:
vue3的渲染器(renderer)是负责将vue组件渲染成实际的DOM的模块,渲染器的创建和初始化是比较复杂的过程,涉及多个步骤和选项。为了保持代码的简洁和模块化,vue3的源码将渲染器的创建和初始化封装在一起,就是createRenderer方法。
问题②:那为什么需要ensureRenderer方法呢?

  • 懒加载:在大多数情况下,我们可能不需要立即创建渲染器,而是在真正需要渲染组件时才创建。这样可以延迟渲染器的创建,节省资源并提高性能
  • 单例模式:通常情况下,我们只需要一个渲染器来处理整个应用程序的渲染工作。ensureRenderer 方法确保了在需要渲染器时只会创建一个,并且后续调用都会返回相同的实例,这就是单例模式的应用
  • 缓存:ensureRenderer 方法可能会缓存已创建的渲染器实例,以便下次调用时直接返回缓存的实例(返回的renderer始终是同一个),而不是重新创建一个新的。这样可以提高性能,并且保证了渲染器的存在性和唯一性

首先,createRenderer 接收一个 rendererOptions 参数并返回一个 Renderer | HydrationRenderer 类型的实例。
rendererOptionspatchPropnodeOps 两个对象的合集,包含了insert(parent.insertBefore)、remove(parent.removeChild)、createElement等DOM的操作方法,以及 patchProp 节点属性对比方法。
createRenderer 是通过 baseCreateRenderer 来创建渲染器(位于packages\runtime-core\src\renderer.ts )。

3、baseCreateRenderer

baseCreateRenderer 的代码就有两千行左右,除了创建渲染器的逻辑外,还定义了与更新和渲染相关的方法,其中就包含diff算法,也就是patch函数。
baseCreateRenderer 最后返回了 renderhydrate 两个属性,以及一个 createAppAPI 返回的方法createApp。

<!-- 先展示跟return直接相关的代码,省略其余代码(太多了) -->
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
	const render: RootRenderFunction = (vnode, container, namespace) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, namespace)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }

  const internals: RendererInternals = {
    p: patch,
    um: unmount,
    m: move,
    r: remove,
    mt: mountComponent,
    mc: mountChildren,
    pc: patchChildren,
    pbc: patchBlockChildren,
    n: getNextHostNode,
    o: options
  }

  let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
                          
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

createHydrationFns:作用是创建用于服务端渲染(SSR)的注水函数(hydration functions)。
在服务端渲染中,首次渲染是在服务器端完成的,服务器会生成一份完整的 HTML 页面,并将 Vue 组件的状态序列化到 HTML 中。当浏览器加载这个页面时,Vue 需要从服务器生成的 HTML 中提取出组件状态,并将其恢复到客户端渲染模式。这个过程就是所谓的“注水”,因为它是将组件状态“注入”到客户端渲染的 HTML 中。
createHydrationFns 函数的作用就是创建用于执行这个注水过程的函数。它返回一个对象,其中包含了两个函数:hydrate 和 hydrateState。

  • hydrate 函数用于将服务器生成的 HTML 与客户端的 Vue 实例进行关联,使得客户端的 Vue 实例能够接管服务器渲染的 HTML,并进行进一步的交互和更新。
  • hydrateState 函数用于提取 HTML 中的组件状态,并将其恢复到客户端的 Vue 实例中,以保持组件状态的一致性。

我们这里并不是服务端渲染,那么 createHydrationFns 参数是没有值的,hydrate 也就是 undefinedrender 作为主要渲染方法,主要负责更新和卸载,将虚拟 DOM(vnode)渲染到指定的容器(container)中。
render 函数接收三个参数,vnode(虚拟节点)、container(组件实例)、namespace(命名空间),这段代码主要逻辑很简单,先判断传入的 vnode 若为null,则表示要销毁当前容器中的内容(更新后的内容是清空挂载元素),则调用 container._vnode 判断容器中是否有内容,如有,则调用 unmount 来销毁之前的虚拟DOM(执行卸载操作);如果传入的 vnode 不为null,表示要渲染新的内容到容器中 ,则调用 patch 方法,将新的虚拟DOM(vnode)与之前的虚拟DOM(container._vnode)进行对比和更新,然后执行相应的副作用函数队列,最后把传入的 vnode 更新到实例的 _vnode(container._vnode) 属性上作为下次对比的旧节点数据。

总结: 这段代码实现了一个简单的渲染函数,负责将虚拟 DOM 渲染到指定的容器中,并处理了销毁旧内容和触发预置回调函数的逻辑 。

4、createAppAPI

通过之前的学习,我们可以知道 createAppAPI 返回的是一个函数,且是用来创建Vue单页应用根实例的方法。

let uid = 0
  
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

    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 WeakSet()

    let isMounted = false

  const app: App = (context.app = {
      _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(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          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(
        rootContainer: HostElement,
        isHydrate?: boolean,
        namespace?: boolean | ElementNamespace,
      ): any {
        if (!isMounted) {
          // #5571
          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.`,
            )
          }
          const vnode = createVNode(rootComponent, rootProps)
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

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

          // HMR root reload
          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(vnode, rootContainer, namespace)
          }
          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 getExposeProxy(vnode.component!) || 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)\``,
          )
        }
      },

      unmount() {
        if (isMounted) {
          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
  }
}

createAppContext 方法返回一个 AppContext 类型的对象,其中包含了app、config、provides等多个属性,其中还有一个 mixins 数组去兼容Vue2。
这个对象的格式跟Vue2构造函数最初生成的app实例属性基本一致。
然后声明了一个 Set 变量 installedPlugins ,用来确保不会重复安装插件
最后,返回根实例对象 app
与上文的进入 createApp 相对应的是,这里的 app实例 已经默认定义了一个 mount 方法,但这个 mount方法比较简单,只是在 isMounted 为false的情况下(还未首次挂载),通过传入的 render 或者 hydrate 方法进行渲染,修改挂载状态,最后创建一个根组件的 exposed 暴漏给 Proxy 对象代理。

三、createApp().mount()全览

6452e5cfaafb4fe1ba29f15425683d64~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png

  • 33
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要下载、安装和运行Vue 3的码,可以按照以下步骤进行操作: 1. 首先,你需要安装Git,以便从GitHub上获取Vue码。你可以在https://git-scm.com/ 上下载并安装Git。 2. 打开命令行终端,并切换到你想要存储Vue码的目录。 3. 使用以下命令克隆Vue码仓库: ``` git clone https://github.com/vuejs/vue-next.git ``` 这将会将Vue码克隆到当前目录下的一个名为vue-next的文件夹中。 4. 进入vue-next文件夹: ``` cd vue-next ``` 5. 安装项目依赖: ``` npm install ``` 这将会使用npm安装Vue项目所需的所有依赖项。 6. 编译Vue码: ``` npm run build ``` 这将会编译Vue码并生成一个dist文件夹,其中包含了编译后的Vue库。 7. 创建一个HTML文件,并在其中引入编译后的Vue库。例如: ```html <!DOCTYPE html> <html> <head> <title>Vue 3 Example</title> <script src="path/to/vue-next/dist/vue.global.js"></script> </head> <body> <div id="app"> {{ message }} </div> <script> const app = Vue.createApp({ data() { return { message: 'Hello Vue 3!' } } }) app.mount('#app') </script> </body> </html> ``` 确保将`path/to/vue-next/dist/vue.global.js`替换为实际的路径。 8. 在浏览器中打开HTML文件,你将看到Vue 3应用程序成功运行,并显示出"Hello Vue 3!"的消息。 通过以上步骤,你可以下载、安装和运行Vue 3的码,并在本地开发环境中进行调试和测试。记得根据你的开发需求进行相应的配置和使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值