实现初始化component的主流程
mini-vue3的同步代码实现点击这里
mini-vue3的所有文章点击这里
对reactivity
的基本实现已经告一段落,如果还没有看之前章的可以点击上面的链接进行观看。虽然还有一些api没有实现,比如说watch
,但是watch
的实现本质上也是使用了reactiveEffect
这个类进行实现的,感兴趣的可以自己尝试进行实现。
完成目标
<!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>
<!-- 这里有个id为app的根容器 -->
<div id="app"></div>
<!-- 这里以模块化的方式引用了index文件 -->
<script src="./index.js" type="module"></script>
</body>
</html>
// index.js
import { h, createApp } from "../../../lib/mini-vue.esm.js";
const App = {
setup() {},
render() {
return h("div", { class: "aaa" }, "hello mini-vue");
},
};
// 调用createApp返回的对象的mount方法将APP组件渲染到浏览器上
createApp(App).mount(document.getElementById("app"));
这一篇文章的主要目的就是实现将一个组件渲染到浏览器上。主要实现的部分流程如图所示
下面会挨个对图中的函数进行简单的实现。
实现h函数以及createVNode函数
// runtime-core/vnode.ts
export function createVNode(type, props: any = {}, children: any = []) {
const vnode = {
type,
props,
children
}
return vnode
}
// rumtime-core/h.ts
import { createVNode } from './createVNode'
export function h(type, props: any = {}, children: any = []) {
return createVNode(type, props, children)
}
createVNode
实际上就是返回一个对象,而h
函数实际上就是调用createVNode函数
。
实现createApp函数
// runtime-core/createApp.ts
import { createVNode } from './createVNode'
import { render } from './renderer'
export function createApp(rootComponent) {
return {
mount(container) {
const vnode = createVNode(rootComponent)
render(vnode, container)
}
}
}
createApp
会返回一个带有mount
方法的对象,mount
方法首先创建一个vnode,然后调用render方法,并将vnode以及container作为参数传进去。
实现render方法
// runtime-core/renderer.ts
export function render(vnode, container) {
patch(vnode, container)
}
render
方法只是调用了patch
方法。而patch
方法是可以后续被递归调用的。
patch方法的实现
function patch(vnode, container) {
const { type } = vnode
if (isObject(type)) {
processComponent(vnode, container)
} else {
processElement(vnode, container)
}
}
patch
函数就是判断当前传入的vnode
是什么类型,然后根据对应的类型进行对应处理。因为一开始我们传递的App
是对象类型,所以一开始会进入processComponent
函数。
实现processComponent函数
// renderer.ts
function processComponent(vnode, container) {
mountComponent(vnode, container)
}
processComponent
就是调用了mountComponent
函数。
实现mountComponent函数
// renderer.ts
import { createComponentInstance, setupComponent } from './components.ts'
function mountComponent(vnode, container) {
// 创建组件实例, 组件实例可以用来保存组件的一些状态信息
const instance = createComponentInstance(vnode)
setupComponent(instance)
setupRenderEffect(instance, container)
}
在mountComponent
函数中,调用了createComponentInstance
用于创建组件实例,调用了setupComponent
用于设置组件状态,调用setupRenderEffect
用于设置运行副作用函数的。
实现createComponentInstance函数和setupComponent函数
// runtime-core/components.ts
export function createComponentInstance(vnode) {
const instance = {
type: vnodel.type,
vnode,
// 用于判断是否被挂载过
isMounted: false
}
return instance
}
export function setupComponent(instance) {
// initProps
// initSlots
setupStatefulComponent(instance)
}
createComponentInstance
实际上就是返回了一个对象。而setupComponent
会初始化props和初始化插槽(后续在进行实现)。然后调用setupStatefulComponent
函数用于设置有状态组件(我们通常写的vue组件都是有状态组件,而函数式组件就是没有状态的)。
实现setupStatefulComponent
// components.ts
function setupStatefulComponent(instance) {
// 这里的component实际上就是我们传入的App对象
const component = instance.type
const { setup } = component
if (setup) {
// 这里就简单的把setup认为都是函数
const setupResult = setup()
handleSetupResult(instance, setupResult)
}
}
setupStatefulComponent
函数主要就是执行判断是否有setup
如果有执行它,然后调用handleSetupResult
函数。而handleSetupResult
主要就是对setup的返回值进行判断,然后在调用finishComponentSetup
函数。
实现handleSetupResult和finishComponentSetup
// components.ts
function handleSetupResult(instance, setupResult) {
if (isObject(setupResult)) {
instance.setupResult = setupResult
}
finishComponentSetup(instance)
}
function finishComponentSetup(instance) {
const component = instance.type
if (component.render) {
instance.render = component.render
}
}
handleSetupResult
主要就是将setupResult
挂载到instance
上,然后调用finishComponentSetup
。而finishComponentSetup
主要就是将render函数赋值给instance。执行完finishComponentSetup
就会返回到mountComponent
函数,然后继续执行setupRenderEffect
函数。
实现setupRenderEffect函数
// renderer.ts
function setupRenderEffect(instance, container) {
const { isMounted } = instance
if (!isMounted) {
// 挂载逻辑
const subTree = instance.subTree = instance.render()
patch(subTree, container)
instance.isMounted = true
} else {
// TODO更新逻辑
}
}
setupRenderEffect
函数会根据isMounted
来判断当前是要执行挂载操作,还是执行更新操作。这里因为第一次进入这里,所以是会执行挂载操作
。而在挂载操作的分支内,会执行instance.render方法,实际上这里的instance.render方法就是我们App对象的render方法。然后将render方法返回的vnode赋值给instance.subTree
。然后重新调用patch,将subTree作为参数。而这次的patch函数中因为type
为div,不是一个对象,所以会走processElement
这个函数。而processElement
会调用mountElement
函数
实现processElement和mountElement函数
// render.ts
function processElement(vnode, container) {
mountElement(vnode, container)
}
function mountElement(vnode, container) {
const { type, props, children } = vnode
// 创建dom元素,并将该元素挂载到vnode上,以便后续使用
const el = vnode.el = document.createElement(type)
// 简单对props进行处理
for (const key in props) {
el.setAttribute(key, props[key])
}
// 处理children
if (typeof children === 'string') {
el.textContent = children
} else if (Array.isArray(children)) {
mountChildren(vnode, el)
}
container.appendChild(el)
}
function mountChildren(vnode, container) {
vnode.forEach(child => {
patch(child, container)
})
}
mountElement
主要进行创建元素,添加props属性,根据children类型不同做不同处理。如children是string类型,那么直接调用el.textContent
赋值即可。如果children是数组类型,那么调用mountChildren
进行处理即可,注意mountChildren的container为新创建的el元素。而mountChildren
实际上就是对vnode的children数组进行遍历然后逐个进行patch。
对代码进行打包
通过上面的实现,我们已经简单的完成了初始化Component,而下面我们需要对这些代码进行打包。然后引用我们打包后的代码在浏览器进行查看。这里我们使用rollup
进行打包。
安装依赖
yarn add rollup @rollup/plugin-typescript tslib --dev
配置rollup.config.js文件
import typescript from "@rollup/plugin-typescript";
export default {
input: "./src/index.ts",
output: [
{
format: "cjs",
file: "lib/mini-vue.cjs.js",
},
{
format: "es",
file: "lib/mini-vue.esm.js",
},
],
plugins: [typescript()],
};
在package.json添加build脚本
然后执行yarn build
即可打包我们的代码。根据实现目标的代码进行测试,会发现浏览器可以显示出hello mini-vue
,为就是说我们的实现是没有问题的。