钩子问题处理_这不是你奶奶的ReactIII:钩子如何处理依赖关系

钩子问题处理

This is the third of four articles devoted to banishing the idea that you can look at functional components using Hooks as if they were like class-based components. The other articles are:

这是四篇文章的第三篇,这三篇文章专门介绍了您可以使用Hooks像使用基于类的组件一样查看功能组件的想法。 其他文章是:

If you haven’t already read the previous two, you may want to take some time and do so.

如果您尚未阅读前两本书,则可能需要花一些时间来阅读。

This article looks at how functional components let you consistently use variables after rendering is complete. It also discusses how to freeze and update information, and gives you the groundwork for side effects in Part Four.

本文研究了功能组件如何在渲染完成后使您始终如一地使用变量。 它还讨论了如何冻结和更新信息,并在第四部分中为副作用提供了基础。

Let’s take one more look at the diagram I created showing how Hooks plug into the single function that comprises a functional component:

让我们再看一下我创建的图,该图显示了Hooks如何插入包含功能组件的单个函数中:

Image for post
Extremely loosely patterned after Dan Abramov’s original class lifecycle diagram on Twitter
在Twitter上 Dan Abramov的 原始课程生命周期图之后,其图案 极为宽松

In Part One, we talked about how the overall Hooks architecture differs from classes. In Part Two, we talked about how state is managed, focusing on the Hooks on the left side of this diagram. In this article, we’ll focus on what makes the Hooks on the right side of the diagram work, and go into the first two.

第一部分中 ,我们讨论了整个Hooks体系结构与类之间的区别。 在第二部分中 ,我们讨论了如何管理状态,着眼于该图左侧的Hooks。 在本文中,我们将重点介绍使图右侧的挂钩起作用的原因,并进入前两个。

封王为王 (Closures Are King)

There’s two huge elephants in the corner of the functional component room: closures and dependencies. We touched on closures in Part One. Closures freeze values at the moment they’re created. Class-based components don’t do that. This is one of the biggest stumbling blocks developers have when they attempt to switch from classes to Hooks.

功能组件室的角落里有两个巨大的大象:闭包和依赖项。 我们在第一部分中谈到了闭包。 闭包会在创建值时冻结它们。 基于类的组件不会这样做。 当开发人员尝试从类切换到Hooks时,这是最大的绊脚石之一。

With classes, when a function is called, it reflects the state of the instance at the moment the function was called. With functional components, when a Hook is called, its function reflects the state of the instance at the moment the component was rendered. It doesn’t matter how much later the Hook’s function is used. It only knows what it knew then.

对于类,当调用函数时,它反映了调用函数时实例的状态 对于功能组件,调用Hook时,其功能反映了呈现组件时实例的状态。 多少小时后使用Hook函数都无关紧要。 它只知道当时的情况。

This helps reduce bugs where the instance’s fields change out from under a side effect. As an example, take a look at the following class-based component:

这有助于减少实例字段因副作用而改变的错误。 作为示例,请看以下基于类的组件:

class Component extends React.Component {
  name;
  
  setName(name) {
    this.name = name;
  }
  
  render() {
    return (
      <button
        onClick={() => {
          this.setName(window.prompt('Who am I?', this.name));
        }}
      >Name me</button>
      <button
        onClick={() => alert(this.name)}
      >Who am I?</button>
    );
  }
}

Here we create two buttons, one to set the instance’s name property, one to read it. But because setName is available outside of the component’s lifecycle, any other component can change it outside of the render phase. A user clicking Who am I? will see the last value given to name, not necessarily the last value they provided when they clicked Name me.

在这里,我们创建两个按钮,一个用于设置实例的name属性,一个用于读取实例。 但是,因为setName在组件的生命周期之外可用,所以任何其他组件都可以在呈现阶段之外更改它。 用户单击Who am I? 将看到为name提供的最后一个值,不一定是单击“ Name me时提供的最后一个值。

With a functional component, the closure makes sure that the value is frozen into the render method:

使用功能组件,闭包可确保将值冻结到render方法中:

