Vue3是从何开始运行的:从CreateApp到mount

894 阅读7分钟

虽然学习vue3的使用,并不会牵扯到多少的源码内容,然而,希望能写出漂亮优雅的代码,和对vue3更深入的使用和理解,提高自己的上限,那么学习和了解源码是必不可少的。虽然网上已经有非常多的优质的源码解读。然而阅读仅限于阅读,自己梳理并且解读一遍,也是对自己的温故而知新。

从GitHub中下载vue3的源码:vuejs/core: 🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web. (github.com) 从packages中,了解到Vue3一些具体的文件。

Vue3 的三大模块, 是reactivity(响应系统),runtime-dom(针对浏览器运行时编写的库,包括DOM事件和属性,处理原生的DOM API)和complier-dom(针对浏览器编写的 runtime-dom暴露出2个重要API:render(渲染),createApp(创建实例)。

梳理流程

createApp().png

什么是vue的入口?

源码的内容有很多,盲目的去了解,会事倍功半,而且不成体系,这里,先从vue3的入口开始说起。无论是从node中直接下载vue包也好,还是直接使用vue脚手架也好,如何在JavaScript中使用vue3,都是从这行代码开始的createApp({setup(){}}).mount('#app')

const { createApp } = Vue
createApp({setup(){}}).mount('#app')

vue-cli main.js

import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

createApp入口

createApp和render API来自core/packages/runtime-dom/src/index.js

// rendererOptions里存储有关补丁和节点相关参数
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
//延迟创建渲染函数,可以让核心渲染逻辑实现摇树优化(消除不需要的依赖来减少不需要的代码)
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

function ensureRenderer() {
  return (
  //缓存处理,存在则返回,如果没有renderer后,再进行对渲染器进行创建。
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}
//渲染处理
export const render = ((...args) => {
  ensureRenderer().render(...args)
}) as RootRenderFunction<Element | ShadowRoot>

export const createApp = ((...args) => {
//创建app实例
  const app = ensureRenderer().createApp(...args)  
//省略_DEV_在开发中运行的代码
  //重写app中的mount方法,原因后面再讲
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
	  //.....省略 将实例中的组件进行相关处理
	  return proxy
  }'
  return app
}) as CreateAppFunction<Element>

createApp就是返回一个对象app,并且重写了mount函数,而创建好的app对象可以调用其他函数来完成相关代码的运行。 然后挂载(.mount('#app'))上去。即为将组件挂在到id为app的DOM节点上去。

这里可见无论是渲染还是创建app都是需要调用ensureRenderer中的render()和createApp(),而ensureRenderer是由createRenderer(rendererOptions)基础创建渲染器的函数返回的。

createApp.png

createRenderer

createRenderer处于core/packages/runtime-core/src/renderer.ts中。 vue3是支持自定义渲染器的,对外暴露createRenderer方法。在源码中有这么一段话: ‘createRenderer函数接受两个泛型参数。 HostNode和HostElement,对应于宿主环境中的Node和Element类型。 例如,对于runtime-dom,HostNode是DOM的Node接口,而HostElement则是DOMElement接口。自定义渲染器可以像这样传入平台的特定类型。’ 以下是如何调用createRenderer方法来创建自定义渲染器的。

const { render, createApp } = createRenderer<Node, Element>({
   patchProp,
   ...nodeOps
})

自定义渲染器先放在一遍,我们继续往下看。

这里 createRenderer(option)继续调用了baseCreateRenderer(option) 这段代码其实就是baseCreateRenderer换用createRenderer名称专门用来暴露出去的API以实现自定义渲染器。

export function createRenderer<
  HostNode = RendererNode, 
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

baseCreateRenderer(options)

baseCreateRenderer(options)创建基础渲染器,options包含了DOM的处理方法和属性Patch方法。

import { createAppAPI, CreateAppFunction } from './apiCreateApp'
// implementation
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
//... 期间是有关node,组件和patch的各种处理,挂载卸载更新渲染等处理。
return {
	render,//把接收到的vnode转换成dom,然后追加到宿主元素上。
	hydrate,//SSR,在服务器端讲vnode生成为html字符串
	createApp:createAppAPI(render, hydrate) //引用 
 }
}

