上篇文章我们了解了Vue
内部是如何渲染一个简单的div
的。还没看到的小伙伴,可以观摩下 跟着文章读一篇,人人都能了解Vue3 系列(一)(从0构建pnpm项目, 跑通内部渲染流程) - 掘金 (juejin.cn)
对于h
函数,我们实现了props
和children
属性的处理。即:
js
h('div', { class: ''}, '显示的内容')
接下来完善下h
函数,支持更多功能:
js
export function h(type: any, propsOrChildren?: any, children?: any, ..._: any[]): VNode {
// 判断参数长度
const l = arguments.length;
// type是一定有的
if (l === 2) {
// 参数为2个,第二个参数 有可能是children (文本、vnode), 也有可能是props
// children: h('div', '') h('div', [])
// props: h('div', {})
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// 第二个参数是 vnode
// h('div', h('span'))
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
// 第二个参数是属性
// h('div', {})
return createVNode(type, propsOrChildren);
} else {
// 第二个参数是children
// h('div', 'xxx')
return createVNode(type, null, propsOrChildren);
}
} else {
// 3种情况
// h('div')
// h('div', {}, '')
// h('div', {}, '', '')
if (l > 3) {
// h('div', {}, '', '')
// 取后两个,多余的参数都转成数组
children = Array.prototype.slice.call(arguments, 2);
} else if (l === 3 && isVNode(children)) {
// h('div', {}, h('span'))
children = [children];
}
return createVNode(type, propsOrChildren, children);
}
}
isObject
和isArray
是从工具库@vue/shared导入的。你可以按照第一篇文章的思路,创建一个独立的文件夹,通过workspace
协议添加到@vue/runtime-core
中。
js
export const isObject = val => val !== null && typeof val === 'object';
export const isArray = Array.isArray;
我们看到还有一个isVNode
函数,这个怎么实现。其实就是在VNode对象上加一个属性:__v_isVNode
,完善下createVNode
函数
js
export function createVNode(type, props, children) {
const vnode = {
// 用来标记是VNdoe对象
__v_isVNode: true,
type,
props,
children,
el: null // 真实节点 初始化为null
};
return vnode;
}
实现isVNode
函数:
js
export function isVNode(value) {
return value ? value.__v_isVNode === true : false;
}
如果是子组件,会将createVNode
第三个参数传入数组,因为子组件可能会不止一个。
我们来验证下刚才我们的改动是否生效:修改下测试代码:
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>
</head>
<body>
<div id="app"></div>
<script type="module">
import { render, h } from '../../runtime-dom/dist/runtime-dom.esm-bundler.js';
const child = h('span', '显示的内容');
render(h('div', { style: { color: 'red' } }, child), app);
</script>
</body>
</html>
怎么不是我们想要的内容?思考以下?
因为我们只处理了文本类型子组件渲染:
针对Vnode类型的子组件还未处理。怎么处理呢?
原理很简单:循环子组件,重新执行patch
方法即可: 伪代码:
js
const mountChildren = (children, el, anchor, parentComponent, start = 0) => {
for (let i = start; i < children.length; i++) {
// 这里的child可能是普通文本(string, number),也可能是vnode,也可能是 [h('span'), h('div')]
const child = normalizeVNode(children[i]);
// 递归处理每个子元素
patch(null, child, el, anchor, parentComponent);
}
};
if (如果是文本节点) {
// 处理文本子节点
setElementText(el, children);
} else if (如果是VNode子节点) {
mountChildren(children, el, null, null);
}
现在面临一个新问题,怎么区分节点类型?
Vue
和React
都在用的位运算:
ts
export const ShapeFlags = {
ELEMENT: 1, // 普通原生类型节点
FUNCTIONAL_COMPONENT, //函数式节点(无状态组件)
STATEFUL_COMPONENT, // 有自己状态的节点类型
TEXT_CHILDREN: 1 << 3, // 内部的Text组件几点
ARRAY_CHILDREN: 1 << 4, // 数组节点
SLOTS_CHILDREN: 1 << 5, // slot插槽节点
TELEPORT: 1 << 6, // TELEPORT组件节点
SUSPENSE: 1 << 7, // SUSPENSE组件节点
COMPONENT_SHOULD_KEEP_ALIVE: 1 << 8, // 需要被KeepAlive组件节点
COMPONENT_KEPT_ALIVE: 1 << 9, // KeepAlive组件节点
COMPONENT: STATEFUL_COMPONENT | FUNCTIONAL_COMPONENT // 包含有状态和无状态组件节点
};
运算逻辑如下
位运算
1 << 1 可以这么理解: 2的1次方 = 2 1 << 2: 2的2次方 = 4
也可以按照二进制来理解: 1 换算成二进制是: 00000001。 1 << 1 代表 1往前移动1个位置:00000010,换算成10进制 就是 2 1 << 2 代表 1往前移动2个位置:00000100,换算成10进制 就是 4 依次类推
类似的: 2 << 3 2 换算成二进制:00000010 然后 1 往前移动3位:00001000 换算成10进制 就是 8
符号( | ) 或运算 : 如果有1 那就换成1 eg. 1 | 2 = 3 => 换算成二进制: 00000001 | 00000010 = 00000011
符号( &) 与运算 : 如果都是1 那就换成1 eg. 1 & 2 = 0 => 换算成二进制: 00000001 & 00000010 = 00000000
依次类推
其实就是加个唯一标识。这里可以不用太深究。
回到createVNode
函数,我们要做下改造,为了跟尽量跟源码保持一致:新增几个内部方法:
js
export function createVNode(type, props, children) {
// 源码里面:
// type: 如果是VNode 需要cloneVNode
// eg. :class="{a: xx, b: xxx}" :class="['classA', {'classB': true }]"
// eg. :style="{'display': 'flex'}" :style="[{'display': 'flex'}, { 'flex': 2 }]"
// props里面如果有class、style 也需要拍平处理
// 这里为了简便:就不处理了
// eg. h('div') h('div', { props }) h('div', { props }, children)
// 如果有值,一定是元素类型
// 如果是对象,则为组件类型
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0;
return createBaseVNode(type, props, children, shapeFlag);
}
js
function createBaseVNode(type, props = null, children = null, shapeFlag) {
const vnode = {
__v_isVNode: true,
type,
props,
children,
el: null, // 真实节点 初始化为null
anchor: null,
target: null,
targetAnchor: null,
component: null,
shapeFlag
};
// 处理子类型和自己的类型
// h('div', props, 'xxx') 渲染为 <div ...props>xxx</div>
// h('div', props, [h('span', props, 'yyyy')]) 渲染为 <div ...props><span ...props>yyyy</span></div>
normalizeChildren(vnode, children);
return vnode;
}
js
export function normalizeChildren(vnode, children) {
let type = 0;
if (!children) {
children = null;
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else if (isObject(children)) {
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// 普通元素 的children 是插槽
// 支持用户这么写: h('div', null, {default: () => 'slots'})
const slot = children.default;
if (isFunction(slot)) {
normalizeChildren(vnode, slot());
}
return;
} else {
type = ShapeFlags.SLOTS_CHILDREN;
}
} else if (isFunction(children)) {
// 默认插槽名称 default
children = { default: children };
type = ShapeFlags.SLOTS_CHILDREN;
} else {
children = String(children);
type = ShapeFlags.TEXT_CHILDREN;
}
vnode.children = children;
vnode.shapeFlag |= type;
}
完善mountElement
方法:
js
const mountElement = (vnode, container) => {
/// ...
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 处理文本子节点
setElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el);
}
/// ...
};
JS
// 挂载子元素
// 递归处理children
// eg. h('div', props, [h('span', props)])
const mountChildren = (children, el, start = 0) => {
for (let i = start; i < children.length; i++) {
// 这里的child可能是普通文本(string, number),也可能是vnode,也可能是 [h('span'), h('div')]
const child = normalizeVNode(children[i]);
// 递归处理每个子元素
// el就是真实的父节点dom
patch(null, child, el);
}
};
到这里,已经完成所有准备工作,看效果:
1: 再来试试子组件自己的样式效果:
测试代码:
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>
</head>
<body>
<div id="app"></div>
<script type="module">
import { render, h } from '../../runtime-dom/dist/runtime-dom.esm-bundler.js';
const child = h('span', { style: { color: '#293' } }, '显示的内容');
render(h('div', { style: { color: 'red' } }, child), app);
</script>
</body>
</html>
2: 列表子组件
js
const childs = [
h('span', { style: { color: '#293' } }, '显示的内容'),
h('span', { style: { color: '#672' } }, '显示的内容2'),
h('span', { style: { color: '#980' } }, '显示的内容3')
];
render(h('div', { style: { display: 'grid' } }, childs), app);
</script>
3: 子组件嵌套
js
const son = h('span', { style: { color: '#909' } }, '我是孙子');
const child = h('span', { style: { color: '#293' } }, son);
render(h('div', { style: { display: 'grid' } }, child), app);
总结下: 处理VNode子节点:
- createVNode方法第三发个参数,如果是VNode类型,要传入数组,因为不止一个子元素
- 区分资源元素类型,针对不同的元素类型,做ShapeFlags类型映射
- mountElement挂载元素方法,区分子元素类型,渲染不同逻辑
- 针对数组子组件,循环遍历执行patch方法,对每个子组件都重新渲染。
下一篇准备讲:处理VNode的事件原理。
感兴趣的小伙伴可以点赞、收藏。也可以在评论区里交流、提意见。我都会认证回复的。