跳至主要內容

12. 实现组件渲染


12. 实现组件渲染

1.组件的挂载流程

组件需要提供一个 render 函数,渲染函数需要返回虚拟 DOM

const VueComponent = {
	data() {
		return { age: 13 };
	},
	render() {
		return h('p', [h(Text, "I'm erxiao sir"), h('span', this.age)]);
	}
};
render(h(VueComponent), document.getElementById('app'));

添加组件类型

h 方法中传入一个对象说明要渲染的是一个组件。(后续还有其他可能)

export const createVNode = (type, props, children = null) => {
	const shapeFlag = isString(type)
		? ShapeFlags.ELEMENT
		: isObject(type)
		? ShapeFlags.STATEFUL_COMPONENT
		: 0;
	// ... 稍后可以根据类型来进行组件的挂载
};

组件的渲染

const patch = (n1, n2, container, anchor?) => {
	// 初始化和diff算法都在这里喲
	if (n1 == n2) {
		return;
	}
	if (n1 && !isSameVNodeType(n1, n2)) {
		// 有n1 是n1和n2不是同一个节点
		unmount(n1);
		n1 = null;
	}
	const { type, shapeFlag } = n2;
	switch (type) {
		// ...
		default:
			if (shapeFlag & ShapeFlags.ELEMENT) {
				processElement(n1, n2, container, anchor);
			} else if (shapeFlag & ShapeFlags.COMPONENT) {
				processComponent(n1, n2, container, anchor);
			}
	}
};
const mountComponent = (n2, container, anchor) => {
	const { render, data = () => ({}) } = n2.type;
	const state = reactive(data());
	const instance = {
		state, // 组件的状态
		isMounted: false, // 组件是否挂载
		subTree: null, // 子树
		update: null,
		vnode: n2
	};
	const componentUpdateFn = () => {
		if (!instance.isMounted) {
			const subTree = render.call(state, state);
			patch(null, subTree, container, anchor);
			instance.subTree = subTree;
			instance.isMounted = true;
		} else {
			const subTree = render.call(state, state);
			patch(instance.subTree, subTree, container, anchor);
			instance.subTree = subTree;
		}
	};
	const effect = new ReactiveEffect(componentUpdateFn, () => update());
	const update = (instance.update = () => effect.run());
	update();
};
const processComponent = (n1, n2, container, anchor) => {
	if (n1 == null) {
		mountComponent(n2, container, anchor);
	} else {
		// 组件更新逻辑
	}
};

2.组件异步渲染

修改调度方法,将更新方法压入到队列中

const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update));
const update = (instance.update = () => effect.run());

批处理操作scheduler.ts

const queue = [];
let isFlushing = false;
const resolvedPromise = Promise.resolve();
export function queueJob(job) {
	if (!queue.includes(job)) {
		queue.push(job);
	}
	if (!isFlushing) {
		isFlushing = true;
		resolvedPromise.then(() => {
			isFlushing = false;
			let copy = queue.slice(0);
			queue.length = 0; // 这里要先清空,防止在执行过程中在加入新的job
			for (let i = 0; i < copy.length; i++) {
				let job = copy[i];
				job();
			}
			copy.length = 0;
		});
	}
}

3.组件 Props、Attrs 实现

PropsAttrs关系是:没有定义在component.props中的属性将存储到attrs对象中

import { render, h, Text, Fragment } from './runtime-dom.js';

const VueComponent = {
	data() {
		return { name: 'erxiao', age: 30 };
	},
	props: {
		address: String
	},
	render() {
		return h('p', [
			h(Text, `${this.name}今年${this.age}岁了`),
			h(Text, `${this.address}`),
			h(Text, `${this.$attrs.a}${this.$attrs.b}`)
		]);
	}
};
render(h(VueComponent, { address: '霍营', a: 1, b: 2 }), app);

initProps

const mountComponent = (vnode, container, anchor) => {
	let { data = () => ({}), render, props: propsOptions = {} } = vnode.type; // 这个就是用户写的内容
	const state = reactive(data()); // pinia 源码就是 reactive({})  作为组件的状态
	const instance = {
		// 组件的实例
		state,
		vnode, // vue2的源码中组件的虚拟节点叫$vnode  渲染的内容叫_vnode
		subTree: null, // vnode组件的虚拟节点   subTree渲染的组件内容
		isMounted: false,
		update: null,
		propsOptions,
		attrs: {},
		props: {}
	};
	vnode.component = instance;
	initProps(instance, vnode.props);
};

componentProps.ts

export function initProps(instance, rawProps) {
	const props = {};
	const attrs = {};
	const options = instance.propsOptions || {}; // 获取组件用户的配置
	if (rawProps) {
		for (let key in rawProps) {
			const value = rawProps[key];
			if (key in options) {
				props[key] = value;
			} else {
				attrs[key] = value;
			}
		}
	}
	instance.props = reactive(props); // 这里应该用shallowReactive,遵循单向数据流原则
	instance.attrs = attrs;
}

属性代理

shared/index.ts

const hasOwnProperty = Object.prototype.hasOwnProperty;
export const hasOwn = (val, key) => hasOwnProperty.call(val, key);
const publicPropertiesMap = {
	$attrs: (i) => i.attrs
};

const mountComponent = (vnode, container, anchor) => {
	// ...
	const instance = {
		// 组件的实例
		// ...
		proxy: null
	};
	vnode.component = instance;
	initProps(instance, vnode.props);
	instance.proxy = new Proxy(instance, {
		get(target, key) {
			const { data, props } = target;
			if (data && hasOwn(data, key)) {
				return data[key];
			} else if (hasOwn(props, key)) {
				return props[key];
			}
			const publicGetter = publicPropertiesMap[key];
			if (publicGetter) {
				return publicGetter(target);
			}
		},
		set(target, key, value) {
			const { data, props } = target;
			if (data && hasOwn(data, key)) {
				data[key] = value;
				return true;
			} else if (hasOwn(props, key)) {
				console.warn(`Attempting to mutate prop "${key}". Props are readonly.`);
				return false;
			}
			return true;
		}
	});
};

4.组件流程整合

const mountComponent = (vnode, container, anchor) => {
	// 1) 创建实例
	const instance = (vnode.component = createComponentInstance(vnode));
	// 2) 给实例赋值
	setupComponent(instance);
	// 3) 创建渲染effect及更新
	setupRenderEffect(instance, container, anchor);
};

创建组件实例

component.ts

export function createComponentInstance(vnode) {
	const instance = {
		// 组件的实例
		data: null,
		vnode, // vue2的源码中组件的虚拟节点叫$vnode  渲染的内容叫_vnode
		subTree: null, // vnode组件的虚拟节点   subTree渲染的组件内容
		isMounted: false,
		update: null,
		attrs: {},
		props: {},
		proxy: null,
		propsOptions: vnode.type.props
	};
	return instance;
}

设置组件属性

const publicPropertiesMap = {
	$attrs: (i) => i.attrs
};
const PublicInstanceProxyHandlers = {
	get(target, key) {
		const { data, props } = target;
		if (data && hasOwn(data, key)) {
			return data[key];
		} else if (hasOwn(props, key)) {
			return props[key];
		}
		const publicGetter = publicPropertiesMap[key];
		if (publicGetter) {
			return publicGetter(target);
		}
	},
	set(target, key, value) {
		const { data, props } = target;
		if (data && hasOwn(data, key)) {
			data[key] = value;
			return true;
		} else if (hasOwn(props, key)) {
			console.warn(`Attempting to mutate prop "${key}". Props are readonly.`);
			return false;
		}
		return true;
	}
};
export function setupComponent(instance) {
	const { props, type } = instance.vnode;
	initProps(instance, props);
	instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers);
	const data = type.data;
	if (data) {
		if (!isFunction(data))
			return console.warn('The data option must be a function.');
		instance.data = reactive(data.call(instance.proxy));
	}
	instance.render = type.render;
}

渲染 effect

const setupRenderEffect = (instance, container, anchor) => {
	const { render } = instance;
	const componentUpdateFn = () => {
		// 区分是初始化 还是要更新
		if (!instance.isMounted) {
			// 初始化
			const subTree = render.call(instance.proxy, instance.proxy); // 作为this,后续this会改
			patch(null, subTree, container, anchor); // 创造了subTree的真实节点并且插入了
			instance.subTree = subTree;
			instance.isMounted = true;
		} else {
			// 组件内部更新
			const subTree = render.call(instance.proxy, instance.proxy);
			patch(instance.subTree, subTree, container, anchor);
			instance.subTree = subTree;
		}
	};
	// 组件的异步更新
	const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update));
	// 我们将组件强制更新的逻辑保存到了组件的实例上,后续可以使用
	const update = (instance.update = () => effect.run());
	update();
};

5.属性更新

const My = {
	props: { address: String },
	render() {
		return h('div', this.address);
	}
};
const VueComponent = {
	data() {
		return { name: 'erxiao', age: 30, flag: false };
	},
	render() {
		return h(Fragment, [
			h('button', { onClick: () => (this.flag = !this.flag) }, '切换渲染'),
			h(My, { address: this.flag ? '霍营' : '回龙观' })
		]);
	}
};
render(h(VueComponent), app);
const updateComponent = (n1, n2) => {
	const instance = (n2.component = n1.component);
	const { props: prevProps } = n1;
	const { props: nextProps } = n2;
	updateProps(instance, prevProps, nextProps);
};
const processComponent = (n1, n2, container, anchor) => {
	if (n1 == null) {
		mountComponent(n2, container, anchor);
	} else {
		// 组件更新逻辑
		updateComponent(n1, n2);
	}
};

props.ts

export const hasPropsChanged = (prevProps = {}, nextProps = {}) => {
	const nextKeys = Object.keys(nextProps);
	if (nextKeys.length !== Object.keys(prevProps).length) {
		return true;
	}
	for (let i = 0; i < nextKeys.length; i++) {
		const key = nextKeys[i];
		if (nextProps[key] !== prevProps[key]) {
			return true;
		}
	}
	return false;
};
export function updateProps(instance, prevProps, nextProps) {
	if (hasPropsChanged(prevProps, nextProps)) {
		// 比较前后属性是否一致
		for (const key in nextProps) {
			// 循环props
			instance.props[key] = nextProps[key]; // 响应式属性更新后会重新渲染
		}
		for (const key in instance.props) {
			// 循环props
			if (!(key in nextProps)) {
				delete instance.props[key];
			}
		}
	}
}

这里我们将更新逻辑放到componentFn中,因为除了属性更新之外,插槽也会导致页面更新

const shouldUpdateComponent = (n1, n2) => {
	const { props: prevProps, children: prevChildren } = n1;
	const { props: nextProps, children: nextChildren } = n2;

	if (prevChildren || nextChildren) return true;

	if (prevProps === nextProps) return false;
	return hasPropsChanged(prevProps, nextProps);
};
const updateComponent = (n1, n2) => {
	const instance = (n2.component = n1.component);
	if (shouldUpdateComponent(n1, n2)) {
		instance.next = n2; // 将新的虚拟节点放到next属性上
		instance.update(); // 属性变化手动调用更新方法
	}
};
export function updateProps(prevProps, nextProps) {
	for (const key in nextProps) {
		// 循环props
		prevProps[key] = nextProps[key]; // 响应式属性更新后会重新渲染
	}
	for (const key in prevProps) {
		// 循环props
		if (!(key in nextProps)) {
			delete prevProps[key];
		}
	}
}
function updateComponentPreRender(instance, next) {
	instance.next = null;
	instance.vnode = next;
	updateProps(instance, instance.props, next.props);
}
const componentUpdateFn = () => {
	if (!instance.isMounted) {
		// ...
	} else {
		let { next } = instance;
		if (next) {
			updateComponentPreRender(instance, next);
		}
		const subTree = render.call(instance.proxy, instance.proxy);
		patch(instance.subTree, subTree, container, anchor);
		instance.subTree = subTree;
	}
};