vue3源码阅读与实现: runtime运行时-h模块在该专栏第一篇文章框架设计概念中提到: `vue`在运行时主要的工
runtime运行时 -h函数
模块总览

到这一篇文章,响应式系统部分就全部结束了,接下来开始另一个核心模块runtime运行时.
在该专栏第一篇文章框架设计概念中提到: vue在运行时主要的工作时将虚拟DOM渲染为真实DOM,在这个过程中主要涉及两个函数:
h:生成虚拟DOMrender:将虚拟DOM渲染
什么是虚拟DOM
虚拟DOM是用来描述真实DOM的一个js对象.这个对象中有许多创建真实DOM的必要属性,这些属性描述了对应真实DOM的结构.然后就可以根据虚拟DOM创建出真实DOM.
那么为什么需要虚拟DOM呢?
因为操作虚拟DOM的性能要比操作真实DOM的性能更快,在复杂更新场景下,这将节省很多性能
h函数
在vue中虚拟DOM的生成依赖于h函数,该函数可以根据参数生成对应的虚拟DOM,
- 第一个参数: 必填,可以是字符串(原生标签)也可以是
Vue的组件定义 - 第二个参数: 可选,标签或者组件的各种属性
- 第三个参数: 可选,标签或者组件的子节点
那么h函数是如何将这些参数转换成虚拟DOM的呢?vue中的虚拟DOM到底长什么样子呢?让我们带着问题,到源码中一探究竟吧!
debugger
使用如下测试用例:
<!DOCTYPE html>
<html>
<head>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h } = Vue
debugger
const vnode = h('div', { class: { test: true } }, 'hello') // 关注点: h函数如何生成虚拟DOM
console.log('vnode:', vnode)
</script>
</body>
</html>
该测试用例只有一个关注点: h函数如何生成虚拟DOM
关注点1:创建虚拟DOM
-
open with live server打开测试用例,进入debugger -
我们知道
h函数的传参形式很灵活,h函数首先将参数统一处理,将type,props,children对应起来,然后传递给createNode

1. 进入`createVNode`,在这里先对`props`中的`class和style`属性进行处理,我们知道vue对这两个属性做了增强,以支持数组,对象的赋值形式,增强就是在这里做的

-
进入
normalizeClass,通过循环拼接出类名,然后返回

-
处理完
props中的class和style,接下来会根据type创建一个变量shapeFlag,这个变量很重要,表示虚拟DOM的类型,我们这里对应的就是ShapeFlags.Element类型,只要记得是Element就好

-
之后进入
createBaseVNode中,这里就是真正创建虚拟DOM的地方,首先定义了一个对象,这个对象就是创建的虚拟DOM

-
然后进入
normalizeChildren函数,对children进行处理:在这个函数中根据children的类型,重新赋值了shapFlag

-
这里根据
children的类型,重新赋值了虚拟DOM的shapeFlag(一个数字),通过按位或操作,使得shapFlag技能表示vnode的类型,也能表示vnode的子元素类型

