Vue初始化详细流程

难得有时间看看源码,所以写了个总结分享一下.如果觉得不对欢迎指正.

另外即使没看过源码,知道里面的流程,对面试也挺有帮助的

你将会知道

  • Vue如何进行初始化
  • 如何实现全局,例如全局组件,指令等的注册,并且可以链式调用
  • 为什么app.config.globalProperties可以注册能够被应用内所有组件实例访问到的全局属性的对象

createApp作为Vue的入口函数,也就是完成初始化的核心函数.它的作用是将你的应用程序实例化并挂载到一个现有的 HTML 元素

具体来说,一个Vue应用,应该有多个组件,而createApp的任务是渲染这些组件中最上层组件(根组件)

因为与渲染相关,所以 渲染器 为createApp提供了渲染能力.

源码位置: packages\runtime-core\src\apiCreateApp.ts

先说一下代码结构,createAppbaseCreateRenderer(创建渲染器)返回,调试源码的时候可以看到

const app = ensureRenderer().createApp(...args);

ensureRenderer里面还是会调用baseCreateRenderer,所以可以得到createApp

js 复制代码
baseCreateRenderer()
// 省略
return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }

可以看到baseCreateRenderer返回的createApp还是由createAppAPI创建的,这个函数就是从apiCreateApp.ts文件引入的。因此这个函数是核心函数。

另外传入的render函数是渲染器赋予createApp的渲染能力

createAppContext

首先先看到这个函数createAppContext

可以看到它直接返回了一个对象,现在只需要把它理解为一个全局上下文对象即可

createAppAPI

接着看createAppAPI,可以看到我们需要的createApp就是这个函数的返回,这里使用了柯里化,简化参数

它的第一个参数render,即是渲染器赋予的渲染能力

createApp

最后我们进入这个最核心的函数一探究竟.接收两个参数: rootComponent rootProps

  1. 首先进入这个判断,我看了git,这是为了解决一个 bug . extend会产生一个新对象副本,将 rootComponent 更改为这个新的对象副本,以确保在 createApp 中使用的是一个不会改变原始对象的对象
js 复制代码
if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

2.相信到这里你已经知道怎么给根组件传递props,那就是rootProps.下面代码很简单就是校验rootProps必须是一个对象

js 复制代码
  if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
  1. 最最核心的从现在开始.
js 复制代码
const context = createAppContext()

提前可以告诉大家,这个context对象,中有一个app属性即应用实例,这个实例中有很多方法,例如有注册组件,指令的函数,那么注册肯定要找个地方存下来吧,存在哪呢,其实还是在context.

并且这个app正是createApp的返回值,想想我们注册全局组件,指令等是不是在这个返回的app进行操作呢.

  1. 我们接着看看app实例上的注册方法是怎么写的

mixin

  1. 组合式api不支持mixin
  2. 如果 mixin 已经存在于 mixins 数组中,它会在开发模式下发出警告
  3. 最后,它将mixin 添加到 context.mixins 数组中
js 复制代码
  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

  1. 如果处于开发模式调用 validateComponentName 函数来验证组件名称是否符合规范
  2. 如果没有提供组件对象,它会返回已经注册的名为 name 的组件
  3. 在开发模式下,如果已经存在名为 name 的组件,它会发出警告,指出该组件已在目标应用程序中注册过
  4. 最后,它将组件对象 component 添加到 context.components 对象中

directive

  1. 调用 validateDirectiveName 函数来验证指令名称是否符合规范
  2. 如果没有提供指令对象,它会返回已经注册的名为 name 的指令
  3. 如果已经存在名为 name 的指令,它会发出警告,指出该指令已在目标应用程序中注册
  4. 最后,它将指令对象 directive 添加到 context.directives 对象中

provide

  1. 在开发环境如果已经提供了具有相同键值(key)的属性。并且这个新的值将会覆盖之前的值
  2. 最后,将其存储在context.provides对象中

context.provides的声明很有意思

provides: Object.create(null)

它创建一个空的对象,并且不会继承任何原型属性,单纯用作映射

plugin

插件有点特殊,它是上面的集合体,所以不需要再全局上下文context中声明

