阅读视图

发现新文章,点击刷新页面。

如祺出行:预计2025年度利润同比大幅改善不少于43.4%

36氪获悉,3月3日,如祺出行发布正面盈利预告,预计2025年度利润同比大幅改善不少于43.4%,综合收入不少于人民币50亿元,同比增加超100%。收入增长得益于网约车订单量提升及技术服务收入增加,利润改善源于运营效率提升与成本优化等。这是该公司自2024年上市以来首份盈利预告。

华能集团旗下海宁投资合伙企业注册资本增至617.51亿元

36氪获悉,爱企查App显示,近日,华能保供投资(海宁)合伙企业(有限合伙)发生工商变更,出资额由580.01亿元人民币增至617.51亿元人民币。 华能保供投资(海宁)合伙企业(有限合伙)成立于2021年8月,执行事务合伙人为华能投资管理有限公司,经营范围为股权投资、创业投资、自有资金投资的资产管理服务,由中国国新旗下国新致远(北京)投资有限公司、国新远航(北京)投资有限公司、中国华能集团有限公司、华能投资管理有限公司共同出资。

天津南港新能源发展公司注册资本增至19.5亿元

36氪获悉,爱企查App显示,近日,天津南港新能源发展有限公司发生工商变更,注册资本由13亿元人民币增至19.5亿元人民币,增幅50%。该公司成立于2025年3月,法定代表人为梁志华,经营范围包括新兴能源技术研发、节能管理服务、污水处理及其再生利用、环境应急治理服务、合同能源管理、太阳能发电技术服务、物业管理、非居住房地产租赁等。股东信息显示,该公司由天津经济技术开发区国有资产监督管理局、天津经济技术开发区南港发展集团有限公司共同持股。

前端组件化样式隔离实战:React CSS Modules、styled-components 与 Vue scoped 对比

前端组件化样式隔离实战:React CSS Modules、styled-components 与 Vue scoped 对比

引言:当组件遇见 CSS

在现代前端开发中,组件化已成为构建用户界面的主流方式。我们将页面拆分为独立、可复用的组件,每个组件管理自己的 HTML、CSS 和 JavaScript。然而,CSS 的设计初衷是全局作用域的 —— 样式一旦定义,就会影响整个页面,这给组件化带来了严峻挑战。

试想一个多人协作的项目:A 同学写了一个按钮组件,类名为 .button;B 同学也写了一个按钮组件,同样用了 .button。当两个组件同时出现在页面上时,后加载的样式会覆盖前者,造成意料之外的 UI 错乱。如何让组件的样式“与世隔绝”,既不影响他人,也不被他人影响?本文将深入探讨 React 和 Vue 生态中三种主流的样式隔离方案:CSS Modulesstyled-componentsVue scoped。我们将通过实际代码,由浅入深地理解它们的原理与用法。

1. CSS 的“先天不足”与组件化的冲突

在传统网页开发中,我们通常这样写 CSS:

/* global.css */
.button {
  background-color: blue;
  color: white;
}

这个 .button 样式会作用于页面上所有带有 class="button" 的元素,无论它身处哪个组件。这种全局性在小型项目中或许无伤大雅,但在组件化架构下却成了灾难。

假设我们有 Button.jsxAnotherButton.jsx 两个组件,分别引入了各自的 CSS 文件:

/* Button.css */
.button { background: blue; }

/* AnotherButton.css */
.button { background: red; }
效果图

image.png

最终页面上两个按钮都会是红色,因为后引入的 AnotherButton.css 覆盖了前者的规则。这就是样式冲突的典型场景。

为了解决这一问题,社区发展出了多种作用域隔离方案,核心思想都是将样式“限定”在组件内部。下面我们分别看看 React 和 Vue 是如何做到的。

2. React 中的 CSS Modules

2.1 什么是 CSS Modules?

CSS Modules 是一种将 CSS 文件编译为局部作用域的技术。它并不是官方的 CSS 规范,而是通过构建工具(如 webpack)在编译时给类名自动添加唯一的哈希字符串,从而实现样式隔离。在 React 项目中,使用 Create React App 或 Vite 脚手架时,开箱即支持 CSS Modules。

2.2 基本用法

我们约定 CSS 文件命名为 *.module.css。在组件中像导入一个对象一样导入样式文件,然后通过对象的属性引用类名。

Button.module.css

.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
}

.txt {
  color: red;
  background-color: orange;
  font-size: 30px;
}

Button.jsx

import styles from './Button.module.css';
console.log(styles); // 输出:{ button: "Button_button__1a2b3c", txt: "Button_txt__4d5e6f" }

export default function Button() {
  return (
    <>
      <h1 className={styles.txt}>你好,世界!!!</h1>
      <button className={styles.button}>My Button</button>
    </>
  );
}
效果图

image.png

在浏览器中,最终渲染的 HTML 类似:

<h1 class="Button_txt__4d5e6f">你好,世界!!!</h1>
<button class="Button_button__1a2b3c">My Button</button>
打开控制台我们点击元素开可以看到每个元素都有唯一的id

image.png

可以看到,原始的类名 .button.txt 被转换成了带有组件名和哈希的唯一类名,从而避免了全局污染。

2.3 多人协作的保障

再来看另一个组件 AnotherButton,它也定义了同名的 .button 样式:

anotherButton.module.css

.button {
  background-color: red;
  color: black;
  padding: 10px 20px;
}

AnotherButton.jsx

import styles from './anotherButton.module.css';

export default function AnotherButton() {
  return <button className={styles.button}>My Another Button</button>;
}

两个组件的样式互不干扰,因为编译后的类名分别是 AnotherButton_button__xxxButton_button__xxx。这正是 CSS Modules 的魅力所在——让开发者无需担心类名冲突,专注于组件本身的样式。

2.4 原理浅析

CSS Modules 的原理并不复杂:在构建阶段,webpack 的 css-loader 会解析 *.module.css 文件,将每个类名映射为一个唯一的标识符(通常是 [文件名]_[类名]__[hash]),同时生成一个映射对象(即 styles)。在 JavaScript 中,我们通过这个映射对象来引用最终的类名,而 CSS 文件中的原始类名则被替换为哈希后的类名。这样,CSS 和 JS 就通过同一份映射关系保证了样式的私有性。

3. React 中的 styled-components

如果说 CSS Modules 是在编译时通过修改类名来实现隔离,那么 styled-components 则代表了另一种思潮:CSS-in-JS,即在 JavaScript 中编写 CSS,并利用 JavaScript 的作用域来实现样式隔离。

3.1 什么是 styled-components?

styled-components 是一个流行的 React 库,它允许你使用 ES6 的模板字符串定义样式组件,这些样式组件会自动生成一个唯一的类名,并将样式注入到 <head> 中。

3.2 基本用法

首先安装 styled-components:

npm install styled-components

然后在组件中创建样式化组件:

import styled from 'styled-components';

// 定义一个带样式的 button 组件
const Button = styled.button`
  background-color: ${props => props.primary ? 'blue' : 'white'};
  color: ${props => props.primary ? 'white' : 'blue'};
  border: 1px solid blue;
  padding: 8px 16px;
  border-radius: 4px;
`;

function App() {
  return (
    <>
      <Button>默认按钮</Button>
      <Button primary>主要按钮</Button>
    </>
  );
}
效果图

image.png 渲染后的 HTML 如下(每个人的截图中的真实类名可能不同):

<button class="sc-axZvf jflFSQ">默认按钮</button>
<button class="sc-axZvf efDizw">主要按钮</button>
打开控制台点开控制台元素,我们同样可以看到每个元素都有唯一id

image.png 这里的 sc-axZvf 是组件标识前缀,同一组件生成的实例共享这个前缀,而 jflFSQ 和 efDizw 则是具体的样式类名,分别对应不同的样式规则(例如一个是默认样式,一个是 primary 样式)。所有样式都被动态地生成为 <style> 标签插入页面头部。

3.3 动态样式与 props

styled-components 的一大优势是支持基于 props 的动态样式。如上例所示,通过 props.primary 可以轻松改变背景色和文字颜色。这比传统 CSS 需要额外维护多个类名要直观得多。

3.4 原理浅析

styled-components 在运行时(runtime)工作:当组件渲染时,它会解析模板字符串中的样式规则,根据 props 计算出最终的 CSS 文本,然后生成一个唯一的类名(如 jflFSQ),并将 CSS 规则以 <style> 标签的形式插入到文档头部。值得注意的是,同一组件(如 Button)的所有实例会共享一个组件级标识(sc-axZvf),而具体样式类名则每个实例或每个变体可能不同。由于每个组件实例都可能生成不同的类名,样式天然是隔离的。同时,它还能自动处理浏览器前缀、关键帧动画等,为开发者提供了良好的体验。

4. Vue 中的 scoped 样式

Vue 作为另一大前端框架,其单文件组件(SFC)提供了内置的样式隔离方案——scoped 属性。

4.1 什么是 scoped?

在 Vue 的单文件组件中,可以在 <style> 标签上添加 scoped 属性,指示该样式只作用于当前组件。它的实现方式是为组件模板中的元素添加唯一的自定义属性(如 data-v-xxxxx),然后通过属性选择器来限制样式的生效范围。每个组件会生成一个唯一的哈希 ID,该组件内的所有元素都会被打上这个 ID 作为属性。

4.2 基本用法

App.vue

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <div>
    <h1 class="txt">Hello world in App</h1>
    <h2 class="txt2">一点点</h2>
    <HelloWorld />
  </div>
</template>

<style scoped>
.txt {
  color: red;
}
.txt2 {
  color: green;
}
</style>

HelloWorld.vue

<template>
  <div>
    <h1 class="txt">你好,世界!!!</h1>
    <h2 class="txt2">一点点</h2>
  </div>
</template>

<style scoped>
.txt {
  color: blue;
}
.txt2 {
  color: orange;
}
</style>
效果图

image.png

4.3 渲染结果与原理

编译后,Vue 会为每个组件生成一个唯一的哈希 ID。假设 App 组件的 ID 为 data-v-7a7a37b1,HelloWorld 组件的 ID 为 data-v-e17ea971。最终渲染的 HTML 结构如下(来自实际截图):

html

<div data-v-7a7a37b1>
  <h1 data-v-7a7a37b1 class="txt">Hello world in App</h1>
  <h2 data-v-7a7a37b1 class="txt2">一点点</h2>
</div>

<div data-v-e17ea971 data-v-7a7a37b1>
  <h1 data-v-e17ea971 class="txt">你好,世界!!!</h1>
  <h2 data-v-e17ea971 class="txt2">一点点</h2>
</div>

仔细观察可以发现:

  • App 组件内的所有元素(包括根 div)都带有自己的 ID data-v-7a7a37b1
  • HelloWorld 组件内的所有元素(包括其根 div)都带有自己的 ID data-v-e17ea971特别地,HelloWorld 的根元素上还额外附加了父组件 App 的 ID data-v-7a7a37b1。这是 Vue 故意设计的,目的是让父组件的样式可以通过属性选择器(如 .txt[data-v-7a7a37b1])作用于子组件的根元素,从而实现父组件对子组件根节点的样式控制(如果父组件样式选择器匹配的话)。

对应的 CSS 会被编译为:

css

.txt[data-v-7a7a37b1] { color: red; }
.txt2[data-v-7a7a37b1] { color: green; }
.txt[data-v-e17ea971] { color: blue; }
.txt2[data-v-e17ea971] { color: orange; }

由于属性选择器的存在,每个组件的样式只作用于带有对应属性的元素,实现了完美的样式隔离。同时,子组件根元素拥有双重属性,使得父组件的样式能够有选择地影响子组件的最外层,保持了样式的可控性。

打开控制台元素,我们就可以看到

image.png

4.4 与 CSS Modules 的对比

Vue 的 scoped 与 React 的 CSS Modules 思路相似,都是通过给选择器附加唯一标识来实现作用域。区别在于:

  • CSS Modules 修改了类名本身,而 Vue 保留了原始类名,额外添加了属性选择器。
  • Vue 的 scoped 无需导入对象,直接在模板中使用原始类名,可读性更好。
  • CSS Modules 需要显式引用 styles 对象,略显繁琐,但胜在灵活(比如可以组合多个类名)。

4.5 原理浅析

