vue3源码解析:应用挂载流程分析

上文,我们分析了const app = createApp(App)创建Vue实例的过程,本次接着分析app.mount("#app")实例挂载的过程

整体流程

当执行 app.mount('#app') 时,主要经过以下步骤:

  1. 创建应用实例 (createApp)
  2. 重写 mount 方法
  3. 执行挂载操作

详细分析

1. 创建应用实例

首先通过 createApp 创建 Vue 应用实例(上一篇文章写了详细过程):

js 复制代码
export const createApp = ((...args) => {
  // 确保渲染器存在并创建应用实例
  const app = ensureRenderer().createApp(...args)
  
  // 开发环境下注入标签检查和编译选项检查
  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }
  
  // ... mount 方法重写
}) as CreateAppFunction<Element>

2. mount 方法重写

在 createApp 中会重写原始的 mount 方法:

js 复制代码
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
  // 1. 规范化容器
  const container = normalizeContainer(containerOrSelector)
  if (!container) return
  
  // 2. 处理组件模板
  const component = app._component
  if (!isFunction(component) && !component.render && !component.template) {
    component.template = container.innerHTML
  }
  
  // 3. 清空容器内容 nodeType === 1 表示一个元素节点(Element Node)
  if (container.nodeType === 1) {
    container.textContent = ''
  }
  
  // 4. 执行实际挂载
  const proxy = mount(container, false, resolveRootNamespace(container))
  
  // 5. 设置挂载标记
  if (container instanceof Element) {
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
  }
  
  return proxy
}

3. 挂载过程的关键步骤

  1. 容器规范化

    • 如果传入的是字符串选择器,会通过 document.querySelector 获取实际 DOM 元素
  2. 模板处理

    • 如果组件没有 render 函数和 template,会使用容器的 innerHTML 作为模板
    • 这种情况下会在开发环境给出警告
  3. 清空容器

    • 挂载前会清空容器的内容
    • 通过设置 textContent = '' 实现
  4. 实际挂载

    • 调用原始 mount 方法进行挂载
    • 传入容器元素和命名空间信息
  5. 标记处理

    • 移除 v-cloak 属性
    • 添加 data-v-app 属性标记已挂载

4. 原始 mount 函数执行流程

在重写的 mount 方法中,最终会调用原始的 mount 函数。这个函数定义在 runtime-core/apiCreateApp.ts 中:

js 复制代码
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  namespace?: boolean | ElementNamespace,
): any {
  if (!isMounted) {
    // 1. 检查容器是否已挂载其他应用
    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.`
      )
    }

    // 2. 创建根组件的 VNode
    const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
    
    // 3. 关联应用上下文
    vnode.appContext = context

    // 4. 处理命名空间
    if (namespace === true) {
      namespace = 'svg'
    } else if (namespace === false) {
      namespace = undefined
    }

    // 5. 执行渲染
    if (isHydrate && hydrate) {
      // SSR 水合模式
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      // 客户端渲染模式
      render(vnode, rootContainer, namespace)
    }

    // 6. 更新挂载状态
    isMounted = true
    app._container = rootContainer
    // 为开发工具和遥测标记容器
    ;(rootContainer as any).__vue_app__ = app

    // 7. 开发环境处理
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      app._instance = vnode.component
      devtoolsInitApp(app, version)
    }

    // 8. 返回组件实例的公共代理
    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)\``
    )
  }
}
关键步骤解析
  1. 挂载检查

    • 检查容器是否已经挂载了其他 Vue 应用
    • 开发环境下会发出警告提示需要先卸载已有应用
  2. VNode 创建

    • 使用 createVNode 创建根组件的虚拟节点
    • 支持自定义元素 VNode (_ceVNode)
  3. 上下文关联

    • 将应用上下文 (appContext) 关联到根 VNode
    • 确保全局配置和资源可以在组件树中访问
  4. 命名空间处理

    • 特殊处理 SVG 命名空间
    • 用于正确渲染 SVG 元素
  5. 渲染处理

    • 支持两种渲染模式:

      • SSR 水合 (hydrate):用于服务端渲染
      • 客户端渲染 (render):常规渲染模式
  6. 状态维护

    • 设置 isMounted 标志
    • 保存容器引用
    • 在容器上标记 Vue 应用实例
  7. 开发工具支持

    • 保存组件实例引用
    • 初始化开发工具集成
  8. 返回值处理

    • 返回根组件实例的公共代理
    • 用于外部访问组件实例

5. 虚拟节点创建详解

在 mount 函数中,通过 createVNode 创建根组件的虚拟节点。这个函数定义在 runtime-core/vnode.ts 中:

