Vue3.2x源码解析(一):vue初始化过程

lxf2023-04-05 08:32:01

系列文章:

  • Vue3.2x源码解析(二):组件初始化
  • Vue3.2x源码解析(三):深入响应式原理
  • Vue3.2x源码解析(四):异步更新队列

1,目录结构

先回顾下 Vue2 源码目录:

├── src
  ├── compiler    #  编译相关的模块
  ├── core        #  核心语法代码
  ├── platforms   #  平台相关
  ├── server      #  服务端渲染相关
  ├── sfc         #  vue单文件组件
  ├── shared      #  公用方法

再看看 Vue3 源码的目录结构:

├──packages
  ├── compiler-core    # 平台无关的编译器核心代码,baseParse生成AST   
  ├── compiler-dom     # 基于compiler-core,针对浏览器的附加插件的编译器
  ├── compiler-sfc     # 编译vue单文件组件 
  ├── compiler-ssr     # 服务端渲染相关的
  ├── reactivity        # vue3响应式模块,例如:ref/reactive/effect
  ├── runtime-core      # 平台无关的运行时核心代码,组件创建/渲染/更新 
  ├── runtime-dom       # 基于runtime-core,针对浏览器的运行时,处理各种原始dom_api
  ├── vue              # 面向公众的完整构建,其中包含编译器和运行时
  ├── shared           # 多个包共享的内部工具,公用方法
  ├── ...            

Vue3采用monorepo是管理项目代码的方式。不同于 Vue2 代码管理,它是在一个 repo中管理多个package项目,每个 package 都有自己的类型声明、单元测试。 package 又可以独立发布,这种部署方式更便于项目的维护和发版。

注意: vue3源码由TS代码编写,阅读源码前请先熟悉TS基本语法。

2,源码入口

vue3每一个项目的开始,都是从vue中引入一个createApp方法,使用这个方法创建一个vue实例,这个实例就是我们的应用实例,一个项目可以按需求创建多个vue实例。

import { createApp } from 'vue'
import App from './App.vue'
// 应用初始化
const app = createApp(App)
app.mount('#app')
  • 这里的App是根组件,作为渲染组件的起点。
  • mount('#app') 表示应用挂载的DOM节点容器。
createApp

接下来就从import { createApp } from 'vue'这句代码开始源码的学习

首先查看createApp的源码:

// packages/runtime-dom/src/index.ts
​
# 创建vue应用实例
export const createApp = ((...args) => {
  // 创建vue实例
  const app = ensureRenderer().createApp(...args)
  // 取出mount方法
  const { mount } = app
  # 重写mount方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 确定挂载节点容器
    const container = normalizeContainer(containerOrSelector)
    // 没有容器直接return 无法挂载应用
    if (!container) return
    
    // 取出根组件
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      // 如果根组件没有render/template,则把容器的内容赋值给根组件
      component.template = container.innerHTML
    }
​
    // clear content before mounting
    // 加载之前清空dom容器内容
    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
})

createApp源码内容看似不多,实则里面执行了非常多的逻辑,后面我们会一直跳转回来查看方法里面的内容。

首先分析第一行代码:

const app = ensureRenderer().createApp(...args)

一进来就执行了一个ensureRenderer方法。

ensureRenderer

我们理解一下这个方法的作用:

// packages/runtime-dom/src/index.ts
# 确定渲染器
function ensureRenderer() {
  // 返回一个渲染器对象
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

可以看出这个方法只有一个作用,返回一个确定的渲染器renderer对象。

在讨论createRenderer之前,有一个重点要关注一下rendererOptions:

const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)

rendererOptions由两部分组成:

  • patchProp:处理元素的props、Attribute、class、style、event事件等。
  • nodeOps:处理dom节点,这个对象里面包含了各种原生dom操作方法。

rendererOptions对象最终会传递到渲染器里面,里面的各种方法最终会在页面渲染的过程中被调用。

(一)renderer渲染器的类型

在知道renderer渲染器如何创建之前,我们先看看renderer对象的类型:

# Renderer web端渲染器   HydrationRenderer服务器端渲染器
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

注意:Hydration开头的相关函数,为SSR服务器端渲染使用,后续会在Vue3源码中多次遇见。

可以看出renderer渲染器可以是web端渲染器,也可以是SSR渲染器,我们在浏览器中使用Vue框架就会确定为web端的渲染器。

(二)rederer渲染器的创建

接下来我们就分析renderer渲染器的创建过程:

// packages/runtime-dom/src/index.ts
renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions)

