突然发现vue3运行时-component & element初始化流程这样子搞

93 阅读5分钟

前言

之前的vue3响应式核心原理也算是告一段落了,接下来开启的就是结合运行时来开发了,更贴切于框架式开发,业务开发和框架开发的区别就在于通用性和严谨程度,所以做框架开发,哪怕是模仿源码,也是有助于自身的开发能力提升

vue3中runtime-core 运行时的源码也能作为一个不错的学习目标,下面就围绕vue3对组件和元素的初始化展开话题

VNode

vue3的dom是围绕着VNode来实现的,所以我们传入组件或元素的时候都会被转为VNode之后再处理,,vue3为什么使用vnode这里不展开说,但一定不是提高效率啊,效率没有原生高,第一,降低心智负担,第二考虑通用性,当然还有别的原因,但不是必要充分条件

patch

patch处理虚拟dom,vnode是subtree结构,到页面上肯定是以真实dom元素展现,所以这些subtree肯定会被处理成真实dom

render

render呢是可以渲染我们的虚拟节点的,然后通过h函数生成虚拟dom,两种生成虚拟节点的方法都是通过render实现,可见render的重要性

初始化流程

下面就直接开始初始化流程了

其实有些写react的开发者更习惯用这种写法来写vue

篇幅展开讲太多了,所以大部分贴源码,初始化流程不用看在干嘛,就看看流程就行了,后续的文章会对源码做详细说明

下面就是创建一个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: 'xin-vue'
        }
    }
}

App根组件引入到main.js,createApp传入App,组件的本质就是个对象嘛

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

一层套一层,俄罗斯套娃,只是更符合语义化和代码美观性,当然学习鱿鱼须的代码风格及规范也是很有必要的

createApp

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

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函数内才决定的,组件本质是对象,所以再判断是不是组件上其实是很简单的,看看是不是对象就行了,如果是元素,那么type就会显示元素名称

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

判断完就需要做分叉处理了,下面先处理组件

processComponent


function processComponent(vNode, container) {
    //init 以及unpate
    //init
    mountComponent(vNode, container)
}

来京城只办三件事

  1. 组件实例化
  2. 处理setup
  3. 处理vnode
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

export function setupComponentInstance(instance) {
    //todo
    //initProps
    //initSlots

    setupStatefulComponent(instance)
}

setupStatefulComponent

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

这里其实就已经到递归环节了,再次调用patch

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

processElement

组件结束到element

function processElement(vNode, container) {
    //init 以及unpate
    //init
    mountElement(vNode, container)
}

mountElement

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

初始化差不多走完了,后面看情况补充打包和测试吧,目前肯定跑不起来,没打包肯定跑不起来

补充

index.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>xin-vue</title>
    <style>
        .bgc-red {
            background-color: red;
        }

        .bgc-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/xin-vue.esm.js'
export const App = {

    render() {
        return h("div",

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

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

main.js


import { createApp } from '../../lib/xin-vue.esm.js'

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

ts肯定跑不起来,所以要打包

安装rollup

根目录创建rollup.config.js

这里的 assert { type: "json" } 是为了断言,不然打包报错

tsconfig.json也要改,把module改为ESNext

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

{
  "name": "啪!你已死亡",
  "version": "1.0.0",
  "description": "",
  "main": "lib/xin-vue.cjs.js",
  "module": "lib/xin-vue.esm.js",
  "type": "module",
  "scripts": {
    "build": "rollup -c rollup.config.js",
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.22.9",
    "@babel/preset-env": "^7.22.9",
    "@babel/preset-typescript": "^7.22.5",
    "@rollup/plugin-typescript": "^11.1.2",
    "@types/jest": "^29.5.3",
    "babel-jest": "^29.6.1",
    "jest": "^29.6.1",
    "rollup": "^3.27.0",
    "tslib": "^2.6.1",
    "typescript": "^5.1.6"
  }
}

都配置好就可以打包了,用live server跑index.html,可以看下dom结构是不是与写的一致

结语

vue3的源码是更偏向于语义化和功能性严格划分的,学习有益身心健康,学习有益健康,不学有益心理健康🙂

如果有不对的地方还希望各位悉数指出,有什么疑问也可以私信我,感谢各位