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 实现
Props
和Attrs
关系是:没有定义在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;
}
};