查看createRenderer源码:

// packages/runtime-core/src/renderer.ts
# 创建渲染器
function createRenderer<HostNode = RendererNode,HostElement = RendererElement>
  (options: RendererOptions<HostNode, HostElement>) {
    return baseCreateRenderer<HostNode, HostElement>(options)
}
# 注意;泛型函数 fn<Node, Element>() {}
// vue3源码量非常大,并且其中有非常多的泛型函数,在刚开始阅读源码的时候,可以减少关注具体的类型,重点放在逻辑过程。可以在后面解惑的时候再回头看具体的类型,这可以减轻刚开始阅读源码的压力。
baseCreateRenderer

继续查看baseCreateRenderer源码:

// packages/runtime-core/src/renderer.ts
# Vue3渲染器核心: 非常重要
function baseCreateRenderer(options, createHydrationFns?) {
  // 确定全局对象
  const target = getGlobalThis()
  target.__VUE__ = true
  
  # rendererOptions对象传递过来的原生dom操作方法
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    insertStaticContent: hostInsertStaticContent
  } = options
  
  # 渲染器内部有几十个工具函数,这里列出几个常用的
  
  // patch,根据vnode的类型,执行不同的逻辑,
  const patch = () => {}
  
  // 加载组件
  const mountComponent = () => {}
  
  // 更新组件
  const mountComponent = () => {}
  
  // 设置组件渲染renderEffect, 类似于vue2 watcher的renderWatcher
  const setupRenderEffect = () => {}
  
  # 渲染应用方法 重点
  const render = () => {}
  
  return {
    render,
    hydrate,
    # 重点:初始化createApp方法
    createApp: createAppAPI(render, hydrate)
  }
}

注意:baseCreateRenderer是基础渲染器,功能非常强大,是Vue3应用渲染的核心,可以创建出不同环境需要的渲染器,自定义渲染器,源码量非常大,这里我们主要介绍渲染器内部的一些重点方法的用途,后面会根据场景再展开每个方法的具体逻辑。

baseCreateRenderer源码量非常多,这里直接看return返回值,返回了一个对象,这就是我们所需要的渲染器对象。

// 渲染器对象有三个方法,主要关注render和createApp
const renderer = {
    render,
    hydrate, // 忽略
    createApp: createAppAPI(render, hydrate)
}

再跳回到ensureRenderer方法,可以看到这个方法的返回值就是渲染器对象renderer

重点关注渲染器对象的render方法和createApp方法。

再跳回到刚开始的createApp源码的第一行代码:

createApp() {
    const app = ensureRenderer().createApp(...args)
}

ensureRenderer生成一个渲染器后就立即调用内部的createApp方法,创建了一个vue应用实例,我们要理解Vue实例的具体创建过程,就得继续查看createApp方法的源码createAppAPI

createAppAPI
// packages/runtime-core/src/apiCreateApp.ts
# 创建vue应用方法
function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  
   # 返回值就是createApp方法源码  
  return function createApp(rootComponent, rootProps = null) { // 参数为根组件:即src/App.vue组件
    if (!isFunction(rootComponent)) {
      rootComponent = { ...rootComponent }
    }
​
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
​
    # 创建vue应用实例上下文 就是确定应用的一些默认配置
    const context = createAppContext()
    // 初始化插件集合,用于存储插件列表
    const installedPlugins = new Set()
    // 初始化应用挂载状态,默认为false
    let isMounted = false
​
    # 创建vue实例对象
    // 同时将应用实例存储到context上下文的app属性
    const app: App = (context.app = {
      # 初始化一些应用属性
      _uid: uid++, // 项目中可能存在多个vue实例,需使用id标识
      _component: // 根组件
      _props: rootProps, // 传递给根组件的props
      _container: null, // dom容器
      _context: context, // app上下文
      _instance: null, // 应用实例
      version, // 版本
    
      # 定义了一个访问器属性app.config,只能读取,不能直接替换
      get config() {
        // 读取的是上下文的config
        return context.config
      },
      set config(v) {},
      
      # 下面是vue应用内部定义的几个方法
      
      // 安装插件
      use() {}
      // 混入
      mixin() {}
      // 注册全局组件
      component() {}
      // 注册自定义指令
      directive() {}
      // 挂载应用
      mount() {}
      // 卸载应用
      unmount() {}
      // 全局数据
      provide() {}
    })
    
    if (__COMPAT__) {
      // 若开启兼容,安装vue2相关的API
      installAppCompatProperties(app, context, render)
    }
    
    # 返回vue实例对象
    return app
  }
}

