reactjs 当直接调用函数时,React如何/为什么知道一个钩子没有在函数组件内部被调用?

rqdpfwrv  于 2023-03-01  发布在  React
关注(0)|答案(1)|浏览(149)

出于好奇,我想直接调用一个React函数组件;就像一个普通函数一样。然而,React抱怨道:

Invalid hook call. Hooks can only be called inside of the body of a function component.

React * 如何知道 * 这一点?下面是示例代码:

const T = () => {
  useEffect(() => console.log("effect"));
  return (
    <>
      <div> hello </div>
    </>
  );
};

我像这样提取useEffect

const T = (func = useEffect) => () => {
  func(() => console.log("effect"));
  return (
    <>
      <div> hello </div>
    </>
  );
};

然后通过以下方式重新触发:T()()不起作用。但是T(f=>f())()起作用了。
这对 every 钩子是真的吗,还是只对React提供的钩子是特殊的?创建一个任意的定制钩子(不依赖于现有的React钩子)也会导致相同的错误?React在内部发生了什么来确定这一点?* 为什么 * 尝试并执行此操作一定是错误?根据我的理解,这些只是返回React元素的JS * 函数 *(React.createElement)它们本身只是JS对象,不是吗?我错过了什么?除了提取每个钩子依赖并传递一个存根实现之外,有没有办法强制执行?

baubqpgj

baubqpgj1#

你可以在React源代码中看到它的设置位置,在挂载的组件之外,标准钩子(useEffectuseState等)被初始化为throwInvalidHookError,这是显而易见的。
在挂载的函数组件中,useEffect被设置为mountEffect函数,该函数执行预期的效果-运行内容。
自定义钩子不会被特别对待(尽管the standard linting rules会报告任何滥用),但是一个不调用任何标准钩子的自定义钩子--无论是直接还是间接--根本不需要成为钩子。如果它调用了标准钩子之一,那么很明显,Invalid hook call异常将被抛出。

  • 为什么一定 * 这是一个错误,试图这样做

这肯定是一个错误,因为标准钩子在React机制中有特殊处理,"rules of hooks"导致每个组件中钩子的顺序一致;它们被强制执行,以便React可以维护与钩子调用相关联的内部状态。
这是React作者的设计选择。设置了一些限制,它导致了实现的优化。
根据我的理解,这些只是返回React元素的JS函数
是的,组件只是返回React节点树表示的JS函数,然后React将此节点树转换为HTML节点,并带有相关的事件处理程序等。
除了提取每个钩子依赖项并传入一个存根实现之外,没有其他方法可以做到这一点吗?
不完全清楚您在这里要做什么,但简短的回答似乎是"正确的,不重新构造代码是没有办法的"。您可以使用常规函数调用常规函数,或者使用钩子调用钩子(和常规函数)。这并不神奇--你可以把你自己的函数变成一个钩子,只要把它的名字以use开头,那么它可以调用其他钩子,只要它遵循钩子的规则。

编辑

我只想像这样调用组件T()。
现在还不清楚你希望通过这样做来解决什么问题。组件的返回值是一个反映组件树的数据结构。你可以通过编写一个无钩子的组件来了解:

const HelloWorld = () =>
  <div>Hello <span id="world">World</span></div>

console.log(HelloWorld());

Sandbox

{
  "type": "div",
  "key": null,
  "ref": null,
  "props": {
    "children": [
      "Hello ",
      {
        "type": "span",
        "key": null,
        "ref": null,
        "props": {
          "id": "world",
          "children": "World"
        },
        "_owner": null,
        "_store": {}
      }
    ]
  },
  "_owner": null,
  "_store": {}
}

因此,这是一个熟悉的树结构,其中包含一些额外的插槽用于React内务处理数据。
我们可以通过从一个已挂载的组件调用HelloWorld()来进一步探索:

const App = () => {
  console.log(HelloWorld());
  return <div></div>
}
...
root.render(<App/>);

Sandbox
这将呈现一个空的<div></div>,但是HelloWorld()的返回值包含一些新的内部元素:

