Vue3源码之初始化渲染流程解读一

595 阅读3分钟

最近有些时间,就想着深入vue3源码的实现背后,看下vue3应用初始化挂载的过程都有哪些步骤,每一个步骤大概做了哪些具体内容。

说明:文章源码部分代码仅仅展示主要功能实现部分,如需查看完整源码,请移步GitHub vue-next

首先,我们以下面的代码来看下我们是如何创建和挂载Vue3应用的

<!DOCTYPE html>
<html lang="en">
<head>
  <title>hello vue3</title>
  <script src="../packages/vue/dist/vue.global.js"></script>
</head>
<body>
  <div id='app'>app</div>
  <script type="module">
    const { createApp, h, ref } = Vue;
    const App = {
      setup() {
        const num = ref(0);
        const btnClick = () => {
          num.value += 1;
        }
        return () => h(
          'button',
          {
            onclick: btnClick
          }, 
          `点我+1--${num.value}`)
      }
    }
    const app = createApp(App).mount("#app");
    console.log(app);
  </script>
</body>
</html>

在上面的代码中,我们可以看到,我们首先通过createApp()方法来创建应用,然后调用createApp()方法返回的实例对象属性方法mount()挂载到指定Dom容器。

1. 入口文件

vue3源码的入口文件位于:packages\vue\src\index.ts

主要功能代码:

if (__DEV__) {
  // dev环境提示,生产环境引入生产包
  initDev()
}
// 缓存编译结果
const compileCache: Record<string, RenderFunction> = Object.create(null)

/**
 * @description 将template转换为render函数
 * $mount内部,在没有render函数的情况下,会将template转换render
 */
function compileToFunction(
  template: string | HTMLElement, // 模板
  options?: CompilerOptions // 编译配置
): RenderFunction {
  // 如果 template 不是字符串
  if (!isString(template)) {
    if (template.nodeType) {
      // 则认为是一个 DOM 节点,获取 innerHTML
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }
  const key = template
  const cached = compileCache[key]
  // 有缓存直接返回
  if (cached) {
    return cached
  }
  // template可以为id选择器,或者普通字符串
  // 如果是 ID 选择器,则获取 DOM 元素,取 innerHTML作为template内容生成render
  // 如果为普通字符串,则将其作为内容生成render
  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    template = el ? el.innerHTML : ``
  }
  // 调用 compile 获取可执行代码code
  const { code } = compile(
    template,
    extend(options)
  )
  // 将 code 转化为 function
  const render = (__GLOBAL__
    ? new Function(code)()
    : new Function('Vue', code)(runtimeDom)) as RenderFunction
  
  // 标记template to render 完成
  (render as InternalRenderFunction)._rc = true
  // 返回 render 方法的同时,将其放入缓存
  return (compileCache[key] = render)
}
// 通过依赖注入的方式,将 compile 函数注入至 runtime 运行时中
registerRuntimeCompiler(compileToFunction)

文件功能点说明:

  • 声明变量用以缓存编译结果
  • 调用registerRuntimeCompiler方法,将模板编译函数注入运行时。
1.1 registerRuntimeCompiler()方法:
let compile: CompileFunction | undefined
export function registerRuntimeCompiler(_compile: any) {
  compile = _compile
}
export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
    const Component = instance.type as ComponentOptions
    if (!instance.render) {
        const template = Component.template;
        Component.render = compile(template, finalCompilerOptions)
    }
}

其主要功能就是将compileToFunction方法添加为render方法。

2. createApp()方法

在入口文件packages\vue\src\index.ts的最后有一行有导出

// packages\vue\src\index.ts
export * from '@vue/runtime-dom'

接着看packages\runtime-dom\src\index.ts文件

// packages\runtime-dom\src\index.ts
export const createApp = ((...args) => {
  // 获取应用实例对象
  const app = ensureRenderer().createApp(...args)
  // 保存app实例mount方法
  const { mount } = app
  // 重写实例的mount方法;我们在createApp().mount()就是此方法。
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 获取挂载点根元素容器
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // 
    const component = app._component
    // 渲染优先级:render > template > container.innerHTML
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // 清空挂载容器内容
    container.innerHTML = '';
    
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      // 挂载容器添加app属性标识
      container.setAttribute('data-v-app', '')
    }
    // 返回根组件实例
    return proxy
  }
  // 返回应用实例,支持链式调用
  return app
}) as CreateAppFunction<Element>

文件功能点说明:

  • 定义createApp()方法
  • 缓存应用实例mount方法并重写mount
  • mount流程
    • 获取挂载元素
    • 渲染优先级:render > template > container.innerHTML
    • 清空挂载容器
    • mount挂载应用并返回应用实例
2.1 ensureRenderer()

方法可以理解为创建渲染器

// packages\runtime-dom\src\index.ts
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
  // render渲染器存在直接返回
  // 不存在则调用createRenderer方法创建渲染器render并缓存
  // createRenderer方法传入参数rendererOptions(平台相关dom操作方法的封装)
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
2.2 createRenderer()

方法功能可以理解为创建渲染器方法

// packages\runtime-core\src\renderer.ts
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

