携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
这一期先讲如何讲虚拟dom结构,虚拟dom怎么转化成真实元素(h和render函数)
划分VNode 的种类
- Element element 对应普通元素,使用 document.createElement 创建。type 指标签名,props 指元素属性,children 指子元素,可以为字符串或数组。为字符串时代表只有一个文本子节点。
// 类型定义
{
type: string,
props: Object,
children: string | VNode[]
}
// 举例
{
type: 'div',
props: {class: 'a'},
children: 'hello'
}
- Text Text 对应文本节点,使用 document.createTextNode 创建。type 因定为一个 Symbol,props 为空,children 为字符串,指具体的文本内容。
// 类型定义
{
type: Symbol,
props: null,
children: string
}
- Fragment Fragment 为一个不会真实渲染的节点。相当于 template 或 react 的 Fragment。type 因定为一个 Symbol,props 为空,children 为数组,表示子节点。最后渲染时子节点会挂载到 Fragment 的父节点上
react里的<> </>
// 类型定义
{
type: Symbol,
props: null,
children: []
}
- Component Component 是组件,组件有自己特殊的一套渲染方法,但组件最终的产物,也是上面三种 VNode 的集合。组件的 type,就是定义组件的对象,props 即是外部传入组件的 props 数据,children 即是组件的 slot(但我们不准备实现 slot,跳过)。
// 类型定义
{
type: Object,
props: Object,
children: null
}
// 举例
{
type: {
template:`{{ msg }} {{ name }}`,
props: ['name'],
setup(){
return {
msg: 'hello'
}
}
},
props: { name: 'world' },
}
还有注释节点、tele、suspense等不讲
ShapeFlags
ShapeFlags 是一组标记,用于快速辨识 VNode 的类型和它的 children 的类型。
复习一下位运算
// 按位与运算
0 0 1 0 0 0 1 1
0 0 1 0 1 1 1 1
&
0 0 1 0 0 0 1 1
// 按位或运算
0 0 1 0 0 0 1 1
0 0 1 0 1 1 1 1
|
0 0 1 0 1 1 1 1
const ShapeFlags = {
ELEMENT: 1, // 00000001
TEXT: 1 << 1, // 00000010
FRAGMENT: 1 << 2, // 00000100
COMPONENT: 1 << 3, // 00001000
//文本子结点
TEXT_CHILDREN: 1 << 4, // 00010000
ARRAY_CHILDREN: 1 << 5, // 00100000
//判断是否有子节点,
CHILDREN: (1 << 4) | (1 << 5), //00110000
};
采用二进制位运算<<和|生成,使用时用&运算判断,例如:
if (flag & ShapeFlags.ELEMENT) //说明是元素节点
再例如,一个值为 33 的 flag,它的二进制值为 00100001,那么它:
说明是元素节点,且有数组子节点
let flag = 33;
flag & ShapeFlags.ELEMENT; // true
flag & ShapeFlags.ARRAY_CHILDREN; // true
flag & ShapeFlags.CHILDREN; // true
- 渲染
Render需要的数据
{
type,
props,
children,
shapeFlag,
}
渲染例子代码
h函数生成Vnode,render函数渲染挂载Vnode。
源码
h
export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');
export const ShapeFlags = {
ELEMENT: 1,
TEXT: 1 << 1,
FRAGMENT: 1 << 2,
COMPONENT: 1 << 3,
TEXT_CHILDREN: 1 << 4,
ARRAY_CHILDREN: 1 << 5,
CHILDREN: (1 << 4) | (1 << 5),
};
/**
* vnode有四种类型:dom元素,纯文本,Fragment,组件
* @param {string | Text | Fragment | Object } type
* @param {Object | null} props
* @param {string | array | null} children
* @returns VNode
*/
export function h(type, props = null, children = null) {
// h函数作用 其实就是判断当前vnode种类。
let shapeFlag = 0;
// 如果type是字符,说明是元素
if (isString(type)) {
shapeFlag = ShapeFlags.ELEMENT;
} else if (type === Text) {
// 文本节点
shapeFlag = ShapeFlags.TEXT;
} else if (type === Fragment) {
shapeFlag = ShapeFlags.FRAGMENT;
} else {
shapeFlag = ShapeFlags.COMPONENT;
}
// 文本子节点和数组子节点,这里要进行或运算
if (typeof children === 'string' || typeof children === 'number') {
shapeFlag |= ShapeFlags.TEXT_CHILDREN;
children = children.toString();
} else if (Array.isArray(children)) {
shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
}
return {
type,
props,
children,
shapeFlag,
};
}
关于元素的attribute和Properties
像
id这些标准属性,可以通过document.body.id这样的方式获取到值,而非标准的属性则只能拿到undefined虽然
setAttribute可以为所有属性赋值,但是有弊端,就是不能赋值布尔类型,赋值false会改成'false',这样拿到还是true不过这种是有限的,所以只要把
checked这类特别注意下就行
render
渲染元素属性举例:
{
class: 'a b',
style: {
color: 'red',
fontSize: '14px',
},
onClick: () => console.log('click'),
checked: '',
custom: false
}
export function render(vnode, container) {
const {shapeFlag} = vnode
if(shapeFlag & ShapeFlags.ELEMENT){
//代表是一个元素节点
mountElement(vnode, container)
}
//fragment则是取出子节点渲染 mountChildren(vnode.children, container)
...其他
}
//渲染元素节点,挂载属性,渲染子节点,拼接到父元素
function mountElement(vnode, container) {
const { type, props, children } = vnode;
const el = document.createElement(type);
mountProps(props,el)
mountChildren(vnode, el);
container.appenChild(el);
}
function mountChildren(vnode, container) {
const { shapeFlag, children } = vnode;
if(shapeFlag & ShapeFlags.TEXT_CHILDREN){
mountTextNode(vnode, container)
}else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN){
children.forEach(child=>render(child,container))
}
}
function mountTextNode(vnode, container, anchor) {
const textNode = document.createTextNode(vnode.children);
container.appenChild(textNode);
}
// innerHtml等属性
const domPropsRE = /[A-Z]|^(value|checked|selected|muted|disabled)$/;
function mountProps(props,el){
for(const key in props){
const value = props[key]
switch(key){
case 'class':
el.className = value
break;
// 虽然 style:{a:b;c:d}这样子的,但是我们直接赋值就行了,不用自己拼,元素内部自动处理
case 'style':
for(const name in value){
el[style][name] = value[name]
}
break;
default:
if (domPropsRE.test(key)) {
// 满足上面正则的,作为domProp赋值
if (value === '' && typeof el[key] === 'boolean') {
// 例如{checked: ''}
value = true;
}
el[key] = value;
}else {
// 例如自定义属性{custom: ''},应该用setAttribute设置为<input custom />
// 而{custom: null},应用removeAttribute设置为<input />
if (value == null || value === false) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
}
}
}
}