Vue源码之createApp组件的创建和mount的过程

945 阅读10分钟

前言:这篇文章可能会有点绕,也可能会很冗杂(每一步都会配上一个图片),这篇文章主要介绍了我阅读vue3源码过程中的一些理解,在此记录下来分享给大家。如果有错误的理解,希望能在评论区指正!

先贴一波预览图(vscode的book mark插件)

image.png

准备工作

  • 下载vue/core,yarn install安装依赖
  • 查看scripte脚本,添加sourcemap(这样浏览器中运行的时候就有映射了)

image.png

  • 删除scripts/dev.js内的两行代码,不然跑起来会报错(增加.git也行,但是我比较懒哈哈)

image.png

  • yarn run dev运行,代码会被打包到这个目录下

image.png

  • 在example文件夹下创建demo文件夹以及index.html文件夹,引入打包后的vue,编写一个简单的Vue的demo

image.png

本地能跑起来的话,准备过程就结束了(不要用dev-server打开)。

image.png

createApp的创建过程

1、调用createApp创建app首先会进入packages/runtime-dom/src/index.ts目录下导出的createApp方法,但是这个方法只是个入口,真正创建app的代码在第58行。58行通过ensureRenderer()方法获取渲染器

image.png

2、ensureRenderer函数只是获取渲染器的入口,本质上获取渲染器是调用了createRenderer方法

image.png

3、但是createRenderer也是一个入口,本质上是返回了调用baseCreateRenderer函数返回的对象(咋封装了那么多层呢)

image.png

4、查看baseCreateRenderer,发现它其实是一个函数重载,真正的baseCreateRenderer竟然有两千行

image.png

5、我们先不管baseCreateRenderer内部的具体实现,我们看它返回的内容。可以看到它返回了一个对象(渲染器对象),对象里面有createApp属性,这个属性的值是通过调用createAppAPI获取的,那么我们下一步看看createAppAPI中具体都做了什么。

image.png

6、createAppAPI其实就是返回一个创建app的函数,调用这个函数的话可以返回一个App类型的值。所以上面渲染器对象中的createApp其实就是这里返回的这个函数,调用它可以获取App对象。

image.png

7、我们看看神秘的App对象到底是什么吧!里面包括很多内容,譬如use使用插件,mixin混入,component声明组件,directive自定义指令,mount挂载等...

image.png

至此,调用createApp方法创建App对象的流程就已经结束啦

mount挂载过程

8、mount其实就是调用上图的App对象中的mount方法。我们看一下mount函数传入的参数类型,这里指定rootContainerElement类型,可是我们在使用的时候明明是可以传入字符串类型来指定挂载的容器的呀?

image.png

9、原因是在createApp获取到App对象之后,对mount进行了重写,使得mount可以传入string类型来指定container。

image.png

10、mount内部调用normalizeContainer来获取字符串对应的dom元素(dom查询的方式)这个dom元素其实就是mount要挂载的container。获取到dom元素之后,调用原来的mount方法,将container传入。

image.png

image.png

11-12、解决了mount的参数问题,我们看看mount内部的具体实现

首先是调用createVNode创建vnode(这里传入的rootComponent其实就是createApp传入的js对象,下图仅用于说明这一点)

image.png

然后再调用render函数将vnode渲染到rootContainer中(rootContainer就是mount指定的countainer元素)

image.png

13-14、我们看看render函数中是怎么将vnode渲染到container中的吧。它其实只是一个入口,内部会进行判断,如果传入的vnode是空,就进行卸载操作,否则就进行patch操作。因为container目前其实就是一个普通的dom元素(dom查询获取到的),内部肯定没有_vnode属性,所以patch第一个参数传入的是null。(patch是核心,我们马上就会说到)。

image.png

15、patch内部会对n1和n2(其实就是旧vnode和新vnode)进行判断

  • 如果n1(旧vnode)存在并且如果新旧vnode的类型不一样,也就是传入了一个新类型的vnode,那么会卸载n1(旧的vnode)。
  • 因为我们之前调用patch时第一个参数是null,所以上面那个判断跳过。取出n2(新vnode)中的一些属性,根据n2的type进行判断执行不同的操作

image.png

16-17、注意,此时的n2(新vnode)是第11-12步根据createApp传入的对象创建的vnode,是组件类型的vnode。所以会进入default中的第一个else if,执行processComponent处理组件的函数。

image.png

18-19、我们来看看processComponent函数内部的实现。内部主要是判断n1(旧vnode)是否存在来执行对应的操作。因为我们patch的时候第一个参数n1传入的是null,所以执行mountComponent挂载组件操作。

image.png

20、让我们继续看看mountComponent内部的实现。它会先根据传入的vnode调用createComponentInstance创建对应的组件实例(组件实例用于保存组件的各种状态,例如data,methods),并且在vnode对象的component属性上挂载。

image.png

21、然后调用setupComponent组件实例进行初始化(我们后面详细介绍,这里先知道有这个初始化流程)。

image.png

22-23、然后调用setupRenderEffect,设置和渲染有关的副作用函数

image.png

setupRenderEffect内部使用了effect函数,这个函数是来自于响应式系统reactive的,如果传入effect的函数中使用了一些响应式数据,那么他们会被自动收集到响应式系统,并且在这个响应式数据改变时重新触发这个函数。

上面说的和渲染有关的副作用函数就是componentEffect函数

image.png

25-26、componentEffect内部的具体实现。通过组件实例内部的isMOunted属性判断组件是否被挂载来执行对应的操作。

image.png

因为我们新建的组件实例肯定是没有挂载的,所以会进入第一个逻辑。这里主要是两个操作:获取到subTree(subTree就是template模板下的dom树),将这些subTree递归的执行patch函数。

image.png

27、这里有一个新的地方就是,vue3其实是支持template内部最外层有多个根的,这是因为在内部获取subTree的时候,如果它有多个根,会在subTree外面包裹一层Fragment

那么对subTree(n2,subTree是新vnode)进行patch的时候会重新进入type的判断。如果是Fragment类型,就执行processFragment,如果有根(vue2会在template内部包裹一层div),就执行processElement

image.png

28、我们先来看有根的情况,也就是template内部包裹了一层div。会进入上图所示的processElement处理dom元素函数

processElement函数内部会对传入的n1(旧vnode)进行判断,如果没值就是mountElement操作,有值就是patchElement操作。

我们这里n1是没有值的,因为我们在25-26步进行patch的时候n1没有值,n2是subTree。所以会进入mountElement函数。

image.png

29、mountElement函数内部进行挂载操作。因为传入的是vnode而不是真实的dom,要挂载肯定是挂载真实dom,所以在内部会先调用hostCreateElement根据type创建对应的dom元素(内部是通过createElement来创建的)。创建出真实dom之后会在vnode的el属性上保存一份。

image.png

image.png

30、创建subTree对应的dom元素之后,判断subTree的类型,如果它内部保存的是字符串,就直接设置这个字符串,如果它内部还有children的话,就对这些children元素执行mountChildren操作,将他们挂载到el上。

image.png

31、而mountChildren内部其实就是遍历传入的children数组,对每一个child进行patch操作。因为这里patch第一个参数是null,所以其实就是进行挂载操作。

image.png

32、回到mountElement函数,当children处理完成后,调用hostInsert将el挂载到container中。

image.png

至此,挂载流程结束啦~

setupComponent初始化组件的过程

先来大致浏览一下整个过程

image.png

先在函数内部判断是否是一个有状态组件(包含data,methods等),然后再初始化props和slots,初始化之后调用setupStatefullComponent执行setup函数

image.png

setupStatefullComponent函数内部会先取出setup并且执行。执行结束后判断结果的类型,因为通常setup直接返回一个对象,所以进入handleSetupResult函数来处理返回结果

image.png

handleSetupResult函数内部主要判断一下返回值的类型,如果是对象的话,就放到Proxy代理中,并且保存在组件实例的setupState属性中。结束之后调用finishSetupComponent来收尾。

image.png

finishSetupComponent函数内部先是将模板通过compile函数编译成对应的render函数,然后将这个render函数保存在组件实例上。

compile其实就是一个编译器,先将模板代码编译成ast抽象语法树,然后转换成对应的render函数(js函数)

image.png

在后面也对vue2的options API进行了支持

image.png

打断点回顾整个流程

image.png

chrom浏览器的调试按钮,分别是过掉整个断点、单步执行、进入某个函数、退出某个函数

image.png

首先进入的是获取app,这里ensureRenderer获取渲染器的过程我就不点进去啦~

image.png

创建app之后,保存mount属性

image.png

重写mount方法

image.png

返回app对象

image.png

执行mount挂载操作

image.png

normalizeContainer获取dom元素

image.png

调用原来保存的mount函数执行挂载操作

image.png

mount内部进行判断,如果没有挂载过就执行第一个逻辑分支

image.png

createVnode创建vnode对象

image.png

创建完成之后调用render渲染vnode到container上

image.png

render函数中进行判断,vnode有值,进行patch(null,vnode)操作

image.png

patch会先对两个vnode的类型进行判断,因为我们这里n1是null,所以跳过

image.png

取出n2的type,进行switch判断

image.png

执行processComponent,处理组件的函数

image.png

processComponent内部对n1进行判断,因为我们这里n1为null,所以执行mountComponent操作

image.png

mountComponent内部会先创建组件实例Instance

image.png

创建完组件实例之后,调用setupComponent进行初始化

image.png

调用setupRenderEffect设置副作用函数

image.png

内部通过响应式系统的effect函数收集依赖,在响应式数据改变时重新执行传入effect的函数。

image.png

在componentEffect内部先获取subTree这个vnode对象,因为我的例子template内有多个根,所以type是Fragment

image.png

可以看到,subTree有两个children

image.png

对subTree进行patch操作

image.png

因为type是Fragment,所以在patch时进入了processFragment函数

image.png

因为n1是null,所以在processFragment函数内部执行了mountChildren方法。Fragment类型是肯定有children的,所以调用mountChildren将children挂载到container中。

image.png

mountChildren内部其实就是遍历children,执行patch操作。

image.png

可以看到当前遍历的children内部就是一个jzsp字符串(template内部的第一个div)

image.png

执行patch的时候,因为是普通dom元素类型,进入了processElement方法。

image.png

该方法内部也是会对n1进行非空判断,我们这里是空,所以执行mountElement操作。

image.png

在mountElement内部会通过hostCreateElement创建dom元素

image.png

可以看到,创建了一个div

image.png

创建完div之后,在mountElement函数内部继续判断vnode的类型,如果它是文本类型的vnode,就直接设置值,如果它有children的话,就执行mountChildren操作,将children挂载到创建的dom元素上。(mountChildren的过程就又回到了前面,属于是递归调用了)

image.png

处理完children之后,mountElement最后会将el挂载到container上。

image.png

总结

其实Vue的源码真的封装的非常好,反复读可以理解一些原理层面的过程,对理解Vue真的很有帮助,加油!