手写 mini-vue3 - runtime-core 流程(二)

142 阅读3分钟

runtime-core 流程总览

image.png

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 的功能:

    1. 根据 vnode 创建组件实例 instance
  • setupComponent 的功能:

    1. 初始化 props
    2. 初始化 slots
    3. 执行组件的 setup 函数,返回结果挂载到 instance
    4. 把组件的 render 函数也挂载到 instance
  • setupRenderEffect 的功能:

    1. 执行 instance.render 函数
    2. 把返回的 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);
}

此时传给 vnodechildren 只能是 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 的核心流程已完成搭建,后面继续完善功能。