【Vue3源码解析】应用实例创建及页面渲染

下载源码

bash 复制代码
git clone https://github.com/vuejs/core.git

写该文章时的Vue版本为:

复制代码
"version": "3.5.13",

这里要注意 pnpm 的版本不能太低,我此时的版本为 9.15.4。更新 pnpm 版本:

bash 复制代码
npm install -g pnpm

然后安装依赖:

bash 复制代码
pnpm i 

然后打包开发环境:

bash 复制代码
pnpm run dev

之后就可以编写自己的测试程序,debugger 调试代码。

Vue3 整体架构

在打包后的文件夹内创建一个自己用来测试的 demo 文件夹,我这里叫做 heo 文件夹

编写测试代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h1>计数器案例</h1>
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
</div>
<script src="../dist/vue.global.js"></script>
<script>
    const {ref, createApp} = Vue
    // 根组件
    const App = {
        setup() {
            const counter = ref(0)

            function increment() {
                counter.value++
            }

            function decrement() {
                counter.value--
            }

            return {
                counter,
                increment,
                decrement
            }
        }
    }
    // 创建全局 app对象,调用 mount 方法挂载到页面上
    const app = createApp(App)
    app.mount('#app')
</script>
</body>
</html>

然后再使用内置插件自带的服务器预览当前页面:

createApp 的创建过程

找到创建 app 实例的 createApp 函数:

从创建 app 对象到创建 渲染器的过程:

js 复制代码
/**
 * 创建一个 Vue 应用实例(入口)。
 *
 * @param {...any} args - 传递给内部创建应用方法的参数。
 * @returns {CreateAppFunction<Element>} 返回一个带有自定义 `mount` 方法的应用实例对象 app。
 */
export const createApp = ((...args) => {
  // 调用确保渲染器存在并创建应用实例 app
  // ensureRenderer() 实例化一个渲染器 渲染器就是把 Vue 代码(组件)渲染到DOM 上
  // ensureRenderer() 最终的返回值 是一个对象
  // {
  //   render, // 核心函数 负责将 VNode 渲染成真实 DOM
  //   hydrate, // SSR 的函数
  //   createApp: createAppAPI(render, hydrate),
  // }
  const app = ensureRenderer().createApp(...args)

  // 在开发模式下注入原生标签检查和编译选项检查
  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  // 获取原始的 mount 方法
  const { mount } = app

  // 自定义(重写) mount 方法,接受DOM或选择器作为参数 当我们调用mount时 本质上在调用重写的 mount 方法
  // 装饰器模式 不改变原有实现 对 mount 进行增强
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 将传入的选择器或元素转换为实际的 DOM 元素 也就是我们传入的要挂载的 DOM
    // normalizeContainer 转换DOM 比如将 "#app" 转为 querySelector 函数拿到DOM
    const container = normalizeContainer(containerOrSelector)
    if (!container) return // 如果DOM不存在,直接返回

    const component = app._component // 获取应用的根组件

    // 如果根组件没有 render 函数或 template 属性,则从容器(#app)内的HTML作为组件的模版
    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // 原因:潜在的执行在 DOM 内的模板中的 JS 表达式。
      // 用户必须确保该模板是可信的。如果由服务器渲染,则模板不应包含任何用户数据。
      component.template = container.innerHTML

      // 2.x 兼容性检查
      if (__COMPAT__ && __DEV__ && container.nodeType === 1) {
        for (let i = 0; i < (container as Element).attributes.length; i++) {
          const attr = (container as Element).attributes[i]
          // 检查是否有 v- 或 : 或 @ 开头的属性,提示用户使用新的挂载方式
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null,
            )
            break
          }
        }
      }
    }

    // 在挂载前清除容器的内容
    if (container.nodeType === 1) {
      container.textContent = ''
    }

    // 调用原始的 mount 方法进行挂载,并返回代理实例
    const proxy = mount(container, false, resolveRootNamespace(container))

    // 如果容器是一个元素,移除 v-cloak 属性并添加 data-v-app 属性
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }

    return proxy
  }

  return app
}) as CreateAppFunction<Element>

