React 实现 useLayoutEffect 与 useEffect

本文将带大家实现 useLayoutEffect 与 useEffect。

先看下如何使用。

js 复制代码
function FunctionComponent() {
    const [count1, setCount1] = useReducer((x) => x + 1, 0)
    const [count2, setCount2] = useState(0)
    
    useLayoutEffect(() => {
        console.log('useLayoutEffect')
    }, [count1])
    
    useEffect(() => {
        console.log('useEffect')
    }, [count2])
    
    return (
        <div className="border">
            <h1>函数组件</h1>
            <button onClick={() => setCount1()}>{count1}</button>
            <button onClick={() => setCount2(count2 + 1)}>{count2}</button>
        </div>
    )
}

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  (<FunctionComponent />) as any
);

两者对比

存储结构相同

存储结构都是单向循环链表。

执行时机不同

  • useLayoutEffect 会在所有的 DOM 变更之后同步调用 effect。
  • useEffect 会在组件渲染到屏幕之后延迟执行 effect。

useLayoutEffect 可能会影响性能,尽可能使用 useEffect。比如在 useLayoutEffect 增加 ajax 请求,由于是同步执行,可能会影响 DOM 的渲染。

实现 useLayoutEffect 与 useEffect

js 复制代码
function pushEffect(
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<any> | void | null
) {
  const effect: Effect = {
    tag: hookFlags,
    create,
    deps,
    next: null,
  };

  let componentUpdateQueue = currentlyRenderingFiber!.updateQueue;
  // 单向循环链表
  if (componentUpdateQueue === null) {
    // 第一个 effect
    componentUpdateQueue = {
      lastEffect: null,
    };
    currentlyRenderingFiber!.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    const firstEffect = lastEffect.next;
    lastEffect.next = effect;
    effect.next = firstEffect;
    componentUpdateQueue.lastEffect = effect;
  }

  return effect;
}

// 存储 effect
function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<any> | void | null
) {
  const hook = updateWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;
  // 依赖项是否发生变化
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevDeps = currentHook.memoizedState.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return;
      }
    }
  }

  currentlyRenderingFiber!.flags |= fiberFlags;
  // * 1. 保存 effect 2. 构建 effect 链表
  hook.memoizedState = pushEffect(hookFlags, create, nextDeps);
}

// useEffect 与 useLayoutEffect 的区别
// 存储结构一样
// effect 和 destroy 函数的执行时机不同
export function useLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<any> | void | null
) {
  return updateEffectImpl(Update, HookLayout, create, deps);
}

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<any> | void | null
) {
  return updateEffectImpl(Passive, HookPassive, create, deps);
}

Commit 阶段

commitReconciliationEffects 增加更新的 case。

js 复制代码
function commitHookEffectListMount(hookFlags: HookFlags, finishedWork: Fiber) {
  const updateQueue = finishedWork.updateQueue;
  let lastEffect = updateQueue!.lastEffect;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & hookFlags) === hookFlags) {
        const create = effect.create;
        create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  if (flags & Placement) {
    // 页面初次渲染 新增插入 appendChild
    // todo 页面更新,修改位置 appendChild || insertBefore
    commitPlacement(finishedWork);
    finishedWork.flags &= ~Placement;
  }
  // 删除
  if (flags & ChildDeletion) {
    // parentFiber 是 deletions 的父 dom 对应的 fiber
    const parentFiber = isHostParent(finishedWork)
      ? finishedWork
      : getHostParentFiber(finishedWork);
    const parentDOM = parentFiber.stateNode;
    commitDeletions(finishedWork.deletions!, parentDOM);
    finishedWork.flags &= ~ChildDeletion;
    finishedWork.deletions = null;
  }

  // 更新
  if (flags & Update) {
    if (finishedWork.tag === FunctionComponent) {
      // 执行 layout effect
      commitHookEffectListMount(HookLayout, finishedWork);
      finishedWork.flags &= ~Update;
    }
  }
}

commitRoot 增加 passive effect阶段,执行 passive effect。

js 复制代码
function commitPassiveEffects(finishedWork: Fiber) {
  switch (finishedWork.tag) {
    case FunctionComponent: {
      if (finishedWork.flags & Passive) {
        commitHookEffectListMount(HookPassive, finishedWork);
        finishedWork.flags &= ~Passive;
      }
      break;
    }
  }
}

function recursivelyTraversePassiveMountEffects(finishedWork: Fiber) {
  let child = finishedWork.child;
  while (child !== null) {
    // 1. 遍历子节点,检查子节点
    recursivelyTraversePassiveMountEffects(child);
    // 2. 如果有 passive effects,执行~
    commitPassiveEffects(finishedWork);
    child = child.sibling;
  }
}

export function flushPassiveEffects(finishedWork: Fiber) {
  // 1. 遍历子节点,检查子节点
  recursivelyTraversePassiveMountEffects(finishedWork);
  // 2. 如果有 passive effects,执行~
  commitPassiveEffects(finishedWork);
}

function commitRoot(root: FiberRoot) {
  // 1. commit 阶段开始
  const prevExecutionContext = executionContext;
  executionContext |= CommitContext;
  // 2.1 mutation阶段, 渲染DOM树
  commitMutationEffects(root, root.finishedWork as Fiber); //Fiber,HostRoot=3
  // 2.2 passive effect阶段,执行 passive effect
  Scheduler.scheduleCallback(NormalPriority, () => {
    flushPassiveEffects(root.finishedWork as Fiber);
  });

  // 3. commit 结束
  executionContext = prevExecutionContext;
  workInProgressRoot = null;
}
相关推荐
欢乐小v20 分钟前
elementui-admin构建
前端·javascript·elementui
霸道流氓气质1 小时前
Vue中使用vue-3d-model实现加载3D模型预览展示
前端·javascript·vue.js
溜达溜达就好1 小时前
ubuntu22 npm install electron --save-dev 失败
前端·electron·npm
慧一居士1 小时前
Axios 完整功能介绍和完整示例演示
前端
晨岳1 小时前
web开发-CSS/JS
前端·javascript·css
22:30Plane-Moon1 小时前
前端之CSS
前端·css
半生过往1 小时前
前端上传 pdf 文件 ,前端自己解析出来 生成界面 然后支持编辑
前端·pdf
晨岳2 小时前
web开发基础(CSS)
前端·css
.又是新的一天.2 小时前
前端-CSS (样式引入、选择器)
前端·css