【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)
  }
}
相关推荐
Ama_tor1 小时前
网页制作05-html,css,javascript初认识のhtml表格的创建
javascript·css·html
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
饼干饿死了1 小时前
实现动态翻转时钟效果的 HTML、CSS 和 JavaScript,附源码
javascript·css·html
林的快手1 小时前
CSS文本属性
前端·javascript·css·chrome·node.js·css3·html5
大胖丫2 小时前
vue 学习-vite api.js
开发语言·前端·javascript
孙桂月2 小时前
ES6相关操作(2)
前端·javascript·es6
遇见很ok2 小时前
js中 ES6 新特性详解
开发语言·javascript·es6
我这一生如履薄冰~2 小时前
简单封装一个websocket构造函数
前端·javascript·websocket
fangcaojushi2 小时前
解决webpack5.54打包图片及图标的问题
前端·vue.js
前端菜鸟日常2 小时前
vue2和vue3的按需引入的详细对比通俗易懂
javascript·vue.js·ecmascript