• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
西檬
帮助5

前言

最近在学习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

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页面中展示处理

<!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="https://juejin.cn/post/main.js" type="module"></script>
</body>

</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

学新通

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

学新通 也是正确的渲染出来了

总结

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhebcc
系列文章
更多 icon
同类精品
更多 icon
继续加载