40
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或其他钩子。

react@next通过安装npm i 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

任何指针都会有所帮助。

谢谢!

4

4 回答 4

9

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

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

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

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

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

让我们看看你的反例,看看会发生什么。为简单起见,我将参考编译后的开发 React 源代码React DOM 源代码,均为 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(). 重新渲染后,调度程序发生了变化,并useState()触发了第 16077 行的函数,该函数调用updateState(). mountState()第 15352 行和updateState()第 15371行的两种方法都返回该count, setCount对。

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

于 2020-05-28T13:55:15.797 回答
4

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

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

记住这些事情我想出了下面的片段

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);

这里的 useStateinitialRender取自 Demo。intialRender用于最初调用组件,它将首先初始化上下文,然后在该上下文中将状态设置为空数组(每个组件上有多个useState,因此我们需要数组来维护它),并且我们需要计数器来制作每个useState的计数,TotalCounter存储每个组件调用的useState总数。

于 2020-11-28T21:45:42.270 回答
4

setState是类上的一个方法Component/PureComponent,因此它将执行Component类中实现的任何操作(包括调用该render方法)。

setState将状态更新卸载到,enqueueSetState因此它绑定到 this 的事实实际上只是使用类和从Component. 有一次,您意识到状态更新实际上并没有由组件本身处理,而这this只是访问状态更新功能的一种便捷方式,那么useState不显式绑定到您的组件就更有意义了。

于 2018-10-27T17:41:41.487 回答
1

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 时,很容易重新渲染;

于 2019-06-05T04:44:59.083 回答