繁体   English   中英

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

[英]How does React.useState triggers re-render?

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找到了以下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时它如何触发重新渲染。

任何指针都会有所帮助。

谢谢!

理解这一点的关键是来自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()行的函数useState()被触发,它调用updateState() 两种方法中, mountState()上线15352和updateState()上线15371,返回count, setCount对。

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

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

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

我也试图以一种非常简单和基本的方式来理解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总数。

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 被传递给 react。 当 setState 被调用时,很容易重新渲染;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM