补充相关的属性
next
next 指向当前组件对应的待更新的虚拟节点
// component.ts
export function createComponentInstance(vnode, parent) {
const component = {
next: null, // 如果组件有变化,那么这个 next 指向待更新的虚拟节点
};
return component;
}
component
// vnode.ts
export function createVNode(type, props?, children?) {
const vnode = {
component: null, // 指组件实例
};
}
- 初始渲染,虚拟节点的
component在mounComponent的时候更新为组件的实例:
function mountComponent(initialVNode, container, parentComponent, anchor) {
// 创建组件实例 instance
const instance = (
initialVNode.component = // 虚拟节点的component其实是一个组件实例
createComponentInstance(initialVNode, parentComponent)
);
}
- 组件更新的情况下,新虚拟节点的
component在updateComponent里面被初始化,instance的更新需要到instance.update执行之后完成。
function updateComponent(n1, n2) {
const instance = (n2.component = n1.component); // 新的虚拟节点的 component 初始化
}
$props 属性
给组件代理对象添加 $props属性,指向组件实例的 props 属性。
// componentPublicInstance.ts
const PublicPropertiesMaps = {
$props: (i) => i.props,
}
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const publicGetter = PublicPropertiesMaps[key];
if(publicGetter) {
return publicGetter(instance);
}
}
}
更新 component 的过程
假设 App 组件嵌套有一个子组件 Child
const App = {
name: 'App',
setup() {
const msg = ref('lallallla');
return {
msg
}
},
render() {
return h('div', { class: 'app' }, [
h(Child, {
msg: this.msg,
})
])
}
}
-
首先是
effect- 在
App组件的render函数内有响应式数据msg发生变化,会自动触发effect - 再到
patch->processElement->patchElement->patchChildren->patchKeyedChildren - 再次来到
patch,但是这次 patch 的是 Child 组件 - 所以会来到
processComponent->updateComponent,在这里会判断是否shouldUpdateComponent shouldUpdateComponent在新旧节点的props值有变化的时候,返回 true- 如果
shouldUpdateComponent是true,会做两件事:instance.next = n2;instance.update();- 该
instance指的是 Child 组件的实例
- 在
-
instance.update被调用- Child 组件的 render 被再次调用,
- 继续 patch Child 组件的儿子节点,最终完成 Child 组件的更新
patch
// n1 -> old, n2 -> new
function patch(n1, n2, container, parentComponent, anchor) {
const { shapeFlag, type } = n2;
switch (type) {
case Fragment:
// ...
break;
case Text:
// ...
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// ...
} else {
// 处理组件
processComponent(n1, n2, container, parentComponent, anchor);
}
break;
}
}
function processComponent(n1, n2, container, parentComponent, anchor) {
if(!n1) {
mountComponent(n2, container, parentComponent, anchor);
// 存在新旧节点,更新
} else {
updateComponent(n1, n2);
}
}
function updateComponent(n1, n2) {
const instance = (n2.component = n1.component);
if(shouldUpdateComponent(n1, n2)) {
instance.next = n2;
instance.update();
} else {
n2.el = n1.el;
instance.vnode = n2;
}
}
shouldUpdateComponent
// componentUpdateUtils.ts
export function shouldUpdateComponent(prevVNode, nextVNode) {
const { props: prevProps } = prevVNode;
const { props: nextProps } = nextVNode;
for(const key in nextProps) {
if(nextProps[key] !== prevProps[key]) {
return true;
}
}
return false;
}
instance.update
function setupRenderEffect(instance, initialVNode, container, anchor) {
// instance.update 是一个 runner
instance.update = effect(() => {
if(!instance.isMounted) {
console.log('init');
const { proxy } = instance;
const subTree = (instance.subTree = instance.render.call(proxy, proxy));
debugger
patch(null, subTree, container, instance, anchor);
// vnode -> element
initialVNode.el = subTree.el;
instance.isMounted = true;
// 更新
} else {
console.log('update');
// 更新 props,需要一个 vnode
const { next, vnode } = instance;
if(next) {
// 设置 新虚拟节点的 el
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const { proxy } = instance;
const subTree = instance.render.call(proxy, proxy);
const prevSubTree = instance.subTree;
instance.sbuTree = subTree;
patch(prevSubTree, subTree, container, instance, anchor);
}
});
}
至此,component 的更新完成。
但是有些疑问:
Element Props 和 Component Props
之前实现的 Element 的 props 更新涉及到 props,这次组件更新也涉及到 props,它们有什么区别呢?
根据 vnode.props 更新 Element 的 Props
// renderer.ts
function processElement(n1, n2, container, parentComponent) {
if(!n1) {
mountElement(n2, container, parentComponent)
} else {
patchElement(n1, n2, container);
}
}
function patchElement(n1, n2, container) {
// 获得新旧 props
oldProps = n1.props || EMPTY_OBJ;
newProps = n2.props || EMPTY_OBJ;
// 在这里更新 el
el = (n2.el = n1.el);
patchChildren(n1, n2, el, parentComponent, anchor);
patchProps(el, oldProps, newProps);
}
function patchProps(el, oldProps, newProps) {
// 添加|更新 props
for (key in newProps) {
prevProp = oldProps[key];
nextProp = newProps[key];
// 对比,是不同的值
if (prevProp !== nextProp) {
// 更新 props
hostPatchProp(el, key, prevProp, nextProp);
}
}
// 移除不存在的旧 props
if (oldProps !== EMPTY_OBJ) {
for (key in oldProps) {
// 新的 props 没有找到旧的 props
if !(key in newProps) {
// 移除旧的 props
hostPatchProp(el, key, oldProps[key], null);
}
}
}
}
根据 vnode.props 判断更新 Component
function updateComponent(n1, n2) {
const instance = (n2.component = n1.component);
if(shouldUpdateComponent(n1, n2)) {
instance.next = n2;
instance.update();
} else {
n2.el = n1.el;
instance.vnode = n2;
}
}
再次分析上面的代码,我们可以看出一些区别:
- 更新 Element 的 Props,其实是实打实地针对标签的属性做更新
h('div', { class: 'red' }, 'Hello World'); // old
h('div', { class: 'blue' }, 'Hello World'); // new
- 更新 Component,
props的作用是用于判断是否需要更新组件,即props有变更,则组件需要更新
h(Child, { msg: '1' }); // old
h(Child, {msg: '2'}); // new
- 组件的更新,是通过调用
instance.update去触发的 - 组件的 vnode.props 是组件级别的,Element 的 vnode.props 是标签级别的
- 组件的 vnode.props 是传给子组件作用于子组件的,Element 的 vnode.props 是作用于本身的
props $props 的区别
// App.js
import { h, ref } from '../../lib/guide-mini-vue.esm.js';
import Child from './Child.js';
export const App = {
name: 'App',
setup() {
const msg = ref('123');
window.msg = msg;
const changeChildProps = () => {
msg.value = '456';
}
return {
msg,
changeChildProps,
}
},
render() {
return h('div', { class: 'app' }, [
h(
'button',
{
onClick: this.changeChildProps,
},
'change child props',
),
h(Child, {
msg: this.msg,
}),
])
}
}
// Child.js
import { h } from '../../lib/guide-mini-vue.esm.js'
export default {
name: 'Child',
setup(props, { emit }) {
return {
msg: props.msg
}
},
render({ $props }) {
return h('div', { class: 'child' }, [
// 因为在传给 setup 的 props,是只读的,所以这里不会变
h('div', {}, 'child - setup - props - msg: ' + this.msg),
h('div', {}, 'child - $props - msg: ' + this.$props.msg),
h('div', {}, 'child - render - $props - msg: ' + $props.msg),
])
}
}
在这个例子中,如果我们改变 msg 的数据,在子组件 Child 中的三种 props 的属性取值会是一样吗?
答案是不一样。
来看下结果:
this.$props.msg、$props.msg 是会变的,但是 this.msg 不会变。
通过之前写的文章,我们应该清楚了 this.msg 是访问的 Child 的 setup 的返回值对象 setupState 的属性。
setup(props, { emit }) {
return {
msg: props.msg
}
},
这里直接把 props.msg 传给了 msg,但是 setup 是不具有 effect 功能的,所以监听不到 props 的变化,所以这里通过 this.msg 获取的值没变。
this.$props.msg、$props.msg 会变,是因为在 render 函数中,实质是获取的 instance 的 props 属性。
const PublicPropertiesMaps = {
$props: (i) => i.props,
}
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const publicGetter = PublicPropertiesMaps[key];
if(publicGetter) {
return publicGetter(instance);
}
}
}
更新 component 的 props 时机
在什么时候完成组件 props 的更新的呢?
是在 updateComponentPreRender 的时候,更新 Child 组件的儿子节点之前。
updateComponentPreRender 是在执行 Child 组件的 render 之前执行的。
function updateComponentPreRender(instance, nextVNode) {
// 更新当前实例的 vnode 为新的
instance.vnode = nextVNode;
// 当前实例的 next 节点清空
instance.next = null;
// 更新当前实例的 props
instance.props = nextVNode.props;
}
所以在Child 组件的子 DOM 完成更新渲染之前,Child 的 Props 已完成了更新,最终页面上显示的 $props.msg、this.$props.msg 是更新后的。