【问题标题】:How does React.useState triggers re-render?React.useState 触发器如何重新渲染?
【发布时间】:2019-04-01 03:28:17
【问题描述】:
import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

在上面的示例中,每当调用 setCount(count + 1) 时,都会发生重新渲染。我很想了解流程。

我尝试查看源代码。我在github.com/facebook/react 找不到任何useState 或其他挂钩的参考。

我通过npm i react@next 安装了react@next,并在node_modules/react/cjs/react.development.js 找到了以下内容

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

在回溯dispatcher.useState()时,我只能找到以下...

function resolveDispatcher() {
  var dispatcher = ReactCurrentOwner.currentDispatcher;
  !(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
  return dispatcher;
}
var ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: null,
  currentDispatcher: null
};

我想知道在哪里可以找到dispatcher.useState() 实现并了解它如何在调用 setState setCount 时触发重新渲染。

任何指针都会有所帮助。

谢谢!

【问题讨论】:

  • 我相信这是fiber 的行为,所以说 Reacts 核心算法。他们将其描述为,React Fiber is an ongoing reimplementation of React's core algorithm.github.com/acdlite/react-fiber-architecture 我相信userState - 在fiber 中创建对象和指针,当它的值发生变化时,它会触发重新渲染。这只是一个猜测,但我希望有类似的东西。
  • 实际上,react 中的 hooks 使用 useState 来调用状态更新,类似于 setState。因此,无论何时更改状态,都会重新渲染组件。

标签: javascript reactjs react-hooks


【解决方案1】:

理解这一点的关键是Hooks FAQ的以下段落

React 如何将 Hook 调用与组件关联起来?

React 跟踪当前呈现的组件。由于 Hooks 的规则,我们知道 Hooks 只能从 React 组件调用(或自定义 Hooks,也只能从 React 组件调用)。

每个组件都有一个内部“记忆单元”列表。它们只是 JavaScript 对象,我们可以在其中放置一些数据。当你调用 useState() 之类的 Hook 时,它会读取当前单元格(或在第一次渲染期间对其进行初始化),然后将指针移动到下一个单元格。这就是多个 useState() 调用各自获取独立本地状态的方式。

(这也说明了Rules of Hooks。Hooks需要按相同顺序无条件调用,否则memory cell和hook的关联就搞砸了。)

让我们看看你的反例,看看会发生什么。为简单起见,我将参考 compiled development React source code 和 React DOM source code,两个版本均为 16.13.1。

该示例在组件挂载并且第一次调用 useState()(在第 1581 行定义)时开始。

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

如您所见,这调用了resolveDispatcher()(在第 1546 行定义)。 dispatcher 在内部引用当前正在渲染的组件。在一个组件中,你可以(如果你敢被解雇),看看调度程序,例如通过

console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current)

如果你在反例中应用这个,你会注意到dispatcher.useState() 指的是react-dom 代码。当组件第一次挂载时,useState 指的是在第 15986 行定义的调用mountState() 的组件。重新渲染后,调度程序发生了变化,并触发了第 16077 行的函数useState(),它调用了updateState()。第 15352 行的 mountState() 和第 15371 行的 updateState() 这两种方法都返回 count, setCount 对。

跟踪ReactCurrentDispatcher 变得非常混乱。然而,它存在的事实已经足以理解重新渲染是如何发生的。 魔术发生在幕后。正如常见问题解答所述,React 会跟踪当前呈现的组件。这意味着,useState()知道它附加到哪个组件,如何找到状态信息以及如何触发重新渲染。

【讨论】:

  • "useState() 知道它附加到哪个组件,如何找到状态信息以及如何触发重新渲染。" ——问题是,我们很好奇它是怎么知道的?对实现及其背后的魔力感到好奇!
【解决方案2】:

setState 是Component/PureComponent 类上的一个方法,所以它会执行Component 类中实现的任何事情(包括调用render 方法)。

setState 将状态更新卸载到enqueueSetState,因此它与此绑定的事实实际上只是使用类并从Component 扩展的结果。曾经,您意识到状态更新实际上并没有由组件本身处理,this 只是访问状态更新功能的一种便捷方式,然后useState 没有显式绑定到您的组件更有意义.

