diff 算法分类
diff 算法按照子节点的数量,可以分成单节点 Diff 和 多节点Diff
- 对于单节点 Diff,可以根据节点类型,分成对象节点和文本节点
- 对于多节点 Diff,就是多对多的比较,一般都可以看做 jsx 节点数组与 fiber 链表之间的比较
单点 Diff
- 对于单点 Diff 而言,首先查看一下 currentFiber 是否也是
文本节点
// 例子1
<p>你好</p> => <p>你好呀</p>
// 例子2:多节点变成了单节点
<div>
<p>111</p>
<p>你好呀</p>
</div> => <div>你好呀</div>
- 形如上面的更新,对于文本的更新就会触发单节点中的文本更新方法 reconcileSingleTextNode,返回新的 fiber
- 然后再给这个fiber 打上 Placement 更新的 tag,然后再 commit 阶段就会找到这里,在页面上更新 dom 了
/**
* 处理文本节点的 Diff -- 单节点 Diff
* @param {fiber} returnFiber 父fiber
* @param {fiber} curretnFirstChild 同一层级的第一个子 fiber,sibling 连接兄弟 fiber,虽然是单节点 diff,但是 这一层的 fiber 不一定只有一个,因为可能是删除操作
* @param {string} textContent jsx 对象的文本,无论是 string,number 还是其他,都先转成 string 处理
* @param {lanes} lanes 优先级相关,在 legacy 模式暂时不用
*/
function reconcileSingleTextNode(
returnFiber,
curretnFirstChild,
textContent,
lanes
) {
// 在文本diff 中,是唯一不需要考虑 key 的,因为只有文本,文本是没有任何属性值的
if (curretnFirstChild !== null && curretnFirstChild.tag === HostText) {
// 当前 fiber 在类型上也是 Text,所以只需要和这个 fiber 对比,其他同层级的 fiber,直接标记删除
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
// 根据 jsx 的 text 和 currentFiber 节点,创建 WIP fiber 节点
const existing = useFiber(currentFirstChild, textContent);
// 父指针指向父 fiber
existing.return = returnFiber
return existing
}
// 如果第一个 fiber 不符合,由于这是文本单节点Diff,不考虑 key,所以直接删除所有旧的fiber,重新开始
deleteRemainingChildren(returnFiber, currentFirstChild);
// 根据 jsx 的文本内容,父节点的 mode - legacy,lanes优先级,新建一个 fiber
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
元素节点 -- 更新
- 与文本节点相比,元素会遍历同层次的所有 fiber 去查找是否更改的机会
- 元素节点会先依赖 key 比对,然后根据类型比对,最后如果找到了进行更新标记并返回,否则删除 currentFiberChild。
- 如果没有匹配成功,根据 element 创建一个新的 fiber,然后挂载起来
/**
* 处理对象节点的 Diff -- 单节点 Diff
* @param {fiber} returnFiber 父fiber
* @param {fiber} curretnFirstChild 同一层级的第一个子 fiber,sibling 连接兄弟 fiber,虽然是单节点 diff,但是 这一层的 fiber 不一定只有一个,因为可能是删除操作
* @param {element} element jsx 中返回的 reactElement
* @param {lanes} lanes 优先级相关,在 legacy 模式暂时不用
*/
function reconcileSingleElement(
returnFiber,
curretnFirstChild,
element,
lanes
) {
const key = element.key
let child = curretnFirstChild
// 遍历 currentFiber, 和 element 进行比较
// 这个和文本区别在于,文本只考虑第一个 fiber 节点,如何类型相同,就删除其他兄弟节点;如果不符合,就删除全部该层级的fiber,不会给其他兄弟机会
// 这里更像相亲,不合适换一个再继续;文本就像非诚勿扰,你只能选一个,如果他不跟你走,你就自己走,其他都不考虑了。
while (!child) {
// key 的重要性在这里体现了,只要 key 不一样,就相当于价值观不一样,再优秀也不考虑了
// 当然如果都没啥价值观,那还是可以看看其他条件的(没有 key 属性)
if (child.key === key) {
// 判断更新前后类型是否一致,tag 就是
switch (child.tag) {
// case Fragment: ...
// case Block: ...
default: {
if (child.elementType === element.type) {
// 这个很好,后面的全部删除不考虑了
deleteRemainingChildren(returnFiber, child.sibling)
const existing = useFiber(child, element.props)
// ref 属于特殊属性,不在 props 中,所以需要单独处理
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber
return existing
}
break;
}
}
// 如果 key 相同,类型不同,也不能要,删除全部,重新开始吧
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 如果 key 不匹配,则直接当前 fiber,换兄弟来
deleteChild(returnFiber, child)
}
// 换兄弟 fiber 来
child = child.sibling
}
// 如果在 fiber 中没有匹配上,那么这个节点就是新增的了
if(element.type === REACT_REACT_FRAGMENT_TYPE){
// ...
}else{
// 根据 element 创建一个 fiber
const create = createFiberFromElement(element,returnFiber.mode,lane)
create.ref = coerceRef(returnFiber, child, element);
create.return = returnFiber
return create
}
}
说明
- 系列文字主要是达到
输出倒逼输入的目的,但是原理可能有丢丢懂了,看源码花费的时间比较多,所以单个问题详细看完时间差不多就么得了。 - 所以单个问题可能多天更完,这也是无奈之举,毕竟能力有限,如果有小伙伴也看源码,探究一下他具体实现如何,可以一起讨论讨论,指导指导一下我
- 日更是习惯的养成,不单单是文章输出,还包括了输入学习,思考,坚持等等一系列,当时间不够的时候,可能只能断章了,后续单个问题会出个汇总,那样就完善了。
- 希望能每日早起,每日学习,每日思考,每日更新,加油。。