Vue 在编译单文件组件时,会为每个组件生成一个唯一的哈希 ID。然后:

  1. 将模板中的所有元素加上该 ID 作为属性(根元素额外加上父组件的 ID,如果存在父组件)。
  2. 将 <style scoped> 中的每条 CSS 规则都加上对应的属性选择器。
  3. 最终生成带作用域的 CSS。

整个过程在构建阶段完成,没有运行时开销,性能极佳。

5. 对比与总结

方案 框架 实现原理 优点 缺点
CSS Modules React / Vue 编译时修改类名,生成哈希映射 静态样式,简单可靠;可与预处理器结合 类名需要引用,模板稍显啰嗦
styled-components React 运行时生成唯一类名,注入 <style> 动态样式能力强;完全组件化;支持 props 运行时开销;包体积较大;调试稍难
Vue scoped Vue 编译时添加唯一属性,属性选择器限制 语法简洁;无运行时开销;保留原始类名 仅适用于 Vue;深度选择器需特殊处理

如何选择?

  • 如果你的项目是 React,且偏好“传统”的 CSS 写法,CSS Modules 是最佳选择,它简单、高效,与设计工具(如 Figma)配合良好。
  • 如果你追求极致的动态样式和组件封装,或者希望将样式也作为组件逻辑的一部分,styled-components 能带来流畅的开发体验。
  • 对于 Vue 项目,scoped 是官方推荐的内置方案,开箱即用,足够满足绝大多数场景。

当然,这些方案并非互斥。在大型项目中,你可能会组合使用它们:用 CSS Modules 处理全局样式库,用 styled-components 处理高频复用的动态组件。重要的是理解每种方案的原理,以便在合适的场景做出正确的选择。

结语

从 CSS 的全局困境到组件样式的精细隔离,前端社区给出了多种优雅的解决方案。无论是 React 的 CSS Modules 和 styled-components,还是 Vue 的 scoped,它们都体现了“关注点分离”到“组件内聚”的思想演进。希望本文能帮助你更好地掌握这些工具,在项目中写出健壮、可维护的样式代码。如果你有更多关于样式隔离的思考或实践,欢迎在评论区交流讨论!

央行:2月中期借贷便利(MLF)净投放3000亿元

36氪获悉,中国人民银行官网今天公布2026年2月中央银行各项工具流动性投放情况。数据显示,2月,中期借贷便利(MLF)净投放3000亿元,常备借贷便利(SLF)净投放0亿元,其他结构性货币政策工具净投放-76亿元。与此同时,公开市场操作方面,2月,公开市场国债买卖净投放500亿元,7天期逆回购净投放-1205亿元,中央国库现金管理净投放0亿元,其他期限逆回购净投放6000亿元。

百奥泰:乌司奴单抗注射液获英国MHRA上市批准

36氪获悉,百奥泰公告,公司近日收到英国药品和健康产品管理局(简称“英国MHRA”)签发的关于Usymro®(BAT2206,乌司奴单抗注射液)上市批准通知。该药品成人适应症为中重度斑块状银屑病(PsO),活动性银屑病关节炎(PsA),中重度活动性克罗恩病(CD);儿童适应症为中重度斑块状银屑病(PsO),中重度活动性克罗恩病(CD)。

Vue3 嵌套路由 KeepAlive:动态缓存与反向配置方案

在电商系统中,用户的操作路径往往是这样的:

进入商品列表页 → 进行筛选、排序、分页 → 点击进入商品详情页 → 查看后返回列表页。

这时,用户通常有一个非常明确的预期:

  • 筛选条件还在
  • 分页状态保持
  • 滚动位置不丢失

但同时,我们也会遇到另一种场景:

  • 从首页、搜索页或其他模块进入商品列表页时
  • 希望列表页是“全新状态”
  • 需要重新请求数据

也就是说:

同一个页面,在不同来源路径下,对“是否缓存”的期望是不同的。

在 Vue 项目开发中,KeepAlive 是提升用户体验的重要工具。但在复杂的嵌套路由场景(例如:Layout → SubLayout → Page)下,我们常常会遇到两个典型痛点:

1️⃣ 缓存失效

明明在路由里配置了 keepAlive: true
但页面返回时依然重新挂载,onMounted 再次触发,状态丢失。

2️⃣ 控制逻辑越来越混乱

当我们尝试实现:

  • 从 A 页面跳到 B 页面时缓存
  • 从 C 页面跳到 B 页面时不缓存

路由守卫里开始出现大量 if-else 判断,
逻辑逐渐变得难以维护,甚至演变成“屎山”。

如何实现:

  • 精准控制缓存来源
  • 支持嵌套路由结构
  • 避免父级 Layout 被误销毁
  • 同时保持代码清晰、可维护

本文将分享一种“反向配置”的缓存设计方案,并深入解析嵌套路由场景下的一个核心原则:

父随子存。

通过这套设计,我们可以在复杂电商场景中,实现精细化、可控且可扩展的页面缓存策略。

一、核心痛点:为什么嵌套路由下的缓存容易失效?

在 Vue 中,KeepAlive 的本质其实是“链路存活”。

换句话说,只要组件所在的这条渲染链路还存在,它的缓存才能继续保留。一旦链路中某一层被销毁,缓存就会随之消失。

假设我们的路由结构是这样的:

  • 一级容器:WebsiteLayout(顶层布局)
  • 二级容器:UserLayout(用户中心布局)
  • 三级页面:UserProfile、UserFavorites 等子页面
WebsiteLayout
  └── UserLayout
        └── UserProfile / UserFavorites

这里有一个很多人忽略的“真相”:

如果父容器(例如 UserLayout)被销毁,那么它内部缓存的所有子页面,也会被瞬间“物理清空”。

即使你在 UserProfile 上配置了keepAlive: true, 只要 UserLayout 这一层被重新挂载, 内部所有缓存都会失效,onMounted 会再次触发。

这就是嵌套路由场景下缓存“看起来配置了却没生效”的根本原因。

很多开发者只给子页面设置缓存,却忽略了一个关键原则:

子页面想存活,父容器必须先存活。

这也是后文要讲的核心设计理念——“父随子存”原则。

二、 解决方案:反向配置 + 递归缓存

1. 路由配置:由“去向页”决定“来源页”

我们不再在每个页面写复杂的判断,而是在详情页声明:“从我这里回退时,请保持谁的缓存”

// router/index.ts
export const routes = [
  {
    path: '/user',
    name: 'User',
    component: () => import('@/layouts/UserLayout.vue'),
    meta: { keepAlive: true }, // 父容器必须支持缓存
    children: [
      {
        path: 'favorites',
        name: 'UserFavorites',
        component: () => import('@/views/user/Favorites.vue'),
        meta: { keepAlive: true }
      }
    ]
  },
  {
    path: '/product/:id',
    name: 'Product',
    component: () => import('@/views/product/index.vue'),
    meta: { 
      // 反向配置:从这里回退时,保护以下页面的缓存
      keepAliveSources: ['UserFavorites', 'Home'] 
    }
  }
];

2. 全局守卫:实现“父随子存”

这是整套方案的灵魂。我们需要在路由守卫中做两件事:

  1. 补全链路:进入子页面时,强制将其所有父 Layout 加入缓存名单。
  2. 精准清理:非合法来源进入时,即时销毁旧缓存。
export const cacheStack = ref<string[]>([]);

router.beforeEach(async (to, from, next) => {
    const fromName = from.name as string;
    const keepAliveSources = to.meta.keepAliveSources as string[] || [];

    // 1️⃣ 核心:补全父路由缓存名单
    // 遍历 to.matched,确保当前路由的所有父 Layout 都在缓存数组中
    to.matched.forEach((record) => {
      if (record.meta.keepAlive && record.name) {
        const name = record.name as string;
        if (!cacheStack.value.includes(name)) {
          cacheStack.value.push(name);
        }
      }
    });

    // 2️⃣ 处理来源页逻辑
    if (from.meta.keepAlive && fromName) {
      // 如果来源页本身声明了需要缓存,保留它
      if (!cacheStack.value.includes(fromName)) cacheStack.value.push(fromName);
    } else if (keepAliveSources.includes(fromName)) {
      // 如果去向页声明了它是合法的回退来源,保留它
      if (!cacheStack.value.includes(fromName)) cacheStack.value.push(fromName);
    } else if (fromName) {
      // 3️⃣ 清理:如果不是合法来源,从名单中移除,触发组件销毁
      const index = cacheStack.value.indexOf(fromName);
      if (index > -1) cacheStack.value.splice(index, 1);
    }

    next();
});

3. 布局组件:视图层的配合

在所有包含 router-viewLayout 组件中,必须使用 include 绑定这个全局名单。

  1. WebsiteLayout.vue
// WebsiteLayout.vue
<template>
    <router-view v-slot="{ Component, route }">
      <keep-alive :include="cacheStackList">
        <component 
          :is="Component" 
          :key="route.fullPath" 
        />
      </keep-alive>
    </router-view>
</template>

<script lang="ts" setup name="WebsiteLayout">
  import { defineComponent, computed } from 'vue';
  import { cacheStack } from '/@/router/guard/index';

  const cacheStackList = computed(() => {
    return cacheStack.value
  })
</script>
  1. UserLayout.vue
// UserLayout.vue
<template>
  <div class="flex flex-col">
    <div class="flex gap-6 py-4">
      <!-- 左侧菜单 -->
      <aside class="w-96 shrink-0">
        <UserMenu />
      </aside>

      <!-- 右侧内容 -->
      <main class="flex-1">
        <router-view v-slot="{ Component, route }">
          <keep-alive :include="cacheStackList">
            <component 
              :is="Component" 
              :key="route.fullPath" 
            />
          </keep-alive>
        </router-view>
      </main>
    </div>
  </div>
</template>
<script setup lang="ts" name="User">
import UserMenu from './userMenu/index.vue'
import { cacheStack } from '/@/router/guard/index';
import { computed } from 'vue';
const cacheStackList = computed(() => {
  return cacheStack.value
})
</script>

三、 方案优势

  1. 物理隔离确保生效:通过 to.matched 递归确保父容器存活,彻底解决了嵌套路由“名单对了但不缓存”的问题。

  2. 配置解耦:新加一个详情页时,只需在详情页 meta 里增加一行回退目标,无需改动任何业务组件。

  3. 内存友好:不满足回退条件时,缓存会被立即 splice 清理,避免内存堆积。

以岭药业:泊马度胺化学原料药上市申请获批

36氪获悉,以岭药业公告,全资孙公司万洋衡水制药有限公司(简称“衡水万洋”)收到国家药监局下发的《化学原料药上市申请批准通知书》,由衡水万洋提交的“泊马度胺”化学原料药上市申请已获批准。泊马度胺(Pomalidomide)是第3代免疫调节剂,具有免疫调节、抗血管生成和抗肿瘤作用,对多种恶性血液疾病有效。

深交所倡议全体深市公司开展“质量回报双提升”专项行动

3月3日,深交所通过上市公司业务专区向深市公司发布关于深入开展“质量回报双提升”专项行动的倡议。此前,已有超470家深市公司积极响应“质量回报双提升”专项行动,在提高上市公司质量、增强投资回报、维护资本市场稳定健康发展方面发挥了积极作用。为进一步推动上市公司高质量发展和投资价值提升,促进资本市场长期健康发展,深交所倡议全体深市公司开展“质量回报双提升”专项行动。(证券时报)

现代汽车2月全球销量30.6万辆,同比下滑5.1%

现代汽车3月3日发布数据显示,今年2月全球销量为30.6528万辆,同比下滑5.1%。其中,韩国本土销量为4.7008万辆,同比下滑17.8%;海外销量为25.952万辆,同比下滑2.3%。(界面)

广州港:2月预计完成货物吞吐量同比下降3.6%

36氪获悉,广州港公告,2026年2月份,公司预计完成集装箱吞吐量180万标准箱,同比下降3.6%;预计完成货物吞吐量4143.7万吨,同比下降3.6%。2026年1—2月,公司预计完成集装箱吞吐量432.9万标准箱,同比增长7.7%;预计完成货物吞吐量9373.4万吨,同比增长4.1%。

Kotlin抽象类与接口:相爱相杀的编程“CP”

一、开篇引入

在 Kotlin 的编程世界里,你是否常常在定义一些通用行为或属性时,纠结于到底该使用抽象类还是接口呢?就像在建造一座大厦时,选择合适的建筑材料至关重要,在 Kotlin 编程中,正确选用抽象类和接口,对于构建健壮、可维护的代码结构同样意义非凡 。今天,我们就一起来深入探讨 Kotlin 中抽象类以及它与接口的区别。

二、Kotlin 抽象类详解

(一)抽象类定义

在 Kotlin 中,抽象类是一种不能被直接实例化的类,就像是一个还未完成的 “蓝图”,它主要的作用是作为其他类的基类(父类) ,为子类提供通用的属性和方法定义,而将具体的实现细节留给子类去完成。我们使用abstract关键字来声明一个抽象类。例如:


abstract class AbstractClass {
    // 这里可以定义抽象属性和抽象方法
    // 也可以定义非抽象属性和非抽象方法
}

抽象类不能被直接实例化,比如不能写成val abstractObj = AbstractClass(),这就如同你不能直接使用一个未完成的蓝图来建造实际的建筑一样。它存在的意义更多是为了提供一种通用的结构和规范,让子类基于它进行扩展和实现 。

(二)抽象类示例

以一个图形相关的程序为例,我们定义一个Shape抽象类:


abstract class Shape {
    // 抽象属性,没有初始化值,必须在子类中重写
    abstract val name: String

    // 抽象方法,没有方法体,必须在子类中重写
    abstract fun calculateArea(): Double

    // 非抽象方法,有具体实现,可以被子类继承或重写
    fun printName() {
        println("形状名称: $name")
    }
}

在这个Shape抽象类中,name是一个抽象属性,它代表图形的名称,每个具体的图形(如圆形、矩形)名称都不同,所以需要在子类中具体实现;calculateArea()是一个抽象方法,用于计算图形的面积,不同图形的面积计算方式不同,也需要子类去实现;而printName()是一个非抽象方法,它会打印出图形的名称,这个方法有具体的实现,子类可以直接继承使用,如果有特殊需求也可以重写。

(三)继承抽象类

当子类继承抽象类时,必须使用override关键字重写所有抽象属性和方法。例如,我们定义Circle(圆形)和Rectangle(矩形)类来继承Shape类:


// 子类Circle继承自抽象类Shape
class Circle(val radius: Double) : Shape() {
    // 重写抽象属性
    override val name: String = "圆形"

    // 重写抽象方法
    override fun calculateArea(): Double {
        return Math.PI * radius * radius
    }
}

// 子类Rectangle继承自抽象类Shape
class Rectangle(val width: Double, val height: Double) : Shape() {
    override val name: String = "矩形"
    override fun calculateArea(): Double {
        return width * height
    }
}

Circle类中,我们重写了name属性为 “圆形”,并实现了calculateArea()方法来计算圆形的面积;在Rectangle类中,同样重写了name属性为 “矩形”,并实现calculateArea()方法来计算矩形面积。通过这种方式,抽象类的抽象成员在子类中得到了具体的实现 。

(四)抽象类特点总结

  1. 不能实例化:抽象类不能直接创建对象,它主要为子类提供一个通用的框架。就像我们不能直接使用一个抽象的 “交通工具” 类来创建一个具体的交通工具,而是需要基于它创建 “汽车”“飞机” 等具体子类的对象。

  2. 可包含抽象和非抽象成员:抽象类中既可以有抽象属性和抽象方法,这些需要子类去实现;也可以有非抽象属性和非抽象方法,子类可以直接继承使用,也可以根据需求重写。

  3. 子类必须实现所有抽象成员:如果一个子类继承了抽象类,那么它必须重写抽象类中的所有抽象属性和方法,否则这个子类也必须声明为抽象类。例如,下面这个只重写了部分抽象成员的类,就必须声明为抽象类:


abstract class Square(val sideLength: Double) : Shape() {
    // 只重写了抽象属性,没有重写抽象方法calculateArea()
    override val name: String = "正方形"
    // 因此Square类也必须是抽象的
}
  1. 可以继承其他类:抽象类可以继承自另一个非抽象类或抽象类,进一步扩展和定制自己的行为和属性。例如:

open class Animal {
    open fun makeSound() {
        println("动物发出声音")
    }
}

abstract class Dog : Animal() {
    override abstract fun makeSound() // 重写并声明为抽象方法,子类必须实现
}

class Puppy : Dog() {
    override fun makeSound() {
        println("小狗汪汪叫")
    }
}

在这个例子中,Dog抽象类继承自Animal类,并重写了makeSound()方法并声明为抽象方法,Puppy类再继承Dog类并实现了makeSound()方法 。

三、Kotlin 接口详解

(一)接口定义

在 Kotlin 中,接口是一种强大的抽象机制,它使用interface关键字来定义。与抽象类不同,接口主要用于定义一组方法的签名,这些方法可以是抽象的,也可以有默认实现 。接口无法存储状态,它就像是一份 “行为契约”,规定了实现它的类应该具备哪些行为 。接口可以包含抽象方法声明和方法实现 ,接口中的属性默认是抽象的,或必须提供getter实现 。例如:


interface MyInterface {
    // 抽象方法,没有方法体,实现接口的类必须实现这个方法
    fun abstractMethod()

    // 带默认实现的方法,实现接口的类可以选择重写这个方法,也可以使用默认实现
    fun methodWithDefaultImplementation() {
        println("这是一个带有默认实现的方法")
    }
}

(二)接口示例

Vehicle接口为例,展示接口中抽象方法和带默认实现方法的定义:


interface Vehicle {
    // 抽象方法,启动车辆,必须在实现接口的类中实现
    fun start()

    // 抽象方法,停止车辆,必须在实现接口的类中实现
    fun stop()

    // 带默认实现的方法,车辆鸣笛,实现接口的类可以选择重写,也可以使用默认实现
    fun honk() {
        println("嘟嘟!")
    }
}

在这个Vehicle接口中,start()stop()是抽象方法,因为不同类型的车辆启动和停止的方式可能不同,需要具体的实现类去实现;而honk()是一个带默认实现的方法,默认情况下车辆鸣笛输出 “嘟嘟!”,如果有特殊的鸣笛需求,实现类也可以重写这个方法 。

(三)实现接口

一个类或对象可以实现一个或多个接口。当一个类实现接口时,它必须实现接口中所有的抽象方法(除非这个类本身也是抽象类) 。以Car类实现Vehicle接口为例:


class Car : Vehicle {
    override fun start() {
        println("汽车启动")
    }

    override fun stop() {
        println("汽车停止")
    }

    // 这里没有重写honk()方法,所以会使用接口中honk()的默认实现
}

Car类中,通过override关键字重写了Vehicle接口中的start()stop()抽象方法,来实现汽车的启动和停止逻辑 。而对于honk()方法,由于没有重写,所以当调用Car对象的honk()方法时,会执行接口中honk()的默认实现,输出 “嘟嘟!” 。

(四)接口继承与解决覆盖冲突

接口可以继承其他接口,通过继承,接口可以扩展和增强自身的功能 。例如:


interface Moveable {
    fun move()
}

interface Flyable : Moveable {
    fun fly()
}

在这个例子中,Flyable接口继承了Moveable接口,这意味着实现Flyable接口的类不仅要实现fly()方法,还要实现Moveable接口中的move()方法 。

当一个类实现多个接口时,如果这些接口中定义了相同签名的方法,就会出现方法覆盖冲突 。例如:


interface A {
    fun foo() {
        println("A中的foo方法")
    }
}

interface B {
    fun foo() {
        println("B中的foo方法")
    }
}

class C : A, B {
    // 必须重写foo()方法来解决冲突
    override fun foo() {
        // 调用A接口中的foo()方法
        super<A>.foo()
        // 调用B接口中的foo()方法
        super<B>.foo()
        println("C中重写的foo方法")
    }
}

在上述代码中,C类实现了AB两个接口,而这两个接口都定义了foo()方法,所以在C类中必须重写foo()方法 。在重写的foo()方法中,通过super<A>.foo()super<B>.foo()分别调用了AB接口中的foo()方法,并添加了自己的逻辑 。这样就解决了方法覆盖冲突的问题 。

四、抽象类与接口的区别

通过前面的介绍,我们对 Kotlin 中的抽象类和接口都有了一定的了解 。接下来,我们来详细对比一下它们之间的区别,以便在实际开发中能够更准确地选择使用。

(一)构造函数

抽象类可以有构造函数,包括主构造函数和次构造函数,用于初始化抽象类中的属性和状态 。例如,我们在Shape抽象类中添加一个主构造函数:


abstract class Shape(val color: String) {
    abstract val name: String
    abstract fun calculateArea(): Double

    fun printName() {
        println("形状名称: $name")
    }
}

在这个例子中,Shape抽象类有一个主构造函数,接受一个color参数,用于表示图形的颜色 。

而接口不能有构造函数 。这是因为接口主要用于定义行为,不存储状态,所以不需要构造函数来初始化 。不过,从 Kotlin 1.9 + 开始,虽然支持接口中定义带默认实现的属性,但仍然不能有构造函数 。

(二)多重继承

在 Kotlin 中,一个类只能继承一个抽象类,即抽象类是单继承的 。这是为了避免多重继承带来的复杂性和冲突 。例如:


abstract class Animal {
    open fun makeSound() {
        println("动物发出声音")
    }
}

abstract class Dog : Animal() {
    override abstract fun makeSound()
}

这里Dog抽象类继承自Animal抽象类,一个类不能同时继承多个抽象类 。

而接口则不同,一个类可以实现多个接口,通过实现多个接口,一个类可以拥有多个不同的行为集合 。例如:


interface Flyable {
    fun fly()
}

interface Runable {
    fun run()
}

class Bird : Flyable, Runable {
    override fun fly() {
        println("鸟儿飞翔")
    }

    override fun run() {
        println("鸟儿奔跑")
    }
}

在这个例子中,Bird类实现了FlyableRunable两个接口,从而具备了飞翔和奔跑的行为 。

(三)属性

抽象类可以包含非抽象属性,这些属性可以有初始值,也可以在构造函数中初始化 。例如,我们在Shape抽象类中添加一个非抽象属性borderWidth


abstract class Shape(val color: String) {
    abstract val name: String
    abstract fun calculateArea(): Double

    val borderWidth: Int = 1

    fun printName() {
        println("形状名称: $name")
    }
}

在这个例子中,borderWidth是一个非抽象属性,它有初始值1

接口中的属性默认是抽象的,必须在实现接口的类中重写并提供具体实现,除非该属性提供了getter的默认实现 。例如:


interface ShapeInterface {
    val name: String
    val borderWidth: Int
        get() = 1
}

class Rectangle : ShapeInterface {
    override val name: String = "矩形"
    override val borderWidth: Int
        get() = super.borderWidth
}

在这个例子中,ShapeInterface接口中的name属性是抽象的,没有默认实现,必须在实现类Rectangle中重写;而borderWidth属性提供了getter的默认实现,在Rectangle类中如果不需要修改其行为,可以直接使用默认实现 。

(四)方法实现

抽象类可以包含非抽象方法的实现,子类可以继承这些方法,也可以根据需要重写它们 。例如,我们在Shape抽象类中的printName()方法就是一个非抽象方法,有具体的实现 。

接口中的方法默认是抽象的,没有方法体,必须在实现接口的类中实现 。不过,从 Kotlin 1.4 + 开始,接口支持方法的默认实现 。例如,我们在Vehicle接口中添加一个带默认实现的方法startEngine()


interface Vehicle {
    fun start()
    fun stop()

    fun honk() {
        println("嘟嘟!")
    }

    fun startEngine() {
        println("发动机启动")
    }
}

在这个例子中,start()stop()方法是抽象的,必须在实现类中实现;而honk()startEngine()方法是带默认实现的方法,实现类可以选择重写这些方法,也可以使用默认实现 。

(五)访问修饰符

抽象类可以使用privateprotectedpublic等访问修饰符来控制成员的访问权限 。private修饰的成员只能在抽象类内部访问,protected修饰的成员可以在抽象类及其子类中访问,public修饰的成员可以在任何地方访问 。例如:


abstract class Shape {
    private val privateProperty: String = "私有属性"
    protected val protectedProperty: String = "受保护属性"
    val publicProperty: String = "公共属性"

    private fun privateMethod() {
        println("这是一个私有方法")
    }

    protected fun protectedMethod() {
        println("这是一个受保护方法")
    }

    fun publicMethod() {
        println("这是一个公共方法")
    }
}

