Skip to content

Latest commit

 

History

History
299 lines (275 loc) · 10.6 KB

update_diff.md

File metadata and controls

299 lines (275 loc) · 10.6 KB

更新流程和diff算法

我们几乎所有流程都要从scheduleUpdateOnFiber开始

其他和流程和mount流程基本一致,值得注意的是 prepareFreshStack 会使用之前构建的root.current基础上复用child但是之前已经创建了一个fiber(root.current.alternate) 相当于两个fiber目前的child指向同一块

我们从beginwork开始看

switch (wip.tag) {
        case HostRoot:
            return updateHostRoot(wip, renderLane);
        case HostComponent:
            return updateHostComponent(wip);
        case HostText:
            return null;
        case FunctionComponent:
            return updateFunctionComponent(wip, wip.type, renderLane);
        case Fragment:
            return updateFragment(wip);
        case ContextProvider:
            return updateContextProvider(wip, renderLane);
        case SuspenseComponent:
            return updateSuspenseComponent(wip);
        case OffscreenComponent:
            return updateOffscreenComponent(wip);
        case MemoComponent:
            return updateMemoComponent(wip, renderLane);
        default:
            if (__DEV__) {
                console.warn('beginWork未实现的类型');
            }
            break;
    }

核心还是不同类型的处理 我们可以主要看看ChildReconciler 在这里会有相应的diff

单节点diff

单节点注意指的是最后更新后变为单一节点

function reconcileSingleElement(
        returnFiber: FiberNode,
        currentFiber: FiberNode | null,
        element: ReactElementType
    ) {
        const key = element.key;
        while (currentFiber !== null) {
            if (currentFiber.key === key) {
                //key 相同
                if (element.$$typeof === REACT_ELEMENT) {
                    if (currentFiber.type === element.type) {
                        let props = element.props;
                        if (element.type === REACT_FRAGMENT) {
                            props = element.props.children;
                        }
                        //type 相同
                        const existing = useFiber(currentFiber, props);
                        existing.return = returnFiber;
                        // 当前节点可复用,标记剩下的节点删除
                        deleteRemainingChildren(returnFiber, currentFiber.sibling);
                        return existing;
                    }
                    //key 相同 type 不同
                    deleteRemainingChildren(returnFiber, currentFiber);
                    break;
                } else {
                    if (__DEV__) {
                        console.warn('还未实现的react类型');
                        break;
                    }
                }
            } else {
                //key不同 删掉旧的
                deleteChild(returnFiber, currentFiber);
                currentFiber = currentFiber.sibling;
            }
        }
        let fiber;
        if (element.type === REACT_FRAGMENT) {
            fiber = createFiberFromFragment(element.props.children, key);
        } else {
            fiber = createFiberFromElement(element);
        }
        fiber.return = returnFiber;
        return fiber;
    }

单节点首先对比传入的key和type(普通元素就是div,p等)如果相同就可以进行复用 调用useFiber

function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode {
    //反复更新只有两个fiber
    const clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0;
    clone.sibling = null;
    return clone;
}

同时如果有兄弟由于更新后是单节点所有会标记删除 如果不同就会一直找其兄弟元素看是否可以复用 如果都没有就会走到createFiberFromElement(通过jsx创建新的fiber) 这样他的孩子节点也都没有了

多节点diff

多节点注意指的是最后更新后变为多节点

function reconcileChildArray(
        returnFiber: FiberNode,
        currentFirstChild: FiberNode | null,
        newChild: any[]
    ) {
        //最后一个可复用fiber在current的index
        let lastPlacedIndex: number = 0;
        //创建的最后一个fiber
        let lastNewFiber: FiberNode | null = null;
        //第一个fiber
        let firstNewFiber: FiberNode | null = null;

        //1.将current保存在map中
        const existingChildren: ExistingChildren = new Map();
        let current = currentFirstChild;

        while (current !== null) {
            const keyToUse = current.key !== null ? current.key : current.index;
            existingChildren.set(keyToUse, current);
            current = current.sibling;
        }

        for (let i = 0; i < newChild.length; i++) {
            //2.遍历newChild,寻找是否可复用
            const after = newChild[i];
            const newFiber = updateFromMap(returnFiber, existingChildren, i, after);

            if (newFiber === null) {
                continue;
            }
            //3.标记移动还是插入
            newFiber.index = i;
            newFiber.return = returnFiber;
            if (lastNewFiber === null) {
                lastNewFiber = newFiber;
                firstNewFiber = newFiber;
            } else {
                lastNewFiber.sibling = newFiber;
                lastNewFiber = lastNewFiber.sibling;
            }
            if (!shouldTrackEffects) {
                continue;
            }
            const current = newFiber.alternate;
            if (current !== null) {
                const oldIndex = current.index;
                if (oldIndex < lastPlacedIndex) {
                    //移动
                    newFiber.flags |= Placement;
                    continue;
                } else {
                    //不移动
                    lastPlacedIndex = oldIndex;
                }
            } else {
                //mount
                newFiber.flags |= Placement;
            }
        }

        //4.将Map中剩下的标记为删除
        existingChildren.forEach((fiber) => {
            deleteChild(returnFiber, fiber);
        });

        return firstNewFiber;
    }

