跟着文章读一篇,人人都能了解Vue3 系列(二)(子组件渲染流程)

上篇文章我们了解了Vue内部是如何渲染一个简单的div的。还没看到的小伙伴,可以观摩下 跟着文章读一篇,人人都能了解Vue3 系列(一)(从0构建pnpm项目, 跑通内部渲染流程) - 掘金 (juejin.cn)

对于h函数,我们实现了propschildren属性的处理。即:

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

isObjectisArray是从工具库@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);
}

现在面临一个新问题,怎么区分节点类型?

VueReact都在用的位运算:

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子节点:

  1. createVNode方法第三发个参数,如果是VNode类型,要传入数组,因为不止一个子元素
  2. 区分资源元素类型,针对不同的元素类型,做ShapeFlags类型映射
  3. mountElement挂载元素方法,区分子元素类型,渲染不同逻辑
  4. 针对数组子组件,循环遍历执行patch方法,对每个子组件都重新渲染。

下一篇准备讲:处理VNode的事件原理。

感兴趣的小伙伴可以点赞、收藏。也可以在评论区里交流、提意见。我都会认证回复的。

相关推荐
我认不到你14 分钟前
antd proFromSelect 懒加载+模糊查询
前端·javascript·react.js·typescript
集成显卡16 分钟前
axios平替!用浏览器自带的fetch处理AJAX(兼容表单/JSON/文件上传)
前端·ajax·json
焚琴煮鹤的熊熊野火25 分钟前
前端垂直居中的多种实现方式及应用分析
前端
我是苏苏1 小时前
C# Main函数中调用异步方法
前端·javascript·c#
转角羊儿1 小时前
uni-app文章列表制作⑧
前端·javascript·uni-app
大G哥1 小时前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python
hong_zc1 小时前
初始 html
前端·html
小小吱2 小时前
HTML动画
前端·html
糊涂涂是个小盆友2 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
浮华似水2 小时前
Javascirpt时区——脱坑指南
前端