baseCreateRenderer的返回值对象,包含了3个对象,render,hydrate(跟服务器有关 ,不谈),createApp。而createApp是由createAppAPI(render,hydrate)所赋值。 因此,createApp调用的函数就是createAppAPI接口(baseCreateRenderer().createApp),并带上了render作为参数进一步处理。

createAppAPI(render, hydrate)

createAppAPI位于 core/packages/runtime-core/src/apiCreateApp.ts中 从这看createAppAPI的具体逻辑处理。注意这里渲染器被当做了参数传递了进去。 这里的代码书写跟之前的创建渲染器一样,将createApp改名后暴露出去作为一个API。

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
	 //相关逻辑处理
  }

关于 createAppAPI() 通过实例来执行的好处就是 可以避免全局污染,产生隔离。之前vue2是在构造函数上进行的修改和扩展,然后new出来的实例之间就会相互污染。

createApp的内部逻辑

return function createApp(rootComponent, rootProps = null) {//rootComponent根组件
    if (!isFunction(rootComponent)) {
      rootComponent = { ...rootComponent }
    }
     //删除有关_DEV_在开发中运行的代码
    const context = createAppContext()//创建了一个app的上下文
    const installedPlugins = new Set()
    let isMounted = false
    //生成一个具体的对象,提供对应的API和相关属性
    const app: App = (context.app = {//将以下参数传入到context中的app里
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,
      version,
      get config() {
        return context.config
      },
      //省略部分具体逻辑处理代码
      //1.避免全局配置会污染 * 2.treeshaking 摇树优化 * 3.语义化
      //插件使用
      use(plugin: Plugin, ...options: any[]) {},
      mixin(mixin: ComponentOptions) {},
      //组件处理
      component(name: string, component?: Component): any {},
      //指令处理
      directive(name: string, directive?: Directive) {},
      //挂载
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
      //如果处于未挂载完毕状态下运行
      if (!isMounted) {
	      //创建一个新的虚拟节点传入根组件和根属性
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // 存储app上下文到根虚拟节点
          // 这将在初始挂载时设置在根实例上。
          vnode.appContext = context
          }
          //渲染虚拟节点,根容器
          render(vnode, rootContainer, isSVG)
          isMounted = true //将状态改变成为已挂载
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app
          return getExposeProxy(vnode.component!) || vnode.component!.proxy
      }},
	  //卸载
      unmount() {},
      
      provide(key, value) {}
    })
    return app
  }

createAppContext上下文创建

函数返回了一个对象,表示实例上下文需要缓存的一些数据对象的初始化。

export function createAppContext(): AppContext { 
	return { 
		app: null as any,
		config: {
			// const NO = () => false 
			isNativeTag: NO, 
			performance: false, 
			globalProperties: {},
			optionMergeStrategies: {},
			isCustomElement: NO, 
			errorHandler: undefined,
			warnHandler: undefined 
		}, 
		mixins: [], 
		components: {},
		directives: {},
		provides: Object.create(null)
	} 
}

createApp(rootComponent,rootProps=null).png

为何要重写mount?

从上面createAppAPI中,创建app对象返回的已经有mount方法了,然后在讲解 ensureRender.createApp的时候,却对mount方法进行了重写。

export const createApp = ((...args) => {
//创建app实例
  const app = ensureRenderer().createApp(...args)  
//省略_DEV_在开发中运行的代码
 //先将app中的与平台无关的mount存到mount中缓存备用
  const { mount } = app
   //重写app中的mount方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
	//对容器标准化
	const container = normalizeContainer(containerOrSelector)
	//如果不存在则返回
    if (!container) return
    const component = app._component
    //如果组件对象中没有方法且没有渲染函数且没有模板,那么直接把innerHTML方法作为组件的模板内容
    if (!isFunction(component) && !component.render && !component.template) {
      // 潜在风险
      // 原因:在DOM模板中可能执行JS表达式。用户必须确保DOM模板是受到信任的。如果是由服务器渲染,该模板不应包含任何用户数据。
      component.template = container.innerHTML
    }
    //挂载前清空innerHTML内容
    container.innerHTML = ''
    //运行与平台无关的mount,实现挂载
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }
  return app
}) as CreateAppFunction<Element>

这里先进行mount的重写是为了适应平台的一些内容的处理,可以兼容vue2的写法,因为vue不单单是为了web平台服务的,是需要进行跨平台渲染的,因此内部不能够包含任何指定平台的内容,createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:先创建VNode,再渲染VNode。

后续相关: