百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Javascript设计模式——提供者模式

suiw9 2024-11-04 15:27 18 浏览 0 评论

让多个子组件都可以共享同一数据


在一些场景下,我们希望让应用中的多个(或者所有)组件可用同一部分数据。当然我们可以通过props一层一层向下级组件传递这部分数据,但实际使用中却往往因为需要在应用中的大量组件对外暴露该属性而使得实操变得困难重重。

如果真的要这么做得话,结局大概率会碰到prop下钻的难题,也就是指在组件树形结构中props传递层级的过深。即便历经重重困难勉强扛过prop下钻困境,也会在未来由于无法确定数据从何而来,而让组件的重构变得几乎不可能。

假如说我们在App组件中初始了一个数据,而在整个应用的组件树中我们还有ListItem, Header和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>

像这样层层向下传递属性的方式很容易让逻辑变得凌乱。即便我们将来想要对data属性改个名字,我们都不得不在所有组件中对其重新命名。而且应用规模越大,这种prop下钻的问题越严重。

如果可以在传递过程中越过那些不需要这个数据的组件层级,才是最佳方案。对于需要使用这个数据的组件应该给予它直接的访问渠道,而不是依赖于prop下钻的方式。

这正是提供者模式使用的最佳场景!使用提供者模式,可以让数据对多个组件可用。我们可以包装所有组件在一个提供者中,而不是通过props一层一层递进式地传递数据。在React语境中,提供者是通过Context对象构造出来的一个高阶组件。我们可以通过调用createContext方法创建一个Context对象。

提供者接受一个value属性,也就是希望向下级组件传递的属性。于是所有被提供者组件包裹起来的组件都拥有对于value属性的访问能力。

const DataContext = React.createContext()

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

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

我们不再需要手动向每一个下级组件传递data属性了!那么,对于ListItem,Header和Text组件来说,他们如何获取到data的值呢?

每个被包裹的组件都可以通过使用useContext钩子函数来获取data。这个钩子函数接受一个通过data引用的context对象,也就是本例中的DataContext。useContext钩子允许我们对context对象进行读写。

const DataContext = React.createContext();

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

  return (
    <div>
      <SideBar />
      <Content />
    </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这个东西。因此我们不必再担心向组件树中不需要使用这个数据的那些层级传递属性,这也让重构变得简单得多。

提供者模式非常适用于需要共享全局数据的场景。最常见的就是向所有组件共享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的提供者对象中,向下传递当前应该使用的背景颜色和字体颜色,而不是向每一个组件显式地传递属性。

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提供者内部包裹着,因此通过提供者的value值随之传递的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可以直接使用themecontext对象来获取其值。

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>
  );
}

钩子函数

向组件传递context对象可以通过创建一个钩子函数的方式。相较于在每一个组件内部都import useContext以及对应的context对象,也可以使用钩子函数来返回需要的context对象,这样显然会更加方便。

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

首先做一点防御性编程的代码,如果通过useContext(ThemeContext)返回的是一个false值,那么应该抛出一个异常。

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

接下来我们会创建一个高阶组件来包裹其他业务组件以便传递提供者的值,而不是直接通过ThemeContext.Provider组件来包裹。这样可以降低context逻辑和渲染组件之间的耦合度,也会提升提供者的复用性。

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钩子函数。

export default function TextBox() {
  const theme = useThemeContext();

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

通过对不同context创建钩子函数,能更好地降低提供者的逻辑代码与渲染组件之间的耦合度。


案例学习

有一些库提供内置的提供者,我们可以直接使用。其中一个不错的例子来自于styled-components。

接下来的讲解并不需要对styled-components具有使用经验

styled-components库提供了一个ThemeProvivder。每一个基于其构建的组件都可以获取这个提供者的值。我们可以直接使用而不需要自行创建context。

回到之前的列表的例子,但这一次直接使用styled-components库提供的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>
  );
}

使用Styled Components之后我们不再向ListItem组件传递行内的style值,而是构建一个基于styled.li的组件。由于该组件基于styled component,因此可以直接获取到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将样式应用于所有通过styled component构建的组件。

// App.js

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};
  `}
`;

优点

利用提供者模式 / (React) Context API允许我们将数据传递给多个组件,而无需手动层层传递组件的属性。

这也会降低在重构代码的过程中不小心引入bug的风险。也就是前述所提及的假设稍后我们想要对某个属性重命名,可能牵涉到整个组件树路径上的所有组件代码的修改。

使用提供者模式会避免处理所谓的prop下钻的问题,这一问题应该被认为是一种反模式。如前所述,prop下钻会造成一种难以理解应用中数据流向的困局,因为在组件内部并不能轻易地定位到数据最初究竟来自于哪个父级组件。而通过提供者模式,则不再需要向不关心该数据的组件传递这个数据。

由于提供者模式允许组件获取其中的数据,因此也可以认为通过它可以维护某种形式上的全局状态。


缺点

