在本小节呢,我们将会去探寻一个组件的初始化流程。
我们先放一下初始化的流程图(来源于崔大)
我们可以写一个 happy path,来看看一个组件是如何实现 mount 的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./main.js" type="module"></script>
</body>
</html>
// main.js
import App from './App.js'
// 和 Vue3 的 API 命名方式一样
createApp(App).mount('#app')
// App.js
export default {
render() {
return h('div', 'hi ' + this.title)
},
setup() {
return {
title: 'mini-vue',
}
},
}
通过这个 happy path,我们就可以一窥 Vue3 的 App 初始化流程了,接下来让我们深究一下
首先,我们可以看到,最入口处其实就是 createApp
,它接收一个 rootComponent
,并内部包含一个 mount
方法,接受一个 rootContainer
下面我们就可以写了
我们目前写的是 runtime-core
的逻辑
// createApp.ts
export function createApp(rootComponent) {
return {
mount(rootContainer) {
// 在 vue3 中,会将 rootComponent 转为一个虚拟节点 VNode
// 后续所有的操作都会基于虚拟节点
// 这里就调用了一个 createVNode 的 API 将 rootComponent 转换为一个虚拟节点
// 【注意了】这个虚拟节点就是程序的入口,所有子节点递归处理
const vnode = createVNode(rootComponent)
// 调用 render 来渲染虚拟节点,第二个参数是容器【container】
render(vnode, document.querySelector(rootContainer))
},
}
}
// vnode.ts
export function createVNode(type, props?, children?) {
// 这里先直接返回一个 VNode 结构,props、children 非必填
return {
type,
props,
children,
}
}
// render.ts
export function render(vnode, container) {
// 这里的 render 调用 patch 方法,方便对于子节点进行递归处理
patch(vnode, container)
}
export function patch(vnode, container) {
// 去处理组件,在脑图中我们可以第一步是先判断 vnode 的类型
// 这里先只处理 component 类型
processComponent(vnode, container)
}
export function processComponent(vnode, container) {
mountComponent(vnode, container)
}
function mountComponent(vnode, container) {
// 通过 vnode 获取组件实例
const instance = createComponentInstance(vnode)
// setup component
setupComponent(instance, container)
// setupRenderEffect
setupRenderEffect(instance, container)
}
export function createComponentInstance(vnode) {
// 这里返回一个 component 结构的数据
const component = {
vnode
}
return component
}
export function setupComponent(instance, container) {
// 初始化分为三个阶段
// TODO initProps()
// TODO initSlots()
// 处理 setup 的返回值
// 这个函数的意思是初始化一个有状态的 setup,这是因为在 vue3 中还有函数式组件
// 函数式组件没有状态
setupStatefulComponent(instance, container)
}
function setupStatefulComponent(instance, container) {
// 这个函数的处理流程其实非常简单,只需要调用 setup() 获取到返回值就可以了
// 那么第一步我们就是要获取用户自定义的 setup
// 通过对初始化的逻辑进行梳理后我们发现,在 createVNode() 函数中将 rootComponent 挂载到了 vNode.type
// 而 vNode 又通过 instance 挂载到的 instance.vnode 中
// 所以就可以通过这里传入的 instance.vnode.type 获取到用户定义的 rootComponent
const component = instance.vnode.type
// 拿到 component 我们就可以拿到 setup 函数
const { setup } = component
// 这里需要判断一下,因为用户是不一定会写 setup 的,所以我们要给其一个默认值
if (setup) {
// 获取到 setup() 的返回值,这里有两种情况,如果返回的是 function,那么这个 function 将会作为组件的 render
// 反之就是 setupState,将其注入到上下文中
const setupResult = setup()
handleSetupResult(instance, setupResult)
}
}
function handleSetupResult(instance, setupResult) {
// TODO function
// 这里先处理 Object 的情况
if (typeof setupResult === 'object') {
// 如果是 object ,就挂载到实例上
instance.setupState = setupResult
}
// 最后一步,调用初始化结束函数
finishComponentSetup(instance)
}
function finishComponentSetup(instance) {
// 这里为了获取 component 方便,我们可以在 instance 上加一个 type 属性
// 指向 vnode.type
const component = instance.type
// 如果 instance.render 没有的话,我们就让 component.render 赋给 instance.render
// 而没有 component.render 咋办捏,其实可以通过编译器来自动生成一个 render 函数
// 这里先不写
if (!instance.render) {
instance.render = component.render
}
}
在 mountElement
中,我们还调用了 setupRenderEffect
function setupRenderEffect(instance, container) {
// 调用 render 和 patch 挂载 component
const subTree = instance.render()
// 下面就是 mountElement 了
patch(subTree, container)
}
下面我们就可以来写一下 mountElement 的逻辑了,我们知道其实一个组件从初始化到挂载到 DOM 中是有调用了两次 patch 的,第一次是 vnode
,第二次应该是 element 了,所以我们要在 patch 逻辑中加入 processElement 的逻辑
export function patch(vnode, container) {
// 去处理组件,在脑图中我们可以第一步是先判断 vnode 的类型
// 如果是 element 就去处理 element 的逻辑
// 因为两次的 vnode.type 的值不一样,所以我们就可以根据这个来进行判断了
if (typeof vnode.type === 'string') {
processElement(vnode, container)
} else if (isObject(vnode.type)) {
processComponent(vnode, container)
}
}
function processElement(vnode, container) {
// 分为 init 和 update 两种,这里先写 init
mountElement(vnode, container)
}
function mountElement(vnode, container) {
// 此函数就是用来将 vnode -> domEl 的
const { type: domElType, props, children } = vnode
// 创建 dom
const domEl = document.createElement(domElType)
// 加入 attribute
for (const prop in props) {
domEl.setAttribute(prop, props[prop])
}
// 这里需要判断children两种情况,string or array
if (typeof children === 'string') {
domEl.textContent = children
} else if (Array.isArray(children)) {
// 如果是 array 就递归调用,并将自己作为 container
mountChildren(vnode, domEl)
}
// 最后将 domEl 加入 dom 树中
container.appendChild(domEl)
}
function mountChildren(vnode, container) {
vnode.children.forEach(vnode => {
// 如果 children 是一个 array,就递归 patch
patch(vnode, container)
})
}
这样一个初始化的基本流程就结束了,顺着本篇文章,你就会捋清楚,初始化的主要工作就是初始化 component 实例,处理 setup、props、slots,以及挂载组件(UI)。