加加 React 30 问 -- 2. Diff 算法是什么(2)-- 单节点文本 Diff?

163 阅读4分钟

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
    }
}

说明

  • 系列文字主要是达到输出倒逼输入的目的,但是原理可能有丢丢懂了,看源码花费的时间比较多,所以单个问题详细看完时间差不多就么得了。
  • 所以单个问题可能多天更完,这也是无奈之举,毕竟能力有限,如果有小伙伴也看源码,探究一下他具体实现如何,可以一起讨论讨论,指导指导一下我
  • 日更是习惯的养成,不单单是文章输出,还包括了输入学习,思考,坚持等等一系列,当时间不够的时候,可能只能断章了,后续单个问题会出个汇总,那样就完善了。
  • 希望能每日早起,每日学习,每日思考,每日更新,加油。。