前言
准备一个demo
<div id="app">
{{ a }}
</div>
...
createApp({
setup() {
const data = reactive({
a: 1111
})
return {
...toRefs(data)
}
}
}).mount('#app')
这段代码中的 createApp
和 mount
两个函数就可以完成 vue
的初始化流程,从这里也能看到分成了两个部分: 一个是创建 app
实例,一个是对这个实例进行挂载。那就下来一点一点的看一下。
createApp 创建实例
函数主要做了两件事
- 创建
app
实例 - 重写
mount
函数
创建 vue
实例仅用了一行代码
const app = ensureRenderer().createApp(...args)
从字面意思可以知道在一个渲染器中有一个 createApp
函数。
那么 ensureRenderer
函数都做了什么呢?
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
可以看到在这里用来获取 renderer
,如果不存在则通过 createRenderer
创建一个。那么 createRenderer
做了什么呢?
跟踪代码最终可以看到 createRenderer
最终调用的是 baseCreateRenderer
函数。这个函数是一个很庞大的函数,因为我们的主线任务是得到一个渲染器,然后从渲染其中拿到 createApp
函数。那么我们就可以先去看一下他的返回值:
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
这里就可以清晰的看到我们调用的 createApp
函数其实调用了 createAppAPI
函数,在调用这两个函数的时候传入了 render
和 hydrate
两个函数。这两个函数都是在 baseCreateRenderer
函数中定义的,render
函数是用来卸载和做更新节点的;hydrate
函数表示注水,是在 SSR
的时候会用到,这里暂时不关心。
接下来就可以看一下 createAppAPI
函数了:
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
// vue 实例
const app: App = (context.app = {
//...
})
// 将 app 实例 return
return app
}
}
createAppAPI
函数接收两个参数,但是函数内部直接 return
了 createApp
函数。在 createApp
函数中定义了上下文也创建了 app
实例并 return
。在 app
中定义了一些我们常见的函数其中就有 mount
函数。
至此,创建 app
实例这一步我们就搞清楚了:
- 得到一个 renderer
- 从 renderer 中拿到 createApp 并调用得到 app 实例
重写 mount
函数主要是将选择器转为 DOM
节点作为容器然后再去调用原来的都 mount
函数。
接下来就来看一下 app
中定义为 mount
函数是如何进行挂载的。# 前言
准备一个demo
<div id="app">
{{ a }}
</div>
...
createApp({
setup() {
const data = reactive({
a: 1111
})
return {
...toRefs(data)
}
}
}).mount('#app')
这段代码中的 createApp
和 mount
两个函数就可以完成 vue
的初始化流程,从这里也能看到分成了两个部分: 一个是创建 app
实例,一个是对这个实例进行挂载。那就下来一点一点的看一下。
createApp 创建实例
函数主要做了两件事
- 创建
app
实例 - 重写
mount
函数
创建 vue
实例仅用了一行代码
const app = ensureRenderer().createApp(...args)
从字面意思可以知道在一个渲染器中有一个 createApp
函数。
那么 ensureRenderer
函数都做了什么呢?
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
可以看到在这里用来获取 renderer
,如果不存在则通过 createRenderer
创建一个。那么 createRenderer
做了什么呢?
跟踪代码最终可以看到 createRenderer
最终调用的是 baseCreateRenderer
函数。这个函数是一个很庞大的函数,因为我们的主线任务是得到一个渲染器,然后从渲染其中拿到 createApp
函数。那么我们就可以先去看一下他的返回值:
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
这里就可以清晰的看到我们调用的 createApp
函数其实调用了 createAppAPI
函数,在调用这两个函数的时候传入了 render
和 hydrate
两个函数。这两个函数都是在 baseCreateRenderer
函数中定义的,render
函数是用来卸载和做更新节点的;hydrate
函数表示注水,是在 SSR
的时候会用到,这里暂时不关心。
接下来就可以看一下 createAppAPI
函数了:
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
// vue 实例
const app: App = (context.app = {
//...
})
// 将 app 实例 return
return app
}
}
createAppAPI
函数接收两个参数,但是函数内部直接 return
了 createApp
函数。在 createApp
函数中定义了上下文也创建了 app
实例并 return
。在 app
中定义了一些我们常见的函数其中就有 mount
函数。
至此,创建 app
实例这一步我们就搞清楚了:
- 得到一个 renderer
- 从 renderer 中拿到 createApp 并调用得到 app 实例
重写 mount
函数主要是将选择器转为 DOM
节点作为容器然后再去调用原来的都 mount
函数。
接下来就来看一下 app
中定义为 mount
函数是如何进行挂载的。
mount 进行挂载
精简后的 mount
函数如下
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 初始化流程
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
render(vnode, rootContainer, isSVG)
}
isMounted = true
// 根容器 #app 的所属
app._container = rootContainer
// 返回的是组件的一个代理,因为这个组件要变成一个可追踪的响应式对象
return vnode.component!.proxy
}
主要是将根据 rootComponent
创建了一个 vnode
,然后调用 render
函数来进行渲染,最后会 return vnode.component.proxy
。
接下来看主要的 render
函数
render 函数
这里的 render
函数是在调用 createAppAPI
函数的时候传入的,也就是在 baseCreateRenderer
中定义的 render
,精简后的 render
:
const render: RootRenderFunction = (vnode, container, isSVG) => {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
container._vnode = vnode
}
主要是判断了走卸载逻辑还是更新逻辑,如果 vnode
不存在则是 ummount
卸载逻辑,反之是 patch
更新逻辑。
patch 函数
patch
函数的前三个参数是 n1、n2、container
分别表示oldVNode
、newVNode
和 container
。
patch
的主要作用就是根据新的 vnode
的类型来判断是一个什么节点,然后针对不同类型的节点来调用不同的函数来比对新旧节点并渲染。
根据 demo
中的调用 createApp
函数时传入的是一个对象,所以被认为是一个组件则会调用 processComponent
来处理。
createApp({/* 这里就是 component */})
const patch: PatchFn = (
n1, // 旧 vnode
n2, // 新 vnode
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = false
) => {
const { type, ref, shapeFlag } = n2
if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
processComponent 函数
在这个函数的主线就是根据是否有旧 vnode
来判断是更新还是初始化挂载,根据我们的主题,只关心 mountComponent
函数就好。
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
) => {
// 挂载流程
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
mountComponent 函数
函数中根据当前 vnode
创建了组件实例 instance
,然后分别将 instance
传入 setupComponent
函数和 setupRenderEffect
函数。
setupComponent
函数主要是用来初始化当前实例上的 props
和 slots
并调用 setup
函数
setupRenderEffect
函数是将当前实例的渲染函数创建为响应式的
const mountComponent: MountComponentFn = (
initialVNode, // 初始化的 vnode / 新的 vnode, 对应的 n2
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
setupComponent(instance)
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
setupComponent 函数
函数中初始化了 props
和 slots
,后边还调用了 setupStatefulComponent
函数,用来调用 setup
函数
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
const { props, children } = instance.vnode
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
setupStatefulComponent(instance, isSSR)
return setupResult
}
接下来看看 setupStatefulComponent
函数
setupStatefulComponent 函数
函数中的主要操作:
对实例上的
ctx
进行代理并绑定到instance
的proxy
属性上instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
对实例上的 ctx 进行代理,未来对 ctx 操作将是响应式的
我们在使用的时候需要使用 instance.proxy 对象
因为 instance.ctx 在 prod 和 dev 坏境下是不同的
创建
setupContext
上下文const setupContext = (instance.setupContext = createSetupContext(instance)
调用
setup
,并在调用的时候传入props
和setupContext
,这也就意味着在使用setup
函数的时候会有这两个参数。const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [instance.props, setupContext] )
接下来又调用了
handleSetupResult
函数对setupResult
进行处理
handleSetupResult 函数
函数中如果 setupResult
是 function
类型,则会把 setupResult
赋值到 instance.render
渲染函数来处理
如果是 object
类型,则将通过 proxyRefs
代理 setupResult
的结果赋值给 instance.setupState
。
instance.setupState = proxyRefs(setupResult)
proxyRefs 的作用就是把 setupResult 对象做一层代理
方便用户直接访问 ref 类型的值
比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
这里也就是官网里面说到的自动解构 Ref 类型
demo 中的 setupResult
是一个对象,所以会被 proxyRefs
代理。
最终还调用了 finishComponentSetup
函数。
finishComponentSetup 函数
在函数中做了两件事
- 如果
instance
中没有渲染函数则通过编译器根据component.template
生成一个并赋值给instance.render
- 兼容
2.x
中的options API
到这一步 setupComponent 函数里的主流程就看完了,接下来可以返回去看 setupRenderEffect 函数了。
setupRenderEffect 函数
这个函数中主要就是定义了 instance.update
,其值就是 effect
函数的返回值,也就是一个响应式的函数 activeEffect
。
const setupRenderEffect = () => {
// create reactive effect for rendering
instance.update = effect(
function componentEffect() {
// ...
}, prodEffectOptions)
}
在调用 effect
的时候传入了 componentEffect
函数,componentEffect
就是要被收集的依赖。
还传入了一个 prodEffectOptions
,在 prodEffectOptions
中有调度器 scheduler
,它的作用就是在触发依赖的时候优先使用 scheduler
来触发我们的依赖。详情可以看 Vue3 Reactive 源码学习。
在 componentEffect
函数中进行了初始化挂载和更新时 patch。
初始化的过程是:
执行
beforeMount
钩子invokeArrayFns(bm)
获取
subTree
并递归调用patch
来处理subTree
patch( null, subTree, container, anchor, instance, parentSuspense, isSVG )
执行
mounted
钩子queuePostRenderEffect(m, parentSuspense)
当执行完这些的时候 APP 实例就已经被成功的挂载到 DOM 上了。
总结
- 先创建
vue
实例,然后将实例进行挂载。 - 在挂载过程中根据传入的组件对象创建对应的虚拟
DOM
。 - 根据虚拟
DOM
创建对应的组件实例。 - 然后初始化
props
和slots
并调用setup
函数。 - 生成渲染函数
- 兼容
2.x
版本的options API
的处理 - 执行
beforeMount
钩子函数 - 获取
subTree
并调用patch
处理 - 调用
mounted
钩子函数
在整个挂载流程中遵循 深度优先。