function Component() {
  const [name, setName] = useState();


  return (
    <button
      onClick={() => {
        setName(window.prompt('Who am I?', name));
      }}
    >Name me</button>
    <button
      onClick={() => alert(name)}
    >Who am I?</button>
  );  
}

In this case, Name me updates the state of the component, and causes the component to rerender with the new value. Every place where you see name in this method, you know that it has the value that was last available when the component was rendered.

在这种情况下,“ Name me更新组件的状态,并使组件以新值重新呈现。 在此方法中看到name每个地方,您都知道它具有呈现组件时最后可用的值。

Every Hook in blue on the right — useMemo, useCallback, useEffect and useLayoutEffect — looks like it takes a function as its first argument. It isn’t just a function. That function becomes a closure the moment the Hook is called. Every time you call one of those Hooks, you create a closure with the information frozen from that render cycle.

右侧每个蓝色的钩子useMemouseCallbackuseEffectuseLayoutEffect看起来都像是将函数作为第一个参数。 这不仅仅是一个功能。 调用Hook的那一刻,该函数就会关闭。 每次调用这些Hook之一时,都会创建一个闭包,其中包含从该渲染周期冻结的信息。

So, if the information is frozen, how do you update the Hook’s closure with new information?

因此,如果信息被冻结,您如何使用新信息来更新Hook的闭合件?

Dependencies.

依赖关系。

依赖掌握王国的钥匙 (Dependencies Hold the Keys to the Kingdom)

The second argument in these Hooks is an array of constants to watch. If any of the constants change from one render to the next, it tells the Hook that it needs to create a new closure using the function that was provided to it.

这些Hook中的第二个参数是要观察的常量数组。 如果任何常量从一个渲染更改为另一个常量,它将告诉Hook,需要使用提供给它的函数创建一个新的闭包。

Let’s take a look at another example from Part One:

让我们看一下第1部分中的另一个示例:

function UserName({ id }) {
  const [userName, setUserName] = useState('');


  const fetchData = useCallback(
    async () => {
      const data = await fetch(`/api/user/${id}`);
      const json = await data.json();
      setUserName(json.user.name);
    },
    [id] // state setters like setUserName never change and do not need to be included as dependencies.
  );


  useEffect(
    () => {
      fetchData();
    },
    [fetchData] // this dependency causes the effect to run when fetchData changes, which happens when id changes.
  );


  return userName && <p>Hello, {userName}</p>
}

In useCallback, a closure is created where the value of id is frozen in the async function. id is also referenced in the dependency array following the function. When the main function is called with a new id value, a new closure is created, capturing that new value of id.

useCallback ,创建一个闭包,其中id的值冻结在async函数中。 函数后面的依赖项数组中也引用了id 。 当使用新的id值调用main函数时,将创建一个新的闭包,捕获该新的id值。

Likewise, useEffect makes a closure that freezes the reference to fetchData. fetchData appears in the dependency array, so that when it changes, the closure in useEffect is updated.

同样, useEffect进行关闭以冻结对fetchData的引用。 fetchData出现在依赖项数组中,因此当它更改时, useEffect的闭包useEffect更新。

封闭件被冻结。 依赖不是。 (Closures are frozen. Dependencies aren’t.)

This may feel subtle, but it can be a cause of bugs if not considered. The function might look like it’s accessing the current state and props, because it refers to them. But it is frozen in time when the values in the dependency array changes. Take the following polling component:

这可能感觉很微妙,但如果不考虑,可能会导致错误。 该函数似乎正在访问当前状态和道具,因为它引用了它们。 但是,当依赖项数组中的值更改时,它会及时冻结。 采取以下轮询组件:

function TimedSessionIndicator({ id, interval }) {
  const [isLoggedIn, setIsLoggedIn] = useState();
  const [name, setName] = useState();


  useEffect(() => {
    const intervalId = setInterval(async () => {
      const response = await fetch(`/api/user/${id}`);
      const { loggedIn, name } = await response.json();
      if (isLoggedIn !== loggedIn) {
        setIsLoggedIn(loggedIn);
        setName(name);
      }
    }, interval);
    
    return () => {
      clearInterval(intervalId);
    };
  }, [isLoggedIn]);
  
  return typeof isLoggedIn === 'undefined'
    ? ''
    : isLoggedIn
      ? <p>Welcome, {name} <a href="/logout">Log out</a><p>
      : <p><a href="/login">Log in</a></p>;
}

