runtime-core 流程总览
createApp
使用 createApp 的例子:
const App = {
name: 'App',
setup() {},
render() {
return h('div', {}, 'Hello world')
}
}
const rootContainer = document.querySelector('#root');
createApp(App).mount(rootContainer);
实现 createApp:
// runtime-core/createApp.ts
export function createApp(rootComponent) {
return {
mount(rootContainer) {
const vnode = createVNode(rootComponent);
render(vnode, container);
}
}
}
createVNode
// runtime-core/vnode.ts
export function createVNode(type, props?, children?) {
const vnode = {
type,
props,
children,
}
return vnode;
}
render
PS: 后面再考虑对比节点更新的情况,现在只实现初始渲染
// runtime-core/renderer.ts
export function render(vnode, container) {
patch(vnode, container);
}
patch
PS: 现在只实现组件和 Elemennt 这两个,后面再补充
// runtime-core/renderer.ts
function patch(vnode, container) {
// 处理组件
if(typeof vnode.type === 'object') {
processComponent(vnode, container);
// 处理 element
} else {
processElement(vnode, container);
}
}
1. processComponent
// runtime-core/renderer.ts
function processComponent(vnode, container) {
// 挂载组件
mountComponent(vnode, container);
}
mountComponent
// runtime-core/renderer.ts
function mountComponent(vnode, container) {
// 创建组件实例 instance
const instance = createComponentInstance(vnode);
// 安装组件
setupComponent(instance);
// 执行组件 render 函数,把返回的 vnode 再次传给 patch 递归
setupRenderEffect(instance, container);
}
-
createComponentInstance的功能:- 根据 vnode 创建组件实例 instance
-
setupComponent的功能:- 初始化 props
- 初始化 slots
- 执行组件的
setup函数,返回结果挂载到instance - 把组件的
render函数也挂载到instance
-
setupRenderEffect的功能:- 执行
instance.render函数 - 把返回的
vnode再次传给patch递归
- 执行
createComponentInstance
// runtime-core/component.ts
export function createComponentInstance(vnode) {
const component = {
vnode,
type: vnode.type,
}
}
setupComponent
// runtime-core/component.ts
export function setupComponent(instance) {
// TODO - initProps
// TODO - initSlots
setupStatefulComponent(instance);
}
setupStatefulComponent
// runtime-core/component.ts
function setupSatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
if(setup) {
const setupResult = setup();
handleSetupResult(instance, setupResult);
}
}
handleSetupResult
// runtime-core/component.ts
function handleSetupResult(instance, setupResult) {
if(typeof setupResult === 'object') {
instance.setupState = setupResult;
}
finishComponentSetup(instance);
}
finishComponentSetup
// runtime-core/component.ts
function finishComponentSetup(instance) {
const Component = instance.type;
if(Component.render) {
instance.render = Component.render;
}
}
setupRenderEffect
function setupRenderEffect(instance, container) {
const subTree = instance.render();
patch(subTree, container); // 把 render 返回的子 vnode 传给 patch 渲染
}
2. processElement
// runtime-core/renderer.ts
function processElement(vnode, container) {
mountElement(vnode, container);
}
mountElement
// runtime-core/renderer.ts
function mountElement(vnode, container) {
// 创建标签
const el = document.createElement(vnode.type);
const { children, props } = vnode;
// 处理标签子节点
if(typeof children === 'string') {
el.textContent = children;
} else if(Array.isArray(children)) {
mountChildren(vnode, el); // 以新建的 el 作为 children 的父容器
}
// 设置标签属性
for(const key in props) {
const val = props[key];
el.setAtrribute(key, val);
}
// 新标签添加到容器标签内部
container.append(el);
}
此时传给 vnode 的 children 只能是 string 或者是数组,且数组的每一项都需要是一个 vnode。
render() {
return h('div', {}, 'Hello')
}
// or
render() {
return h('div', {}, [
h('div', {}, 'a'),
h('div', {}, 'b'),
// 'c', // 不允许以字符串的形式作为 children 的项目
])
}
mountChildren
// runtime-core/renderer.ts
function mountChildren(vnode, container) {
vnode.forEach(v => {
// 此时的 container 已经由 rootContainer 变成了新建的 el
patch(v, container);
})
}
h 函数
// runtime-core/h.ts
export function h(vnode, props?, children?) {
return createVNode(vnode, props, children);
}
实现 rollup 打包
安装依赖
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.22.20",
"@babel/preset-typescript": "^7.23.0",
"@rollup/plugin-typescript": "^11.1.4",
"babel": "^6.23.0",
"rollup": "^3.29.4",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
rollup.config.js
import typescript from '@rollup/plugin-typescript';
export default {
input: './src/index.ts',
output: [
{
format: 'cjs',
file: 'lib/guide-mini-vue.cjs.js'
},
{
format: 'es',
file: 'lib/guide-mini-vue.esm.js'
}
],
plugins: [typescript()]
}
打包命令
"scripts": {
"build": "rollup -c rollup.config.js"
}
生成 tsconfig.json
tsc --init
处理 ts
yarn add tslib --dev
tslib 是一个由 TypeScript 团队提供的库,它提供了一些 TypeScript 编译器生成的 JavaScript 代码所需的运行时帮助函数。这些函数包括一些用于辅助类、函数和对象的实现的工具函数,以及一些用于帮助 TypeScript 编译器生成更加紧凑的输出的函数。
通过引入 tslib,可以使 TypeScript 编译器生成的 JavaScript 代码更加精简,减少重复代码,提高运行时性能。
使用打包后的文件
打包后在根目录下生成输出的文件:
mini-vue/lib/guide-mini-vue.cjs.js
mini-vue/lib/guide-mini-vue.esm.js
// example/main.js
import { App } from './App.js';
import { createApp } from '../lib/guide-mini-vue.esm.js';
const rootContainer = document.querySelector('#id');
createApp(App).mount(rootContainer);
// example/App.js
import { h } from '../lib/guide-mini-vue.esm.js';
export const App = {
name: 'App',
render() {
return h('div', { class: 'app' }, 'hello');
},
setup() {
return {}
}
}
<!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="id"></div>
<script type="module" src="main.js"></script>
</body>
</html>
至此,mini-vue - runtime-core 的核心流程已完成搭建,后面继续完善功能。