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