在这个例子中,privatePropertyprivateMethod()是私有的,只能在Shape抽象类内部访问;protectedPropertyprotectedMethod()是受保护的,可以在Shape抽象类及其子类中访问;publicPropertypublicMethod()是公共的,可以在任何地方访问 。

接口成员默认是public的,不能有private修饰符 。这是因为接口的主要目的是定义一组可供其他类实现的行为,这些行为通常是对外公开的 。例如:


interface MyInterface {
    fun method1()
    fun method2()
}

在这个MyInterface接口中,method1()method2()方法默认都是public的,不能声明为private

五、使用建议与场景

(一)抽象类使用场景

当你需要定义一个通用的基类,并且这个基类包含一些通用的属性、方法以及构造函数时,抽象类是一个很好的选择 。例如,在 Android 开发中,我们经常会创建一个BaseActivity抽象类,它包含了一些所有 Activity 都通用的逻辑,如设置布局、初始化视图、加载数据等:


import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

abstract class BaseActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getLayoutId())
        initViews()
        initData()
    }

    // 抽象方法,由子类实现,返回布局ID
    abstract fun getLayoutId(): Int

    // 抽象方法,由子类实现,初始化视图
    abstract fun initViews()

    // 抽象方法,由子类实现,加载数据
    abstract fun initData()
}

然后,具体的 Activity 类可以继承自BaseActivity,并实现其中的抽象方法 。例如:


class MainActivity : BaseActivity() {
    override fun getLayoutId(): Int {
        return R.layout.activity_main
    }

    override fun initViews() {
        // 初始化视图的具体逻辑
    }

    override fun initData() {
        // 加载数据的具体逻辑
    }
}

通过这种方式,我们可以将通用的逻辑提取到BaseActivity抽象类中,减少代码重复,提高代码的可维护性和可扩展性 。

(二)接口使用场景

当你只需要定义一组抽象的行为,并且希望一个类可以实现多个这样的行为时,接口是更合适的选择 。例如,在一个图形绘制库中,我们可以定义多个接口来表示不同的功能:


// 定义一个可绘制的接口
interface Drawable {
    fun draw()
}

// 定义一个可点击的接口
interface Clickable {
    fun onClick()
}

// 定义一个可拖动的接口
interface Draggable {
    fun drag()
    fun drop()
}

然后,一个类可以实现多个接口,以具备多种行为 。比如一个Button类可以同时实现DrawableClickableDraggable接口:


class Button : Drawable, Clickable, Draggable {
    override fun draw() {
        println("绘制按钮")
    }

    override fun onClick() {
        println("按钮被点击")
    }

    override fun drag() {
        println("按钮被拖动")
    }

    override fun drop() {
        println("按钮被放下")
    }
}

通过接口,我们可以让不同的类灵活地组合不同的行为,而不受单继承的限制,使代码更加灵活和可复用 。

六、总结回顾

通过今天的学习,我们深入了解了 Kotlin 中抽象类和接口这两个重要的概念 。抽象类像是一个未完成的蓝图,不能被直接实例化,它为子类提供通用的属性和方法定义,子类继承抽象类时必须重写所有抽象成员 。而接口则是一份行为契约,定义了一组方法签名,一个类可以实现多个接口,以获得多种行为 。

它们在构造函数、多重继承、属性、方法实现以及访问修饰符等方面都存在明显的区别 。在实际的 Kotlin 开发中,我们要根据具体的需求来选择使用抽象类还是接口 。如果需要定义一个通用的基类,并且这个基类包含构造函数、非抽象属性和方法,那么抽象类是合适的选择 ;如果只是需要定义一组抽象的行为,并且希望一个类可以实现多个这样的行为,接口则更为合适 。 希望大家在今后的 Kotlin 编程中,能够熟练运用抽象类和接口,构建出更加健壮、灵活和可维护的代码 。如果对今天的内容还有任何疑问,欢迎在评论区留言交流 。

郎酒股份投资11亿元设立3家新公司

36氪获悉,爱企查App显示,近日,泸州龙马郎销售有限公司、古蔺红花郎销售有限公司、古蔺郎酒电子商务有限公司成立,法定代表人分别为梅刚、李俊、梅刚,注册资本分别为5亿元人民币、5亿元人民币、1亿元人民币,经营范围包括酒类经营、食品销售、食品互联网销售等。股东信息显示,上述三家公司均由四川郎酒股份有限公司全资持股。

Angular学习笔记24:Angular 响应式表单 FormArray 与 FormGroup 相互嵌套

Angular 响应式表单 FormArray 与 FormGroup 相互嵌套

在类文件中(组件的TS文件):
声明一个Form表单:
  

 public validateForm: FormGroup;


在构造方法中:
  

  private fb: FormBuild;


    声明一个FormBuild的对象

在构造方法中:
  

constructor(private fb: FormBuilder) {
     this.validateForm = this.fb.group({
        name: [null],
        sex : [null],
        age : [null],
        address: this.fb.array([
            new FormGroup({
                street : new FormControl(null),
                country: new FormControl(null),
            }),
        ]),
    });
  }

这样在组件中就构造出来了一个嵌套了FormArray的FormGroup,
这个时候,需要将validateForm这个表单中address的属性实例成一个FormArray

使用Angular中的get 方法

  

 get addressFormArray(){
        return this.validateForm.controls['address'] as FormArray;
    }

这个时候,在组件中就会生成一个变量:addressFormArray;
    当想对表单中的address中的控件进行操作,可以直接对变量:addressFormArray进行操作;

1.对validateForm中的address增加一对新的 street 和 country 有两种方法:
    a.使用变量addressFormArray,具体如下:
      

  this.addressFormArray.push(
            new FormGroup({
                street : new FormControl(null),
                country: new FormControl(null),
            }),
        )


    b.直接对validateForm进行操作
      

 (this.validateForm.controls['address'] as FormArray).push(
            new FormGroup({
                street : new FormControl(null),
                country: new FormControl(null),
            })
        )

对validateForm的增加,可以放在一个事件的方法里

2.去掉validateForm中的address对某一对属性的控制,
    正常情况下,是可以知道在当前删除的是 street 和 country在address这个数组中的下标,从而可以快速准确的删除,同样,删除也是可以有两种方式:
        a.使用变量addressFormArray,具体如下:
          

  this.addressFormArray.removAt(需要删除元素的下标)


        b.直接对validateForm进行操作,具体如下:
          

 (this.validateForm.controls['address'] as FormArray).removeAt(需要删除的数组的下标)