使用方式是app.use(插件名,参数),之后他会调用自身的install方法

  1. 如果插件已经被注册,它将发出一个警告
  2. 插件存在且具有 install 方法,那么它会调用该 install 方法,将应用程序对象 app 传递给插件,同时还可以传递其他选项。这允许插件初始化自己,并在应用程序上执行所需的操作。
  3. 如果插件不存在 install 方法,但是它本身是一个函数,也会被认为是有效的插件,并将该函数调用,同样传递应用程序对象 app 和其他选项
  4. 否则注册失败
js 复制代码
  const installedPlugins = new Set()
   
  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
      },

可以发现这些方法都返回了app实例,因此可以链式调用

mount

isMounted 代表在这个容器上有没有应用程序实例挂载,这里使用到了闭包,举个例子,flag的值会被改变

js 复制代码
function a() {
    let flag = false;
    const app = {
        setFlag() {
            console.log(flag);
            flag = true;
        },
        setFlag1() {
            console.log(flag);
            flag = false;
        }
    };
    return app;
}
const app=a()
app.setFlag();
app.setFlag1();

如果isMounted为false,执行mount,创建VNode,利用render函数进行渲染,isMounted变为true.

另外还存储了一些其他数据,这里只需要先有个印象。

js 复制代码
 if (!isMounted) {
          const vnode = createVNode(rootComponent, rootProps)
          vnode.appContext = context
          render(vnode, rootContainer, isSVG)
          isMounted = true
          app._container = rootContainer
          }

unmount

如果没有挂载过将会提示 Cannot unmount an app that is not mounted,否则调用render函数卸载

重写mount

虽然是重写,但是并不代表取代之前在app实例中的mount函数。相反app实例中的mount是一个标准的可跨平台函数,所以重写的mount函数还是要用到它。

packages\runtime-dom\src\index.ts

可以看到这个文件还有一个createApp函数,也就是在这里进行重写

js 复制代码
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    //  app._component代表的是根组件
    // 在app实例初始化就有声明 app:{_component: rootComponent as ConcreteComponent}
    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, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) 
  1. 解构出app中的mount,接着开始重写
  2. normalizeContainer标准化容器元素
  3. 如果应用程序的根组件不是一个函数组件且没有 render 方法和 template 字段,那么说明它可能是基于 DOM 元素的模板。在这种情况下,代码将会将容器元素的 innerHTML 设置为组件的模板,以供后续渲染使用
  4. 在挂载之前,代码会将容器元素的内容清空,确保容器是空的。这样做是为了避免在挂载应用程序时与容器中的现有内容发生冲突或干扰。
  5. 最后,调用了解构出的 mount 方法,将处理过的容器元素作为参数传入,执行真正的挂载操作

现在知道了重写mount做了容器以及根组件的跨平台(其实还有兼容vue2的内容这里知道即可),因此,可以看出重写mount是为了进一步提升跨平台能力

app.config.globalProperties

它是一个用于注册能够被应用内所有组件实例访问到的全局属性的对象

先说说app实例中还有一个访问器属性config

js 复制代码
   // 如果在开发模式下不能替换整个 app.config 对象,而应该通过修改对象里各个配置选项来自定义应用程序的行为
      get config() {
        return context.config
      },

      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },

context初始化的config

js 复制代码
   config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },

现在我们只需要在其中的globalProperties属性中添加属性,就可以全局共享数据了,类似于Vuex的功能。

创造不易,有帮助的话点个赞吧,要不然没信心写下去了🦾。

相关推荐
慧一居士32 分钟前
<script setup>中的setup作用以及和不带的区别对比
前端
RainbowSea1 小时前
NVM 切换 Node 版本工具的超详细安装说明
java·前端
读书点滴1 小时前
笨方法学python -练习14
java·前端·python
Mintopia1 小时前
四叉树:二维空间的 “智能分区管理员”
前端·javascript·计算机图形学
慌糖1 小时前
RabbitMQ:消息队列的轻量级王者
开发语言·javascript·ecmascript
Mintopia1 小时前
Three.js 深度冲突:当像素在 Z 轴上玩起 "挤地铁" 游戏
前端·javascript·three.js
Penk是个码农1 小时前
web前端面试-- MVC、MVP、MVVM 架构模式对比
前端·面试·mvc
markyankee1011 小时前
Vue.js 入门指南:从零开始构建你的第一个应用
vue.js
MrSkye1 小时前
🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥
前端·javascript·面试
白瓷梅子汤2 小时前
跟着官方示例学习 @tanStack-form --- Linked Fields
前端·react.js