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

时间:2018-10-27 17:24:13

标签: 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>
  );
}

在上面的示例中,每当调用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时如何触发重新渲染。

任何指针都会有所帮助。

谢谢!

5 个答案:

答案 0 :(得分:3)

了解这一点的关键是Hooks FAQ

中的以下段落

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

React跟踪当前渲染的组件。多亏了挂钩规则,我们知道挂钩只能从React组件(或自定义挂钩-也只能从React组件调用)中获得。

内部有一个与每个组件关联的“内存单元”列表。它们只是JavaScript对象,我们可以在其中放置一些数据。当您调用诸如useState()之类的Hook时,它将读取当前单元格(或在第一个渲染期间对其进行初始化),然后将指针移至下一个单元格。这就是多个useState()分别调用时如何获得独立的本地状态的方法。

(这也解释了Rules of Hooks。挂钩必须以相同的顺序无条件地调用,否则内存单元格和挂钩的关联会混乱。)

让我们看一下您的反例,看看会发生什么。为简单起见,我将同时参考两个版本16.13.1的compiled development React source codeReact DOM source code

该示例在安装组件并首次调用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() 知道它附加到哪个组件,如何查找状态信息以及如何触发重新呈现。

答案 1 :(得分:2)

setStateComponent/PureComponent类的方法,因此它将执行Component类中实现的所有操作(包括调用render方法)。

setState将状态更新卸载到enqueueSetState中,因此绑定到此状态的事实实际上仅是使用类并从Component扩展的结果。一次,您意识到状态更新实际上不是由组件本身处理的,并且this只是访问状态更新功能的便捷方式,因此useState并未明确绑定到组件更有意义。

答案 2 :(得分:2)

答案 3 :(得分:2)

我还尝试以一种非常简单和基本的方式来理解 useState 背后的逻辑,如果我们仅查看其基本功能(不包括优化和异步行为),那么我们发现它基本上在做4个共同点,

  1. 维护国家,要做的主要工作
  2. 重新渲染通过其调用的组件,以便调用者组件可以获得状态的最新值
  3. 由于它导致了调用方组件的重新呈现,这意味着它也必须维护该组件的实例或上下文,这还使我们能够一次对多个组件使用 useState
  4. li>
  5. 因为我们可以在组件内部自由使用任意数量的 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);

此处 useState initialRender 来自Demo。 intialRender 最初用于调用组件,它将首先初始化 context ,然后在该上下文上将 state 设置为空数组(每个组件上有多个 useState ,因此我们需要数组来对其进行维护),还需要 counter 来为每个 useState TotalCounter 存储每个组件被调用的 useState 的总数。

答案 4 :(得分:0)

FunctionComponent不同。过去,它们是纯净的,简单的。但是现在他们有了自己的状态。 容易忘记,使用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时,很容易重新渲染;