We have an effect that polls the server over a provided interval, updating whether or not the user is logged in and who they are. If their logged-in state changes, the dependency on line 18 tells the effect to update with the new version of isLoggedIn. This clears the polling interval and starts a new one with the new logged in state value.

我们的作用是在给定的时间间隔内轮询服务器,从而更新用户是否已登录以及他们是谁。 如果其登录状态发生变化,则第18行的依赖项将告知效果,以使用isLoggedIn的新版本进行更新。 这将清除轮询间隔,并使用新的登录状态值开始一个新的轮询间隔。

If id or interval change, however, those dependencies aren’t tracked, so the closure isn’t updated. As long as isLoggedIn stays the same, the values for id and interval can change all they want outside the Hook, but the closure is still frozen with the old values for id and interval. Only when isLoggedIn changes does the function update with the new id and interval.

但是,如果idinterval更改,则不会跟踪这些依赖项,因此不会更新闭包。 只要isLoggedIn保持不变, idinterval的值就可以更改它们在Hook之外的所有内容,但是闭包仍将冻结为idinterval的旧值。 仅当isLoggedIn更改时,该函数才会使用新的idinterval更新。

The dependencies track every render. The closure does not.

依赖项跟踪每个渲染。 闭包没有。

React’s react-hooks/exhaustive-deps ESLint warning helps us keep track of when we aren’t tracking all of the external variables we’ve pulled into these closures.

React的react-hooks/exhaustive-deps ESLint警告可帮助我们跟踪何时不跟踪已放入这些闭包中的所有外部变量。

空的依赖项数组 (Empty dependency arrays)

Sometimes you’ll see a Hook that takes an empty dependency array, like this:

有时,您会看到一个Hook带有一个空的依赖项数组,如下所示:

const defaultUser = useMemo(
  () => ({
    id: -1,
    loggedIn: false,
    name: ''
  }),
  []
);

When a Hook takes an empty dependency array, it tells React that this closure will never change. The function is run once, and the returned value is frozen until the component is unmounted. This can be valuable for creating default object literals like the one shown above, or for creating one-time side effects that only run when the component is first mounted. (We’ll discuss side effect creation in Part Four, when we get to useEffect and useLayoutEffect.)

当一个Hook接受一个空的依赖数组时,它告诉React这个闭包将永远不会改变。 该函数运行一次,返回的值将被冻结,直到卸载该组件。 这对于创建默认的对象文字(如上面所示的文字)或创建仅在首次安装组件时才运行的一次性副作用很有用。 (当我们使用useEffectuseLayoutEffect时,我们将在第四部分中讨论副作用的创建。)

This is different from calling a Hook with no dependency array at all. That tells React to update the closure every single time, even if it isn’t necessary. This can be quite expensive and is rarely useful. Even if you want a side effect to rerun on every render, you can find a less expensive way to do this, like tracking change events, having a side effect create a MutationObserver, or create an interval-based polling mechanism.

这与根本没有依赖项数组的Hook调用不同。 这告诉React每次都更新闭包,即使不是必需的。 这可能非常昂贵,并且很少有用。 即使您希望在每个渲染上重新运行副作用,您也可以找到一种更便宜的方法来执行此操作,例如跟踪更改事件,产生副作用以创建MutationObserver或创建基于间隔的轮询机制。

永远不要忽视React钩/详尽的下降。 (Never Ignore react-hooks/exhaustive-deps.)

How many times have you seen that annoying warning and decided to stick an // eslint-disable-line comment on it and call it a day?

您看到过多少次烦人的警告,并决定在其上贴上// eslint-disable-line注释,并称之为一天?

Don’t.

别。

When React complains about you not checking your dependencies properly, it’s telling you one of two things:

当React抱怨您没有正确检查依赖项时,它告诉您以下两件事之一:

  1. Your closure will become stale and have old data in it.

    您的关闭将变得过时,并且其中包含旧数据。
  2. Your dependencies aren’t actually related to your closure.

    您的依赖关系实际上与关闭没有关系。

In the first case, there’s probably another variable you’ve forgotten to track. In the second case, you’re asking React to update something, not because the update impacts the closure, but because the dependency is hiding a piece of business logic.

在第一种情况下,您可能忘记了另一个变量。 在第二种情况下,您是在要求React更新某些内容,不是因为更新会影响闭包,而是因为依赖项隐藏了一部分业务逻辑。

If a dependency for foo updates a closure based on bar, there’s naturally going to be a question of why the two are connected. If it’s not obvious why the dependency is associated with the closure, there’s probably some fragility baked into the code, some magic happening somewhere else that would “make this all make sense” — if you happened to know what was happening in the grandfather component.

如果对foo的依赖关系更新了基于bar的闭包,那么自然就会有一个问题,为什么两者要连接。 如果不清楚依赖关系为何与闭包相关联,则可能是代码中散布了一些易碎性,而其他地方发生了一些魔术,这些魔术将“使一切都变得有意义”-如果您偶然知道祖父组件中发生了什么。

By keeping your dependencies localized and transparent, you encapsulate your logic and dependencies into a single unit, making it more understandable for the next person who has to read your code.

通过使依赖关系保持本地化和透明,您可以将逻辑和依赖关系封装到一个单元中,从而使下一个必须阅读代码的人更容易理解。

</soapbox>

</ soapbox>

Alright, now that we’ve looked at how closures and dependencies work together, let’s look at the Hooks that take advantage of this to reduce rendering and more clearly identify data relationships.

好了,既然我们已经了解了闭包和依赖项是如何协同工作的,那么让我们来看看利用此钩子减少渲染并更清楚地标识数据关系的Hook。

useMemo:减少计算时间,定义依赖关系,修复对象文字引用 (useMemo: Reduce Calculation Time, Define Dependencies, Fix Object Literal References)

useMemo() has three basic purposes. First, the docs say it lets you save a value for future use, so you only have to calculate it once and reuse it many times. This can be valuable for information that is expensive to calculate, such as a collection of JSX that only changes when specific data points change.

useMemo()具有三个基本目的。 首先,文档说它可以让您保存一个值以备将来使用,因此您只需要计算一次就可以多次使用。 这对于计算代价昂贵的信息可能非常有价值,例如仅在特定数据点发生更改时才更改的JSX集合。

To be honest, I haven’t found a lot of use cases for that. What I’ve found useful, however, are two other uses: reducing the complexity of dependencies, and keeping references to an object literal from changing.

老实说,我还没有找到很多用例。 但是,我发现有用的还有两个其他用途:减少依赖项的复杂性,以及避免更改对对象文字的引用。

useMemo clearly defines what its result depends on, with its dependency array. This can be useful to simplify dependency arrays in other Hooks:

useMemo及其依赖项数组明确定义了结果取决于什么。 这对于简化其他Hook中的依赖项数组很有用:

function ChatNotification({ userId, roomId }) {
  const [notificationCount, setNotificationCount] = useState(0);


  const folderData = useMemo(
    () => {
      if (!userId || !roomId) {
        return null;
      }
      return { userId, roomId };
    },
    [userId, roomId]
  );
  
  useEffect(
    () => {
      if (!folderData) {
        return;
      }
      
      const polling = async () => {
        const response = await fetch('/chat/monitor', {
          method: 'POST',
          body: JSON.stringify(folderData)
        });
        const json = await response.json();
        setNotificationCount(json.count);
      };
      
      const intervalId = setInterval(polling, 5000);
      
      return () => {
        clearInterval(intervalId);
      };
    },
    [folderData]
  );
  
  return <div>{notificationCount}</div>;
}

In this component, folderData is only updated if userId and roomId change. In turn, useEffect only updates when folderData changes.

在此组件中,仅在userIdroomId更改时更新folderData 。 反过来, useEffect仅在folderData更改时更新。

