前言
上一篇博客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
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函数内部它做了三件事
-
将组件实例化这样组件身上的属性以及后续的props,slots我们都可以获取到
-
对setup的处理,在此阶段我们初始化props slots,以及处理setup的返回值,最后设置好返回之后的一个render函数
-
对上一步处理之后的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="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结构
也是正确的渲染出来了
总结
vue3的渲染过程相比于reactivity更加抽象了一点,还是需要多debug好好理解里面的过程,主要就是Vnode,render,patch,然后再对后续的处理一步步理解,这里只是最基本的初始化流程,后续的todo功能我会慢慢学习再次完善的,有不妥的地方,欢迎指出,最后,希望大家能够多多支持,要是能跟上一篇一样的效果就很好啦哈哈!