方法比较简单,通过调用并返回baseCreateRenderer方法实现

2.3 baseCreateRenderer

创建渲染器

// packages\runtime-core\src\renderer.ts
// 此方法定义了一系列patch相关的处理方法,我们只看主要的结构
function baseCreateRenderer(
  options: RendererOptions, // 平台相关 dom节点和属性操作方法
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // 获取传入options 平台相关节点操作方法
    const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        forcePatchProp: hostForcePatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        createComment: hostCreateComment,
        setText: hostSetText,
        setElementText: hostSetElementText,
        parentNode: hostParentNode,
        nextSibling: hostNextSibling,
        setScopeId: hostSetScopeId = NOOP,
        cloneNode: hostCloneNode,
        insertStaticContent: hostInsertStaticContent
      } = options
    // 定义核心patch方法
    const patch: PatchFn = () => {}
    ...
    // 作为createAppAPI参数传入,app.mount()阶段在createVNode()之后调用
    const render: RootRenderFunction = (vnode, container, isSVG) => {
        // vnode 新传入虚拟dom
        // container._vnode 旧虚拟dom
        if (vnode == null) {
          if (container._vnode) {
            // 新vnode为空,存在旧vnode情况下,销毁旧组件
            unmount(container._vnode, null, null, true)
          }
        } else {
          // 创建or更新
          patch(container._vnode || null, vnode, container, null, null, null, isSVG)
        }
        // 保存vnode
        container._vnode = vnode
    }
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    }
}
2.4 createAppAPI
// runtime-core\src\apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  // rootComponent根组件应用配置,也就是createApp(args)传入的args参数
  return function createApp(rootComponent, rootProps = null) {
    // appContext对象
    const context = createAppContext()
    // 保存当前注册plugin
    const installedPlugins = new Set()
    // 当前未挂载标识
    let isMounted = false
    
    const app: App = (context.app = {
      // 注册实例方法,app.use(plugin)
      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          // plugin 注册不能重复
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          // plugin是一个对象,对象包含有install:function
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          // plugin 是一个function
          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.mixin
      mixin(mixin: ComponentOptions) {
        // mixin只能用于options api
        if (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin)
            if (mixin.props || mixin.emits) {
              context.deopt = true
            }
          } 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
      },
      // 注册实例方法app.component
      component(name: string, component?: Component): any {
        if (__DEV__) {
          // 验证组件name,不能用html标签作为组件名称
          validateComponentName(name, context.config)
        }
        // component 不能重复注册
        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
      },
      // 注册实例方法 app.directive
      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
      },
      // 注册app.mount()方法,在packages\runtime-dom\src\index.ts文件createApp方法内部,重写既是该方法
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          // 获取vnode
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG)
            }
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // vnode渲染真实dom
            render(vnode, rootContainer, isSVG)
          }
          // 挂载标志位
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app
          // 返回挂载根组件实例
          return vnode.component!.proxy
        }
      },

      unmount() {
        if (isMounted) {
          // 卸载
          render(null, app._container)
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            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] = value

        return app
      }
    })
    // 返回app实例
    return app
  }
}
2.5 createApp()方法内部流程总结
  • 方法导出位置
// packages\runtime-dom\src\index.ts
/**
* @param {args} 根组件实例
*/ 
export const createApp = ((...args) => {
  // 获取应用实例对象
  const app = ensureRenderer().createApp(...args)
  ...
  ...
  // 返回应用实例,支持链式调用
  return app
})
  • ensureRender
// packages\runtime-dom\src\index.ts
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
  • createRenderer
// packages\runtime-core\src\renderer.ts
/**
* @param {options} 平台相关dom方法
*/ 
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
  • baseCreateRenderer
// packages\runtime-core\src\renderer.ts
/**
* @param {options} 平台相关dom方法
*/ 
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    ...
    ...
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    }
}
  • createAppAPI
// runtime-core\src\apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  // rootComponent根组件应用配置,也就是createApp(args)传入的args参数
  return function createApp(rootComponent, rootProps = null) {
    // appContext对象
    const context = createAppContext()
    // 保存当前注册plugin
    const installedPlugins = new Set()
    // 当前未挂载标识
    let isMounted = false
    const app: App = (context.app = {
      // 注册实例方法,app.use(plugin)
      use(plugin: Plugin, ...options: any[]) {
         ....
        // 返回应用实例,支持链式调用
        return app
      },
      // 注册实例方法app.mixin
      mixin(mixin: ComponentOptions) {
        ....
        return app
      },
      // 注册实例方法app.component
      component(name: string, component?: Component): any {
        return app
      },
      // 注册实例方法 app.directive
      directive(name: string, directive?: Directive) {
        return app
      },
      // 注册app.mount()方法
      mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {
         // 返回挂载根组件实例
         return vnode.component!.proxy
      },
      unmount() {},
      provide(key, value) {
        return app
      }
    })
    // 返回app实例
    return app
  }
}

createApp方法内部主要有以下实现:

  • 初始化创建appContext对象
  • 变量声明缓存已注册的插件
  • 定义app实例对象并声明初始化app应用Api方法:use(),mixin(),component(),directive(),mount(),unmount(),provide()