By splitting dependencies in this way, we can reduce the amount we need to keep track of in a specific circumstance. We can look at the object created by useMemo and know it is dependent on userId and roomId, but not care about any other values used by this component. Likewise, the useEffect call shows us we care when folderData changes, but we don’t have to care about why it changes. We just need to know an update happens when folderData changes.

通过以这种方式拆分依赖关系,我们可以减少在特定情况下需要跟踪的数量。 我们可以看一下useMemo创建的对象,知道它依赖于userIdroomId ,但不关心此组件使用的任何其他值。 同样, useEffect调用向我们显示了folderData发生更改时,我们很在意,但我们不必担心它为何更改。 我们只需要知道folderData更改时发生的更新folderData

useMemo is also useful for preventing infinite renders when saving object literals to state:

useMemo在将对象文字保存为状态时,还可用于防止无限渲染:

function InfiniteRender({ name, value }) {
  [tuple, setTuple] = useState();
  // A new object literal is created every time
  const tupleLiteral = [name, value];
  // Changed object reference forces setTuple to rerender every time
  setTuple(tupleLiteral);
  return (
    <dl>
      <dt>tuple[0]</dt>
      <dd>tuple[1]</dd>
    </dl>
  );
}


function Render({ name, value }) {
  [tuple, setTuple] = useState();
  // A new literal is only returned if name or value change
  const memoizedTuple = useMemo(
    () => [name, value],
    [name, value]
  );
  // Only forces a rerender when memoizedTuple changes due to name or value changing
  setTuple(memoizedTuple);
  return (
    <dl>
      <dt>tuple[0]</dt>
      <dd>tuple[1]</dd>
    </dl>
  );
}

In this case, line 4 creates a new object literal every time, forcing setTuple to rerender every time. State setters force a rerender if it gets a new object reference. If you memoize the literal, the state setter will recognize when you give it the exact same object, and it won’t rerender if it does. It will only rerender when it gets a new object, which will only happen when a dependency changes.

在这种情况下,第4行每次都会创建一个新的对象常量,从而迫使setTuple每次都setTuple呈现。 如果获得新的对象引用,状态设置器将强制重新渲染。 如果记住文字,状态设置器将在您给它完全相同的对象时进行识别,并且如果提供则不会重新渲染。 它只有在获得新对象时才会重新渲染,只有在依赖项更改时才会发生。

useCallback:函数的useMemo (useCallback: useMemo for functions)

useCallback is simply a shorthand for returning a function from a useMemo closure:

useCallback只是从useMemo闭包返回函数的简写useMemo

const sumNumbersFromUseMemo = useMemo(
  () => (x, y) => x + y + z,
  [z]
);


const sumNumbersFromUseCallback = useCallback(
  (x, y) => x + y + z,
  [z]
);

While you can use useMemo for functions, useCallback highlights the act of creating a closure for a function instead of a value. It can be used to simplify dependency chains, the same way that useMemo can. This was illustrated in the Dependencies section’s first example above.

虽然可以将useMemo用于函数,但useCallback突出显示了为函数而不是值创建闭包的行为。 它可以用来简化依赖链,就像useMemo一样。 上面的“依赖关系”部分的第一个示例对此进行了说明。

useCallback can also be used to give other components the option of not rerendering. When a function memoized with useCallback is sent to another component as a property, that component can compare it to the previous version of the property, see that it’s the same, and choose to not rerender if nothing else changed:

useCallback还可以用于为其他组件提供不重新呈现的选项。 当使用useCallback useCallback的函数作为属性发送到另一个组件时,该组件可以将其与该属性的先前版本进行比较,看是否相同,并选择不重新渲染(如果没有其他更改):