在某些场景下,过度使用提供者模式则会造成性能问题。所有消费提供者上下文的组件都会在状态发生变更时重新渲染。

看下面的例子,一个简单的计数器,当Button组件中的Increment按钮被点击一次,计数器都会+1。另外还有一个Reset组件,其中的Reset按钮被点击时,会重置计数器为0。

当你点击Increment按钮时,不仅仅是计数器被重新渲染,在Reset组件内部的日期也会被重新渲染。

// index.js

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因此count变化时也随之重新进行渲染。在小型应用中这不太能感受到有什么影响。但在大型应用中频繁传递或者更新提供者上下文会造成大量组件重新渲染,这将对性能造成消极影响。

为了避免组件在消费提供者上下文时的重复渲染,可以对ContextProvivder进行粒度更细的拆分,避免不相关的数据变更对组件触发重新渲染,或者使用useMemo对组件依赖项状态进行判断,如无依赖状态变更则不会重新渲染。

原文地址:https://www.patterns.dev/posts/provider-pattern/

相关推荐

5款Syslog集中系统日志常用工具对比推荐

一、为何要集中管理Syslog?Syslog由Linux/Unix系统及其他网络设备生成,广泛分布于整个网络。因其包含关键信息,可用于识别网络中的恶意活动,所以必须对其进行持续监控。将Sys...

跨平台、多数据库支持的开源数据库管理工具——DBeaver

简介今天给大家推荐一个开源的数据库管理工具——DBeaver。它支持多种数据库系统,包括Mysql、Oracle、PostgreSQL、SLQLite、SQLServer等。DBeaver的界面友好...

强烈推荐!数据库管理工具:Navicat Premium 16.3.2 (64位)

NavicatPremium,一款集数据迁移、数据库管理、SQL/查询编辑、智能设计、高效协作于一体的全能数据库开发工具。无论你是MySQL、MariaDB、MongoDB、SQLServer、O...

3 年 Java 程序员还玩不转 MongoDB,网友:失望

一、什么场景使用MongoDB?...

拯救MongoDB管理员的GUI工具大赏:从菜鸟到极客的生存指南

作为一名在NoSQL丛林中披荆斩棘的数据猎人,没有比GUI工具更称手的瑞士军刀了。本文将带你围观五款主流MongoDB管理神器的特性与暗坑,附赠精准到扎心的吐槽指南一、MongoDBCompass:...

mongodb/redis/neo4j 如何自己打造一个 web 数据库可视化客户端?

前言最近在做neo4j相关的同步处理,因为产线的可视化工具短暂不可用,发现写起来各种脚本非常麻烦。...

solidworks使用心得,纯干货!建议大家收藏

SolidWorks常见问题...

统一规约-关乎数字化的真正实现(规范统一性)

尽管数字化转型的浪潮如此深入人心,但是,对于OPCUA和TSN的了解却又甚少,这难免让人质疑其可实现性,因为,如果缺乏统一的语义互操作规范,以及更为具有广泛适用的网络与通信,则数字化实际上几乎难以具...

Elasticsearch节点角色配置详解(Node)

本篇文章将介绍如下内容:节点角色简介...

产前母婴用品分享 篇一:我的母婴购物清单及单品推荐

作者:DaisyH8746在张大妈上已经混迹很久了,有事没事看看“什么值得买”已渐渐成了一种生活习惯,然而却从来没有想过自己要写篇文章发布上来,直到由于我产前功课做得“太过认真”(认真到都有点过了,...

比任何人都光彩照人的假期!水润、紧致的肌肤护理程序

图片来源:谜尚愉快的假期临近了。身心振奋的休假季节。但是不能因为这种心情而失去珍贵的东西,那就是皮肤健康。炙热的阳光和强烈的紫外线是使我们皮肤老化的主犯。因此,如果怀着快乐的心情对皮肤置之不理,就会使...

Arm发布Armv9边缘AI计算平台,支持运行超10亿参数端侧AI模型

中关村在线2月27日消息,Arm正式发布Armv9边缘人工智能(AI)计算平台。据悉,该平台以全新的ArmCortex-A320CPU和领先的边缘AI加速器ArmEthos-U85NPU为核心...

柔性——面向大规模定制生产的数字化实现的基本特征

大规模定制生产模式的核心是柔性,尤其是体现在其对定制的要求方面。既然是定制,并且是大规模的定制,对于制造系统的柔性以及借助于数字化手段实现的柔性,就提出了更高的要求。面向大规模定制生产的数字化业务管控...

创建PLC内部标准——企业前进的道路

作者:FrankBurger...

标准化编程之 ----------- 西门子LPMLV30测试总结

PackML乃是由OMAC开发且被ISA所采用的自动化标准TR88.00.02,能够更为便捷地传输与检索一致的机器数据。PackML的主要宗旨在于于整个工厂车间倡导通用的“外观和感觉”,...

取消回复欢迎 发表评论: