React.useState如何触发重新渲染?

sar*_*tam 11 javascript reactjs react-hooks

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>
  );
}
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,无论何时setCount(count + 1)调用都会发生重新渲染.我很想学习这个流程.

我试着查看源代码.我useStategithub.com/facebook/react找不到任何引用或其他钩子.

我安装了react@nextvia npm i react@next,发现了以下内容node_modules/react/cjs/react.development.js

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
Run Code Online (Sandbox Code Playgroud)

在追溯时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;
}
Run Code Online (Sandbox Code Playgroud)
var ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: null,
  currentDispatcher: null
};
Run Code Online (Sandbox Code Playgroud)

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

任何指针都会有所帮助.

谢谢!

sau*_*ger 5

理解这一点的关键是来自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);
}
Run Code Online (Sandbox Code Playgroud)

正如您所注意到的,这个调用resolveDispatcher()(在第 1546 行定义)。该dispatcher内部是指,因此目前所呈现的组件。在组件中,您可以(如果您敢被解雇)查看调度程序,例如通过

console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current)
Run Code Online (Sandbox Code Playgroud)

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

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


小智 5

我还尝试以一种非常简单和基本的方式理解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);
Run Code Online (Sandbox Code Playgroud)

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


Sot*_*sis 4

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

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