持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情
不知不觉,runtime-core模块已经来到第15篇文章了,已经快接近尾声了
今天我们会来实现更新元素的逻辑,也就是对于一个vnode,它的chilren发生变化的时候,应当如何处理,才能使得数据children的变化导致视图也更新,这就要用到我们前面实现的reactivity模块的响应式功能
思路就是将渲染函数作为响应式数据的依赖,当响应式数据更新的时候重新执行渲染函数,使视图也更新
但是这会带来一个问题,如果一个vnode它的children只有一个,那么更新它很简单,但是如果children是一个数组,其中有很多个vnode,但是只有其中一个或几个vnode发生了变化,难道我们要把全部已经渲染的children数组卸载,然后再挂载吗?这样做无疑是很耗费性能的
所以这就引出了我们的diff算法,在mini-vue里我会使用双端diff算法处理数组children的更新,不过diff算法有点复杂,不能在一篇文章中讲完,我会放到下一篇去讲解
这篇文章我们首先来看看如何处理children的以下三种变化情况:
array->texttext->arraytext->text
1. 案例环境
import { h, ref } from '../../lib/plasticine-mini-vue.esm.js';
export const App = {
name: 'App',
setup() {
const count = ref(0);
const onClick = () => {
count.value++;
};
return {
count,
onClick,
};
},
render() {
return h('div', {}, [
h('div', {}, `count: ${this.count}`),
h('button', { onClick: this.onClick }, 'click'),
]);
},
};
App组件中会渲染一个文本,文本中使用到了响应式变量count的值
并且有一个按钮,我们希望当点击这个按钮时,会让响应式变量count的值加1,并且会更新到视图上
先来看看目前这个案例的运行效果
可以看到,
count的值并没有被显示出来,因为它现在是一个对象
理想的情况应当是在
render函数中通过this访问setupState中的ref响应式变量时,获取到的是它的value属性的值,而不是RefImpl对象本身
接下来就要去处理一下这个问题
2. 访问响应式变量时自动返回value属性
从上面的分析就可以知道,我们应当去setupState相关的地方修改,找到返回setupState的地方,将ref中的value取出返回,这个可以用之前在reactivity模块实现的proxyRefs去处理
返回setupState的地方是src/runtime-core/component.ts中的handleSetupResult函数
function handleSetupResult(instance, setupResult: any) {
// TODO 处理 setupResult 是 function 的情况
if (typeof setupResult === 'object') {
instance.setupState = setupResult;
}
finishComponentSetup(instance);
}
我们只要把这里赋值的setupResult通过proxyRefs包裹即可,proxyRefs会帮助我们把ref属性自动解构出value值的
function handleSetupResult(instance, setupResult: any) {
// TODO 处理 setupResult 是 function 的情况
if (typeof setupResult === 'object') {
- instance.setupState = setupResult;
+ instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
现在就可以直接在render函数中访问ref响应式变量,并且不需要通过value属性去获取了
3. 依赖收集渲染函数
目前我们点击按钮时,响应式数据虽然更新了,但是相应的使用了响应式数据的渲染函数并不会更新,这是因为它虽然使用到了响应式数据,但是并没有被effect包裹,所以不会被作为响应式数据的依赖进行收集
既然如此,那么我们只需要用effect包裹一下就可以了,找到render函数调用的地方,用effect将它包裹起来
// src/runtime-core/renderer.ts
function setupRenderEffect(instance, container) {
effect(() => {
const { proxy, vnode } = instance;
const subTree = instance.render.call(proxy);
// subTree 可能是 Component 类型也可能是 Element 类型
// 调用 patch 去处理 subTree
// Element 类型则直接挂载
patch(subTree, container, instance);
// subTree vnode 经过 patch 后就变成了真实的 DOM 此时 subTree.el 指向了根 DOM 元素
// 将 subTree.el 赋值给 vnode.el 就可以在组件实例上访问到挂载的根 DOM 元素对象了
vnode.el = subTree.el;
});
}
现在响应式数据更新后,render函数就会被重新执行了
虽然是执行了渲染函数了,但是明显有问题,渲染函数并没有对数据进行更新,而是直接重新渲染了新的整个内容,这是因为我们还没有实现更新
Element的逻辑,接下来就会带着大家去实现一下更新Element的逻辑
4. 区分组件是初始化还是更新状态
首先我们要能够区分组件的状态,这样才能判断是否需要对其进行更新渲染
当组件首次被渲染时,应当是一个初始化的状态,而当其已经被初始化过的时候,后续再次对其调用渲染函数时则应当走更新的逻辑
可以给组件实例添加一个isMounted属性,标记其是否被挂载过,被挂载过则执行更新逻辑,否则就是初始化渲染的逻辑
export function createComponentInstance(vnode, parent) {
console.log('createComponentInstance -- parent: ', parent);
const component = {
vnode,
type: vnode.type,
setupState: {},
props: {},
emit: () => {},
slots: {},
provides: parent ? parent.provides : {},
parent,
+ isMounted: false,
};
component.emit = emit as any;
return component;
}
然后在调用渲染函数之前判断一下,首次挂载的时候才调用,更新时我们先控制台输出表示执行更新操作
function setupRenderEffect(instance, container) {
effect(() => {
if (!instance.isMounted) {
console.log('init');
const { proxy, vnode } = instance;
const subTree = instance.render.call(proxy);
// subTree 可能是 Component 类型也可能是 Element 类型
// 调用 patch 去处理 subTree
// Element 类型则直接挂载
patch(subTree, container, instance);
// subTree vnode 经过 patch 后就变成了真实的 DOM 此时 subTree.el 指向了根 DOM 元素
// 将 subTree.el 赋值给 vnode.el 就可以在组件实例上访问到挂载的根 DOM 元素对象了
vnode.el = subTree.el;
instance.isMounted = true; // 初始化后及时将其标记为已挂载
} else {
console.log('update');
}
});
}
return {
createApp: createAppAPI(render),
};
5. 重构渲染逻辑 -- 增加更新vnode的渲染逻辑
既然现在要处理更新逻辑,patch函数的参数就肯定要更改了,由原来只负责处理渲染新的vnode变成了现在需要对比新旧vnode进行更新,所以要对patch函数进行重构
5.1 更新的时候获取新旧vnode
首先我们要在该更新的时候,调用patch函数之前,先将新旧vnode获取出来,可以给组件实例再新增一个属性subTree,记录当前组件实例的render方法返回的vnode
export function createComponentInstance(vnode, parent) {
console.log('createComponentInstance -- parent: ', parent);
const component = {
vnode,
type: vnode.type,
setupState: {},
props: {},
emit: () => {},
slots: {},
provides: parent ? parent.provides : {},
parent,
isMounted: false,
+ subTree: {},
};
component.emit = emit as any;
return component;
}
然后就要处理subTree属性的更新逻辑,应当在初次挂载的时候赋值进行初始化,更新的时候赋值为新的vnode进行更新
function setupRenderEffect(instance, container) {
effect(() => {
if (!instance.isMounted) {
console.log('init');
const { proxy, vnode } = instance;
- const subTree = instance.render.call(proxy);
+ const subTree = (instance.subTree = instance.render.call(proxy));
// subTree 可能是 Component 类型也可能是 Element 类型
// 调用 patch 去处理 subTree
// Element 类型则直接挂载
patch(subTree, container, instance);
// subTree vnode 经过 patch 后就变成了真实的 DOM 此时 subTree.el 指向了根 DOM 元素
// 将 subTree.el 赋值给 vnode.el 就可以在组件实例上访问到挂载的根 DOM 元素对象了
vnode.el = subTree.el;
instance.isMounted = true; // 初始化后及时将其标记为已挂载
} else {
console.log('update');
+ const { proxy, vnode } = instance;
+ const subTree = instance.render.call(proxy); // 新 vnode
+ const prevSubTree = instance.subTree; // 旧 vnode
+ instance.subTree = subTree; // 新的 vnode 要更新到组件实例的 subTree 属性 作为下一更新的旧 vnode
+ console.log('old vnode', prevSubTree);
+ console.log('new vnode', subTree);
+ patch(subTree, container, instance);
}
});
}
先检验一下是否能够在该更新的时候获取到新旧vnode
确实能够获取,那么接下来就可以开始重构
patch函数了
5.2 重构patch函数
原来的patch函数签名如下
function patch(vnode, container, parentComponent = null) {}
由于只能接收一个结点,不能够处理对比新旧vnode进行更新的逻辑,所以我们需要修改一下参数,用n1表示旧vnode,n2表示新vnode
function patch(n1, n2, container, parentComponent = null) {}
然后修改对patch的调用,初次挂载vnode的时候,n1为null,表示没有旧的vnode
而更新的时候,prevSubTree是旧vnode,subTree是新vnode
function setupRenderEffect(instance, container) {
effect(() => {
if (!instance.isMounted) {
const { proxy, vnode } = instance;
const subTree = (instance.subTree = instance.render.call(proxy));
// subTree 可能是 Component 类型也可能是 Element 类型
// 调用 patch 去处理 subTree
// Element 类型则直接挂载
// 初次挂载 n1 不存在
- patch(subTree, container, instance);
+ patch(null, subTree, container, instance);
// subTree vnode 经过 patch 后就变成了真实的 DOM 此时 subTree.el 指向了根 DOM 元素
// 将 subTree.el 赋值给 vnode.el 就可以在组件实例上访问到挂载的根 DOM 元素对象了
vnode.el = subTree.el;
instance.isMounted = true; // 初始化后及时将其标记为已挂载
} else {
const { proxy, vnode } = instance;
const subTree = instance.render.call(proxy); // 新 vnode
const prevSubTree = instance.subTree; // 旧 vnode
instance.subTree = subTree; // 新的 vnode 要更新到组件实例的 subTree 属性 作为下一更新的旧 vnode
- patch(subTree, container, instance);
+ patch(prevSubTree, subTree, container, instance);
}
});
}
6. patchElement
由于目前我们的案例场景就是对一个普通div元素的更新,是一个Element类型的vnode,所以我们先实现对Element的更新逻辑,实现一个patchElement函数
function processElement(n1, n2: any, container: any, parentComponent) {
if (!n1) {
// n1 不存在表示是首次挂载,应当执行初始化的逻辑
mountElement(n2, container, parentComponent);
} else {
// n1 存在表示更新 调用 patchElement 执行更新的逻辑
patchElement(n1, n2, container);
}
}
/**
* @description 对比 n1 和 n2 虚拟结点 找出不同的部分进行更新
* @param n1 旧结点
* @param n2 新结点
* @param container 容器
*/
function patchElement(n1, n2, container) {
console.log('n1', n1);
console.log('n2', n2);
// 找出 props 的不同
// 找出 children 的不同
}
现在能够在
patchElement中获取到新旧虚拟结点了,接下来要做的就是找出它们的不同的地方,然后作出更新即可
6.1 patchProps
首先来处理一下vnode的props的更新逻辑,对比新旧vnode的每一个props,然后会有以下三种情况;
- 新
prop和旧prop都存在,但是值不相同 -- 修改 - 新
prop的值为null 或 undefined,但是旧的prop是存在的 -- 删除 - 新
prop直接key都不存在了,说明该删除了 -- 删除
6.1.1 prop值发生改变
实现一个patchProps函数,接收n1和n2,对比它们的props值,遇到不同的则进行更新
为了和之前自定义渲染器中用户传入的自定义的patchProp进行区分,这里先对自定义渲染器中的参数进行解构重命名
export function createRenderer(options) {
const {
createElement: hostCreateElement,
patchProp: hostPatchProp,
insert: hostInsert,
} = options;
// ...
}
接下来就去实现patchProps
/**
* @description 对比新旧结点的 props 进行更新
* @param n1 旧结点
* @param n2 新结点
*/
function patchProps(oldProps, newProps) {
for (const key in newProps) {
const next = newProps[key];
const prev = oldProps[key];
if (next !== prev) {
hostPatchProp(el, key, prev, next);
}
}
}
遍历新props的每一个key,并且如果和旧的props中对应的值不相等时,就调用自定义渲染器的hostPatchProp方法去进行更新,由于hostPatchProp需要接收el参数,所以我们需要给patchProps新增一个el参数,并且还需要修改自定义渲染器的hostPatchProp,让它能够接收到新旧prop
那么这个el应该从哪里得到呢?el应当是从旧的vnode中获取的,并且要注意,新的vnode在更新之后也会变成下一次更新的旧vnode,因此获取el的同时还要给n2.el赋值
function patchElement(n1, n2, container) {
const el = (n2.el = n1.el);
const oldProps = n1.props ?? {};
const newProps = n2.props ?? {};
// 找出 props 的不同
patchProps(el, oldProps, newProps);
// 找出 children 的不同
}
/**
* @description 对比新旧结点的 props 进行更新
* @param n1 旧结点
* @param n2 新结点
*/
function patchProps(el, oldProps, newProps) {
for (const key in newProps) {
const next = newProps[key];
const prev = oldProps[key];
if (next !== prev) {
hostPatchProp(el, key, prev, next);
}
}
}
由于修改了hostPatchProp,所以我们还需要去runtime-dom中修改以下它的实现
function patchProp(el, key, prevValue, nextValue) {
const isOn = (key: string) => /^on[A-Z]/.test(key);
// 处理事件监听
if (isOn(key)) {
el.addEventListener(key.slice(2).toLowerCase(), nextValue);
} else {
el.setAttribute(key, nextValue);
}
}
最后别忘记了修改使用了hostPatchProp的代码,要给它们加上相应的参数
比如mountElement中就用到了这个函数,因此要修改
function mountElement(vnode: any, container: any, parentComponent) {
// 将创建的元素挂载到 vnode 上 从而让组件实例能够访问到
const el = (vnode.el = hostCreateElement(vnode.type));
const { children, shapeFlag } = vnode;
// children
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
el.textContent = children;
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el, parentComponent);
}
// props
const { props } = vnode;
for (const [key, value] of Object.entries(props)) {
- hostPatchProp(el, key, value);
+ hostPatchProp(el, key, null, value);
}
hostInsert(el, container);
}
现在更新props的功能就算完成了,接下来就要去测试一下看看是否真的可以更新props
6.1.2 测试修改props值
在App.js中定义一下props,并将它作为根组件的props,然后添加一个按钮,点击的时候会修改props的值
export const App = {
name: 'App',
setup() {
const count = ref(0);
const props = ref({
name: 'foo',
age: 20,
});
const onClick = () => {
count.value++;
};
const changeProps = () => {
props.value.name = props.value.name === 'foo' ? 'bar' : 'foo';
props.value.age++;
};
return {
count,
onClick,
changeProps,
props,
};
},
render() {
return h('div', { name: this.props.name, age: this.props.age }, [
h('div', {}, `count: ${this.count}`),
h('button', { onClick: this.onClick }, 'click'),
h(
'button',
{
onClick: this.changeProps,
},
'changeProps'
),
]);
},
};
现在每次点击changeProps按钮,都会让根组件props的name在foo和bar之间反复横跳,而age值则加1,如果我们的更新props的功能实现了的话,应当能够修改相应DOM元素上的props值
可以看到已经成功了!
6.1.3 props值不存在
在这个场景中,我们会将props的值置为undefined,看看能否顺利将props删除掉,由于真正的更新props的逻辑是交给自定义渲染器的hostPatchProp实现的,所以我们要修改runtime-dom中的patchProp函数
function patchProp(el, key, prevValue, nextValue) {
const isOn = (key: string) => /^on[A-Z]/.test(key);
// 处理事件监听
if (isOn(key)) {
el.addEventListener(key.slice(2).toLowerCase(), nextValue);
} else {
+ if (nextValue === undefined || nextValue === null) {
+ el.removeAttribute(key);
} else {
el.setAttribute(key, nextValue);
}
}
}
在App.js中新增一个按钮,点击后会将props中的name置为undefined
// setup
const removeProps = () => {
props.value.name = undefined;
};
return {
count,
onClick,
props,
changeProps,
removeProps,
};
// render
h('button', { onClick: this.removeProps }, 'removeProps')
6.1.4 props key 不存在
如果是新的vnode中,对应的props的key不存在了,说明也是要执行删除逻辑的,这时候就不是在hostPatchProp中处理了,因为即便是props的key不存在,其判断结果也是undefined,也依然会删除,逻辑是正确的,不需要更改
我们这次需要更改的是patchProps中对oldProps中的key进行遍历,如果遍历出在newProps中不存在的key,则需要进行删除
function patchProps(el, oldProps, newProps) {
for (const key in newProps) {
const next = newProps[key];
const prev = oldProps[key];
if (next !== prev) {
hostPatchProp(el, key, prev, next);
}
}
+ // 遍历 oldProps 找出不存在于 newProps 中的 key 进行删除
+ for (const key in oldProps) {
+ if (!(key in newProps)) {
+ hostPatchProp(el, key, oldProps[key], null);
+ }
+ }
}
现在在App.js中再添加一个按钮,点击之后会将props赋值为一个新的对象,这个对象中没有了age属性
// setup
const removeProps2 = () => {
// 移除掉 age 属性
props.value = {
name: 'foo',
};
};
// render
h('button', { onClick: this.removeProps2 }, 'removeProps2')
6.1.5 提高代码健壮性
- 实际上当新旧
props对象是同一个对象的时候,没必要执行遍历它们的key的操作,这纯粹在浪费时间
function patchProps(el, oldProps, newProps) {
if (oldProps !== newProps) {
for (const key in newProps) {
const next = newProps[key];
const prev = oldProps[key];
if (next !== prev) {
hostPatchProp(el, key, prev, next);
}
}
// 遍历 oldProps 找出不存在于 newProps 中的 key 进行删除
for (const key in oldProps) {
if (!(key in newProps)) {
hostPatchProp(el, key, oldProps[key], null);
}
}
}
}
- 旧
props对象是一个空对象的时候没必要进行遍历
注意,js中两个空对象字面量进行逻辑判断的时候是不相等的,虽然都是空对象,但是是两个指向不同内存地址的引用,要进行空对象判断,应当创建一个空对象,然后在oldProps和newProps赋值的时候赋值为这个空对象,判断的时候也是使用这个空对象而不是使用字面量的方式
在shared模块下导出一个空对象
// src/shared/index.ts
export const EMPTY_OBJ = {};
然后修改oldProps和newProps初始化的逻辑
function patchElement(n1, n2, container) {
const el = (n2.el = n1.el);
- const oldProps = n1.props || {};
- const newProps = n2.props || {};
+ const oldProps = n1.props || EMPTY_OBJ;
+ const newProps = n2.props || EMPTY_OBJ;
// 找出 props 的不同
patchProps(el, oldProps, newProps);
// 找出 children 的不同
}
再添加oldProps为空对象的判断逻辑
function patchProps(el, oldProps, newProps) {
if (oldProps !== newProps) {
for (const key in newProps) {
const next = newProps[key];
const prev = oldProps[key];
if (next !== prev) {
hostPatchProp(el, key, prev, next);
}
}
// 遍历 oldProps 找出不存在于 newProps 中的 key 进行删除
+ if (oldProps !== EMPTY_OBJ) {
for (const key in oldProps) {
if (!(key in newProps)) {
hostPatchProp(el, key, oldProps[key], null);
}
}
+ }
}
}
6.2 patchChildren
props的更新逻辑处理完毕之后就要来实现一下children的更新逻辑了,对于children的更新,需要先清除children有哪些类型
前面我们通过ShapeFlags区分不同vnode的类型,并且还能够区分它们的children的类型
// src/shared/shapeFlags.ts
export const enum ShapeFlags {
ELEMENT = 1,
STATEFUL_COMPONENT = 1 << 1,
TEXT_CHILDREN = 1 << 2,
ARRAY_CHILDREN = 1 << 3,
SLOT_CHILDREN = 1 << 4,
}
主要考虑text类型和array类型的子节点的更新
6.2.1 讨论更新的情况
主要有以下四种更新情况:
- 旧
children是array,新children是text - 旧
children是text,新children也是text - 旧
children是text,新children是array - 旧
children是array,新children也是array
其中前三种的处理比较容易
- 第一种情况,只需要将旧的
children数组中的内容清空,然后修改children为text类型,并赋值对应的text内容即可 - 第二种情况则直接修改文本内容即可
- 第三种情况需要先清空文本的内容,再将
array挂载上去
6.2.2 array变成text
1. 案例场景
首先看看array类型的children变成text类型的children的处理逻辑,先搭建一个案例场景
import { h, ref } from '../../lib/plasticine-mini-vue.esm.js';
const prevChildren = [h('p', {}, 'foo'), h('p', {}, 'bar')];
const nextChildren = 'text children';
export const ArrayToText = {
name: 'ArrayToText',
setup() {
const toggleChildren = ref(true);
window.toggleChildren = toggleChildren;
return {
toggleChildren,
};
},
render() {
return this.toggleChildren
? h('div', {}, prevChildren)
: h('p', {}, nextChildren);
},
};
子组件中根据toggleChilren这一响应式变量的值去渲染不同的vnode,为了方便测试,在组件内部将toggleChildren挂载到window中,然后通过控制台中修改响应式数据的值的方式触发更新逻辑
父组件中直接渲染子组件
export const App = {
name: 'App',
setup() {},
render() {
return h('div', { id: 'root' }, [h(ArrayToText)]);
},
};
可以看到输出的新旧
vnode的shapeFlag是有变化的,array类型的shapeFlag为8,text类型的shapeFlag为4,再加上元素自身是Element类型的,shapeFlag为1,因此最终的shapeFlag为所有shapeFlag之和
2. patchChildren
创建一个patchChilren函数,先处理从array到text的转换
function patchChildren(n1, n2) {
// n2 的 children 是 text 类型
const prevShapeFlag = n1.shapeFlag;
const { shapeFlag } = n2;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// n1 的 children 是 array 类型
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// TODO 1. 将 array 的内容卸载 2. 将 text 的内容挂载
}
}
}
目前要做的就是两件事:
- 将 array 的内容卸载
- 将 text 的内容挂载
移除array的内容
卸载array的内容可以创建一个unmountChildren函数,传入要卸载的children
function unmountChildren(children) {
for (let i = 0; i < children.length; i++) {
// 获取到 vnode 中的 el
const el = children[i].el;
// 调用自定义渲染器中的 remove 逻辑
hostRemove(el);
}
}
由于移除元素会和DOM操作有关,因此可以归于自定义渲染器的功能,所以改为调用自定义渲染器的remove方法,然后我们再去实现runtime-dom中相应的渲染器的remove方法
+ /**
+ * @description 移除子元素
+ * @param child 子元素
+ */
+ function remove(child) {
+ const parent = child.parentNode;
+ if (parent) {
+ parent.removeChild(child);
+ }
+ }
const renderer: any = createRenderer({
createElement,
patchProp,
insert,
+ remove,
});
设置text的内容
需要将什么东西设置为text呢?
实际上应当是将这里的整个
div元素内的子结点改成text结点,因此我们需要获取到这个div元素,给patchChildren函数加上一个container参数,存放整个children的根元素
- function patchChildren(n1, n2) {}
+ function patchChildren(n1, n2, container) {}
container从哪里来呢?container实际上就是旧的vnode中的el,也就是n1.el
- patchChildren(n1, n2);
+ patchChildren(n1, n2, el);
由于也是涉及到DOM操作,所以可以把它抽象成自定义渲染器的接口函数,现在再给自定义渲染器增加一个setElementText函数,用于修改元素的文本内容,由于新的文本内容来自n2.children,所以该函数要接收的text参数就是n2.children,而Element则是patchChildren的container
const c2 = n2.children;
hostSetElementText(container, c2);
export function createRenderer(options) {
const {
createElement: hostCreateElement,
patchProp: hostPatchProp,
insert: hostInsert,
remove: hostRemove,
+ setElementText: hostSetElementText,
} = options;
// ...
}
然后在runtime-dom中实现这个新的接口函数
+ function setElementText(el, text) {
+ el.textContent = text;
+ }
const renderer: any = createRenderer({
createElement,
patchProp,
insert,
remove,
+ setElementText,
});
6.2.3 text变成text
还是先搭建一下案例场景
export const TextToText = {
name: 'TextToText',
setup() {
const toggleTextToText = ref(true);
window.toggleTextToText = toggleTextToText;
return {
toggleTextToText,
};
},
render() {
return this.toggleTextToText
? h('p', {}, 'old text')
: h('p', {}, 'new text');
},
};
修改toggleTextToText变量的值会导致渲染函数重新执行,切换不同的文本
实现思路很简单,在原来的基础上,添加一个else分支调用hostSetElementText即可,因为原先的if分支已经判断了旧结点为array,那么else就意味着就结点不是array,只能是text了,对于text到text的转换,直接修改文本内容即可
function patchChildren(n1, n2, container) {
// n2 的 children 是 text 类型
const prevShapeFlag = n1.shapeFlag;
const { shapeFlag } = n2;
const c2 = n2.children;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 新 children 是 text 类型
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧 children 是 array 类型 -- 从 array 变为 text
// 卸载 array 的内容
unmountChildren(n1.children);
// 挂载 text 的内容
hostSetElementText(container, c2);
+ } else {
+ // 旧 children 是 text 类型 -- 从 text 变为 text
+ hostSetElementText(container, c2); // 直接修改文本内容即可
+ }
}
}
6.2.4 从text变成array
text变成array我们需要先清空text中的内容,然后再将array挂载上去
- function patchChildren(n1, n2, container) {
+ function patchChildren(n1, n2, container, parentComponent) {
// n2 的 children 是 text 类型
const prevShapeFlag = n1.shapeFlag;
const { shapeFlag } = n2;
const c2 = n2.children;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 新 children 是 text 类型
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧 children 是 array 类型 -- 从 array 变为 text
// 卸载 array 的内容
unmountChildren(n1.children);
// 挂载 text 的内容
hostSetElementText(container, c2);
} else {
// 旧 children 是 text 类型 -- 从 text 变为 text
hostSetElementText(container, c2); // 直接修改文本内容即可
}
+ } else {
+ // 新 children 是 array 类型
+ if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
+ // 旧 children 是 text 类型 -- 从 text 变为 array
+
+ // 清空旧结点中的文本内容
+ hostSetElementText(container, '');
+
+ // 挂载新结点中 array 的内容
+ mountChildren(c2, container, parentComponent);
+ }
+ }
}
mountChildren需要一个parentComponent参数,我们给patchChildren加上这个参数后传给mountChildren即可,然后再沿着调用链往上添加parentComponent参数,直到能够获取到该参数为止