写点什么

Vue3 源码 | createApp 都干了什么?

用户头像
梁龙先森
关注
发布于: 2021 年 03 月 21 日
Vue3源码 | createApp都干了什么?

创建 Vue 应用实例,是一个项目的开始,这篇文章看看 createApp 内部都发生了什么。当然如果你对 Vue3 响应式系统感兴趣,也可以先看看这两篇文章:

Vue3源码 | 深入理解响应式系统上篇-reactive

Vue3源码 | 深入理解响应式系统下篇-effect

VUE3 使用

这里看下 Vue3 是如何进行应用初始化。

// vue3 应用初始化import { createApp } from 'vue'import App from './app'const app = createApp(App)app.mount('#app')
// 组件渲染和未捕获错误配置的处理程序app.config.errorHandler = (err, vm, info) => {}// 添加全局属性app.config.globalProperties.$http = () => {} // 这里相当于挂载到Vue2的 Vue.prototype// 指定一种方法识别Vue之外定义的自定义元素app.config.isCustomElement = tag => tag.startsWith('ion-')// 注册组件app.component('my-component', {})// 检索组件const MyComponent = app.component('my-component')// 注册指令app.directive('my-directive',{})// 设置一个可以注入到应用程序内所有组件中的值。组件应使用inject来接收提供的值。app.provide('user', 'administrator')// 卸载应用程序 app.unmount()// 安装vue插件import MyPlugin from './plugins/MyPlugin'app.use(MyPlugin)...
复制代码

Vue3 跟 Vue2 初始化区别不大,都是创建应用实例,然后挂载到 DOM 节点上。这里也可以看出,Vue 是通过 JS 渲染页面,跟传统页面 DOM 直出的方式是不一样。DOM 直出,简单说是,请求后返回的 HTML 页面是最终的呈现效果。

createApp 流程

createApp

createApp 用于创建应用实例,下面看看内部代码实现:

export const createApp = ((...args) => {  // 创建app实例  const app = ensureRenderer().createApp(...args)
const { mount } = app // 重写mount方法 app.mount = (containerOrSelector: Element | 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) container.removeAttribute('v-cloak') container.setAttribute('data-v-app', '') return proxy }
return app}) as CreateAppFunction<Element>
复制代码

通过上面源码解析,我们可以看出 createApp 主要是干了两件事:

  1. 创建 app 实例,并返回该实例

  2. 重写 mount 方法

看完会存在两个主要疑问,ensureRenderer 是干啥用的?为什么要重写 mount 方法,而不直接使用呢?

ensureRenderer

用于创建渲染器,渲染器核心代码处于 runtime-core/src/renderer.ts 文件。

baseCreateRenderer// 可以看出render存在2种类型的渲染器let renderer: Renderer<Element> | HydrationRenderer
// 延迟创建渲染器,当用户只是使用reactive响应库,可以做tree-shaking优化function ensureRenderer() { return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))}
export function createRenderer< HostNode = RendererNode, HostElement = RendererElement>(options: RendererOptions<HostNode, HostElement>) { return baseCreateRenderer<HostNode, HostElement>(options)}
// HydrationRenderer渲染器,也是只在调用的时候创建,方便做tree-shaking优化export function createHydrationRenderer( options: RendererOptions<Node, Element>) { return baseCreateRenderer(options, createHydrationFunctions)}
复制代码

从分析可以看出:

  1. 存在 2 种类型的渲染器,它们都是基于 baseCreateRenderer 函数创建,此函数存在重载。

  2. 渲染器都是通过延迟创建,方便不使用的时候做 tree-shaking

baseCreateRenderer

这个函数是个大家伙,1800 行左右的代码,包含了组件渲染的核心逻辑。这里只抽离挂载过程相关逻辑,涉及组件更新等其他渲染逻辑,留到后面再深入研究。

