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

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

前言

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

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

image.png

这里的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

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'
        }
    }
}

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


const rootContainer = document.querySelector("#app")
createApp(App).mount(rootContainer)

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

createApp()

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


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对象


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

h函数

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了,后面会为大家证明这个,我们这里先写逻辑


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


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,做进一步的处理


function mountComponent(vNode, container) {
    const instance = createComponentInstance(vNode)
    setupComponentInstance(instance)
    setupRenderEffect(instance, container)
}

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

createComponentInstance


export function createComponentInstance(vNode) {
    const component = {
        vNode,
        type: vNode.type
    }
    return component
}

setupComponentInstance

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


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元素做准备


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

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


function finishComponentSetup(instance) {
    const component = instance.type
    instance.render = component.render
}

setupRenderEffect

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

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

processElement

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


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了


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)
}


//挂载元素后代
function mountChildren(vNode, container) {
    vNode.children.forEach((v) => {
        patch(v, container)
    })
}

    

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

最终展示

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


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'
        }
    }
}
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


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

image.png

以及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

image.png

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

image.png

也是正确的渲染出来了

总结

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

相关文章
|
2天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
13 3
|
27天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
32 8
|
27天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
30 1
|
27天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
36 1
|
1月前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
7天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
1月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
41 1
vue学习第一章
|
1月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
30 1
|
1月前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
38 1
vue学习第四章
|
1月前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
25 1
vue学习第7章(循环)