到此,返回这个虚拟DOM,h函数就执行完毕了
总结
h函数主要做了三件事情:
- 根据容器和子元素的类型计算该虚拟
dom的shapeFlag,该属性非常重要,在之后的渲染环节会根据shapeFlag不同而进行不同的渲染逻辑 - 增强
class,style - 创建虚拟
DOM对象
实现h函数
定义类型
ShapeFlags中定义了vnode和children的类型:
一共包括两种vnode,三种children(对源码中类型有删减)
这在之后的渲染中非常重要
在在packages/shared/src/shapeFlags.ts中
export const enum ShapeFlags {
// vnode类型
ELEMENT = 1, // 普通标签
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 有状态组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, // 有状态 | 函数式组件
//children类型
TEXT_CHILDREN = 1 << 3, // Text类型
ARRAY_CHILDREN = 1 << 4, // Array类型
SLOTS_CHILDREN = 1 << 5, // 插槽类型
}
除了上述的几种vnode类型,还有需定义一些其他的类型,如文本节点,代码片段,注释,这里只额外定义了三种
在packages/runtime-core/src/vnode.ts
// 定义三种虚拟DOM类型
export const Text = Symbol("Text");
export const Comment = Symbol("Comment");
export const Fragment = Symbol("Fragment");
h函数
这个函数中主要处理参数:
在packages/runtime-core/src/h.ts
import { isArray, isObject, isVnode } from "@vue/shared";
import { createVnode } from "./vnode";
export function h(type: any, propsOrChildren: any, children: any) {
// 处理参数
const l = arguments.length;
if (l === 2) {
// 只有两个参数,第二个参数可能是children也可能是props也有可能是vndoe
const isProps = isObject(propsOrChildren) && !isArray(propsOrChildren);
if (isProps) {
if (isVnode(propsOrChildren)) {
return createVnode(type, null, [propsOrChildren]);
}
return createVnode(type, propsOrChildren);
} else {
return createVnode(type, null, propsOrChildren);
}
} else {
if (l > 3) {
children = [...arguments].slice(2);
} else if (l === 3 && isVnode(children)) {
children = [children];
}
return createVnode(type, propsOrChildren, children);
}
}
在packages/shared/src/index.ts中添加isVnode方法:
export const isVnode = (v: any): v is Vnode => {
return v && v.__V_isVnode === true;
};
createVnode函数
这个函数主要增强class和style同时根据type确定shapeFlag的值
在packages/runtime-core/src/vnode.ts
export function createVnode(type: any, props: any, children?: any) {
// 增强class和style
if (props) {
const { class: klass } = props;
if (klass) {
props.class = normalizeClass(klass);
}
}
// 根据type创建shapflag
const shapeFlag = isString(type) // string类型表示是普通标签
? ShapeFlags.ELEMENT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT //如果是对象,就是组件
: 0;
return createBaseVnode(type, props, children, shapeFlag);
}
/**
* @message: 增强class处理,以便支持class的多种写法: 字符串,数组,对象
*/
function normalizeClass(klass: any): String {
let res = "";
if (isString(klass)) {
return klass;
}
if (isArray(klass)) {
res = klass.reduce((acc, curr) => {
acc += ` ${normalizeClass(curr)}`;
return acc;
}, "");
} else if (isObject(klass)) {
res = Object.entries(klass).reduce((acc, [key, value]) => {
if (value) {
acc += key;
}
return acc;
}, "");
}
return res.trim();
}
createBaseVnode函数
这个函数主要用来创建vnode然后根据children的类型,修改shapFlag的值
在packages/runtime-core/src/vnode.ts
/**
* @message: 创建vnode
*/
function createBaseVnode(
type: any,
props: any,
children: any,
shapeFlag: number
) {
// 生成vnode
const vnode: Vnode = {
__v_isVnode: true,
type,
props: props,
children,
shapeFlag,
};
normalizeChildren(vnode, children);
return vnode;
}
/**
* @message: 根据children类型,设置shapeFlag的值
*/
function normalizeChildren(vnode: Vnode, children: any) {
let type = 0;
if (children == null) {
children = null;
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else if (isObject(children)) {
} else if (isFunction(children)) {
} else {
vnode.children = String(children);
// 为 type 指定 Flags
type = ShapeFlags.TEXT_CHILDREN;
}
// 修改 vnode 的 chidlren
vnode.children = children;
// 按位或赋值,赋值前shapeFlag表示的是vnode的类型,通过或运算,shapeFlag就能同时表示其子元素的类型了
vnode.shapeFlag |= type;
}
总结
h函数的整体逻辑并不复杂,主要是一些虚拟DOM属性的处理,需要关注的就是shapFlag的处理过程,这个属性将在render部分重点使用
转载自:https://juejin.cn/post/7401315308005179432