Vue3大侠修炼手册3- 初始化流程分析(1)

731 阅读5分钟

「这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战

Vue3大侠修炼手册3- 初始化流程分析(1)

题记

在上一篇文章我们搭建了我们的调试环境,我们可以针对源码的改动随意的进行调试。在这篇文章中我们将进行源码分析的第一步:初始化流程的分析

热身训练

我们先来看看一般我们的初始化流程都怎样写:

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

从上面简化后的代码我们可以看出,在初始化的过程中,我们一般经历了两大步骤

  1. createApp,进行Vue实例的创建
  2. app.mount, 进行组件的挂载操作。

所以今天我们只要把这两步的过程分析透彻,就算是完成了初始化流程的分析。

Vue打包简单分析

在分析初始化流程之前我们先简略了解一下Vue代码库打包过程。

在上一篇文章中,我们代码打包用到了

pnpm dev

启动服务器我们使用了 pnpm serve

分析dev.js

我们在执行pnpm dev时,执行的命令是node scripts/dev.js 我们就看看在scripts/dev.js, 都做了哪些内容。

在这个文件中我们着重看一下这几句代码,分析内容在语句后的注释内,不过多赘述。


const { build } = require('esbuild') // 使用 esbuild 进行打包,好处是打包速度比较快

const target = args._[0] || 'vue' // 要访问的packages中的包, 在这里为'vue'

const format = args.f || 'global' // 导出的文件引用系统的格式,在这里为 'global'


const pkg = require(resolve(__dirname, `../packages/${target}/package.json`)) // 获取package.json信息

// resolve output

const outputFormat = format.startsWith('global')

? 'iife' // 我们打包结果是 'iife'格式

: format === 'cjs'

? 'cjs'

: 'esm'

  

const postfix = format.endsWith('-runtime')

? `runtime.${format.replace(/-runtime$/, '')}`

: format

  

const outfile = resolve(

__dirname,

`../packages/${target}/dist/${target}.${postfix}.js` // 我们的输出文件在 packages/vue/dist/vue.global.js

)

// 下面为esbuild build的配置

build({

entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)], // 入口文件,为 packages/vue/src/index.ts

outfile,

bundle: true,

external,

sourcemap: true, // 开启sourcemap
...,
watch: {

onRebuild(error) { 

if (!error) console.log(`rebuilt: ${relativeOutfile}`) // rebuild时,会在控制台输出生成文件的相对路径

}

}
})

我们再来简单的看一下入口文件(packages/vue/src/index.ts)都做了什么

Pasted image 20220207220742.png 从上图我们可以看到, 入口文件主要声明了一个compileToFunction函数,并把它在RuntimeCompiler进行注册,同时以compile的名称引用出去,并把@vue/runtime-dom中的所有对外暴露的内容都引用出去。

compileToFunction的内容,我们就不过多赘述,总之它的作用就是接收模板字符串 然后返回对应的render函数。

好了以上就是我们对我们的Vue打包内容的简要分析,接下来我们书接上文,通过packages/vue/examples/composition/todomvc.html 进行Vue3初始化流程的分析。

初始化流程分析

createApp部分的分析

我们首先把断点放到todomvc.html中的createApp看都是执行了哪些内容。

Pasted image 20220207222455.png 经过断点追踪,我们发现createApp其实是调用了ensureRenderer().createApp(...args)。其返回值就是我们的 app实例。

我们在packages/runtime-dom/src/index.ts这个文件中找到了ensureRenderer这个方法,发现它是同文件下createRenderer的封装,进而我们寻找createRenderer,发现它是定义在runtime-core/src/renderer.ts的方法,而这个方法竟也是对同文件下 baseCreateRenderer函数的封装。 baseCreateRenderer函数就是调用ensureRenderer()时真正的执行函数,我们可以看到,这个函数有2000+行的代码其返回了一个含有三个属性的对象。

Pasted image 20220207223331.png

我们简单观察可以发现第一个属性render是把vnode转化为真实dom的方法。第三个属性createApp就是我们ensureRenderer().createApp(...args) 调用的createApp方法,它是同级目录下apiCreateApp.tscreateAppAPI定义的函数,该函数返回了一个名为createApp的内部函数。在这个函数内部顶一个名为app的实例对象。

在这里我们思考一下,为啥要封装一个createAppAPI方法来做createApp的事情?

因为想对render方法进行扩展,使用createAppAPI 可以通过参数的方式直接调用render而不用关心render的细节,同时让createAppAPI内部的代码变得通用。

在实例对象上我们可以看到

  • _uid, _component, _props, _container, _context等属性,这些很明显是作者声明的内部属性,不希望使用者进行访问。
  • 同时我们也观察到use, mixin, component,directive,mount, unmount, provide等方法,这些都是我们比较熟悉的api.

Pasted image 20220207224211.png

至此,我们已经通过代码调试,完成了Vue实例创建的过程。 我们总结一下,Vue3的createApp函数,是通过调用内部的baseCreateRenderer().createApp()进行创建实例的。 返回的实例是一个内部有use, mixin, mount等方法的对象

app.mount() 部分分析。

mount的调用,根据我们上面的分析,那我第一反应,八九不离十应该是刚刚createApp返回实例中的mount方法吧,为了严谨期间我们仍从todomvc.html中看起。这次我们把断点放到mount处,看都是执行了哪些内容。

Pasted image 20220207225549.png

我们顺着断点执行,发现调用的mount方法,已经不仅仅是原来在createApp时的实例方法,该方法在packages/runtime-dom/src/index.ts文件中进行了重写,该方法是对createApp中原方法的enhancement,最终还是会调用 createApp中的mount方法进行处理。

Pasted image 20220207225731.png 接下来我们就分析一下,这个mount执行都经历了什么?

我们发现此处的mount方法,除去一些附加的内容(我们暂时不关心),其核心仍然是执行了createApp内部的mount方法。

Pasted image 20220207230959.png 而createApp内部的mount方法主要做了两件事

  • 调用createVNode方法,创建vnode
  • 调用render(vnode, rootContainer, isSVG)方法,把vnode转化为真实dom,然后绑定到rootContainer上。

由于mount内部的流程相对来说比较复杂,牵扯到内部调用的patch方法的分支流程和递归调用等,我们下一节再细讲。在这里,我们先对挂载内容,做一个初步的了解。挂载基本上就是让传入的组件数据和状态转化为真实的dom,并追加到宿主元素上。

结尾

今天我们主要通过todomvc.html这个页面分析了Vue3初始化的创建实例和挂载过程。Vue内部是通过执行baseCreateRenderer().createApp()进行创建实例的,实例是一个有mixin, mount等方法的对象。 实例的挂载过程中创建了根节点的虚拟dom,并通过render(baseCreateRenderer返回的 render函数)函数,让其转化为真实dom并追加到宿主元素的过程。