3.在模版文件中如何显示
    

<form [FormGroup]="validateForm">
        <div> 
            ... 
            <!-- 关于直接在FormGroup的部分省略 -->
        </div>
    <div FormArrayName="adderss">
        <div *ngFor="let address of validateForm.controls['address'].controls;let i = index">
            <div [formGroupName]="i">
                <div>
                    在这里就可以自己定义address 中FormGroup的内容了,增加关于FormGroup的控件。
                </div>
            </div>
            
        </div>
    </div>

 

Angular6学习笔记13:HTTP(3)

HTTP

继学习笔记12以后,可以模拟向后端发送get/post/put/delete请求了。在项目中,有一个table,这个table的数据非常多,就好比现在的heroList,需要根据用户输入的信息发送给远端服务器,让远端服务器通过这个信息,返回搜索结果。现在要检索heroList中的信息,就需要一个输入框,让用户输入检索的值,然后将这个值发送给远端服务器(模拟),然后让远端服务器(模拟)返回检索的结果。

1.在heroService中创建一个关于搜索的方法:searchHeroes()

searchHeroes(term: string): Observable<Hero[]> {
  if (!term.trim()) {
    return of([]);
  }
  return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
    tap(_ => this.log(`found heroes matching "${term}"`)),
    catchError(this.handleError<Hero[]>('searchHeroes', []))
  );
}

在这个方法中,当没有搜素词,则返回一个空的数组,当有搜索词的时候,在url中拼接上name

2.在仪表盘的组件中添加搜索功能

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
      routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>

<app-hero-search></app-hero-search>

这里会让这个应用挂了,因为找不到<app-hero-search></app-hero-search>(接下来创建)

3.创建HeroSearchComponent

利用angular CLI 创建组件

ng generate component hero-search

CLI 生成了 HeroSearchComponent 的三个文件,并把该组件添加到了 AppModule 的声明中。

修改HeroSearch组件的模版文件:

<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <ul class="search-result">
    <li *ngFor="let hero of heroes$ | async" >
      <a routerLink="/detail/{{hero.id}}">
        {{hero.name}}
      </a>
    </li>
  </ul>
</div>

在模版文件中,创建了keyup 事件,这个keyup事件绑定会调用该组件的 search() 方法,并传入新的搜索框的值。

*ngFor 是在一个名叫 heroes$ 的列表上迭代,在这里$ 是一个命名惯例,用来表明 heroes$ 是一个 Observable,而不是数组。

正常情况下,*ngFor是不能直接使用Observable,此时,就要用到管道,利用管道字符(|),后面紧跟着一个 async,它表示 Angular 的 AsyncPipeAsyncPipe 会自动订阅到 Observable,这样你就不用再在组件类中订阅了。

美化这个HeroSearch组件,修改heroSearch的CSS文件

.search-result li {
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
  list-style-type: none;
}

.search-result li:hover {
  background-color: #607D8B;
}

.search-result li a {
  color: #888;
  display: block;
  text-decoration: none;
}

.search-result li a:hover {
  color: white;
}
.search-result li a:active {
  color: white;
}
#search-box {
  width: 200px;
  height: 20px;
}


ul.search-result {
  margin-top: 0;
  padding-left: 0;
}

修改HeroSearch 的类文件

import { Component, OnInit } from '@angular/core';

import { Observable, Subject } from 'rxjs';

import {
   debounceTime, distinctUntilChanged, switchMap
 } from 'rxjs/operators';

import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
  heroes$: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(private heroService: HeroService) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes$ = this.searchTerms.pipe(
      // wait 300ms after each keystroke before considering the term
      debounceTime(300),

      // ignore new term if same as previous term
      distinctUntilChanged(),

      // switch to new search observable each time the term changes
      switchMap((term: string) => this.heroService.searchHeroes(term)),
    );
  }
}

注意,heroes$ 声明为一个 Observable

a.RxJS Subject 类型的 searchTerms

Subject 既是可观察对象的数据源,本身也是 Observable。可以像订阅任何 Observable 一样订阅 Subject。还可以通过调用它的 next(value) 方法往 Observable 中推送一些值,就像 search() 方法中一样。search() 是通过对文本框的 keystroke 事件的事件绑定来调用的。

private searchTerms = new Subject<string>();

search(term: string): void {
  this.searchTerms.next(term);
}

每当用户在文本框中输入时,这个事件绑定就会使用文本框的值(搜索词)调用 search() 函数。 searchTerms 变成了一个能发出搜索词的稳定的流。

b.串联 RxJS 操作符

每当用户击键后就直接调用 searchHeroes() 将导致创建海量的 HTTP 请求,浪费服务器资源并消耗大量网络流量。

应该怎么做呢?ngOnInit() 往 searchTerms 这个可观察对象的处理管道中加入了一系列 RxJS 操作符,用以缩减对 searchHeroes() 的调用次数,并最终返回一个可及时给出英雄搜索结果的可观察对象(每次都是 Hero[] )。

this.heroes$ = this.searchTerms.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((term: string) => this.heroService.searchHeroes(term)),
);
  • 在传出最终字符串之前,debounceTime(300) 将会等待,直到新增字符串的事件暂停了 300 毫秒。 你实际发起请求的间隔永远不会小于 300ms。

  • distinctUntilChanged() 会确保只在过滤条件变化时才发送请求。

  • switchMap() 会为每个从 debounce 和 distinctUntilChanged 中通过的搜索词调用搜索服务。 它会取消并丢弃以前的搜索可观察对象,只保留最近的。

借助 switchMap 操作符, 每个有效的击键事件都会触发一次 HttpClient.get() 方法调用。 即使在每个请求之间都有至少 300ms 的间隔,仍然可能会同时存在多个尚未返回的 HTTP 请求。

switchMap() 会记住原始的请求顺序,只会返回最近一次 HTTP 方法调用的结果。 以前的那些请求都会被取消和舍弃。

注意,取消前一个 searchHeroes() 可观察对象并不会中止尚未完成的 HTTP 请求。 那些不想要的结果只会在它们抵达应用代码之前被舍弃。

 

 

 

 

❌