【讨论】:

  • 谢谢。我意识到我在问题的倒数第二句中使用了setState。这是一个无意的错字,应该是setCount。我已经相应地更新了这个问题。请随时更新您的答案。
【解决方案3】:

我也试着用非常简单和基本的方式理解useState背后的逻辑,如果我们只看它的基本功能,不包括优化和异步行为,我们发现它基本上是在做4 个共同点,

  1. 维护状态,主要工作
  2. 重新渲染调用它的组件,以便调用者组件可以获得最新的状态值
  3. 由于它导致调用者组件的重新渲染,这意味着它也必须维护该组件的实例或上下文,这也允许我们同时为多个组件使用 useState。李>
  4. 因为我们可以在组件内随意使用任意数量的 useState,这意味着它必须为同一组件内的每个 useState 维护一些标识。

牢记这些事情,我想出了下面的 sn-p

const Demo = (function React() {
  let workInProgress = false;
  let context = null;

  const internalRendering = (callingContext) => {
    context = callingContext;
    context();
  };

  const intialRender = (component) => {
    context = component;
    workInProgress = true;
    context.state = [];
    context.TotalcallerId = -1; // to store the count of total number of useState within a component
    context.count = -1; // counter to keep track of useStates within component
    internalRendering(context);
    workInProgress = false;
    context.TotalcallerId = context.count;
    context = null;
  };

  const useState = (initState) => {
    if (!context) throw new Error("Can only be called inside function");

     // resetting the count so that it can maintain the order of useState being called

    context.count =
      context.count === context.TotalcallerId ? -1 : context.count; 

    let callId = ++context.count;

    // will only initialize the value of setState on initial render
    const setState =
      !workInProgress ||
      (() => {
        const instanceCallerId = callId;
        const memoizedContext = context;
        return (updatedState) => {
          memoizedContext.state[instanceCallerId].value = updatedState;
          internalRendering(memoizedContext);
        };
      })();

    context.state[callId] = context.state[callId] || {
      value: initState,
      setValue: setState,
    };

    return [context.state[callId].value, context.state[callId].setValue];
  };

  return { useState, intialRender };
})();

const { useState, intialRender } = Demo;

const Component = () => {
  const [count, setCount] = useState(1);
  const [greeting, setGreeting] = useState("hello");

  const changeCount = () => setCount(100);
  const changeGreeting = () => setGreeting("hi");

  setTimeout(() => {
    changeCount();
    changeGreeting();
  }, 5000);

  return console.log(`count ${count} name ${greeting}`);
};

const anotherComponent = () => {
  const [count, setCount] = useState(50);
  const [value, setValue] = useState("World");

  const changeCount = () => setCount(500);
  const changeValue = () => setValue("React");

  setTimeout(() => {
    changeCount();
    changeValue();
  }, 10000);

  return console.log(`count ${count} name ${value}`);
};
intialRender(Component);
intialRender(anotherComponent);

这里useState和initialRender取自Demo。 intialRender 用于初始调用组件,它会先初始化 context,然后在该上下文中将 state 设置为空数组(有每个组件上有多个 useState,因此我们需要数组来维护它)并且我们还需要 counter 来计算每个 useState 和 TotalCounter 存储每个组件调用的 useState 总数。

【讨论】:

    【解决方案4】:

    FunctionComponent 是不同的。在过去,它们是纯粹的、简单的。但现在他们有了自己的状态。 很容易忘记,react 使用 createElement 包装了所有 JSX 节点,还包括 FunctionComponent。

    function FunctionComponent(){
      return <div>123</div>;
    }
    const a=<FunctionComponent/>
    //after babel transform
    function FunctionComponent() {
      return React.createElement("div", null, "123");
    }
    
    var a = React.createElement(FunctionComponent, null);
    

    FunctionComponent 被传递给反应。调用 setState 时,很容易重新渲染;

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-09
      • 2015-07-14
      • 1970-01-01
      相关资源
      最近更新 更多