搭建调试环境
为了弄清楚 Vue3 的初始化,倡议先克隆 Vue3 到本地。
git clone https://github.com/vuejs/vue-next.git
装置依赖
npm install
批改 package.json,将 dev 命令加上 --sourcemap
不便调试,并运行 npm run dev
// package.json
...
"scripts": {
"dev": "node scripts/dev.js --sourcemap",
...
}
...
在 packages/vue 目录下减少 index.html,内容如下
<!-- index.html -->
<div id="app">
{{count}}
</div>
<script src="./dist/vue.global.js"></script>
<script>
Vue.createApp({setup() {const count = Vue.ref(0);
return {count};
}
}).mount('#app');
</script>
在浏览器关上 index.html,程序失常运行则能够开始进行下一步调试。
进行调试
如果在上面的流程中迷失了方向,倡议先看一下结尾的总结,再回过头来看这一段。
在 createApp 的地位打上断点,而后刷新页面进断点,开始调试。
具体大家能够自行调试,我在这里就大略形容一下初始化流程。
createApp
进入 createApp 外部,会跳转到 packages/runtime-dom/src/index.ts 的 createApp,执行实现返回 app 实例。
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {const app = ensureRenderer().createApp(...args)
...
return app
}) as CreateAppFunction<Element>
接下来持续看 ensureRenderer 的实现。
// packages/runtime-dom/src/index.ts
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
这里的 renderer 是个单例,初始时会调用 createRenderer 创立,再持续深刻。
来到 packages/runtime-core/src/renderer.ts,能够看到 createRenderer 又会调用 baseCreateRenderer。
// packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {return baseCreateRenderer<HostNode, HostElement>(options)
}
baseCreateRenderer 的实现有 2000 行,咱们只须要关注几个关键点就能够了。
其返回值也就是 ensureRenderer() 的返回值
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此处省略 2000 行
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
接下来回到开始的地位,在执行完 ensureRenderer() 后会接着执行 createApp,这个 createApp 就是上一步返回的 createApp,再接着看看做了哪些工作。
// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {const app = ensureRenderer().createApp(...args)
...
return app
}) as CreateAppFunction<Element>
ensureRenderer 返回的 createApp 由 createAppAPI 实现,接下来再看看 createAppAPI 是如何实现的吧。
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {return function createApp(rootComponent, rootProps = null) {
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) {}})
return app
}
}
能够看到 createAppAPI 会返回一个 createApp 函数,也就是咱们调用 createApp,当 createApp 执行完之后会返回 app 实例。app 实例还会有 use、mixin、component、directive 等办法,能够为全局 app 增加一些扩大。
案例如下,传入的第一个参数为上一步的 rootComponent 也就是根组件。
// index.html
const app = Vue.createApp({})
.use(xxx)
.component(xxx)
.mount(xxx)
mount
createApp 创立 app 实例后,要渲染到页面上还须要调用 mount。接下来看看 mount 又做了什么工作。还是在 createAppAPI 外部
// packages/runtime-core/src/apiCreateApp.ts
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
...
if (isHydrate && hydrate) {hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
...
return vnode.component!.proxy
} else if (__DEV__) {// 开发环境正告揭示,app 不能够反复挂载}
}
最终会执行 render(vnode, rootContainer, isSVG)
这一行代码,接下来看看调用 createAppAPI 时传入的 renderer。
回到 baseCreateRenderer 中,能够看到在 return 时调用 createAppAPI 传入的 renderer。
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此处省略 2000 行
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
其中 renderer 在 baseCreateRenderer 中定义了
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
}
在 index.html 的例子中,第一次执行 render 时 vnode 是由 rootComponent
创立进去,rootComponent 则是 createApp 时传入的对象。
container
为 app 容器,也就是 id 为 app 的 div , container._vnode
为 undefined
。
所以最终会进入 patch。
patch 的逻辑同样位于 baseCreateRenderer 中。代码太长了,这里就讲一下思路。在 patch 中会判断 vnode 的 type 或 shapeFlag 执行对应的操作。
因为第一次 patch 时,vnode 是一个组件,会进入 ShapeFlags.COMPONENT
的判断内,执行 processComponent
进行组件的解决。
而后会触发 mountComponent
挂载组件,从而触发 setupComponent(instance)
初始化组件的 props、slots、setup 等将须要 proxy 代理的数据做好筹备,以及将 template 进行编译为 render。
// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-creaate the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
...
setupComponent(instance)
...
}
...
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
...
}
紧接着会执行 setupRenderEffect
,该办法会将渲染函数封装成一个副作用,当依赖的响应式数据发生变化时,会主动从新执行。
重点关注 componentUpdateFn
,代码太长了,这里也简略讲下吧,第一次会执行 instance.isMounted
为 undefined,则会进入创立流程,其中会执行 const subTree = (instance.subTree = renderComponentRoot(instance))
创立子树,而后通过 patch 递归创立子节点,完结后 instance.isMounted = true
。
一旦依赖发生变化,componentUpdateFn
会被从新执行,instance.isMounted
为 true,则会尽心更新的解决,具体就不再开展了。
至于为什么依赖发生变化 componentUpdateFn
会被从新执行,这个咱们留在下一篇文章中介绍,记得关注我。
总结
- 在 index.html 调用
createApp
时会先通过ensureRenderer
和baseCreateRenderer
生成上面的对象
// baseCreateRenderer 返回值
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
- 持续调用
baseCreateRenderer
返回的createApp
,这里的createApp
实际上调用的是createAppAPI
返回的函数。
createAppAPI
执行实现返回 app 实例。
- index.html 在创立好 app 后接着调用
mount
进行挂载,mount
的实现在createAppAPI
外部。
mount
执行时会调用 render
函数,该 render
在 baseCreateRenderer
传入。
render
则开始patch
进行渲染,patch
外部会进行递归渲染子节点。
以上就是 Vue3 的 createApp 和 mount 的大抵流程。至于第一步为什么须要通过 ensureRenderer
和 baseCreateRenderer
?
baseCreateRenderer
次要是平台无关的逻辑解决,寄存在 runtime-core 中。
当 patch
的时候须要操作 dom,则会调用内部传入的办法进行操作,这样就能够更不便实现跨端。
ensureRenderer
寄存在 runtime-dom 中,次要为 baseCreateRenderer
提供一系列 dom 操作的函数。
如果咱们要自定义渲染器,那么只须要实现 ensureRenderer
即可。而不是像 Vue2 须要 fork 一份,大大提高了 Vue3 的利用范畴。
好了,这篇文章就水到这里吧。如有谬误的中央,心愿还能在评论区指出,感激!
下篇文章将解析 Vue3 的响应式原理,如果有趣味的话别忘了关注我呀,咱们一起学习、提高。