实现一个 Mini React:核心功能详解 - useLayoutEffect 的实现, 从底层理解运行机制

144 阅读4分钟

xdm,又要到饭了,又更新代码了!

总结一下上一篇完成的内容,

  1. 完成了useDeferredValue的实现, 从底层理解运行机制。

有兴趣的可以点这里查看useDeferredValue的实现, 从底层理解运行机制

这一章节我们实现一个 useLayoutEffect。

useLayoutEffect

如果你进入这个api的官方页面 (react.dev/reference/r… 你就会遇见一个提示信息,其建议你尽量使用useEffect, 这个api会有影响性能的可能性。

这个有点矛盾但又不矛盾?矛盾在于既然可能有性能问题,那么为什么还需要开发这个api给开发者用呢? 其实呀,说清楚点就是,无法保证每一个开发者都是专业级的,避免这个api的使用姿势不正确进而有性能的问题。

useEffect vs useLayoutEffect

useEffect 我们已经从0-1开发过了,了解了其底层原理(有兴趣的小伙伴可以查看之前的文章)。这2个api从名字就可以知道,作用非常类似。

确实,这2个api的用途都基本一样, 不同的点在于它们运行的时机,以及一个async 还有一个 sync。

useLayoutEffect 会在dom操作进行完毕之后ui更新之前调用,其运行机制属于同步。也就是说,如果你的callback (effect) 任务非常复杂,则可能会有卡顿(即一个frame无法完成任务)。

useEffect 属于异步任务,其内部会配合使用concurrent mode 完成。性能部分由react进行优化了。

useLayoutEffect 的实现

即然上面说的这2个api完成的任务基本一样只是运行时机不同,那么,我们先看下之前我们完成的useEffect源码实现。这样可以更加直观的知道2个的底层区别

function useEffect(effect, deps) {
    // workInProgress, 当前处理的fiber 单元
    const oldHook = workInProgress.alternate && workInProgress.alternate.hooks && workInProgress.alternate.hooks[workInProgress.currentHook];
    
    const hasChanged = oldHook ? !deps || deps.some((dep,index) => dep!==oldHook.deps[index])
    
    const hook = {
        effect: hasChanged ? effect : null,
        deps: deps,
        cleanup: oldHook ? oldHook.cleanup : null
    }
    
    workInProgress.hooks.push(hook)
    workInProgress.currentHook++
    
    if (hasChanged) {
        workInProgress.effects.push(hook)
    }
}

function useLayoutEffect(effect, deps) {
    // workInProgress, 当前处理的fiber 单元
    const oldHook = workInProgress.alternate && workInProgress.alternate.hooks && workInProgress.alternate.hooks[workInProgress.currentHook];
    
    const hasChanged = oldHook ? !deps || deps.some((dep,index) => dep!==oldHook.deps[index])
    
    const hook = {
        effect: hasChanged ? effect : null,
        deps: deps,
        cleanup: oldHook ? oldHook.cleanup : null
    }
    
    workInProgress.hooks.push(hook)
    workInProgress.currentHook++
    
    if (hasChanged) {
        workInProgress.layoutEffects.push(hook)
    }
}

上面的2个函数基本一样,只是 layoutEffects的所有hook都在 layoutEffects 数组里面(这个数组定义在每一个fiber上面),下面罗列出了我们之前定义的fiber结构,现在需要添加这个数组,

// FiberNode 类定义

class FiberNode {

    constructor(type, props, parent = null, sibling = null, child = null) {

        this.type = type; // 组件类型

        this.props = props; // 组件属性

        this.parent = parent; // 父节点

        this.sibling = sibling; // 兄弟节点

        this.child = child; // 子节点

        this.effectTag = null; // 标记需要执行的操作(如更新)

        this.hooks = []; // 组件的 Hook 列表

        this.currentHook = 0; // 当前 Hook 的索引

        this.alternate = null; // 交替的 Fiber

        this.stateNode = null; // 真实 DOM 节点
        
        this.layoutEffects = []; // 存储layout副作用

        this.effects = []; // 存储副作用的列表

    }

}

接着我们修改前几章的 commitwork 函数, 使其让所有的 useLayoutEffect 需要在dom更新之后但是渲染之前完成(sync), useEffect 的副作用函数需要在dom渲染之后,异步完成。

function commitWork(fiber) {

    let currentFiber = fiber;

    while (currentFiber !== null) {

        // 只对宿主组件(Host Component)执行 DOM 操作

        if (currentFiber.type && typeof currentFiber.type === 'string') {

            // 找到最近的具有 DOM 父节点的 Fiber

            const parentDom = getParentDom(currentFiber.parent);

            if (!parentDom) {

                console.error('无法找到 DOM 父节点');

                return;

            }

            // 根据 effectTag 执行相应的操作

            switch (currentFiber.effectTag) {

                case 'PLACEMENT':

                    if (currentFiber.stateNode) {

                        parentDom.appendChild(currentFiber.stateNode);

                    }

                    break;

                case 'UPDATE':

                    if (currentFiber.stateNode) {

                    updateDom(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);

                    }

                    break;

                case 'DELETION':

                    commitDeletion(currentFiber, parentDom);

                    break;

                default:

                    break;

            }

        }

   
        // Execute layout effects 
        if (currentFiber.layoutEffects && currentFiber.layoutEffects.length > 0) { 
            currentFiber.layoutEffects.forEach(hook => { 
                if (hook.cleanup) { hook.cleanup(); } 
                const cleanup = hook.effect(); 
                if (typeof cleanup === 'function') { 
                    hook.cleanup = cleanup; 
                } 
            }); 
            currentFiber.layoutEffects = []; 
        } 
        // Traverse child nodes 
        if (currentFiber.child) { 
            currentFiber = currentFiber.child; 
            continue; 
        } 
        // Traverse sibling nodes 
        while (currentFiber !== null) { 
            if (currentFiber.sibling) { 
                currentFiber = currentFiber.sibling; 
                break; 
            } 
            currentFiber = currentFiber.parent; 
        }
 
    }
    
   
    schedulePassiveEffects(fiber)
}

function schedulePassiveEffects(fiber) {
  setTimeout(() => {
    let currentFiber = fiber;

    while (currentFiber !== null) {
      // Execute passive effects
      if (currentFiber.effects && currentFiber.effects.length > 0) {
        currentFiber.effects.forEach(hook => {
          if (hook.cleanup) {
            hook.cleanup();
          }
          const cleanup = hook.effect();
          if (typeof cleanup === 'function') {
            hook.cleanup = cleanup;
          }
        });
        currentFiber.effects = [];
      }

      // Traverse child nodes
      if (currentFiber.child) {
        currentFiber = currentFiber.child;
        continue;
      }

      // Traverse sibling nodes
      while (currentFiber !== null) {
        if (currentFiber.sibling) {
          currentFiber = currentFiber.sibling;
          break;
        }
        currentFiber = currentFiber.parent;
      }
    }
  }, 0); // Schedule after paint
}

Layout Effects(useLayoutEffects): 从代码可以清楚的知道,它的运行时机是在commit 阶段,即dom 操作之后但是渲染之前。

Passive Effects(useEffect): 运行时机commit阶段之后,ui渲染之后。

那么面试常问的useLayoutEffect 为什么会有性能问题,以及使用场景?

useLayoutEffect 运行时机,ui 渲染之前,dom 操作之后,sync机制。使用不准确会有阻挡ui渲染的可能,即ui响应变慢,用户体验不佳,有卡顿感。

使用场景非常显而易见,如果你想要获取dom操作之后的属性值比如宽/长等数据,你还有其他逻辑依赖这几个数据操作ui或其他,则这个就是它的使用场景。它保证给你更新后的dom的属性值,你可以利用它运行自己的逻辑,这个逻辑由于sync机制会保证ui渲染之前运行。

这篇我们尝试实现了一个自己的useLayoutEffect版本,讲解了它的使用场景以及解决了什么问题以及性能如何可能被影响的可能。

那么下一篇将从0-1实现开发 useRef 。

如果文章对你有帮助,请点个赞支持一下!

啥也不是,散会。