vue3对组件以及element元素初始化的流程原来是这样实现的

前言

上一篇博客vue3响应式原理的内容得到了大家的认可以及支持,在此非常感谢家人们,希望能为大家更多地输出自己所学的知识!

最近在学习vue3中runtime-core 运行时的源码,对vue3对组件以及dom元素初始化的流程有了全新的认知,今天为大家带来的是vue3如何对组件以及dom元素进行初始化, 先来看一下初始化流程图

这里的todo代表之后要处理的步骤,我们先把初始化的大体流程过一遍

初始化的核心

VNode

我们知道vue3对dom元素的处理是基于VNode(虚拟节点) ,所以当我们传入的组件以及元素也会转化成VNode,做后续处理,关于vue3为什么用VNode,这里就不展开讨论了,其他博主写的应该很清楚,我们这里只将如何实现

render

另外一个核心就是render函数,它可以渲染我们的虚拟dom ,通过渲染函数h来生成虚拟dom, 在vue3中,有两种办法生成虚拟dom,一种是通过template模板,另外一种就是通过render函数,但是第一种底层也是将其转化为render函数来生成的,可见render函数的重要性

patch

pacth函数是来处理虚拟dom的,我们知道VNode是一种树形结构,一层一层的,最终VNode的形式肯定就是element元素了,我们最终要的是element元素,所以我们必须得考虑到递归的情况,这也是流程图中又反过来再次调用patch的原因

手写源码

我们先来初始化一下,创建一个example来实现一下最终效果

这个就是我们传入的根组件结构,也就是createAPP(App)传入的App

js 复制代码
export const App = {

    render() {
        return h("div",

            {
                id: 'root',
                class: ["red", "layhead"]
            }
            , [h("p", { class: "red" }, "red"), h("p", { class: 'blue' }, "blue")])
    },

    setup() {
        return {
            msg: 'meng-vue'
        }
    }
}

这里是我们最终完成的结构,要能够将元素渲染在页面上

js 复制代码
const rootContainer = document.querySelector("#app")
createApp(App).mount(rootContainer)

下面会分模块进行,依次实现createApp,render patch以及流程图上的功能

createApp()

我们在接受到App之后,要调用mount方法将其绑定到根容器中,并且将App根组件转化为虚拟节点

js 复制代码
import { createVNode } from "./vnode"
import { render } from "./render"

export function createApp(rootComponent) {
    return {
        mount(rootContainer) {
            // 先转换成vNode
            // component -> VNode
            //所有components基于vNode操作
            const vNode = createVNode(rootComponent)

            render(vNode, rootContainer)
        }
    }
}

createVNode

我们要实现的VNode它可能没有属性,也可能没有children所以可选,我们这里的createVNode操作还是很简单的,我们这里的type可以看到就是上面传过来的App对象

js 复制代码
export function createVNode(type, props?, children?) {
    return {
        type,
        props,
        children
    }
}

h函数

js 复制代码
import { createVNode } from "./vnode";

export function h(type, props, children){
    return createVNode(type, props, children)
}

render

在render中我们要处理的步骤就多起来了,在render内部我们会将处理的逻辑交给patch,在pacth中,我们可以看到流程图对VNode的类型做处理,这里的类型就是是一个组件,还是一个element元素,对这两种,我们有不同的初始化方案,基于什么判断呢,我们要根据type判断,我们知道当传入的是组件时是一个对象类型,在它内部的render函数处理之后,最终是一个element元素,它的type就是一个string了,后面会为大家证明这个,我们这里先写逻辑

js 复制代码
export function render(vNode, container) {
    //patch
    patch(vNode, container)
}

function patch(vNode, container) {
    //处理组件 判断是不是element类型
    //是element走element逻辑
    //可以log一下vNode看看类型 是object->组件 是string -> element

    console.log(vNode.type);

    if (typeof vNode.type === 'string') {
        processElement(vNode, container)
    } else if (isObject(vNode.type)) {
        processComponent(vNode, container)
    }
}

在我们判断VNode类型之后会出现不同的解决方案,也就是processElement来处理element,processComponent来处理component

接下来先来实现processComponent

processComponent

js 复制代码
function processComponent(vNode, container) {
    //init 以及unpate
    //init
    mountComponent(vNode, container)
}

}

我们这里只做初始化处理init,所以在processComponent函数内,它会将component初始化,也就交给了mountComponent函数来处理

看流程图在mountComponent函数内部它做了三件事

  1. 将组件实例化这样组件身上的属性以及后续的props,slots我们都可以获取到

  2. 对setup的处理,在此阶段我们初始化props slots,以及处理setup的返回值,最后设置好返回之后的一个render函数

  3. 对上一步处理之后的VNdoe进行处理,得到subTree进而再次调用pacth方法进行递归,以获取最终的element,做进一步的处理

js 复制代码
function mountComponent(vNode, container) {
    const instance = createComponentInstance(vNode)

    setupComponentInstance(instance)
    setupRenderEffect(instance, container)
}

接下来分别实现以下这三个函数

createComponentInstance

js 复制代码
export function createComponentInstance(vNode) {
    const component = {
        vNode,
        type: vNode.type
    }
    return component
}

setupComponentInstance

上面我们提到会对setup的返回值做处理,这里处理的步骤在setupStatefulComponent函数内

js 复制代码
export function setupComponentInstance(instance) {
    //todo
    //initProps
    //initSlots

    setupStatefulComponent(instance)
}

function setupStatefulComponent(instance) {
    const component = instance.type
    const { setup } = component
    if (setup) {
        const setupResult = setup()
        //判断返回值是Function还是Object
        handlerSetupResult(instance, setupResult)
    }

}

基于setup返回值的类型,我们做进一步处理对函数类型,对象类型做不同处理,我们这里先实现Object类型的处理,将我们setup的返回值挂载到组件实例上,最后设置好返回之后的一个render函数,我们要为组件对象转换为element元素做准备

js 复制代码
function handlerSetupResult(instance, setupResult) {
    //todo
    //function
    if (typeof setupResult === 'object') {
        instance.setupState = setupResult
    }
    //判断是否有render
    finishComponentSetup(instance)
}

如果我们没有了render说明已经到了element元素这一步了,如果还有我们就将组件身上的VNode给到它的实例对象身上,方便之后的调用

js 复制代码
function finishComponentSetup(instance) {
    const component = instance.type
    instance.render = component.render

}

setupRenderEffect

我们接收到来自上面的instance,调用它的render来渲染虚拟dom,得到一个虚拟节点树也就是subTree,然后递归调用patch方法得到最终的element元素

js 复制代码
function setupRenderEffect(instance, container) {
    const subTree = instance.render()
    //vnode -> patch -> Mountelement
    patch(subTree, container)
} 

processElement

到这里我们的VNode类型是component的处理已经结束,接下来要处理初始化element的了,根据流程图可以看到类似于组件的处理方式

js 复制代码
function processElement(vNode, container) {
    //init 以及unpate
    //init
    mountElement(vNode, container)
}

在这一步对element元素的处理,我们传入h函数的结构是这样的h("div", {class: "red"}, "hi meng"),因此我们可以看到这个type就是我们VNode的type这里也就是div元素, props以及children就是属性以及子类

这一步我们要将虚拟dom转为真实dom, 如果子类是string那么我们dom的值就是string类型,如果子类是一个数组,类似于一个父元素包含了很多的子元素,那么它的子类也是一堆虚拟dom,我们要通过patch将其也通过processElement的过程进行处理,注意这里要挂载的容器就是我们的el了

js 复制代码
function mountElement(vNode, container) {

    //type就是元素类型
    const el = document.createElement(vNode.type)

    //children就是el的值如果是基本类型就这样处理, 如果children是Array代表有后代,就用另外一种方式
    const { children } = vNode
    if (typeof children === 'string') {
        el.textContent = children
    } else if (Array.isArray(children)) {
        mountChildren(vNode, el)
    }

    //props就是属性
    const { props } = vNode
    for (const key in props) {
        const val = props[key]
        el.setAttribute(key, val)
    }

    container.append(el)
}
js 复制代码
//挂载元素后代
function mountChildren(vNode, container) {
    vNode.children.forEach((v) => {
        patch(v, container)
    })
}

