useEffect

执行的时机,赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。 在函数组件主体内()这里指 react 渲染阶段 改变 dom, 添加订阅 设置定时器, 或执行其他包含副作用的操作都是不被允许的,因为这可能产生莫名其妙的 bug

原理

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

useLayOutEffect(){
  useEffectImpl(UpadteEffect, UnmountMutation | MountLayout, create, inputs)
}
useEffect(){
  useEffectImpl(
    UpadteEffect | PassiveEffect,
    UnmountMutation | MountPassive, create, inputs)
}

因为他是匿名函数 所以每次都不一样

function useEffectImpl(fiberEffectTag, hookEffectTag, create, inputs): void {
  workInProgressHook = createWorkInProgressHook();
  let nextInputs = inputs !== undefined && inputs !== null ? inputs : [create];
  componentUpadteQueue.lastEffect = effect.next = effect

}

接着 commitHookEffectList 这个方法

如果有传入的第二个return 消除副作用

我们就去执行 destory() 方法

const destory = effect.destory;
effect.destory = null;
if(destory != null){
  destory()
}
function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedExpirationTime: ExpirationTime,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
      break;
    }
  }
}

用法

function App2(props) {
  const [count, setCount] = useState(0);
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });
  //useState 按照第一次返回的顺序给你state的

  useEffect(() => {
    document.title = count;
  });

  useEffect(() => {
    console.log("count", count);
  }, [count]);

  useEffect(() => {
    window.addEventListener("resize", onResize, false);
    return () => {
      window.removeEventListener("resize", onResize, false);
    };
  }, []);

  useEffect(() => {
    document.querySelector('#size').addEventListener("click", onClick, false);
/*     return () => {
      document.querySelector('#size').removeEventListener("click", onClick, false);
    }; */
  });

  const onClick = () => {
    console.log("click");
  };

  const onResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  };
  return (
    <div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click({count})
      </button>
      {count % 2 ? (
        <span id="size">
          size:({size.width}, {size.height})
        </span>
      ) : (
        <p id="size">
          size:({size.width}, {size.height})
        </p>
      )}
    </div>
  );
}

// 不太关心是mount 还是update

第二个传入的参数表达的意思是只有第二个参数数组每一项都不变的情况下 useEffect 才不会执行

第二个参数 undefined 空数组 非数组

原理

const allDeps: any[][] = [];
let effectCursor: number = 0;
function useEffect(callbak: () => void, deps:any[]){
  if(!allDeps[effectCursor]){
    //  初次渲染: 赋值 + 调用回调函数
    allDeps[effectCursor]= deps;
    ++effectCursor;
    callbak();
    return
  }

  const currentEffectCursor = effectCursor;
  const rawDeps = allDeps[currentEffectCursor];

  const isChanged = rawDeps.some(
    (dep: any, index: number) => dep !== deps[index]
  );

  if(isChanged){
    callbak();
    allDeps[effectCursor] = deps;
  }
  ++effectCursor;
}



const rootElement = document.getElementById("root");
render(<App />, rootElement);

useEffect 和 useLayoutEffect的区别

流程

  1. react 在 diff 后,会进入到 commit 阶段,准备把虚拟 DOM 发生的变化映射到真实 DOM 上

  2. 在 commit 阶段的前期,会调用一些生命周期方法,对于类组件来说,需要触发组件的 getSnapshotBeforeUpdate 生命周期,对于函数组件,此时会调度 useEffect 的 create destroy 函数

  3. 注意是调度,不是执行。在这个阶段,会把使用了 useEffect 组件产生的生命周期函数入列到 React 自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行

// 可以近似的认为,React 做了这样一步,实际流程中要复杂的多

setTimeout(() => {
      const preDestory = element.destroy;
      if (preDestory) prevDestroy();
      const destroy = create();
      element.destroy= destroy;
}, 0);
  1. 随后,就到了 React 把虚拟 DOM 设置到真实 DOM 上的阶段,这个阶段主要调用的函数是 commitWork,commitWork 函数会针对不同的 fiber 节点调用不同的 DOM 的修改方法,比如文本节点和元素节点的修改方法是不一样的。

  2. commitWork 如果遇到了类组件的 fiber 节点,不会做任何操作,会直接 return,进行收尾工作,然后去处理下一个节点,这点很容易理解,类组件的 fiber 节点没有对应的真实 DOM 结构,所以就没有相关操作

  3. 但在有了 hooks 以后,函数组件在这个阶段,会同步调用上一次渲染时 useLayoutEffect(create, deps) create 函数返回的 destroy 函数

  4. 注意一个节点在 commitWokr 后,这个时候,我们已经把发生的变化映射到真实 DOM 上了

  5. 但由于 JS 线程和浏览器渲染线程是互斥的,因为 JS 虚拟机还在运行,即使内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上

  6. 此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执行。

  7. 对于 react 来说,commit 阶段是不可打断的,会一次性把所有需要 commit 的节点全部 commit 完,至此 react 更新完毕,JS 停止执行

  8. 浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成

  9. 浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生的函数

最后更新于