function baseCreateRenderer(  options: RendererOptions,  createHydrationFns?: typeof createHydrationFunctions): any {      // 其他关于组件渲染逻辑以及hydrate相关这里省略      // 渲染函数   const render: RootRenderFunction = (vnode, container) => {    if (vnode == null) {      // 虚拟节点不存在,则销毁      if (container._vnode) {        unmount(container._vnode, null, null, true)      }    } else {      // 虚拟节点存在,则更新或创建      patch(container._vnode || null, vnode, container)    }    flushPostFlushCbs()    // 缓存虚拟节点数据,作为已完成渲染的标识    container._vnode = vnode  }    return {    render,    hydrate,    createApp: createAppAPI(render, hydrate)  }}
复制代码

可以看出 baseCreateRenderer 主要实现了:

  1. 实现了组件渲染的创建、更新、卸载等核心逻辑(后续解读)

  2. 返回渲染函数,以及创建应用实例方法,当然还有 hydrate

createAppAPI

组件渲染核心部分后面看,这里看看创建实例的 API 实现。

export function createAppAPI<HostElement>(  render: RootRenderFunction,  hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {  // createApp接收2个参数,根组件和根组件的属性  return function createApp(rootComponent, rootProps = null) {    if (rootProps != null && !isObject(rootProps)) {      __DEV__ && warn(`root props passed to app.mount() must be an object.`)      rootProps = null    }		    // 创建一个上下文对象    const context = createAppContext()    const installedPlugins = new Set()
let isMounted = false // 重写上下文对象的app属性 const app: App = (context.app = { _component: rootComponent as Component, _props: rootProps, _container: null, _context: context,
version, // 暴露的应用实例的配置 get config() { return context.config },
set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, // 插件的安装,可以看出,插件如果是对象,必须有install方法;如果是函数,则默认是安装方法 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 },
mixin(mixin: ComponentOptions) { if (__FEATURE_OPTIONS_API__) { if (!context.mixins.includes(mixin)) { context.mixins.push(mixin) } } return app },
component(name: string, component?: PublicAPIComponent): any { if (__DEV__) { validateComponentName(name, context.config) } // 组件存在,则返回 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 },
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 },
mount(rootContainer: HostElement, isHydrate?: boolean): any { if (!isMounted) { // 根据根组件创建虚拟节点 const vnode = createVNode(rootComponent as Component, rootProps) vnode.appContext = context
// HMR root reload if (__DEV__) { context.reload = () => { render(cloneVNode(vnode), rootContainer) } } render(vnode, rootContainer) 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) devtoolsUnmountApp(app) } },
provide(key, value) { context.provides[key as string] = value return app } })
return app }}
复制代码

从上面的代码,我们可以了解到 createAppAPI 主要实现了:

  1. 创建定义一个实例上下文 context,包含属性和方法

  2. 重写扩展 context.app 方法,实现用户可以对上下文相关属性的自定义操作,也就是应用实例暴露的 api 实现,比如自定义指令、混入 mixin、组件等提供用户自定义实现。

  3. 根据根组件和属性在 mount 方法中完成虚拟节点 vNode 的转换,并通过 render 喊完成渲染,关于渲染函数在 baseCreateRender 已经说过。

总结

至此分析了 createApp 大致的流程,内部更细致的实现,后续再根据内容深入分析,这里再总结下整个过程。

  1. 执行 createApp 首先会创建渲染器,这里要注意的是存在 2 种渲染器类型,并且它们都是通过延迟创建的,主要目的是当用户只引用 reactive 响应式框架的时候,方便进行 tree-shaking 优化。且两种渲染器都是基于 baseCreateRender 方法来实现。

  2. baseCreateRender 函数执行后会返回 render 渲染函数和 createApp 方法,其中 render 函数是组件创建、更新和卸载的主要核心逻辑实现。createApp 则用于创建应用实例,进行应用实例的初始化。

  3. createAppAPI 用于生成默认的应用上下文 context,这里定义了应用实例具备的属性和方法,并通过重写扩展 context.app 属性,让用户能够进行对上下文的自定义操作,比如自定义组件、指令、mixin、插件安装等一系列操作。并存在 mount 方法完成将根组件转为虚拟节点 vNode,并通过render 函数完成对 vNode 的渲染。


下一篇,我们将立足这篇文章,接着深入 render 函数内部,先看看 vNode 是什么,再瞧瞧 DOM Diff 过程。


发布于: 2021 年 03 月 21 日阅读数: 26
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
Vue3源码 | createApp都干了什么?