function LoginForm({ name, submitCredentials }) {
  const [updatedName, setUpdatedName] = useState(name);
  const [password, setPassword] = useState();


  // We don't bother memoizing this because updatedName or password will change frequently.
  // The parent form this submits will be rerendered every time. That should not be expensive
  // as long as the children are also inexpensive to render.
  const submitForm = e => {
    e.preventDefault();
    submitCredentials(updatedName, password);
  };
  
  const changeName = useCallback(
    e => setUpdatedName(e.currentTarget.value),
    []
  );
  
  const changePassword = useCallback(
    e => setPassword(e.currentTarget.value),
    []
  );


  // Inputs only change when values change.
  // Callbacks prevent rerendering because the onChange handlers don't change.
  return (
    <form onSubmit={submitForm}>
      <div>
        Name: <input value={updatedName} onChange={changeName} />
      </div>
      <div>
        Password: <input type="password" value={password} onChange={changePassword}/>
      </div>
    </form>
  );
}

The empty dependency arrays in the example tell React to build once and never again. This can be really useful to keep other dependencies from updating. In this case, because the inputs’ onChange handlers will never change, the form will only rerender an input if its value changes.

该示例中的空依赖项数组告诉React再次构建。 这对于防止其他依赖项更新非常有用。 在这种情况下,由于输入的onChange处理函数将永远不会更改,因此仅当输入值更改时,表单才会重新呈现输入。

While this is a pretty trivial optimization for an input field, if the component is, say, output for a database with methods for making updates, rerendering the entire database every time can be painful.

尽管这对于输入字段来说是一个非常简单的优化,但是,例如,如果该组件是具有进行更新的方法的数据库输出,那么每次重新呈现整个数据库都会很痛苦。

提防过早的优化 (Beware Premature Optimization)

A lot of useMemo and useCallback’s benefits come from reducing the frequency of rerenders and updates. They can come at a price, though. They can make code unnecessarily more complex. Like any optimization, they may not be useful, until they are.

useMemouseCallback的许多好处来自减少重新渲染和更新的频率。 不过,它们可以付出代价。 它们会使代码不必要地更加复杂。 像任何优化一样,它们可能直到有用才有用。

Where useMemo and useCallback show their everyday value is in chunking code into smaller, dependent pieces, and preventing infinite renders. You can use them to make dependency trees, reducing the number of variables you need to track in place by consolidating them in different Hooks. You can also use them to keep object literal creation from triggering unnecessary updates. But beyond that, wait and see what your browser experience shows you before going deeper.

useMemouseCallback展示其日常价值的地方是将代码分块为更小的相关部分,并防止无限渲染。 您可以使用它们创建依赖关系树,通过将它们合并到不同的Hook中来减少需要跟踪的变量数量。 您还可以使用它们来防止对象文字创建触发不必要的更新。 但是除此之外,在深入了解之前,请先看看您的浏览器体验会向您显示什么。

tl; dr (tl;dr)

Alright, so this was a bunch of concepts. The main things to take away include:

好了,所以这是一堆概念。 要带走的主要内容包括:

  • Functional components use closures to keep values consistent with what’s being rendered, regardless of when those values are eventually used. This helps keep values from changing out from under you, which can happen more easily in class-based components.

    功能组件使用闭包来使值与呈现的内容保持一致,无论最终何时使用这些值。 这有助于防止值从您的底下改变出来,而这在基于类的组件中更容易发生。
  • Dependencies keep you from recreating the same data or rerunning the same code when nothing has changed.

    依赖关系使您无法在没有任何更改的情况下重新创建相同的数据或重新运行相同的代码。
  • useMemo helps you prevent infinite renders and lets you consolidate dependencies.

    useMemo可帮助您防止无限渲染,并可以合并依赖项。

  • useCallback adds to what useMemo does by giving child components the opportunity to optimize for unchanged functions.

    useCallback通过使子组件有机会针对未更改的函数进行优化来增加useMemo功能。

Alright, that’s it for the memoization hooks. In Part Four, we’ll take a look at what sets functional components far apart from class-based components: side effect management. See you next week!

好了,便是备忘录挂钩。 在第四部分中,我们将研究什么使功能组件与基于类的组件相距甚远:副作用管理。 下周见!

翻译自: https://medium.com/@michaellandis/this-aint-your-grandma-s-react-iii-how-hooks-handle-dependencies-696cbdef025d

钩子问题处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值