携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
这次讲 组件 的渲染和更新,组件有多种写法,这里用简单的进行举例,还有组件的主动和被动更新机制。
认识props和attribute
const Comp = {
props: ['foo'],
render(ctx) {
//由于bar不在props,所以获取不到,id为空
return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
},
};
const vnodeProps = {
foo: 'foo',
bar: 'bar', // 会作为节点属性自动挂载,最常见的例子就是 class、style 和 id。
};
const vnode = h(Comp, vnodeProps);
render(vnode, root); // 渲染为<div class="a" bar="bar">foo</div>
Comp.props
决定它接收哪些外部传入的 vnodeProps
,把它放入 instance.props
,而其他属性会添加进 instance.attrs
,作为根节点属性。render
中的 ctx
只会使用 instance.props
。
updateProps 拆解传入参数
function updateProps(instance, vnode) {
const { type: Component, props: vnodeProps } = vnode;
const props = (instance.props = {});
const attrs = (instance.attrs = {});
// 拆出attrs和props
for (const key in vnodeProps) {
// 遍历传入的props,如果被组件接收,则传入props,如果没有,则给attrs
if (Component.props?.includes(key)) {
props[key] = vnodeProps[key];
} else {
attrs[key] = vnodeProps[key];
}
}
// toThink: props源码是shallowReactive,确实需要吗?
// 需要。否则子组件修改props不会触发更新
instance.props = reactive(instance.props);
}
// 为什么type是Component?
h(type, props = null, children = null) {
return {
type,
}
}
fallThrough 节点继承attrs
// 节点继承attrs
function fallThrough(instance, subTree) {
if (Object.keys(instance.attrs).length) {
subTree.props = {
...subTree.props, // 这里不清楚有没有必要
...instance.attrs,
};
}
}
normalizeVNode
在render
中返回有时候是数组节点有时候是字符串,这里做一下统一兼容,如:
render(ctx) {
return [
h('div', null, ctx.count),
h('div', null, ctx.count),]
}
export function normalizeVNode(result) {
// 如果是数组,如上,作为fragment的子节点即可
if (Array.isArray(result)) {
return h(Fragment, null, result);
}
// 直接返回 return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
if (isObject(result)) {
return result;
}
// 文本 数字说明是文本节点
return h(Text, null, result.toString());
}
// 组件渲染
normalizeVNode(
Component.render(instance.ctx)
));
例子
1
const Comp = {
props: ['foo'],
render(ctx) {
//由于bar不是props,所以获取不到,id为空
return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
},
};
const vnodeProps = {
foo: 'foo',
bar: 'bar', // 会作为节点属性自动挂载,最常见的例子就是 class、style 和 id。
};
const vnode = h(Comp, vnodeProps);
render(vnode, root); // 渲染为<div class="a" bar="bar">foo</div>
2 setup
const Comp ={
setup() {
const count = ref(0);
const add = () => count.value++;
return {
count,
add,
};
},
render(ctx) {
return [
h('div', null, ctx.count),
h(
'button',
{
onClick: ctx.add,
},
'add'
),
];
},
}
const vnode = h(Comp);
render(vnode, document.body);
待续
以后
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
基础渲染实现
实现例子1
export function mountComponent(vnode, container, anchor, patch) {
const { type: Component } = vnode;
// createComponentInstance
const instance = (vnode.component = {
props: {},
attrs: {},
setupState: null,
ctx: null,
update: null,
isMounted: false,
subTree: null,
next: null, // 组件更新时,把新vnode暂放在这里
});
// 拆解参数 props和attr
updateProps(instance, vnode);
// 源码:instance.setupState = proxyRefs(setupResult) 取值ref时可以不用.value去取
// 如果有setup,返回的值要作为ctx的参数
instance.setupState = Component.setup?.(instance.props, {
attrs: instance.attrs,
});
// 真实源码中 代理了props,找不到props的属性再去setupState里找
instance.ctx = {
...instance.props,
...instance.setupState,
};
// 生成子节点
const subTree = (instance.subTree = normalizeVNode(
Component.render(instance.ctx)
));
// 获取节点的属性 instance.attr
fallThrough(instance, subTree);
// mount 子节点
patch(null, subTree, container, anchor);
}
更新实现
实现例子2,会响应式更新
用effect
包裹render
函数,render
函数里用到了响应式数据,所以当响应式数据变化时,effect
触发执行里面的render
函数,从而更新视图
instance.update = effect(
() => {
if (!instance.isMounted) {
// mount 初始化渲染,和更新区别是更新是有旧节点去patch的
const subTree = (instance.subTree = normalizeVNode(
Component.render(instance.ctx)
));
// 如果有属性attr,要作为节点属性传过去。
fallThrough(instance, subTree);
// 相对于mount
patch(null, subTree, container, anchor);
instance.isMounted = true;
// 生成渲染后,把el赋值给vnode。
vnode.el = subTree.el;
} else {
// update
const prev = instance.subTree;
const subTree = (instance.subTree = normalizeVNode(
Component.render(instance.ctx)
));
fallThrough(instance, subTree);
patch(prev, subTree, container, anchor);
vnode.el = subTree.el;
}
}
);
这个就是主动更新机制的实现,还有被动更新
被动更新
当父节点有属性变动时,重新渲染,导致子节点也重新渲染
const Child = {
props: ['foo'],
render(ctx) {
return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
},
};
const Parent = {
setup() {
const vnodeProps = reactive({
foo: 'foo',
bar: 'bar',
});
return { vnodeProps };
},
render(ctx) {
return h(Child, ctx.vnodeProps);
},
};
render(h(Parent), root);
重渲染,走patch
,走processComponent
// vnode用到component属性
h(type, props = null, children = null) {
return {
。。。
component: null, // 组件的instance
};
}
// 渲染更新Component组件
function processComponent(n1, n2, container, anchor) {
if (n1 == null) {
mountComponent(n2, container, anchor, patch);
} else {
//走这里
updateComponent(n1, n2);
}
}
// 更新组件
function updateComponent(n1, n2) {
//更新新节点,把旧节点的方法给新节点用
n2.component = n1.component;
// 被动更新标志
n2.component.next = n2;
n2.component.update();
}
// n2.component.update();
instance.update = effect(
() => {
if (!instance.isMounted) {
。。。
} else {
// update
// instance.next存在,代表是被动更新。否则是主动更新
if (instance.next) {
// 重新取值最新的父组件传过来的 props
// 拿到新节点,这里九路十八弯,要跟上车
vnode = instance.next;
instance.next = null;
updateProps(instance, vnode);
instance.ctx = {
...instance.props,
...instance.setupState,
};
}
//同上的渲染代码
const prev = instance.subTree;
const subTree = (instance.subTree = normalizeVNode(
Component.render(instance.ctx)
));
fallThrough(instance, subTree);
patch(prev, subTree, container, anchor);
vnode.el = subTree.el;
})
源码是有shouldUpdateComponent
判断的,vue3
是主动提供,不像react
那样子,当父组件更新时,子组件会判断传入的参数是否有变化,如果有变化再更新,没有就不继续更新了
createApp
createApp({
data() {
return {
count: 0,
};
},
methods: {
add() {
this.count++;
},
},
render(ctx) {
return [
h('div', null, ctx.count),
h(
'button',
{
onClick: ctx.add,
},
'add'
),
];
},
}).mount('#app');
实现
这种写法是为了和vue2类似
export function createApp(rootComponent) {
const app = {
mount(rootContainer) {
// rootContainer : #app
if (typeof rootContainer === 'string') {
rootContainer = document.querySelector(rootContainer);
}
render(h(rootComponent), rootContainer);
},
};
return app;
}