Coder Social home page Coder Social logo

Comments (9)

creamidea avatar creamidea commented on September 26, 2024

useSyncExternalStore

React18 已经内置,packages/react-reconciler/src/ReactFiberHooks.new.js
3 个参数

  • subscribe 函数类型,其参数是 React 给出的更新函数 handleStoreChange,当外部源有更新时可以调用,通知 React 更新
  • getSnapshot 和 getServerSnapshot 是给 React 获取当前外部源最新状态的函数,用于内部新/旧比较,决定是否触发更新

mountSyncExternalStore 函数

如果没有 block 的优先级,那么就要放入源一致性检查

  • fiber.flags |= StoreConsistency
  • fiber.updateQueue 上放入 check 的方式
    if (!includesBlockingLane(root, renderLanes)) {
      pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
    }

这个检查在函数 performConcurrentWorkOnRoot 里,在 render 结束之后(The render completed.)会做一次检查,renderWasConcurrent && !isRenderConsistentWithExternalStores(finishedWork) 为真(A store was mutated in an interleaved event),再次调用 renderRootSync


获取当前对应的 hook,hook.memoizedState = nextSnapshot

等价于使用了 useEffect(subscribeToStore, [subscribe])

fiber.flags = PassiveEffect | PassiveStaticEffect; 
effect.tag = HookHasEffect | HookPassive

hook.memoizedState = effect

// effect hook
create = subscribeToStore.bind(null, fiber, inst, subscribe)
deps = [subscribe]

后续调用,等价于 useEffect(updateStoreInstance) 其中没有设置 memoizedState 这步
updateStoreInstance 用于更新内部记录的外部源最新值。该函数被存储在 fiber.updateQueue*

pushEffect(
  HookHasEffect | HookPassive,
  updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
  undefined,
  null,
);

fiber.updateQueue 何时消费?

在 commitPassiveMountOnFiber 通过 commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork) 函数内被读取消费

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024

升级React17,Toast组件不能用了

https://juejin.cn/post/6974025581557973005

import { useEffect, useState } from "react";
import { createPortal } from "react-dom";

function ToastButton() {
  const [show, setShow] = useState(false);

  useEffect(() => {
    const handler = () => {
      setShow(false);
    };

    document.addEventListener("click", handler);

    return () => {
      document.removeEventListener("click", handler);
    };
  }, [show]);

  console.log('## ToastButton', show)

  return (
    <div>
      <button onClick={() => {
        setShow(true)
      }}>toast button</button>
      {show && <div>toast</div>}
    </div>
  );
}

function PortalButton() {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(true)}>portal button</button>
      {show && createPortal("portal", document.body)}
    </div>
  );
}

export function App() {
  return <>
    <ToastButton />
    <PortalButton />
  </>
}

toast 文案在点击 toast button 之后显示立即消失,视觉上就没有出现过

分析如下:
onClick 在捕获阶段冒泡阶段被触发执行,设置为 true 进入更新逻辑 setState
ensureRootIsScheduled

  • 安排 performSyncWorkOnRoot 执行
  • 调度微任务 flushSyncCallbacks 任务(scheduleMicrotask),flushSyncCallbacks 里面执行的函数就是上面的 performSyncWorkOnRoot
    微任务是在宏任务最后阶段,还没有离开宏任务,所以还是处于 onClick 调用栈内
    performSyncWorkOnRoot 「同步执行」,在 commitRoot 阶段,执行完
    BeforeMutation -> Mutation -> LayoutMutation 之后,有一段代码
    includesSomeLane(pendingPassiveEffectsLanes, SyncLane) 这里看这次更新是不是有同步,
    有就调用 flushPassiveEffects。意味着这里会同步执行了 effect,即这里在 body 上注册监听事件 handler
    以上都是同步,所以此时还只是冒泡到 div#root,继续往上来到 body,此时就会触发设置 false 的回调函数
    toast 被删除

pendingPassiveEffectsLanes 设置也是在 commitRoot 阶段
根据 rootDoesHavePassiveEffects 为真值时,设置

  • rootWithPendingPassiveEffects = root
  • pendingPassiveEffectsLanes = lanes;

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024

React内部的性能优化没有达到极致?

https://juejin.cn/post/7073692220313829407

image

import { useState } from "react";

export function App() {
  const [num, updateNum] = useState(0);
  console.log("App render", num);

  return (
    <div onClick={() => updateNum(1)}>
      <Child />
    </div>
  );
}

function Child() {
  console.log("child render");
  return <span>child</span>;
}

执行结果

初次渲染

App render 0
child render

点击一次

App render 1
child render

此时 fiber(App),fiber.lanes = 1 和 fiber.alternate.lanes = 1 设置值
(猜测原因是:dispatchSetState 时绑定的 fiber 是第一次渲染时绑定的 fiber,引入并发之后,可能被中断,就要保证中断之后 lanes 上的信息不消失。所以 2 个都要设置)
在 begin work 阶段和 renderWithHook 内,都会重置 workInProgress 的 lanes workInProgress.lanes = NoLanes。alternate 的 lanes 不会被重置

performUnitOfWork 内
begin work 阶段,判断 oldProps === newProps 为真(其父节点 HostRoot 是 cloneChildFibers 直接复制,所以 props 是相同的)。但是还是进入 updateFunctionComponent 处理,只是 didReceiveUpdate 为 false。didReceiveUpdate 将在 renderWithHooks 里,如同注释所说,Component 里调用 useState 等 Hooks 时,会根据 state 的变化情况,处理 didReceiveUpdate

// renderWithHooks
var children = Component(props, secondArg); // Check if there was a render phase update


// updateReducer(useState)
if (!objectIs(newState, hook.memoizedState)) {
  markWorkInProgressReceivedUpdate();
}

处理完 begin work 之后,会执行下面的代码,将「未来」的 props 变成「现在」的 props

unitOfWork.memoizedProps = unitOfWork.pendingProps;

completeWork
childLanes 冒泡处理是在 completeWork 内处理

点击一次

App render 1

fiber(App) 没能进入 eagerState 处理逻辑,因为 alternate.lanes 不为 0

begin work 处理 fiber(App) 时oldProps === newProps 为真(其父节点 HostRoot 是 cloneChildFibers 直接复制,所以 props 是相同的)。但是还是进入 updateFunctionComponent 处理,只是 didReceiveUpdatefalse。

updateFunctionComponent 后半段代码

if (current !== null && !didReceiveUpdate) {
  // 处理 workInProgress 的 updateQueue 和 flags
  // 处理 current 的 lanes,去掉当前的更新优先级
  bailoutHooks(current, workInProgress, renderLanes);

  // !includesSomeLane(renderLanes, workInProgress.childLanes) 判断成功
  // 直接返回 null。于是,fiber(Child) 进入 bailout 处理。没有让 Child 渲染。
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

于是,fiber(Child) 进入 bailout 处理。没有让 Child 渲染。

点击一次

(None)

进入 eagerState 的处理逻辑,没有 state, props, context 变化,于是什么都不会发生 :)

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024

useTransition 使用

import { useEffect, useState, useTransition } from "react";

export function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  console.log('render', count, isPending)

  useEffect(() => {
    // debugger
    // strict mode + development mode 时,执行 2 次,模拟 OffScreen
    // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state
    console.log('run effect');
  }, []);

  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            // 优先级 64
            setCount(c => c + 1)
          })
        }}
      >
        {isPending
          ? 'pending...'
          : `App Change ${count}`
        }
      </button>
    </>
  );
}

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024

didReceiveUpdate

didReceiveUpdate 判断是否有 props, state, context 变化
第一次赋值,发生在 beginWork,主要目的:有 propscontext 的变化
第二次赋值,发生在 useState 阶段,主要目的:有 state 的变化

React 几个比较,决定是否渲染

  • lanes(当前和之前)
  • eagerState(第一个更新才有)
  • props(直接引用比较)
  • context(lazy or directly)
  • state(call FunctionComponent 才能比较)

React 几个关键状态重置

  • renderRootSync/renderRootConcurrent 阶段可能会 prepareFreshStack(重置 rootWorkInProgress, workInProgress, ...)
  • beginWork 会清除 Wip.lanes
  • runWithHooks 会清除 Wip.lanes
  • useState 会清除 queue.pending (setState 之后的队列)
  • commit before mutation 之前
    • 重置 root.finishedWork = null;root.finishedLanes = NoLanes;
    • 重置 root.callbackNode = null;root.callbackPriority = NoLane;
    • 同时重置 workInProgressRoot, workInProgress, workInProgressRootRenderLanes
    • markRootFinished 处理 pendingLanes, suspendedLanes, entangledLanes, ...
  • commit mutation 之后,重置 root.current = finishedWork;

更新从 dispatchSetState 开始

  • 更新 update 入队 hook.queue.pending
  • fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes) 是否是第一个更新
    • eagerState 处理,状态计算之后没有发生,直接退出。(没有 perform work 进入调度)
  • 进入 scheduleUpdateOnFiber

调度任务 performSyncWorkOnRootperformConcurrentWorkOnRoot 被执行

  • beginWork: 判断 props 是否发生变化(引用比较,浅比较)
    • 没有发生变化,且 context 也没有发生变化,进入 bailoutOnAlreadyFinishedWork (省略一些特殊组件的处理)
      • 子组件没有更新,返回 null,进入回溯阶段 complete work
      • 有更新,clone 直接子组件,返回其第一个子组件
  • 进入 updateXXX 逻辑之后,就有机会调用 render() 或 FunctionComponent
  • FunctionComponent 被调用之后,useState(updateState - updateReducer) 就会被执行
    • 执行过程,操作更新链表,计算符合优先级的更新得出状态 例子
    • useState 内部会比较新状态和旧状态是否一致。决定后续是否执行 bailoutHooks(去掉 PassiveEffect 标记,在 flushPassiveEffects 内不执行)
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
  • render 阶段,commit 阶段 ...

时间分片的原理

符合条件的更新,会将 performConcurrentWorkOnRoot 放入调度器。符合分片的任务会执行 renderRootConcurrent

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    // workInProgressRoot, workInProgressRootRenderLanes 发生变化,说明在这之前调用了 prepareFreshStack 或者 commit 被执行
    // 说明任务变了,被高优先级的任务抢占了,任务重新开始(第一次也会进入这里)
    prepareFreshStack(root, lanes);
  }

  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);

  // ...
}

workLoopConcurrent 里面有时间分片的逻辑 shouldYield:现在是固定的时间分片时长 5ms。如果当前任务执行时间大于 5ms 就会退出 while 循环回到 performConcurrentWorkOnRoot,退出码为 RootInProgress

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

退出码为 RootInProgress,再次进入 ensureRootIsScheduled,此时 existingCallbackPriority === newCallbackPriority 为真,就直接退出 ensureRootIsScheduled
此时代码回到 performConcurrentWorkOnRoot

  if (root.callbackNode === originalCallbackNode) {
    // The task node scheduled for this root is the same one that's
    // currently executed. Need to return a continuation.
    return performConcurrentWorkOnRoot.bind(null, root);
  }

callbackNode 和 originalCallbackNode 一致,没有发生任务抢占。将当前任务返回,让调度器下次调用。从而实现时间分片的能力,从 devtool performance 输出来看,就是 5 ms 的 Call
image

在分片的间隙,给浏览器有时间去处理其他任务。此时 timer 定时器触发高优先级更新,高优先级任务一般是同步任务 performSyncWorkOnRoot(在一个宏任务内),最后会调用 ensureRootIsScheduled。如果还有任务,即 root.pendingLanes 不为空,那么就会创建新的调度任务放入调度器等待执行(也就是被中断的任务需要从头开始,有些组件会被执行 2 次)。

如果高优先级任务是异步任务,那么就会进入调度器。调度器进行排序入队,开始执行。

Q: 时间分片,在每个分片最后会注册任务,那么当高优先级抢占时,这个任务会被取消吗?

A:会被取消。首先有一个背景知识点:全局只会有一个 root
image

所以在后续任务进入 ensureRootIsScheduledconst existingCallbackNode = root.callbackNode; 就是获取上一个任务(最后一个时间分片的任务),接下来判断优先级,「肯定」是新任务的优先级更高,所以先取消上一个任务,再创建一个新的任务。在新的任务末尾,会重新调用 ensureRootIsScheduled,将中断的任务重新安排唤起。

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024
// 渲染几次?
// 渲染 3 次
// 第一次 mount
// 第二次 useEffect 注册的函数运行触发更新(状态值变了,is(newState, hook.memoizedState) 为 false。useState)
// 第三次 还是 useEffect 注册的函数运行触发更新(状态值没变,进入 bailout 阶段 bailoutHooks,此时去掉了 Passive Effect。updateFunctionComponent)
// 后续不打印,因为 useEffect 不运行
import { useEffect, useState } from "react";

