如何更新元素的子节点
首先回顾下元素的子节点是怎么被挂载的,看mountElement函数:
function mountElement(vnode, container){
const el = vnode.el = createElement(vnode,type)
// 挂载子节点,首先判断children的类型 字符串,数组等
// 如果是字符串类型,说明是文本子节点
if(typeof vnode.children === 'string'){
setElementText(el,vnode.children)
}else if(Array.isArray(vnode.children)){
// 如果是数组,说明是多个子节点
vnode.children.forEach(child => {
patch(null, child, el)
})
}
if(vnode.props){
for(const key in vnode.props){
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container)
}
注意这里在挂载的时候要区分子节点类型,为什么要区分子节点的类型?其实这是一个规范性问题,因为只有子节点的类型是规范化的,才有利于编写更新逻辑。那么应该怎么规范化vnode.children,在搞清楚这个问题之前,先要搞清楚一个HTML页面中,元素的子节点都有哪些情况。如下面HTML代码所示:
<!-- 没有子节点 -->
<div></div>
<!-- 文本子节点 -->
<div>Some Text</div>
<!-- 多个子节点 -->
<div>
<p>
</p>
</div>
一个元素的子节点无非以下三种情况
- 没有子节点,此时vnode.children的值为null
- 文本子节点,此时vnode.children的值为字符串
- 其他情况,无论是单个元素子节点,还是多个子节点,都可以用数组来表示
因此渲染器执行更新时,新旧子节点都分别是三种情况之一。因此可以总结出更新子节时有九种可能。(新节点有三种情况,旧节点也有三种情况,组合起来用九种情况)
但落实到代码,会发现其实不需要完全覆盖九种情况,下面先看patchElement函数的代码:
function patchElement(n1,n2){
const el = n2.el = n1.el
const oldProps = n1.props
const newProps = n2.props
// 第一步 更新props
for(const key in newProps){
if(newProps[key] !== oldProps[key]){
patchProps(el,key,oldProps[key],newProp[key])
}
for(const key in oldProps){
if(!(key in newProps)){
patchProps(el,key,oldProps[key],null)
}
}
// 第二步 更新children
patchChildren(n1,n2,el)
}
}
patchChildren的函数实现如下
function patchChildren(n1,n2,container){
// 判断新子节点的类型是否是文本节点
if(typeof n2.children === 'string'){
// 旧子节点的类型有三种可能
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下都不需要做
if(Array.isArray(n1,children)){
n1.children.forEach((c)=>{unmount(c1))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, n2.childrien )
}else if(Array.isArray(n2.children)){
// 如果是一组子节点
// 先判旧子节点是否也是一组子节点
if(Array.isArray(n1.children)){
// 如果是一组子节点,需要用到Diff算法
// 后面会详细介绍Diff算法
}else{
// 这里,旧子节点要么是文本子节点,要么不存在
// 但无论是哪种情况,都只需要将容器清空,然后将新的一组子节点逐个挂载
setElement(container, '')
n2.children.forEach(c => patch(null,c,container))
}
}else{
// 代码运行到这里,说明新子节点不存在
// 如果旧子节点是一组,只需逐个卸载即可
if(Array.isArray(n1.children)){
n1.children.forEach(c=>unmount(c))
}else if(typeof n1.children === 'string'){
// 旧子节点是文本子节点,清空内容即可
setElementText(container, '')
}
// 如果也没有就子节点,那么什么都不要做
}
}