【vue3 原理】初始化组件流程
摘要
这篇文章的内容包括:调用 createApp 时初始化组件的主流程、虚拟节点的本质及作用和拆箱的概念
准备 demo
在编写核心代码先准备测试用的 demo
index.html 作为 Vue 的容器,引入入口文件 main.js,并指定为 type 为 module,接下来使用 ESM 模块规范
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>
以上文件有三个注意点:
- 需要一个
id为app或者任意字符串的元素,作为应用的根容器元素 - 引入入口文件
main.js,应用初始化的逻辑都会从这里开始 - 指定
type为module是因为入口文件使用ESM模块规范
main.js
createApp(App).mount('#app')
其作用和前面提到一样,初始化应用
用法和 Vue 一样:
createApp传入根组件创建 app 实例- 然后使用 app 实例上的
mount方法将应用挂载到根容器元素上
App.js
export const App = {
setup(){
return {
msg: 'World'
}
},
render(){
return h('div', {}, 'Hello, ' + this.msg)
}
}
App是根组件,本质是一个对象- 导出给
main.js进行挂载 - 使用组合式 API的写法
render最终要返回虚拟节点,h函数就是用来创建虚拟节点的
创建 app 实例
runtime-core/createApp.js
export function createApp(rootComponent){
return {
mount(rootContainer){
const vnode = createVNode(rootComponent)
}
}
}
- 接收根组件并返回带有
mount方法的对象,这个对象就是app实例 mount方法接受一个根容器,mount方法内部需要先调用createVNode将组件对象转换成vnode对象,即虚拟节点,因为之后不管是视图的初始化操作还是更新操作都要基于vnode进行,本质就是操作对象,后面统一称呼为vnode
创建虚拟节点
runtime-core/vnode.js
export function createVNode(type, props, children) {
const vnode = {
type,
props,
children
}
return vnode
}
- 虚拟节点本质是对象
type属性表示虚拟节点的类型,因为除了组件类型还有元素等类型需要区分处理props表示组件或元素的接收的propschildren表示组件或元素的子节点
h.js
export function h(type, props, children){
return createVNode(type, props, children)
}
前面[[初始化组件流程#准备 demo|demo]]使用到的h函数实际就是createVNode的简写形式,因为组件的render方法一定要返回虚拟节点,简写形式更加方便用户调用
渲染根组件
runtime-core/createApp.js
export function createApp(rootComponent){
return {
mount(rootContainer){
const vnode = createVNode(rootComponent)
+ render(vnode, rootContainer)
}
}
}
- 将前面得到的
vnode交给render函数处理接下来的操作,表示开始初始化根组件, rootContianer作为根容器元素,后面挂载时需要用到
runtime-core/renderer.js
export function render(vnode, container){
patch(vnode, container)
}
render函数拿到根组件vnode要做的只有一件事,就是调用patch函数对根组件进行拆箱,先继续往下看
拆箱
runtime-core/renderer.js
export function patch(vnode, container){
processComponent(vnode, container)
}
该函数的作用是拆箱,
因为组件由一层层嵌套的结构组成,组件既可以嵌套普通元素,也可以嵌套组件
组件可以看成是一个箱子,这个箱子里包含了一些物品的同时也包含了另外一些小箱子,这些小箱子里又包含了另外一些物品,拆箱顾名思义就是将箱子一个个打开,把里面的物品按照原来的层次结构拿出来摆放在桌子上,而这个桌子就是我们指定的根容器
实际表现就是根据vnode的类型处理不同的逻辑,然后决定是否需要递归拆箱,这里我们先实现处理vnode是组件类型的逻辑
处理组件
runtime-core/renderer.js
export function processComponent(vnode, container){
mountComponent(vnode, container)
}
处理组件也分两种情况:
- 视图初始化
- 视图更新
这里先关注初始化
挂载组件
runtime-core/mountComponent.js
export function mountComponent(vnode) {
const instance = createComponentInstance(vnode)
}
挂载组件首先要将基于组件类型的vnode转换成组件实例,因为和普通元素不同,作为组件之后要保存各种属性,比如setup返回的结果,是否已挂载、组件代理对象等等,后面会一点点补全
创建组件实例
runtime-core/component.js
export function createComponentInstance(vnode){
const component = {
vnode
}
return component
}
组件实例就是一个对象,目前只需要保存vnode的数据即可
初始化组件
runtime-core/component.js
export function mountComponent(vnode, container){
const instance = createComponentInstance(vnode)
+ setupComponent(instance)
}
前面拿到了组件实例,接下来需要初始化组件得到各种数据,保存在组件实例上
runtime-core/component.js
export function setupComponent(instance){
setupStatefulComponent(instance)
}
初始化组件时有两种情况需要判断:
- 有状态的组件
- 无状态的函数式组件
暂时先考虑处理有状态的组件
初始化有状态的组件
runtime-core/component.js
export function setupStatefulComponent(instance){
const { setup,render } = instance.vnode.type
const setupResult = setup()
instance.setupState = setupResult
instance.render = render
}
- 这里假设组件保证有
setup方法和render, - 调用
setup获取返回结果作为状态保存到组件实例上 render只保存到组件实例上,等到后面调用
调用组件 render
runtime-core/component.js
export function mountComponent(vnode, container){
const instance = createComponentInstance(vnode)
setupComponent(instance)
+ setupRenderEffect(instance, container)
}
runtime-core/renderer.js
export function setupRenderEffect(instance, container){
const subTree = instance.render()
patch(subTree, container)
}
- 经过前面初始化组件后, 组件实例上已经有状态和
render方法 - 调用
render获取下一层的vnode进行递归拆箱
总结
经过上面的步骤,我们已经得到了初始化组件的大致流程,用一张流程图概括如下:

预告
到这里我们还无法从视图上看到效果,因为对根组件拆箱后只是得到了下一个层元素类型的vnode,没有实现对元素类型vnode的渲染逻辑,下一节我们会实现这一部分
转载自:https://juejin.cn/post/7266421018125697065