「这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战」
Vue3大侠修炼手册3- 初始化流程分析(1)
题记
在上一篇文章我们搭建了我们的调试环境,我们可以针对源码的改动随意的进行调试。在这篇文章中我们将进行源码分析的第一步:初始化流程的分析
热身训练
我们先来看看一般我们的初始化流程都怎样写:
const { createApp } = Vue
const app = createApp({})
app.mount('#app')
从上面简化后的代码我们可以看出,在初始化的过程中,我们一般经历了两大步骤
createApp
,进行Vue实例的创建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)都做了什么
从上图我们可以看到, 入口文件主要声明了一个compileToFunction
函数,并把它在RuntimeCompiler
进行注册,同时以compile
的名称引用出去,并把@vue/runtime-dom
中的所有对外暴露的内容都引用出去。
而compileToFunction
的内容,我们就不过多赘述,总之它的作用就是接收模板字符串 然后返回对应的render
函数。
好了以上就是我们对我们的Vue
打包内容的简要分析,接下来我们书接上文,通过packages/vue/examples/composition/todomvc.html
进行Vue3初始化流程的分析。
初始化流程分析
createApp部分的分析
我们首先把断点放到todomvc.html
中的createApp
看都是执行了哪些内容。
经过断点追踪,我们发现createApp其实是调用了ensureRenderer().createApp(...args)
。其返回值就是我们的 app
实例。
我们在packages/runtime-dom/src/index.ts
这个文件中找到了ensureRenderer
这个方法,发现它是同文件下createRenderer
的封装,进而我们寻找createRenderer
,发现它是定义在runtime-core/src/renderer.ts
的方法,而这个方法竟也是对同文件下 baseCreateRenderer
函数的封装。
baseCreateRenderer
函数就是调用ensureRenderer()
时真正的执行函数,我们可以看到,这个函数有2000+
行的代码其返回了一个含有三个属性的对象。
我们简单观察可以发现第一个属性render
是把vnode
转化为真实dom
的方法。第三个属性createApp
就是我们ensureRenderer().createApp(...args)
调用的createApp
方法,它是同级目录下apiCreateApp.ts
中createAppAPI
定义的函数,该函数返回了一个名为createApp
的内部函数。在这个函数内部顶一个名为app
的实例对象。
在这里我们思考一下,为啥要封装一个createAppAPI方法来做createApp的事情?
因为想对render方法进行扩展,使用createAppAPI 可以通过参数的方式直接调用render而不用关心render的细节,同时让createAppAPI内部的代码变得通用。
在实例对象上我们可以看到
- 有
_uid, _component, _props, _container, _context
等属性,这些很明显是作者声明的内部属性,不希望使用者进行访问。 - 同时我们也观察到
use, mixin, component,directive,mount, unmount, provide
等方法,这些都是我们比较熟悉的api.
至此,我们已经通过代码调试,完成了Vue实例创建的过程。
我们总结一下,Vue3的createApp函数,是通过调用内部的baseCreateRenderer().createApp()进行创建实例的。 返回的实例是一个内部有use, mixin, mount
等方法的对象
app.mount() 部分分析。
mount
的调用,根据我们上面的分析,那我第一反应,八九不离十应该是刚刚createApp返回实例中的mount
方法吧,为了严谨期间我们仍从todomvc.html
中看起。这次我们把断点放到mount
处,看都是执行了哪些内容。
我们顺着断点执行,发现调用的mount
方法,已经不仅仅是原来在createApp时的实例方法,该方法在packages/runtime-dom/src/index.ts
文件中进行了重写,该方法是对createApp中原方法的enhancement,最终还是会调用 createApp中的mount方法进行处理。
接下来我们就分析一下,这个mount执行都经历了什么?
我们发现此处的mount
方法,除去一些附加的内容(我们暂时不关心),其核心仍然是执行了createApp内部的mount
方法。
而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
并追加到宿主元素的过程。