我们自己开发的时候根组件都是 template 属性,而如果没有template 或者 render ,则从容器(#app)内的HTML作为组件的模版,例如:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--将该 div 作为 作为组件的模版-->
<div id="app">
    <h1>计数器案例</h1>
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
</div>
<script src="../dist/vue.global.js"></script>
<script>
    const {ref, createApp} = Vue
    // 根组件
    const App = {
        setup() {
            const counter = ref(0)

            function increment() {
                counter.value++
            }

            function decrement() {
                counter.value--
            }

            return {
                counter,
                increment,
                decrement
            }
        }
    }
    // 创建全局 app对象,调用 mount 方法挂载到页面上
    const app = createApp(App)
    app.mount('#app')
</script>
</body>
</html>

创建渲染器和 mount 的执行

从创建虚拟节点到渲染页面的过程:

ts 复制代码
// nodeOps 节点操作(比如insert/remove/createComment创建注释元素 等)和 patchProp 属性操作(比如 class/style/@click 等)
const rendererOptions = /*@__PURE__*/ extend({ patchProp }, nodeOps)

// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

/**
 * 确保渲染器已被创建
 *
 * 此函数用于检查全局变量 `renderer` 是否已存在,如果不存在,则使用给定的 `rendererOptions`
 * 创建一个新的渲染器并赋值给 `renderer`,以确保渲染器的单例模式
 * 这种方法保证了在整个应用中只有一个渲染器实例,从而优化性能 如果有多个渲染器实例可能导致状态管理和渲染更新的冲突和重复
 *
 * rendererOptions - 定制化 针对不同平台(移动端/SSR)的渲染器配置
 *
 * @returns {Renderer} 已存在的渲染器或新创建的渲染器实例
 */
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}
ts 复制代码
/**
 * The createRenderer function accepts two generic arguments:
 * HostNode and HostElement, corresponding to Node and Element types in the
 * host environment. For example, for runtime-dom, HostNode would be the DOM
 * `Node` interface and HostElement would be the DOM `Element` interface.
 *
 * Custom renderers can pass in the platform specific types like this:
 *
 * ```js
 * const { render, createApp } = createRenderer<Node, Element>({
 *   patchProp,
 *   ...nodeOps
 * })
 * ```
 */
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement> {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

因此 createRenderer 才是创建渲染器的代码实现(有2000行代码,这里不再粘代码)。它的返回值很重要是:

js 复制代码
{
  render, // 核心函数 负责将 VNode 渲染成真实 DOM
  hydrate, // SSR 的函数
  createApp: createAppAPI(render, hydrate),
}
ts 复制代码
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
  // 返回的函数 也就是我们调用createApp()函数创建的真实app(这里使用函数柯里化传递了render和hydrate)
  return function createApp(rootComponent, rootProps = null) {
    // 如果不是函数组件 转化为对象的形式
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

    // rootProps 如果不是对象则警告
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    // 创建app上下文实例 存储 app配置信息
    const context = createAppContext()
    // 安装 plugins 使用set防止重复安装
    const installedPlugins = new WeakSet()
    const pluginCleanupFns: Array<() => any> = []
    // 目前刚刚创建app实例 没有挂载到dom上
    let isMounted = false
    // app 实例对象 函数的最后返回 该app实例对象
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,
      //...

      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
      },

      //...

      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.`,
            )
          }
          // 根据传入的根组件和属性 创建对应的 VNode
          const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          // 在 vnode 存储上下文
          vnode.appContext = context

          //...

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // 非ssr 则将上面的虚拟节点渲染到dom上
            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 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)\``,
          )
        }
      },
        
      //...

      return app
  }
}

patch 代码执行过程:

patch 组件的 children 过程:

组件内部 effect 处理过程:

createVNode 的过程(h 函数的本质)

packages/runtime-core/src/h.ts

ts 复制代码
/**
 * 创建一个虚拟节点(VNode),基于提供的类型、属性和子节点。
 * 对外暴露为 h 函数,内部使用 createVNode 函数来创建虚拟节点。
 *
 * @param type VNode的组件类型,通常是一个表示DOM元素或组件函数的字符串。
 * @param propsOrChildren VNode的属性或子节点,取决于上下文。
 * @param children VNode的子节点,可以是单个VNode或VNode数组。
 * @returns 返回创建的VNode。
 */
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  // 获取参数的数量,以确定如何处理它们。
  const l = arguments.length

  // 如果只提供了两个参数,则进一步根据第二个参数的类型进行区分。
  if (l === 2) {
    // 判断第二个参数是否为对象且不是数组,以决定它是属性还是子节点。
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 单个VNode且没有属性
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // 有属性但没有子节点
      return createVNode(type, propsOrChildren)
    } else {
      // 省略属性
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    // 处理超过三个参数的情况,将第二个参数之后的所有参数转换为子节点数组。
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      // 如果正好提供了三个参数且第三个参数是单个VNode,则将其封装到数组中。
      children = [children]
    }
    // 最后,使用属性和子节点创建VNode。
    return createVNode(type, propsOrChildren, children)
  }
}
(type, propsOrChildren)
    } else {
      // 省略属性
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    // 处理超过三个参数的情况,将第二个参数之后的所有参数转换为子节点数组。
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      // 如果正好提供了三个参数且第三个参数是单个VNode,则将其封装到数组中。
      children = [children]
    }
    // 最后,使用属性和子节点创建VNode。
    return createVNode(type, propsOrChildren, children)
  }
}
相关推荐
程序员Bears10 分钟前
从零打造个人博客静态页面与TodoList应用:前端开发实战指南
java·javascript·css·html5
清风细雨_林木木1 小时前
Vue 2 项目中配置 Tailwind CSS 和 Font Awesome 的最佳实践
前端·css·vue.js
小宁爱Python1 小时前
深入掌握CSS Flex布局:从原理到实战
前端·javascript·css
weifont2 小时前
React中的useSyncExternalStore使用
前端·javascript·react.js
初遇你时动了情2 小时前
js fetch流式请求 AI动态生成文本,实现逐字生成渲染效果
前端·javascript·react.js
几何心凉2 小时前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程3 小时前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
懒羊羊我小弟4 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts
运维@小兵4 小时前
vue访问后端接口,实现用户注册
前端·javascript·vue.js
雨汨4 小时前
web:InfiniteScroll 无限滚动
前端·javascript·vue.js