js 复制代码
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false,
): VNode {
  // 1. 类型检查和处理
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }

  // 2. 处理已存在的 VNode
  if (isVNode(type)) {
    // 克隆现有 VNode(用于 <component :is="vnode"/> 场景)
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    // 块树优化相关处理
    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
        currentBlock[currentBlock.indexOf(type)] = cloned
      } else {
        currentBlock.push(cloned)
      }
    }
    cloned.patchFlag = PatchFlags.BAIL
    return cloned
  }

  // 3. 组件类型规范化
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 4. Props 处理
  if (props) {
    // 处理响应式 props
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    // class 规范化
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    // style 规范化
    if (isObject(style)) {
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // 5. 确定节点类型标志
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT                              // 普通元素
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE                          // Suspense 组件
      : isTeleport(type)
        ? ShapeFlags.TELEPORT                        // Teleport 组件
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT            // 有状态组件
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT         // 函数式组件
            : 0

  // 6. 开发环境的性能警告
  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    type = toRaw(type)
    warn(
      `Vue received a Component that was made a reactive object. This can ` +
      `lead to unnecessary performance overhead...`
    )
  }

  // 7. 创建基础 VNode
  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}
创建虚拟节点的关键步骤
  1. 类型验证与降级

    • 检查节点类型的有效性
    • 无效类型降级为注释节点
  2. VNode 复用处理

    • 处理动态组件场景
    • 合并 refs 和子节点
    • 维护块树优化
  3. 组件规范化

    • 处理类组件的选项
    • 兼容 2.x 的组件写法
  4. Props 规范化

    • 处理响应式属性
    • 规范化 class 和 style
    • 确保响应式对象的可变性
  5. 节点类型标记

    • 通过位运算确定节点类型
    • 支持多种组件类型:
      • 普通元素
      • Suspense 组件
      • Teleport 组件
      • 有状态组件
      • 函数式组件
  6. 性能优化

    • 检测并警告响应式组件定义
    • 建议使用 markRaw 或 shallowRef 重要概念
  1. ShapeFlags
    • 使用位运算标记节点类型
    • 便于快速判断节点特征
  2. BlockTree优化
    • 跟踪动态节点
    • 提升更新性能

6. Render 函数执行流程

在 mount 函数中,创建完虚拟节点后会调用 render 函数进行渲染。render 函数定义在 runtime-core/renderer.ts 中:

js 复制代码
const render: RootRenderFunction = (vnode, container, namespace) => {
  if (vnode == null) {
    // 1. 卸载逻辑
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 2. 挂载或更新逻辑
    patch(container._vnode || null, vnode, container, null, null, null, namespace)
  }
  // 3. 缓存 vnode 引用
  container._vnode = vnode
}
渲染函数的核心逻辑
  1. 卸载处理

    • 当传入的 vnode 为 null 时,执行卸载操作
    • 通过 unmount 函数清理已存在的组件实例
  2. 更新处理

    • 调用 patch 函数处理实际的 DOM 更新

    • 参数说明:

      • container._vnode: 旧的虚拟节点(首次挂载为 null)
      • vnode: 新的虚拟节点
      • container: 容器元素
      • namespace: 命名空间(用于 SVG 等特殊元素)
  3. 状态维护

    • 将新的 vnode 缓存到容器上
    • 用于下次更新时的对比
Patch 函数的作用

patch 函数是渲染系统的核心,它负责:

  1. 首次挂载

    • 创建并插入新的 DOM 元素
    • 初始化组件实例
  2. 更新处理

    • 对比新旧虚拟节点
    • 执行最小化的 DOM 操作
  3. 类型处理

    • 根据节点类型选择不同的处理策略:

      • 组件节点
      • 元素节点
      • 文本节点
      • 注释节点等
重要概念
  1. 渲染上下文
    • 维护渲染相关的状态
    • 提供渲染所需的工具函数

总结

本文,通过分析 app.mount('#app'),我们知道vue会创建虚拟节点,然后调用patch函数对虚拟节点进行diff,最终生成到真实DOM。

相关推荐
難釋懷3 小时前
Vue-github 用户搜索案例
前端·vue.js
晚风3083 小时前
组件传参方式
前端·vue.js
qq_12498707533 小时前
基于Spring Boot+vue框架的武隆旅游网站设计与实现(源码+论文+调试+安装+售后)
vue.js·spring boot·毕业设计·旅游
&白帝&4 小时前
vue中常用的api($set,$delete,$nextTick..)
前端·javascript·vue.js
要加油哦~4 小时前
vue | async-validator 表单验证库 第三方库安装与使用
前端·javascript·vue.js
meng半颗糖5 小时前
vue3 双容器自动扩展布局 根据 内容的多少 动态定义宽度
前端·javascript·css·vue.js·elementui·vue3
SouthernWind5 小时前
Vista AI 演示—— 提示词优化功能
前端·vue.js
LannyChung8 小时前
vue2组件之间的双向绑定:单项数据流隔离
vue.js
独立开阀者_FwtCoder8 小时前
“复制党”完了!前端这6招让你的网站内容谁都复制不走!
前端·javascript·vue.js