到这里我们初始化的大体流程已经走完了,我们将虚拟dom渲染成真实dom,这里的逻辑就是渲染逻辑,接下来我将通过例子为大家演示我们的成果

最终展示

我会将我们的结果能够在一个html页面中展示处理

index.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>
    <style>
        .red {
            background-color: red;
        }

        .blue {
            background-color: blue;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script src="main.js" type="module"></script>
</body>

</html>
app.js 复制代码
import { h } from '../../lib/guide-meng-vue.esm.js'
export const App = {

    render() {
        return h("div",

            {
                id: 'root',
                class: ["red", "layhead"]
            }
            , [h("p", { class: "red" }, "red"), h("p", { class: 'blue' }, "blue")])
    },

    setup() {
        return {
            msg: 'meng-vue'
        }
    }
}
main.js 复制代码
import { createApp } from '../../lib/guide-meng-vue.esm.js'

import { App } from './app.js'
//vue3
const rootContainer = document.querySelector("#app")
createApp(App).mount(rootContainer)

因为我们写的ts在html中无法使用,我们需要将其转化为js文件,这里我选择的是用rollup来打包,将我们的ts文件输出为js文件,这里的安装过程就不说了,主要展示如何配置,这里在引入package.json时要在断言 assert是运行时的一个断言,不然会报错

在根目录下创建一个rollup.config.js

rollup.config.js 复制代码
import typescript from '@rollup/plugin-typescript';
import pkg from './package.json' assert { type: "json" };
export default {
    input: "./src/index.ts",
    output: [
        //1.cjs -> commonJs
        //2.esm
        {
            format: "cjs",
            file: pkg.main
        },
        {
            format: "es",
            file: pkg.module
        },
    ],
    plugins: [typescript()]
}

主要要改一下tsconfig.json里面的文件以及packge.json的文件, 在tsconfig.json中将module改为ESNext

以及package.json的文件

package.json 复制代码
{
  "name": "reactive",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "lib/guide-meng-vue.cjs.js",
  "module": "lib/guide-meng-vue.esm.js",
  "scripts": {
    "test": "jest",
    "build": "rollup -c rollup.config.js",
    "prepare": "husky install",
    "commitlint": "commitlint --config commitlint.config.cjs -e -V"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/jest": "^29.5.3",
    "jest": "^29.6.1",
    "rollup": "^3.27.0",
    "tslib": "^2.6.1",
    "typescript": "^5.1.6"
  },
  "devDependencies": {
    "@babel/core": "^7.22.9",
    "@babel/preset-env": "^7.22.9",
    "@babel/preset-typescript": "^7.22.5",
    "@commitlint/cli": "^17.6.7",
    "@commitlint/config-conventional": "^17.6.7",
    "@rollup/plugin-typescript": "^11.1.2",
    "babel-jest": "^29.6.1",
    "husky": "^8.0.0"
  }
}

万事具备,我们运行build命令,就能够在lib文件夹下面看到打包好的js文件了,这里我们用esm就可以 用live server打开index.html

可以看到以及渲染出来了,另外看log,我们在前面说如何区分component和element,根据一个是Object一个是string这里的上面两个是不是就是的,另外看一下dom结构

也是正确的渲染出来了

总结

vue3的渲染过程相比于reactivity更加抽象了一点,还是需要多debug好好理解里面的过程,主要就是Vnode,render,patch,然后再对后续的处理一步步理解,这里只是最基本的初始化流程,后续的todo功能我会慢慢学习再次完善的,有不妥的地方,欢迎指出,最后,希望大家能够多多支持,要是能跟上一篇一样的效果就很好啦哈哈!

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax