文章目录
前言
前不久,Vue3已经成为正式版了,因此打算将Vue3源码分析输出为博客,顺便巩固一下知识
对于Vue3中的代码,在博客中,我会去去除掉一些有关dev,ssr的代码,这些代码不影响我们分析流程。
对于分析的方法,我都会在之前给出放名以及路径,方便大家查看。
在每一部分之后,我会通过链接给出这一部分的注释,大家可以点击链接去查看当前部分的注释。
好了,让我们开始搭建环境吧。
分析环境搭建
开始搭建分析环境
1.获取源码
git clone一份Vue3源码到本地
git clone https://github.com/vuejs/core.git
clone之后的目录结构如下
2.安装pnpm
因为Vue3使用的包管理工具是pnpm,因此先全局安装pnpm
npm install -g pnpm
3.修改package.json
进入到package.json文件中,修改一下scripts的dev,加上–sourcemap,以便之后断点调试
"dev": "node scripts/dev.js --sourcemap",
注: 可以删除掉devDependencies中的puppeteer,安装依赖时,这个依赖太大了,经常卡着
4.安装依赖
pnpm install
5.dev打包
pnpm run dev
运行打包之后,去看看packages/vue/dist文件夹下是否有两个文件,如图
如果有的话,就代表前面的步骤成功了,ctrl + c 结束打包。
进入下一步,找入口
怎么找入口
上次vue2中我是通过打包的路径寻找入口文件的,这次打算使用断点调试的方法去寻找。
在哪打断点
找到packages/vue/expamles/composition中的todomvc.html,如图
ctrl+alt+o用浏览器打开todomvc.html (vscode的open插件)
打开F12,source,找到todomvc.html文件,找到createApp方法,在这里打上断点,如图
运行断点
刷新页面,让我们来点击这个按钮,如图
点击之后,进入到下一步,也就是说todomvc里面的createApp方法进入到了这里,
现在要找到当前文件的位置,右键点击当前文件的选项卡,有reveal in sidebar,此时就能看到当前文件的目录了,如图
此时文件路径为: packages/runtime-dom/src/index.ts
这就是我们要去往的路径
createApp
createApp方法 路径: core\packages\runtime-dom\src\index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// 下面先不看
}) as CreateAppFunction<Element>
createApp方法做了什么?
- 方法内部调用了ensureRenderer方法
- 使用ensureRenderer方法调用后的返回值去调用createApp方法
从断点给出的路径,我们成功找到了createApp方法,方法内部却是调用了ensureRenderer方法,并用其返回值再调用createApp
因此我们要去找ensureRenderer方法(找的过程我就不说了,方法的路径我都会给出的)
ensureRenderer
ensureRenderer方法 路径: core\packages\runtime-dom\src\index.ts
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
ensureRenderer方法做了什么?
- 返回一个renderer
- renderer由createRenderer方法创建
ensureRenderer方法内部返回一个renderer(渲染器),但我们这是首次执行,renderer肯定是不存在的,因此会调用后面的createRenderer方法来创建renderer,所以接着看createRenderer方法的实现
createRenderer
createRenderer方法 路径: core\packages\runtime-core\src\renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
createRenderer方法做了什么?
- 返回baseCreateRenderer方法调用后的返回值
好家伙,createRenderer内部又返回了一个baseCreateRenderer方法调用之后的返回值,行,那继续看baseCreateRenderer方法的实现
baseCreateRenderer
baseCreateRenderer方法 路径: core\packages\runtime-core\src\renderer.ts
// 前面的2000行实现先忽略,先看返回值
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
baseCreateRenderer方法做了什么?
- 返回了一个对象,此对象就是渲染器对象,并且拥有三个方法: render、hydrate、createApp: createAppAPI()
这个baseCreateRenderer函数,2200行代码,极其恐怖哦,先忽略内部实现,让我们看看这个函数的返回值
返回值是一个包含render,hydrate,createApp三个方法的渲染器对象(注意: createApp的值是createAppAPI执行后返回的函数)
这个渲染器对象会作为createRenderer的返回值赋值给ensureRenderer内部的renderer
而renderer对象作为ensureRenderer方法调用后的返回值回到createApp内部,此时createApp内部如下
export const createApp = ((...args) => {
// renderer是ensureRenderer方法的返回值
const app = renderer.createApp(...args)
}) as CreateAppFunction<Element>
所以,此时createApp内部是通过renderer渲染器调用createApp方法
我们知道渲染器对象上的createApp方法的值是createAppAPI方法调用后的返回值,因此来看createAppAPI的实现
createAppAPI
createAppAPI方法 路径: core\packages\runtime-core\src\apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// 忽略内部实现
}
}
createAppAPI方法做了什么?
可以看到,createAppAPI内部返回了一个createApp方法
也就是说baseCreateRenderer返回的渲染器对象中的createApp方法就是调用这createAppAPI方法返回的createApp方法
结合上一步的renderer.createApp(…args)来看,renderer的createApp方法其实就是调用createAppAPI方法返回的createApp方法
因此可以得出,todomvc里的createApp方法其实就是执行当前createAppAPI内部的createApp方法
文字描述可能不太好理解,可以结合代码和图和注释(通过github commit的diff)来看 如图
知道了实际调用的是哪个createApp之后,那我们来看这个返回的方法的实现
createAppAPI返回的createApp
createAppAPI方法内部返回的方法(其实也就是我们调用createApp) 路径: core\packages\runtime-core\src\apiCreateApp.ts
function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {},
set config(v) {},
use(plugin: Plugin, ...options: any[]) {},
mixin(mixin: ComponentOptions) {},
component(name: string, component?: Component): any {},
directive(name: string, directive?: Directive) {},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {},
unmount() {},
provide(key, value) {}
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
createApp方法做了什么?
- 初始化app实例,上面有一些属性和方法
- 返回app实例
可以看到,此方法就是初始化一个app实例并返回,app上带有的这几个方法,是不是在vue3中经常使用的?
use,mixin,component,directive,mount,provide以及一些属性
到此,createAppAPI的createApp方法已经执行完毕,返回了一个app实例
但是实际调用的createApp还没执行完,所以接着看后面的操作
mount
继续看之前我们进入的createApp内部
createApp方法内部 路径: core\packages\runtime-dom\src\index.ts
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {}
return app
在执行完createApp之后还做了什么?
省去了一些dev和实现,能看到在我们经过上面的返回的渲染器执行createApp之后,还执行了相关操作
- 解构出mount方法,以便添加副作用
- 对app.mount添加副作用,并执行解构出的mount
- 返回app实例
对1、2两步不懂得可以看我的注释 解构mount,添加副作用并执行
此时,用户调用的createApp方法执行完毕,得到一个app实例,此实例的mount方法被添加了副作用。
那么用户在调用createApp后会做什么?通常都是链式调用mount方法
createApp().mount()
因此我们来看mount方法,先不管副作用
mount执行
在第二步里面执行了mount方法,因此我们来看mount方法
mount方法 路径: core\packages\runtime-core\src\apiCreateApp.ts
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
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.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
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)
}
//todo To: getExposeProxy
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
}
mount方法做了什么?
- 首先判断isMounted变量,在首次挂载时,isMounted为false,因此走if分支
- 通过createVnode方法创建vnode
- 通过render方法将vnode转换为dom并挂载 (这里的render方法是baseCreateRenderer返回的render方法)
- 将isMounted变量置为true,下次再进来就走else分支报错了
- 返回vnode.component.proxy这些
可以看到最主要的步骤就是通过render方法将vnode进行挂载,因此来看render方法的实现
这一部分的注释在: mount
render
这里的render方法是baseCreateRenderer中的render方法
render方法 路径: core\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 {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
render方法做了什么?
- 判断vnode(新的虚拟节点)是否为空
- 如果vnode为空,并且container._vnode(老的虚拟节点)存在,代表要执行卸载操作,因此执行unmout方法
- 如果vnode不为空,调用patch方法
我们这次分析,是组件初始化挂载,所以vnode肯定是有值的,因此会走else分支,执行patch方法,继续看patch方法的实现
这一部分的注释在:render
patch
patch方法 路径: core\packages\runtime-core\src\renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 上面的忽略,先看大体功能
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment()
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement()
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent()
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process()
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process()
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// 后面忽略
}
patch方法做了什么?
- 判断n2的类型
- 根据类型执行相应的process方法
忽略一些其他处理,让我们来看主要功能,根据n2的类型来判断(n2就是新的vnode)
根据n2类型来执行相应的process+Type方法
因为此次分析,我们是组件初始化挂载,因此是component类型,所以会执行processComponent(),所以来看看processComponent实现
这一部分的注释在这 patch
processComponent
processComponent方法 路径: core\packages\runtime-core\src\renderer.ts
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
processComponent方法做了什么?
- 判断n1(老的虚拟节点)是否为空
- 如果n1为空,并且shapeFlags是COMPONENT_KEPT_ALIVE类型(补丁,涉及位运算,之后会说),会执行activate方法
- 如果n1为空,shapeFlags不是COMPONENT_KEPT_ALIVE类型,执行mountComponent方法
- 如果n1不为空,执行updateComponent方法
首先判断n1,如果n1是null,然后判断是否是keepAlive组件,如果是keepAlive组件,用activate将组件从虚拟容器中移出来
如果不是keepAlive组件,就执行mountComponent进行组件挂载
如果n1不为null,那么就是更新,因此会调用updateComponent
这次分析是组件初始化,所以n1为null,并且不是keepAlive组件,因此调用mountComponent进行组件挂载,所以来看用mountComponent方法的实现
这一步的注释: processCompoent
mountComponet
mountComponent方法 路径: core\packages\runtime-core\src\renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance())
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
mountComponent方法做了什么?
- createComponentInstance
- setupCompoent
- setupRenderEffect
可以看到把一些其他处理去掉之后,mountComponent调用了三个方法
这三个方法及其重要,因此将其留在下一章中讲解
这一部分的注释在: mountComponent
总结
这一章做了什么?
- 获取源码,通过在createApp方法打断点调试,找到ensureRenderer方法
- 经过一系列复杂的操作,得知调用的createApp实际是调用createAppAPI方法返回的createApp方法
- 调用完createApp方法后,解构出mount方法,对其添加副作用,并执行mount方法
- mount方法内部调用了render方法去挂载
- render方法内部调用了patch方法
- patch方法根据类型执行相应操作,这一次执行了processComponent方法
- processComponent根据n1是否存在,去执行相应操作,这一次执行mountComponent方法进行挂载
- mountComponent方法调用了三个重要的方法,这三个方法下一期会讲解(todo)
这篇留的todo 如图