export function App() {
  const [, updateNum] = useState(0);

  useEffect(() => {
    updateNum(1)
  })

  console.log('render App');

  return 'App';
}

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024
// add 函数内部的函数 batching 执行,所以每次增加 1
// 在 React18 实现里面,会在 ensureRootIsScheduled 内注册 micro task: performSyncWorkOnRoot
// 也就是在一个宏任务内执行。且由于是 click 触发更新,lanes 是 1。不会被进入到异步任务调度
// 在 React17 里,会在 dispatchDiscreteEvent 里面调用 flush sync queue(里面会先取消注册的 schedule 调度,然后开始同步执行)
// 所以也没有进入 异步任务调度 执行


// 如果是在 componentDidMount 通过 setTimeout 触发更新

// 传统模式,首次渲染 2 (setTimeout 里面是同步执行)
// 更新优先级 16。该更新级由 getCurrentEventPriority 返回 DefaultEventPriority,没有事件就返回默认优先级 16
// 进入 schedule 调度逻辑进行调度 performConcurrentWorkOnRoot 执行(但是不会时间分片,16 属于 Block 范围)

// 并发模式,首次渲染 1
// 更新优先级 1。该更新级由 getCurrentEventPriority 返回 getCurrentUpdatePriority,CurrentUpdatePriority 会在合成事件系统里 dispatchDiscreteEvent 设置 setCurrentUpdatePriority(DiscreteEventPriority)
// 进入 schedule 调度逻辑进行调度 performConcurrentWorkOnRoot 执行(但是不会时间分片,16 属于 Block 范围)



import { Component } from "react";

export class App extends Component {
  state = {
    count: 0,
  };

  add = () => {
    this.setState({
      count: this.state.count + 1,
    });
    this.setState({
      count: this.state.count + 1,
    });
  }

  componentDidMount() {
    setTimeout(this.add)
  }

  render() {
    return (
      <div>
        <div>{this.state.count}</div>
        <button onClick={this.add}>Add</button>
      </div>
    );
  }
}

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024
import { useEffect, useLayoutEffect, useState } from "react";

export function App() {
  const [state, setState] = useState(0)

  useLayoutEffect(() => {
    setState(1)
  }, [])

  useEffect(() => {
    setState(2)
    setState(3)
  }, [])

  console.log('render', state)

  return <div>{state}</div>
}

React 18 并发模式

render 0
render 1
render 3

原因:useEffect 注册的函数是在 flushPassiveEffects 中执行(由 useLayoutEffect 中 setState 注册的 performSyncWorkOnRoot 调用)

export function flushPassiveEffects(): boolean {
  if (rootWithPendingPassiveEffects !== null) {
    ...
    // 最高优先级为 DefaultEventPriority,也就是 16
    const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
    ...
    try {
      ReactCurrentBatchConfig.transition = null;

      // 设置当前总更新的优先级为 16
      // 这里表明,从 useEffect 里触发最高为 16
      // 这里的优先级会影响 dispatchSetState 里 requestUpdateLane 的返回值,这个例子返回 16
      setCurrentUpdatePriority(priority);
      return flushPassiveEffectsImpl();
    } finally {
      ...
    }
  }
  return false;
}

从该代码可以得知,useEffect 里面触发的更新最高更新也只会是 16。这样就会和 useLayoutEffect 触发的更新「错开」(因为 useLayoutEffect 触发的更新为 1)。这就导致 React 会先执行完 useLayoutEffect 触发的更新,也就是第二次输出 render 1。然后在其 commit 阶段的最后,调用 ensureRootIsScheduled 函数,将还未执行的 16 优先级注册到调度器内,所以第三次输出就是 performConcurrentWorkOnRoot 函数引发的。

legacy 模式

render 0
render 3

传统模式和并发模式的区别在于 requestUpdateLane 始终返回的是 1。也就是 useEffect 和 useLayoutEffect 的优先级一致。那么在 useState 计算状态的时候,就不会跳过,这也就是为什么第二次渲染 3。也因为优先级相同,在 commit 阶段的最后,root.pendingLanes 为 0,这样 ensureRootIsScheduled 也就不会安排后续任务。所以总共输出 2 次。

function requestUpdateLane(fiber) {
  ...
  if ((mode & ConcurrentMode) === NoMode) {
    return SyncLane;
  } else if ( (executionContext & RenderContext) !== NoContext && workInProgressRootRenderLanes !== NoLanes) {
    ...
  }
  ...
}

from creamidea.github.com.

creamidea avatar creamidea commented on September 26, 2024

image

image

首次渲染时,DOM 是一次性插入

from creamidea.github.com.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.