手写mini-vue3:实现初始化component的主流程

563 阅读5分钟

实现初始化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"));

这一篇文章的主要目的就是实现将一个组件渲染到浏览器上。主要实现的部分流程如图所示

image.png 下面会挨个对图中的函数进行简单的实现。

实现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脚本

image.png

然后执行yarn build即可打包我们的代码。根据实现目标的代码进行测试,会发现浏览器可以显示出hello mini-vue,为就是说我们的实现是没有问题的。