设计模式 - Provider 模式

在某些情况下,我们希望为应用程序中的许多(如果不是全部)组件提供数据。尽管我们可以使用 props 将数据传递给组件,但如果应用程序中的几乎所有组件都需要访问 prop 的值,这可能很难做到。

我们经常遇到所谓的属性钻探(prop drilling)问题,这在我们将属性向下传递至组件树的深层时就会出现。重构依赖属性的代码几乎不可能,也很难知道某些数据来自哪里。

假设我们有一个包含某些数据的 App 组件。在组件树的下方,我们有一个 ListItemHeader 和 Text 组件,它们都需要这些数据。为了将这些数据传递到这些组件,我们必须将其传递到多个组件层。

在我们的代码库中,这将如下所示:

function App() {
  const data = { ... }

  return (
    <div>
      <SideBar data={data} />
      <Content data={data} />
    </div>
  )
}

const SideBar = ({ data }) => <List data={data} />
const List = ({ data }) => <ListItem data={data} />
const ListItem = ({ data }) => <span>{data.listItem}</span>

const Content = ({ data }) => (
  <div>
    <Header data={data} />
    <Block data={data} />
  </div>
)
const Header = ({ data }) => <div>{data.title}</div>
const Block = ({ data }) => <Text data={data} />
const Text = ({ data }) => <h1>{data.text}</h1>

以这种方式传递 props 会变得非常混乱。如果我们将来想重命名data属性,我们必须在所有组件中重命名它。您的应用程序越大,支柱钻孔就越棘手。

如果我们可以跳过不需要使用此数据的所有组件层,那将是最佳的。我们需要有一些东西,让需要访问 data 价值的组件直接访问它,而不依赖于 props 钻孔。

这就是 Provider模式 可以帮助我们的地方!借助 Provider模式,我们可以将数据提供给多个组件。我们可以将所有组件包装在一个 Provider 中,而不是通过 prop 将数据传递到每一层。Provider 是 Context 对象提供给我们的高阶组件。我们可以创建一个 Context 对象,使用 React 为我们提供的 createContext 方法。

Provider 收到一个 value 属性,其中包含我们想要传递的数据。包装在此 Provider 中的所有组件都可以访问 value 属性的值。

const DataContext = React.createContext()

function App() {
  const data = { ... }

  return (
    <div>
      <DataContext.Provider value={data}>
        <SideBar />
        <Content />
      </DataContext.Provider>
    </div>
  )
}

我们不再需要手动将 data props 传递给每个组件!那么,ListItemHeader 和 Text 组件如何访问data的值呢?

每个组件都可以使用 useContext 钩子访问data。此钩子接收 data 具有引用的上下文,在本例中为 DataContextuseContext 钩子允许我们在上下文对象中读取和写入数据。

const DataContext = React.createContext();

function App() {
  const data = { ... }

  return (
    <div>
      <DataContext.Provider value={data}>
        <SideBar />
        <Content />
      </DataContext.Provider>
    </div>
  )}

const SideBar = () => <List />
const List = () => <ListItem />
const Content = () => <div><Header /><Block /></div>


function ListItem() {
  const { data } = React.useContext(DataContext);
  return <span>{data.listItem}</span>;
}

function Text() {
  const { data } = React.useContext(DataContext);
  return <h1>{data.text}</h1>;
}

function Header() {
  const { data } = React.useContext(DataContext);
  return <div>{data.title}</div>;
}

不使用data值的组件根本不需要处理data。我们再也不用担心通过不需要 prop 值的组件将 prop 向下传递几个级别,这使得重构变得容易得多。

Provider 模式对于共享全局数据非常有用。Provider 模式的一个常见用例是与许多组件共享主题 UI 状态。

假设我们有一个显示列表的简单应用程序。

// index.js
import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);


// App.js
import React from "react";
import "./styles.css";

import List from "./List";
import Toggle from "./Toggle";

export default function App() {
  return (
    <div className="App">
      <Toggle />
      <List />
    </div>
  );
}


// List.js
import React from "react";
import ListItem from "./ListItem";

export default function Boxes() {
  return (
    <ul className="list">
      {new Array(10).fill(0).map((x, i) => (
        <ListItem key={i} />
      ))}
    </ul>
  );
}


// Toggle.js
import React from "react";

export default function Toggle() {
  return (
    <label className="switch">
      <input type="checkbox" />
      <span className="slider round" />
    </label>
  );
}


// ListItem.js
import React from "react";

export default function ListItem() {
  return (
    <li>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat.
    </li>
  );
}

我们希望用户能够通过切换开关在浅色模式和深色模式之间切换。当用户从深色模式切换到浅色模式时,反之亦然,背景颜色和文本颜色应该会发生变化!我们可以将组件包装在 ThemeProvider 中,并将当前主题颜色传递给 Provider,而不是将当前主题值向下传递给每个组件。

export const ThemeContext = React.createContext();

const themes = {
  light: {
    background: "#fff",
    color: "#000",
  },
  dark: {
    background: "#171717",
    color: "#fff",
  },
};

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  const providerValue = {
    theme: themes[theme],
    toggleTheme,
  };

  return (
    <div className={`App theme-${theme}`}>
      <ThemeContext.Provider value={providerValue}>
        <Toggle />
        <List />
      </ThemeContext.Provider>
    </div>
  );
}

由于 Toggle 和 List 组件都包装在 ThemeContext Provider 中,因此我们可以访问作为value传递给 Provider 的值 theme 和 toggleTheme

在 Toggle 组件中,我们可以使用 toggleTheme 函数相应地更新主题。

import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function Toggle() {
  const theme = useContext(ThemeContext);

  return (
    <label className="switch">
      <input type="checkbox" onClick={theme.toggleTheme} />
      <span className="slider round" />
    </label>
  );
}

List 组件本身并不关心主题的当前值。但是,ListItem 组件可以!我们可以直接在 ListItem 中使用 theme 上下文。

import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function TextBox() {
  const theme = useContext(ThemeContext);

  return <li style={theme.theme}>...</li>;
}

我们不必将任何数据传递给不关心主题当前值的组件。

// App.js
import React, { useState } from "react";
import "./styles.css";

import List from "./List";
import Toggle from "./Toggle";

export const themes = {
  light: {
    background: "#fff",
    color: "#000"
  },
  dark: {
    background: "#171717",
    color: "#fff"
  }
};

export const ThemeContext = React.createContext();

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeContext.Provider value={{ theme: themes[theme], toggleTheme }}>
        <>
          <Toggle />
          <List />
        </>
      </ThemeContext.Provider>
    </div>
  );
}


// Toggle.js
import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function Toggle() {
  const theme = useContext(ThemeContext);

  return (
    <label className="switch">
      <input type="checkbox" onClick={theme.toggleTheme} />
      <span className="slider round" />
    </label>
  );
}

Hooks

我们可以创建一个钩子来为组件提供上下文。我们不必在每个组件中导入 useContext 和 Context,而是可以使用一个钩子来返回我们需要的上下文。

function useThemeContext() {
  const theme = useContext(ThemeContext);
  return theme;
}

为了确保它是一个有效的主题,如果 useContext(ThemeContext) 返回一个虚假值,让我们抛出一个错误。

function useThemeContext() {
  const theme = useContext(ThemeContext);
  if (!theme) {
    throw new Error("useThemeContext must be used within ThemeProvider");
  }
  return theme;
}

我们可以创建一个 HOC,包装组件以提供其值,而不是直接使用 ThemeContext.Provider 组件包装组件。这样,我们可以将上下文逻辑与渲染组件分开,从而提高 Provider 的可重用性。

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  const providerValue = {
    theme: themes[theme],
    toggleTheme,
  };

  return (
    <ThemeContext.Provider value={providerValue}>
      {children}
    </ThemeContext.Provider>
  );
}

export default function App() {
  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider>
        <Toggle />
        <List />
      </ThemeProvider>
    </div>
  );
}

现在,每个需要访问 ThemeContext 的组件都可以简单地使用 useThemeContext 钩子。

通过为不同的上下文创建挂钩,可以很容易地将 Provider 的逻辑与呈现数据的组件分开。

个案研究

有些库提供内置的提供者,我们可以在消费组件中使用这些值。styled-components就是一个很好的例子。

了解此示例并不需要具备styled-components的使用经验。

styled-components 库为我们提供了一个 ThemeProvider。每个样式化的组件都可以访问此Provider 的价值!我们可以使用提供给我们的 API,而不是自己创建上下文 API!

让我们使用相同的 List 示例,并将组件包装在从styled-component库导入的 ThemeProvider 中。

import { ThemeProvider } from "styled-components";

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider theme={themes[theme]}>
        <Toggle toggleTheme={toggleTheme} />
        <List />
      </ThemeProvider>
    </div>
  );
}

我们不会将内联style prop 传递给 ListItem 组件,而是将其设置为 styled.li 组件。由于它是一个样式化的组件,我们可以访问theme的价值!

import styled from "styled-components";

export default function ListItem() {
  return (
    <Li>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat.
    </Li>
  );
}

const Li = styled.li`
  ${({ theme }) => `
     background-color: ${theme.backgroundColor};
     color: ${theme.color};
  `}
`;