{
  "type": "div",
  "key": null,
  "ref": null,
  "props": { "children": ... },
  "_owner": FiberNode,
  "_store": {}
}

FiberNode值暗示了React在幕后所做的事情-Fiber is the algorithm that does what React doesFiberNode is an internal data structure it uses to do that
如果我们给HelloWorld()添加一个钩子调用会发生什么?

const HelloWorld = () => {
  useEffect(() => console.info("Howdy!"));
  return <div>Hello <span id="world">World</span></div>
}

Sandbox
当然,如果我们直接调用它,那么我们会得到熟悉的Invalid hook call错误,但是如果我们从一个挂载的组件调用它......我们会在控制台输出上看到Howdy!
这是因为,正如你提到的,这些都是普通的老式Javascript函数。从HelloWorld()<App/>调用useEffect()与直接从<App/>调用useEffect()的工作相同。然而,这与从<HelloWorld/>调用useEffect()是 * 不 * 相同的。
让我们试试别的方法,如果我们创建一个<HelloWorld/>元素会发生什么?

const App = () =>
  {
    console.log("<HelloWorld/>", <HelloWorld/>);
    return <div/>
  }
...
render(<App/>);

Sandbox
我们不再得到Howdy!,返回值也不同:

<HelloWorld/>
{
  "type": f HelloWorld() {}
  "key": null,
  "ref": null,
  "props": {},
  "_owner": FiberNode,
  "_store": {}
}
  • 非常 * 有趣。当函数组件通过JSX引用时,* 函数不会被调用 *。<HelloWorld/>只是调用createElement(HelloWorld)的语法糖,只有在呈现该元素时,函数才会被调用。

通过比较返回值,我们还可以看到子节点从外部不再可见,树结构只是一个HelloWorld节点,而不是嵌套的div/span/text结构。
让我们再试一次,如果我们只是偶尔调用HelloWorld呢?

const App = () => {
  const [count, setCount] = useState(1);
  return (
    <div onClick={() => setCount((c) => c + 1)}>
      Click #{count}
      <hr />
      {count <= 1 &&
        /* render HelloWorld component */
        <HelloWorld /> }
        
      { count <= 2 &&
        /* call HelloWorld function */
        console.log("HelloWorld()", HelloWorld()) }
    </div>
  );
};

Sandbox
单击<div>将更新count,每次调用不同的对象。
在第一次加载时,我们看到Hello World!呈现在浏览器中,控制台显示:

Howdy!
Howdy!

HelloWorld()被调用了两次(这为我们提供了两个问候语)-一次是在呈现它时,另一次是在直接调用时。
单击以获得第二个渲染,我们将看到:

Howdy!

我们不再呈现<HelloWorld/>,所以我们只看到来自直接调用HelloWorld()的问候语。
单击以获取第三个渲染...

Rendered fewer hooks than expected.

啊哈!我们违反了钩子的规则,React抱怨了这一点。前两次我们渲染<App/>时,调用了useEffect()钩子,但第三次没有。钩子不能有条件地调用。

  • 但是......当我们有条件地渲染<HelloWorld/>时,它没有抱怨。这是同一个函数调用同一个钩子!这太离谱了,太不公平了,等等。* 每个组件示例都有自己的分类账来跟踪钩子。当我们直接调用HelloWorld()时,useEffect()会在App示例中被跟踪。当我们渲染<HelloWorld/>时,useEffect()HelloWorld示例中被跟踪。如果我们渲染<HelloWorld/>多次,每个示例跟踪它自己的useEffect()调用。

抱歉,在React函数组件和效果的幕后进行了冗长但不特别全面的窥视。我希望它在某种程度上解决了你的开放式问题:“为什么尝试这样做一定是一个错误?”/“我错过了什么?”
最简单的答案是“当您将<Foo/>放入组件树时,您就是not calling it directly“。
根据你最初的假设:“出于好奇,我想直接调用一个React函数组件”,你发现了发生了什么。React抱怨!如果你有一匹驮马,你去除了氧气和重力,它也不太好用。

相关问题