前言:这篇文章可能会有点绕,也可能会很冗杂(每一步都会配上一个图片),这篇文章主要介绍了我阅读vue3源码过程中的一些理解,在此记录下来分享给大家。如果有错误的理解,希望能在评论区指正!
先贴一波预览图(vscode的book mark插件)
准备工作
- 下载
vue/core
,yarn install安装依赖 - 查看scripte脚本,
添加sourcemap
(这样浏览器中运行的时候就有映射了)
- 删除
scripts/dev.js
内的两行代码,不然跑起来会报错(增加.git也行,但是我比较懒哈哈)
- yarn run dev运行,代码会被打包到这个目录下
- 在example文件夹下创建demo文件夹以及index.html文件夹,引入打包后的vue,编写一个简单的Vue的demo
在本地能跑起来的话,准备过程就结束了(不要用dev-server打开)。
createApp的创建过程
1、调用createApp
创建app首先会进入packages/runtime-dom/src/index.ts
目录下导出的createApp
方法,但是这个方法只是个入口,真正创建app的代码在第58行。58行通过ensureRenderer()方法获取渲染器
2、ensureRenderer
函数只是获取渲染器的入口,本质上获取渲染器是调用了createRenderer
方法
3、但是createRenderer
也是一个入口,本质上是返回了调用baseCreateRenderer
函数返回的对象(咋封装了那么多层呢)
4、查看baseCreateRenderer
,发现它其实是一个函数重载,真正的baseCreateRenderer
竟然有两千行!
5、我们先不管baseCreateRenderer
内部的具体实现,我们看它返回的内容。可以看到它返回了一个对象(渲染器对象),对象里面有createApp属性,这个属性的值是通过调用createAppAPI获取的,那么我们下一步看看createAppAPI
中具体都做了什么。
6、createAppAPI
其实就是返回一个创建app的函数,调用这个函数的话可以返回一个App类型的值。所以上面渲染器对象中的createApp
其实就是这里返回的这个函数,调用它可以获取App对象。
7、我们看看神秘的App对象到底是什么吧!里面包括很多内容,譬如use使用插件,mixin混入,component声明组件,directive自定义指令,mount挂载等...
至此,调用
createApp
方法创建App对象的流程就已经结束啦
mount挂载过程
8、mount
其实就是调用上图的App对象中的mount
方法。我们看一下mount
函数传入的参数类型,这里指定rootContainer是Element类型,可是我们在使用的时候明明是可以传入字符串类型来指定挂载的容器的呀?
9、原因是在createApp
获取到App对象之后,对mount
进行了重写,使得mount
可以传入string类型来指定container。
10、mount
内部调用normalizeContainer
来获取字符串对应的dom元素(dom查询的方式)这个dom元素其实就是mount要挂载的container。获取到dom元素之后,调用原来的mount
方法,将container传入。
11-12、解决了mount
的参数问题,我们看看mount
内部的具体实现
首先是调用createVNode
创建vnode(这里传入的rootComponent其实就是createApp传入的js对象,下图仅用于说明这一点)
然后再调用render
函数将vnode渲染到rootContainer中(rootContainer就是mount
指定的countainer元素)
13-14、我们看看render
函数中是怎么将vnode渲染到container中的吧。它其实只是一个入口,内部会进行判断,如果传入的vnode是空,就进行卸载操作,否则就进行patch
操作。因为container目前其实就是一个普通的dom元素(dom查询获取到的),内部肯定没有_vnode属性,所以patch第一个参数传入的是null。(patch是核心,我们马上就会说到)。
15、patch
内部会对n1和n2(其实就是旧vnode和新vnode)进行判断
- 如果n1(旧vnode)存在并且如果新旧vnode的类型不一样,也就是传入了一个新类型的vnode,那么会卸载n1(旧的vnode)。
- 因为我们之前调用patch时第一个参数是null,所以上面那个判断跳过。取出n2(新vnode)中的一些属性,根据n2的type进行判断执行不同的操作。
16-17、注意,此时的n2(新vnode)是第11-12步根据createApp传入的对象创建的vnode,是组件类型的vnode。所以会进入default中的第一个else if,执行processComponent
处理组件的函数。
18-19、我们来看看processComponent函数内部的实现。内部主要是判断n1(旧vnode)是否存在来执行对应的操作。因为我们patch的时候第一个参数n1传入的是null,所以执行mountComponent挂载组件操作。
20、让我们继续看看mountComponent
内部的实现。它会先根据传入的vnode调用createComponentInstance
创建对应的组件实例(组件实例用于保存组件的各种状态,例如data,methods),并且在vnode对象的component属性上挂载。
21、然后调用setupComponent
对组件实例进行初始化(我们后面详细介绍,这里先知道有这个初始化流程)。
22-23、然后调用setupRenderEffect
,设置和渲染有关的副作用函数
setupRenderEffect
内部使用了effect
函数,这个函数是来自于响应式系统reactive的,如果传入effect的函数中使用了一些响应式数据,那么他们会被自动收集到响应式系统,并且在这个响应式数据改变时重新触发这个函数。
上面说的和渲染有关的副作用函数就是
componentEffect
函数
25-26、componentEffect
内部的具体实现。通过组件实例内部的isMOunted属性判断组件是否被挂载来执行对应的操作。
因为我们新建的组件实例肯定是没有挂载的,所以会进入第一个逻辑。这里主要是两个操作:获取到subTree(subTree就是template模板下的dom树),将这些subTree递归的执行patch
函数。
27、这里有一个新的地方就是,vue3其实是支持template内部最外层有多个根的,这是因为在内部获取subTree的时候,如果它有多个根,会在subTree外面包裹一层Fragment。
那么对subTree(n2,subTree是新vnode)进行patch的时候会重新进入type的判断。如果是Fragment类型,就执行processFragment
,如果有根(vue2会在template内部包裹一层div),就执行processElement
。
28、我们先来看有根的情况,也就是template内部包裹了一层div。会进入上图所示的processElement
处理dom元素函数。
processElement
函数内部会对传入的n1(旧vnode)进行判断,如果没值就是mountElement
操作,有值就是patchElement
操作。
我们这里n1是没有值的,因为我们在25-26步进行patch的时候n1没有值,n2是subTree。所以会进入mountElement
函数。
29、mountElement
函数内部进行挂载操作。因为传入的是vnode而不是真实的dom,要挂载肯定是挂载真实dom,所以在内部会先调用hostCreateElement
来根据type创建对应的dom元素(内部是通过createElement
来创建的)。创建出真实dom之后会在vnode的el属性上保存一份。
30、创建subTree对应的dom元素之后,判断subTree的类型,如果它内部保存的是字符串,就直接设置这个字符串,如果它内部还有children的话,就对这些children元素执行mountChildren
操作,将他们挂载到el上。
31、而mountChildren
内部其实就是遍历传入的children数组,对每一个child进行patch操作。因为这里patch
第一个参数是null,所以其实就是进行挂载操作。
32、回到mountElement
函数,当children处理完成后,调用hostInsert
将el挂载到container中。
至此,挂载流程结束啦~
setupComponent初始化组件的过程
先来大致浏览一下整个过程
先在函数内部判断是否是一个有状态组件(包含data,methods等),然后再初始化props和slots,初始化之后调用setupStatefullComponent
执行setup函数
setupStatefullComponent
函数内部会先取出setup
并且执行。执行结束后判断结果的类型,因为通常setup直接返回一个对象,所以进入handleSetupResult
函数来处理返回结果
handleSetupResult
函数内部主要判断一下返回值的类型,如果是对象的话,就放到Proxy
代理中,并且保存在组件实例的setupState
属性中。结束之后调用finishSetupComponent
来收尾。
finishSetupComponent
函数内部先是将模板通过compile
函数编译成对应的render
函数,然后将这个render函数保存在组件实例上。
compile其实就是一个编译器,先将模板代码
编译成ast抽象语法树
,然后转换成对应的render函数(js函数)
在后面也对vue2的options API进行了支持
打断点回顾整个流程
chrom浏览器的调试按钮,分别是过掉整个断点、单步执行、进入某个函数、退出某个函数。
首先进入的是获取app,这里ensureRenderer获取渲染器的过程我就不点进去啦~
创建app之后,保存mount属性
重写mount方法
返回app对象
执行mount挂载操作
normalizeContainer获取dom元素
调用原来保存的mount函数执行挂载操作
mount内部进行判断,如果没有挂载过就执行第一个逻辑分支
createVnode创建vnode对象
创建完成之后调用render渲染vnode到container上
render函数中进行判断,vnode有值,进行patch(null,vnode)操作
patch会先对两个vnode的类型进行判断,因为我们这里n1是null,所以跳过
取出n2的type,进行switch判断
执行processComponent,处理组件的函数
processComponent内部对n1进行判断,因为我们这里n1为null,所以执行mountComponent操作
mountComponent内部会先创建组件实例Instance
创建完组件实例之后,调用setupComponent进行初始化
调用setupRenderEffect设置副作用函数
内部通过响应式系统的effect函数收集依赖,在响应式数据改变时重新执行传入effect的函数。
在componentEffect内部先获取subTree这个vnode对象,因为我的例子template内有多个根,所以type是Fragment
可以看到,subTree有两个children
对subTree进行patch操作
因为type是Fragment,所以在patch时进入了processFragment函数
因为n1是null,所以在processFragment函数内部执行了mountChildren方法。Fragment类型是肯定有children的,所以调用mountChildren将children挂载到container中。
mountChildren内部其实就是遍历children,执行patch操作。
可以看到当前遍历的children内部就是一个jzsp字符串(template内部的第一个div)
执行patch的时候,因为是普通dom元素类型,进入了processElement方法。
该方法内部也是会对n1进行非空判断,我们这里是空,所以执行mountElement操作。
在mountElement内部会通过hostCreateElement创建dom元素
可以看到,创建了一个div
创建完div之后,在mountElement函数内部继续判断vnode的类型,如果它是文本类型的vnode,就直接设置值,如果它有children的话,就执行mountChildren操作,将children挂载到创建的dom元素上。(mountChildren的过程就又回到了前面,属于是递归调用了)
处理完children之后,mountElement最后会将el挂载到container上。
总结
其实Vue的源码真的封装的非常好,反复读可以理解一些原理层面的过程,对理解Vue真的很有帮助,加油!