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功能我会慢慢学习再次完善的,有不妥的地方,欢迎指出,最后,希望大家能够多多支持,要是能跟上一篇一样的效果就很好啦哈哈!

相关文章
|
7天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
7天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
15天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
35 7
|
15天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
15天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
37 1
|
JavaScript 前端开发
vue开发:对Element上传功能的二次封装
最近公司老项目改用vue开发,前端框架采用element ui,这个框架风格还是很漂亮的,只是上传功能有一些问题,比如:limit=1限制上传数量后,后面的添加按钮没有隐藏,再用就是如果上传图片组,很多需求需要对图片组进行排序修改,基于这两个需求,对element的el-upload组件进行了二次封装。
2453 0
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
6天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
6天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
6天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。