lastPlacedIndex 最后一个可复用fiber在current的index lastNewFiber 创建的最后一个fiber firstNewFiber 第一个fiber

  const keyToUse = current.key !== null ? current.key : current.index;

这里可以看到我们如果没有自动设置key的话就是用fiber的index,这个index其实就是元素array的索引

我们会根据这个keyToUse记录在map里,用来和新的element就是jsx生成的reactElement的对象 我们会走进updateFromMap 这个函数核心就是取出与新的key相同的element然后比对type如果一样说明可以直接复用走进useFiber 同时删掉map里的该key,如果不可以复用则走进createFiberFromElement重新创建fiber 之后核心逻辑就是判断标记移动还是插入

           if (lastNewFiber === null) {
                lastNewFiber = newFiber;
                firstNewFiber = newFiber;
            } else {
                lastNewFiber.sibling = newFiber;
                lastNewFiber = lastNewFiber.sibling;
            }
            if (!shouldTrackEffects) {
                continue;
            }
            const current = newFiber.alternate;
            if (current !== null) {
                const oldIndex = current.index;
                if (oldIndex < lastPlacedIndex) {
                    //移动
                    newFiber.flags |= Placement;
                    continue;
                } else {
                    //不移动
                    lastPlacedIndex = oldIndex;
                }
            } else {
                //mount
                newFiber.flags |= Placement;
            }

我们会记录最后一个新产生的节点 lastNewFiber和第一个firstNewFiber 如果没有标记shouldTrackEffects直接退出本次循环 同时如果是newFiber.alternate不存在证明该节点为mount阶段直接Placement插入 我们比对改变只要让后面的节点插在前面就好 所以只需记录lastPlacedIndex最后一个不需移动的index 如果之前fiber的index小于lastIndex证明要往前插入所以要标记Placement 移动的判断依据:记录当前遍历到的新节点在旧节点列表中对应的index,当遍历element时, 「当前遍历到的element」 一定是 「所有已遍历的element」 中最靠右那个。

拿着新的fiber寻找新的fiber在旧fiber列表中的位置,以上一次找到的位置为坐标。 所以只需要记录 「最后一个可复用fiber」 在current中的index(lastPlacedIndex),在接下来的遍历中:

  • 如果接下来遍历到的 「可复用fiber」 的index < lastPlacedIndex,则标记Placement
  • 否则,不标记

再到最后我们把map里没用的fiber标记删除即可

existingChildren.forEach((fiber) => {
            deleteChild(returnFiber, fiber);
 });

将 firstNewFiber返回出去

completeWork

comleteWork就是通过比对props然后标记Update flag 如果ref有变化就标记,同时冒泡lane和flag

commitWork

主要分为

  • beforeMutation
  • commitMutationEffects
  • commitLayoutEffects

commitMutationEffects

主要进行更新props 绑定Ref 收集update的useEffect回调函数 对有placement进行一定的插入 同时指行删除操作 删除时要把useEffect的清理函数推入,解绑ref

const commitMutationEffectsOnFiber = (
    finishedWork: FiberNode,
    root: FiberRootNode
) => {
    const { flags, tag } = finishedWork;

    if ((flags & Placement) !== NoFlags) {
        commitPlacement(finishedWork);
        finishedWork.flags &= ~Placement;
    }
    //flags Update
    if ((flags & Update) !== NoFlags) {
        commitUpdate(finishedWork);
        finishedWork.flags &= ~Update;
    }
    //flags ChildDeletion
    if ((flags & ChildDeletion) !== NoFlags) {
        const deletions = finishedWork.deletions;

        deletions?.forEach((childToDelete) => {
            commitDelete(childToDelete, root);
        });
        finishedWork.flags &= ~ChildDeletion;
    }

    if ((flags & PassiveEffect) !== NoFlags) {
        //收集回调
        commitPassiveEffect(finishedWork, root, 'update');
        finishedWork.flags &= ~PassiveEffect;
    }

    if ((flags & Ref) !== NoFlags && tag === HostComponent) {
        safelyDetachRef(finishedWork);
    }

    if ((flags & Visibility) !== NoFlags && tag === OffscreenComponent) {
        const isHidden = finishedWork.pendingProps.mode === 'hidden';
        hideOrUnhideAllChildren(finishedWork, isHidden);
        finishedWork.flags &= ~Visibility;
    }
};