我们现在可以使用 ThemeProvider 轻松地将样式应用于所有样式组件!

import React, { useState } from "react";
import { ThemeProvider } from "styled-components";
import "./styles.css";

import List from "./List";
import Toggle from "./Toggle";

export const themes = {
  light: {
    background: "#fff",
    color: "#000"
  },
  dark: {
    background: "#171717",
    color: "#fff"
  }
};

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider theme={themes[theme]}>
        <>
          <Toggle toggleTheme={toggleTheme} />
          <List />
        </>
      </ThemeProvider>
    </div>
  );
}


// ListItem.js
import React from "react";
import styled from "styled-components";

export default function ListItem() {
  return (
    <Li>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat.
    </Li>
  );
}

const Li = styled.li`
  ${({ theme }) => `
    background-color: ${theme.backgroundColor};
    color: ${theme.color};
  `}
`;

权衡

优点

 Provider 模式/上下文 API 可以将数据传递到许多组件,而无需手动将数据传递到每个组件层。

它降低了在重构代码时意外引入错误的风险。以前,如果我们以后想要重命名一个 prop,我们必须在使用此值的整个应用程序中重命名此 prop。

我们不再需要处理 prop 向下传递,这可以被视为一种反模式。以前,理解应用程序的数据流可能很困难,因为并不总是清楚某些 prop 值的来源。使用 Provider 模式,我们不再需要将不必要的 prop 传递给不关心这些数据的组件。

使用 Provider 模式可以轻松保持某种全局状态,因为我们可以为组件提供对此全局状态的访问权限。

缺点

在某些情况下,过度使用 Provider 模式可能会导致性能问题。使用上下文的所有组件都会在每次状态更改时重新呈现。

让我们看一个例子。我们有一个简单的计数器,每次单击 Button 组件中的 Increment 按钮时,该计数器的值都会增加。我们在 Reset 组件中还有一个 Reset 按钮,它将计数重置回 0

但是,当您单击Increment”时,您可以看到重新渲染的不仅仅是计数。Reset组件中的日期也会重新渲染!

import React, { useState, createContext, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
import moment from "moment";

import "./styles.css";

const CountContext = createContext(null);

function Reset() {
  const { setCount } = useCountContext();

  return (
    <div className="app-col">
      <button onClick={() => setCount(0)}>Reset count</button>
      <div>Last reset: {moment().format("h:mm:ss a")}</div>
    </div>
  );
}

function Button() {
  const { count, setCount } = useCountContext();

  return (
    <div className="app-col">
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <div>Current count: {count}</div>
    </div>
  );
}

function useCountContext() {
  const context = useContext(CountContext);
  if (!context)
    throw new Error(
      "useCountContext has to be used within CountContextProvider"
    );
  return context;
}

function CountContextProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
}

function App() {
  return (
    <div className="App">
      <CountContextProvider>
        <Button />
        <Reset />
      </CountContextProvider>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Reset 组件也重新呈现,因为它使用了 useCountContext。在较小的应用程序中,这并不重要。在较大的应用程序中,将经常更新的值传递给许多组件可能会对性能产生负面影响。

若要确保组件不会使用包含可能更新的不必要值的 Provider,您可以为每个单独的用例创建多个  Provider。

  • 41
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Java项目中,设计模式非常重要,几乎无处不在。Java本身的设计中就融入了设计模式的思想,包括AWT、JDBC、集合类、IO管道和Web框架等。虽然篇幅有限,无法详细讲解每一个设计模式,但我会尽力在有限的空间内清楚地介绍它们。 在Java项目中常见的设计模式有观察者模式、访问者模式和工厂模式等。 观察者模式定义了一种一对多的依赖关系,当被观察对象的状态发生变化时,所有观察者都会收到通知并进行相应的更新。观察者接口(Observer)中通常包含一个update()方法,用于更新观察者的状态。 访问者模式用于封装一些作用于某个对象结构中各个元素的操作,可以在不改变这些元素的类的前提下,定义新的操作。访问者接口(Visitor)中通常包含一个visit()方法,用于访问具体的元素。 工厂模式是一种创建对象的设计模式,它使用工厂方法来处理创建对象的问题,而不是由客户端直接new一个对象。工厂模式中常见的接口是提供者接口(Provider),它定义了一个produce()方法,用于创建对象。 除了观察者模式、访问者模式和工厂模式,Java项目中还有很多其他的设计模式,如单例模式、策略模式、装饰者模式等。每个设计模式都有自己的用途和适用场景,可以根据项目需求选择合适的设计模式来提高代码的可维护性和可扩展性。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值