以上就是createApp的源码,简单来说就是内部创建了一个vue实例对象,这个对象内部初始化了一些全局的属性和方法,和vue2源码中的initGlobalAPI逻辑比较类似,比如use/mixin/component/directive,这些都是我们比较熟悉的全局方法了。然后根据配置处理vue2兼容的相关API,最后返回了实例对象。

以上的内容就是刚开始createApp源码中第一行代码的逻辑:

createApp() {
    const app = ensureRenderer().createApp(...args)
    ...
}
// 所以我们在项目只使用了一行代码就创建一个vue应用,实际上框架内部已经执行了非常多的逻辑
const app = createApp(App)

我们再跳回到刚开始的createApp源码:

createApp() {
    const app = ensureRenderer().createApp(...args)
    # 1,取出原来的mount方法
    const { mount } = app
    // 2,重写app的mount方法
    app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
        
        ...
        # 调用mount开始应用真正的加载
        const proxy = mount(container, false, container instanceof SVGElement)
        return proxy
    }
    
    return app
}

在创建vue应用实例后,取出mount方法,然后立即重写了app实例的mount加载方法,最后返回了vue应用实例。

这里有一个重点是两个Mount方法的区别:

  • 第一个mount方法为:vue应用的渲染,侧重为调用render方法进行应用渲染。
  • 第二个mount方法为:vue应用的挂载,侧重为将应用挂载到container节点容器。

这里的加载执行顺序是:先获取目标DOM节点容器,进行应用挂载,然后调用render方法进行应用渲染。即先执行重写的mount方法,然后在内部执行原来的mount加载方法。

const app = createApp(App)
# 当然,一般在执行mount加载之前,都会注册一些全局组件和插件资源
app.component()
app.directive()
app.use(Element)
app.use(pinia)
# 加载应用
app.mount('#app')

分析到这里,以上的内容就是createApp源码的基本内容。主要作用就是确定渲染器,创建一个Vue应用实例,最后进行加载,下面我们将继续分析vue加载的详细过程。

3,应用加载

# 1,执行重写的mount,应用挂载
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
   ...
   # 2,执行原来的mount,应用渲染
   const proxy = mount(container, false, container instanceof SVGElement)
   return proxy
}

根据前面的分析,两个加载的执行顺序及内容我们都了解了,下面我们分析应用加载渲染的具体过程:

// packages/runtime-core/src/apiCreateApp.ts
# 应用加载
mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {
    # 首次isMounted为false,执行加载渲染
    if (!isMounted) {
        # 创建根组件vnode对象,默认编译生成的组件对象只有setup和render方法
    	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.
    	// 将app的上下文存储在根虚拟节点
    	vnode.appContext = context

    	// 开发环境下的热更新
        if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG)
            }
        }
    	// isHydrate水合函数与ssr有关
    	if (isHydrate && hydrate) {
    		hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    	} else {
    		# 开始应用渲染
    		render(vnode, rootContainer, isSVG)
    	}
    	# 应用渲染完成,设置应用为已挂载状态
    	isMounted = true
    	// 设置应用容器节点#app
    	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 getExposeProxy(vnode.component!) || vnode.component!.proxy
    } 
}

上面的应用加载过程主要有三个重点:

  • 创建根组件vnode对象。
  • 执行render方法开始渲染应用【整个应用的渲染过程都在里面】。
  • 渲染完成,设置应用为已挂载状态。

其中最重要的就是渲染逻辑,下面我们继续分析render方法:

// packages/runtime-core/src/Renderer.ts
# 渲染方法
const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      # 第一次vnode是根组件的内容,开始应用的渲染
      // 首次container还没有_vnode属性为underfined ,即旧的_vnode为null
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    # 执行冲刷任务
    flushPreFlushCbs()
    flushPostFlushCbs()
    // 定义_vnode属性:存储当前vnode
    container._vnode = vnode
}

到这里,我们就可以对Vue应用的初始化过程做一个总结,简单来说就是:

  • 创建Vue应用实例。
  • 确定应用挂载容器节点。
  • 创建根组件vnode对象。
  • 执行render加载渲染应用。

在生产环境下:Vue应用的初始化只会执行一次,除非主动刷新页面。

在开发环境下:由于源码变化热更新的支持,Vue应用可以多次执行初始化渲染。

// 开发环境下的热更新
if (__DEV__) {
    context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
    }
}

下节我们继续分析patch里面的内容及组件的初始化过程。