React使用有限状态机的实现示例

目录
  • 什么是有限状态机?
    • 有限状态机的示例
  • 有限状态机和软件开发、计算机科学有什么关系?
  • 举了那么多例子,但我该怎么展示在前端开发中使用状态机呢?
  • React 应用程序中的注册表单的传统实现方式
  • 还有其他风险
  • 把表单重构为有限状态机
    • 状态
    • 事件
  • 使用 xstate 将有限状态机可视化
  • 通过 9 个步骤完成重构
    • 步骤1: 为上下文添加类型定义
    • 步骤2: 添加将状态映射到组件的函数
    • 步骤3: 将上下文添加到有限状态机的定义中
    • 步骤4: 定义一个将改变上下文的函数
    • 步骤5: 将这个函数添加到有限状态机的 actions 中
    • 步骤6: 将由 previous 和 next 事件触发这个操作
    • 步骤7: 向组件添加 useMachine Hook
    • 步骤8: 通过 onPrevious 和 onNext 函数将事件发送到有限状态机
    • 步骤9: 渲染当前状态对应的组件
  • 有限状态机的安全性,使得我们的表单同样安全
  • 在 React 中使用有限状态机的概括

在 React 中使用有限状态机,似乎不是一个寻常的话题。因为有限状态机通常和前端关系不大。 但是最近我发现了一个非常棒的技巧,可以在复杂的 React 项目中发挥有限状态机的作用。可以很好的提高程序的安全性。 下面我们就来看看吧。

什么是有限状态机?

有限状态机,英文是 Finite State Machine,简称 FSM,有时候也被称为有限状态自动机(Finite State AutoMation)。它是一种描述系统行为的数学计算模型,也就是一种抽象机。它可以替代图灵机等其他类型的模型。 有限状态机由给定对象的所有可能状态以及它们之间的转换组成。和图灵机相比,它的计算能力较低。这种计算能力的区别意味着 FSM 的计算能力更加有限,因为它具有有限数量的状态,不如的话就是无限状态机了。 更重要的是:状态机的一条规则是:它在任何时候都只处于一种状态。 由于有限状态机会根据适当的组合或预定的时间顺序产生某些动作,因此我们可以在现代社会的任何地方找到有限状态机的影子。这些包括自动售货机、电梯,甚至是红绿灯。

有限状态机的示例

一个现实中完美匹配有限状态机的例子是红绿灯。我们来分析一下红绿灯的工作方式: 它有四种状态:

  • 停车-红灯。
  • 准备开车-红灯和黄灯。
  • 开车-绿灯。
  • 准备停车-黄灯。

它有四种状态的转换:

  • 停车->准备开车。
  • 准备开车->开车
  • 开车->准备停车。
  • 准备停车->停车。

我们可以看到,我们有有限数量的状态和状态的转换。另外,红绿灯在任何时候都只能处于一种状态。这意味着我们在这处理的是有限状态机。 更重要的是,通过实现有限状态机,我们可以保证,模型不会发生意外。以红绿灯为例,红绿灯绝对不会出现直接从绿灯转换成红灯的情况。

有限状态机和软件开发、计算机科学有什么关系?

其实有很多关系。特别是游戏开发,很多游戏中都会大量使用有限状态机。 举个例子,大家应该都玩过超级马里奥这款 2D 游戏。马里奥在游戏里可以做什么呢? 他可以: 静止、朝右走、朝左走、跳跃。 从代码的角度来看,它对应的就是摇杆事件。

  • 什么都不按-默认设置静止状态。
  • 按左键-触发设置朝左走状态的朝左走事件。
  • 按右键-触发设置朝右走状态的朝右走事件。
  • 按跳跃键-触发设置跳跃状态的跳跃事件。
  • 松开按键-触发设置静止状态的静止事件。

举了那么多例子,但我该怎么展示在前端开发中使用状态机呢?

无论是从上面的概念还是具体的场景,我都是想保证你对有限状态机有一个了解。 接下来我来讲讲有限状态机在前端的应用场景。 首先我得承认,在前端开发中有限状态机并不是那么常见。我认为这个现象的主要原因是因为它不是实现功能最简单也不是最快的方法。 这有点像 TypeScript,它会让你慢一点,它会带来一些复杂性。但最终每个人都会从中受益。 为了证明我的这种观点并非毫无根据,我会展示一个我曾经开发的 React 项目中使用有限状态机的示例。 这是一个很简单的注册表单,分为三个部分。每个部分都会根据当前填写的进度进行渲染。

React 应用程序中的注册表单的传统实现方式

我先快速演示一下我实现上述表单功能的方法。 首先,我要定义所有的组件以及初始状态。

const Step = {
  Company: 0,
  Director: 1,
  Contact: 2,
} as const;

const Views = [<CompanyDataFormPart />, <DirectorDataFormPart />, <ContactDataFormPart />];

const initialStep = Step.Account

接下来我们定义状态:

const [currentStep, setCurrentStep] = useState<number>(initialStep)

最后是组件本身:

<>
  <div className="stepsContainer">
    <Steps current={currentStep} labelPlacement="vertical" size="small">
      {Object.keys(Step).map(s => (
        <Steps.Step title={s} />
      ))}
    </Steps>
  </div>

  <Spacer />

  <FormPart
    onPrevious={() => {
      setCurrentStep(prev => prev - 1);
    }}
    onNext={() => {
      setCurrentStep(prev => prev + 1);
    }}
      >
    {Views[currentStep]}
  </FormPart>
</>

这一切看上去似乎很正常,表单可以切换到下一个和上一个步骤。但这里存在一个很明显的错误。 那就是程序没有考虑边界问题。这意味着 currentStep 的值可能超过最大步骤,也就是 2,也可能低于 0。 如果要修复它,我们会写出下列代码:

onPrevious={() => {
  setCurrentStep(prev => Math.max(prev - 1, 0))
}}
onNext={()=>{
  setCurrentStep(prev => Math.min(prev + 1, Views.length - 1))
}}

还有其他风险

这个代码运行起来确实没有问题,但还是会有一些潜在风险。 在软件开发中,很少会出现你一个人负责整个项目的情况,一般来说是一整个团队在协作,这也就意味着有许多其他开发人员会检查你的代码,并且会视图理解它并可能会修改它。 我们假设有个人在表单顶部写了一个方法,直接跳到了第三步。

const functionWithBadLogic = () => {
  setCurrentStep(3);
}

这是一个很好的反面教材,第三步在我们的表单中压根就不存在。 另外一个例子是下面这样的:

onNext={() => {
  setCurrentStep(prev => Math.min(prev + 2, Views.length -1))
}}

在这个代码中有什么问题吗?如果给定顺序中需要所有的步骤,为什么会有人跳过一个步骤? 这是最后一个例子:

const Step = {
  Company: 0,
  Director: 1,
  Contact: 2,
} as const;

const Views = [
  <CompanyDataFormPart />,
  <DirectorDataFormPart />,
  <ContactDataFormPart />,
  <div>I should not be there!</div>
]

这些错误中的任何一个投入生产都可能会出现下面这种情况:

这似乎看上去不是什么大问题

也许确实不是什么大问题。但是你要知道,我的例子是很简单的一个流程。在真实的项目中,会有更大更复杂的恶项目,例如用于银行和汇款的金融类程序,用于审批和工作流的办公类程序。 如果我们在一个地方定义所有可能出现的状态和状态之间的转换,会不会更容易?类似于某种约定,我们可以很容易的查看其中的整个逻辑,并且确保不会发生其他任何约定之外的事情。 实现这种模式的那个东西就是有限状态机。

把表单重构为有限状态机

首先,我们先来只关注 onNext 和 onPrevious 函数。我们想要制造一台机器,我们用下面的状态和事件来描述它的行为,也就是为这台机器的特性设计一个模型。

状态

  • company
  • director
  • contact

事件

  • next:按顺序切换到下一个状态。
  • prev:按顺序切换到上一个状态。

实现起来像下面这样:

const formMachine = createMachine({
  id: 'formState',
  initial: 'company',
  states: {
    company: {
      on: {
        next: { target: 'director' }
      }
    },
    director: {
      on: {
        previous: { target: 'company' },
        next: { target: 'contact' },
      },
    },
    contact: {
      on: {
        previous: { target: 'director' },
      },
    },
  }
})

现在让我们来分析一下这段代码。 createMachine 方法接受由 id、initial 和 states 共同组成的对象,这三个字端的作用分别是:

  • id:唯一标识符。
  • initial:初始状态
  • states:所有状态,其中的键是状态的名称,值是描述状态的对象。

接下来我们再分析一下 director 这个状态:

  • 它有一个名字,叫做 director。
  • 它可以对两个事件做出反应:previous 事件,将状态转换为 company。next 事件,将状态设置为 contacct。

使用 xstate 将有限状态机可视化

感谢 xstate 的开发人员,我们可以将上面的代码粘贴到 xstate 的在线可视化编辑器中。这个工具可以展示出有限状态机的所有可能的状态和事件。 我们的状态机是这个样子:

我承认为了实现这样一个简单的小功能编写这么多代码似乎有些过度设计,但是我们继续往下看,我保证你会相信使用有限状态机是值得的。

通过 9 个步骤完成重构

我们实现了有限状态机,但是我们还没有做最重要的事情。我们必须重构渲染逻辑。 接下来我要为有限状态机实现一些上下文。

步骤1: 为上下文添加类型定义

type Context = {
  currentView: ReactNode;
}

步骤2: 添加将状态映射到组件的函数

const mapStateToComponent: Record<string, ReactNode> = {
  company: <CompanyDataFormPart />,
  director: <DirectorDataFormPart />,
  contact: <ContactDataFormPart />,
}

步骤3: 将上下文添加到有限状态机的定义中

context: {
  currentView: <CompanyDataFormPart />,
}

步骤4: 定义一个将改变上下文的函数

const changeComponent = assign<Context>({
  currentViewe: (context, event, { action }) => {
    return mapStateToComponent[action.payload as string];
  }
})

步骤5: 将这个函数添加到有限状态机的 actions 中

{
  actions: {
    changeComponent,
  }
}

步骤6: 将由 previous 和 next 事件触发这个操作

{
  director: {
    on: {
      previous: {
        target: 'company',
        actions: {
          type: 'changeComponent',
          payload: 'company'
        }
      },
      next: {
        target: 'contact',
          actions: {
            type: 'changeComponent',
            payload: 'contact'
          }
        }
    }
  }
}

步骤7: 向组件添加 useMachine Hook

const [current, send] = useMachine(formMachine)

步骤8: 通过 onPrevious 和 onNext 函数将事件发送到有限状态机

onPrevious={() => {
  send('previous');
}}
onNext={() => {
  send('next');
}}

步骤9: 渲染当前状态对应的组件

{current.context.currentView}

我们马上就要完成了!

有限状态机的安全性,使得我们的表单同样安全

我们再回来看看之前举的最后一个反例。

const Step = {
  Company: 0,
  Director: 1,
  Contact: 2,
} as const;

const Views = [
  <CompanyDataFormPart />,
  <DirectorDataFormPart />,
  <ContactDataFormPart />,
  <div>I should not be there!</div>
]

可以看到,Step 和 Views 是解耦的。我们通过逐步渲染分页进度面板的值来进行映射,并且使用当前索引来渲染 Views 数组中的元素。 在我们的有限状态机中如何用更好的方式来实现这一点? 我们首先来稍微改变一下上下文。

export type View = {
  Component: ReactNode;
  step: number;
}

export type Context = {
  currentView: View;
}

接下来修改一下 mapStateToComponent 这个函数,顺便把函数名也改掉。

const mapStateToView: Record<string, View> = {
  company: {
    Component: <CompanyDataFormPart />,
    step: 0,
  },
  director: {
    Component: <DirectorDataFormPart />,
    step: 1,
  },
  contact: {
    Component: <ContactDataFormPart />,
    step: 2,
  },
};

最后为我们的有限状态机添加一些类型,将类型和 actions 移到不同的文件里。 现在我们的代码像下面这样: formMachine.types.ts

import { ReactNode } from 'react';
import { StateNode } from 'xstate';

export type Event = { type: 'NEXT' } | { type: 'PREVIOUS' };

export type View = {
  Component: ReactNode;
  step: number;
};

export type Context = {
  currentView: View;
};

export type State = {
  states: {
    company: StateNode;
    director: StateNode;
    contact: StateNode;
  };
};

formMachine.actions.ts

import { assign } from 'xstate';

import { CompanyDataFormPart } from '../components/CompanyDataFormPart/CompanyDataFormPart';
import { ContactDataFormPart } from '../components/ContactDataFormPart/ContactDataFormPart';
import { DirectorDataFormPart } from '../components/DirectorDataFormPartt/ContactDataFormPart';

import { Context, View } from './formMachine.types';

export const mapNameToView: Record<string, View> = {
  company: {
    Component: <CompanyDataFormPart />,
    step: 0,
  },
  director: {
    Component: <DirectorDataFormPart />,
    step: 1,
  },
  contact: {
    Component: <ContactDataFormPart />,
    step: 2,
  },
};

export const changeView = assign<Context, Event>({
  currentView: (_context, _event, { action }) => {
    if (typeof action.payload !== 'string') {
      throw new Error('Action payload should be string');
    }

    return mapNameToView[action.payload];
  },
});

formMachine.ts

import { MachineConfig, MachineOptions, createMachine } from 'xstate';

import { mapNameToView, changeView } from './formMachine.actions';
import { State, Context } from './formMachine.types';

const initialStateName = 'company';

const formMachineConfig: MachineConfig<Context, State, Event> = {
  id: 'formState',
  initial: initialStateName,
  context: {
    currentView: mapNameToView[initialStateName],
  },
  states: {
    company: {
      on: {
        NEXT: { target: 'director', actions: { type: 'changeView', payload: 'director' } },
      },
    },
    director: {
      on: {
        PREVIOUS: { target: 'company', actions: { type: 'changeView', payload: 'company' } },
        NEXT: { target: 'contact', actions: { type: 'changeView', payload: 'contact' } },
      },
    },
    contact: {
      on: {
        PREVIOUS: { target: 'director', actions: { type: 'changeView', payload: 'director' } },
      },
    },
  },
};

const formMachineOptions: Partial<MachineOptions<Context, Event>> = {
  actions: { changeView },
};

export const formMachine = createMachine(formMachineConfig, formMachineOptions);

export const formMachineStates = Object.keys(formMachine.states);

App.tsx

import React from 'react';
import { useMachine } from '@xstate/react';
import Steps from 'rc-steps';

import { FormPart } from './components/FormPart/FormPart';
import { Spacer } from './components/Spacer/Spacer';
import { formMachine, formMachineStates } from './formMachine/formMachine';

function App() {
  const [current, send] = useMachine(formMachine);

  return (
    <div className="app">
      <div className="stepsContainer">
        <Steps current={current.context.currentView.step} labelPlacement="vertical" size="small">
          {formMachineStates.map(s => (
            <Steps.Step title={s} key={s} />
          ))}
        </Steps>
      </div>

      <Spacer />

      <FormPart
        onPrevious={() => {
          send('PREVIOUS');
        }}
        onNext={() => {
          send('NEXT');
        }}
      >
        {current.context.currentView.Component}
      </FormPart>
    </div>
  );
}

在 React 中使用有限状态机的概括

可能你会说,“它看起来仍然很复杂。”。但是请你记住,如果你正在为一家大公司研发一个非常重要的项目,那其中一点微小的错误都可能会导致非常严重的资金损失。 最后我总结一下使用有限状态机的几个优势:

  • 类型安全。我们永远都不会使用在类型定义中的状态以外的状态,否则会编译错误。
  • 不会有错误的状态和错误的转换。如果不改变有限状态机的定义,那么不可能有人能够做到从第1步直接跳转到第3步。
  • 所有的逻辑都在一个位置进行描述。

到此这篇关于React使用有限状态机的实现示例的文章就介绍到这了,更多相关React 有限状态机内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈React之状态(State)

    在React当中,当你更新组件的state,然后新的state就会重新渲染到页面中.在这个时候不需要你操作任何DOM.你也可以认为组件在React当中是一个状态机(State Machines).当用户进行操作时会实现不同的状态,然后再渲染到你的页面中,让你的页面与数据始终保持一致. 如何定义State 定义一个合适的State,是正确创建组件的第一步.State必须能代表一个组件UI呈现的完整状态集,即组件的任何UI改变,都可以从State的变化中反映出来:同时,State还必须是代表一个组件

  • react-native android状态栏的实现

    react-native 开发App的时候难免会遇到状态栏的,背景颜色和字体颜色与App内容页面,色调适配,间言之就是将状态栏颜色与App颜色一致,使用户界面更加整体. 1.android设备系统元素 导航栏:就是设备顶部的网络.时间.电量等信息栏 ActionBar: 返回按钮以及系统默认的header区域,RN开发中一般不会用到,RN中在navigation中进行定制 导航栏: 设备下方的物理返回.回桌面.选择应用程序等系统导航栏 2.状态栏的呈现形式 默认展示,一直显示手机系统的状态栏 透

  • 关于React状态管理的三个规则总结

    目录 前言 No.1 一个关注点 No.2 提取复杂的状态逻辑 No.3 提取多个状态操作 总结 前言 React 组件内部的状态是在渲染过程之间保持不变的封装数据.useState() 是 React hook,负责管理功能组件内部的状态. 我喜欢 useState() ,它确实使状态处理变得非常容易.但是我经常遇到类似的问题: 我应该将组件的状态划分为小状态,还是保持复合状态? 如果状态管理变得复杂,我应该从组件中提取它吗?该怎么做? 如果 useState() 的用法是如此简单,那么什么时

  • 基于React Hooks的小型状态管理详解

    目录 实现基于 React Hooks 的状态共享 使用感受 本文主要介绍一种基于 React Hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式. 实现基于 React Hooks 的状态共享 React 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 Redux.MobX 等.这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度. 实际上很多时候,我不想定义 mutation 和

  • React全局状态管理的三种底层机制探究

    目录 前言 props context state 总结 前言 现代前端框架都是基于组件的方式来开发页面.按照逻辑关系把页面划分为不同的组件,分别开发不同的组件,然后把它们一层层组装起来,把根组件传入 ReactDOM.render 或者 vue 的 $mount 方法中,就会遍历整个组件树渲染成对应的 dom. 组件都支持传递一些参数来定制,也可以在内部保存一些交互状态,并且会在参数和状态变化以后自动的重新渲染对应部分的 dom. 虽然从逻辑上划分成了不同的组件,但它们都是同一个应用的不同部分

  • react18中react-redux状态管理的实现

    react的状态管理还是挺多的现在流行的有以下五种: Recoil MobX XState Redux Context 今天我们来讲一下react众多状态管理之一的redux,虽然这个我不太喜欢用,但是我觉得简单的状态管理谁都会,但是难的就是程序员的分水岭,所以今天来给大家讲一下redux. 下面我们来讲讲redux的优点: 可以在众多组件中传值,突破react单向数据流的限制 不仅支持react还支持vue等主流框架 当然是好用方便啦 接下来我们讲一下啥时候去使用他 在我们有好多组件,但是组件

  • 浅谈React 属性和状态的一些总结

    一.属性 1.第一种使用方法:键值对 <ClaaNameA name = "Tom" /> <ClaaNameA name = {Tom} /> <ClaaNameA name = {"Tom"} /> <ClaaNameA name = {[1,2,3]} />//数组 <ClaaNameA name = {FunctionNAme} /> //定义一个函数 2.第二种方法:三个点的展开对象形式 var

  • React使用有限状态机的实现示例

    目录 什么是有限状态机? 有限状态机的示例 有限状态机和软件开发.计算机科学有什么关系? 举了那么多例子,但我该怎么展示在前端开发中使用状态机呢? React 应用程序中的注册表单的传统实现方式 还有其他风险 把表单重构为有限状态机 状态 事件 使用 xstate 将有限状态机可视化 通过 9 个步骤完成重构 步骤1: 为上下文添加类型定义 步骤2: 添加将状态映射到组件的函数 步骤3: 将上下文添加到有限状态机的定义中 步骤4: 定义一个将改变上下文的函数 步骤5: 将这个函数添加到有限状态机

  • React+EggJs实现断点续传的示例代码

    技术栈 前端用了React,后端则是EggJs,都用了TypeScript编写. 断点续传实现原理 断点续传就是在上传一个文件的时候可以暂停掉上传中的文件,然后恢复上传时不需要重新上传整个文件. 该功能实现流程是先把上传的文件进行切割,然后把切割之后的文件块发送到服务端,发送完毕之后通知服务端组合文件块. 其中暂停上传功能就是前端取消掉文件块的上传请求,恢复上传则是把未上传的文件块重新上传.需要前后端配合完成. 前端实现 前端主要分为:切割文件.获取文件MD5值.上传切割后的文件块.合并文件.暂

  • react实现Radio组件的示例代码

    本文旨在用最清楚的结构去实现一些组件的基本功能.希望和大家一起学习,共同进步 效果展示: 测试组件: class Test extends Component { constructor(props) { super(props) this.state = { active:1 } } onGroupChange(value) { this.setState({ active: value }) } render() { return ( <div> <RadioGroup onChan

  • React中前端路由的示例代码

    目录 一. url是什么 二. 使用步骤 一. url是什么 访问不同url, 展示不同的组件 二. 使用步骤 安装React路由:命令行中执行npm install react-router-dom --save(注意此处的版本为npm install react-router-dom@4.3.1 --save) 两个js文件,分别为list.js和newButton.js,要实现访问localhost:3000/button的时候就显示button.js:访问localhost:3000/l

  • 30行代码实现React双向绑定hook的示例代码

    目录 使用Proxy代理数据 使用useRef创建同一份数据引用 添加更新handler 去除多次Proxy 添加缓存完善代码 总结 Sandbox 示例 Vue和MobX中的数据可响应给我们留下了深刻的印象,在React函数组件中我们也可以依赖hooks来实现一个简易好用的useReactive. 看一下我们的目标 const CountDemo = () => { const reactive = useReactive({ count: 0, }); return ( <div onCl

  • react redux及redux持久化示例详解

    目录 一.react-redux 二.redux持久化 一.react-redux react-redux依赖于redux工作. 运行安装命令:npm i react-redux: 使用: 将Provider套在入口组件处,并且将自己的store传进去: import FilmRouter from './FilmRouter/index' import {Provider} from 'react-redux' import store from './FilmRouter/views/red

  • 利用React实现虚拟列表的示例代码

    目录 列表项高度固定 代码实现 列表项高度动态 代码实现 思路说明 一些需要注意的问题 结尾 大家好,我是前端西瓜哥.这次我们来看看虚拟列表是什么玩意,并用 React 来实现两种虚拟列表组件. 虚拟列表,其实就是将一个原本需要全部列表项的渲染的长列表,改为只渲染可视区域内的列表项,但滚动效果还是要和渲染所有列表项的长列表一样. 虚拟列表解决的长列表渲染大量节点导致的性能问题: 一次性渲染大量节点,会占用大量 GPU 资源,导致卡顿: 即使渲染好了,大量的节点也持续占用内存.列表项下的节点越多,

  • react编写可编辑标题示例详解

    目录 需求 初始需求 方案设计 方案一 span + contentEditable 思路 代码如下 在这个方案中遇到的问题 存在的问题 方案二 直接用input处理展示和编辑 踩到的坑 需求 因为自己换工作到了新公司,上周入职,以前没有使用过react框架,虽然前面有学习过react,但是并没有实践经验 这个需求最终的效果是和石墨标题修改实现一样的效果 初始需求 文案支持可编辑 用户点击位置即光标定位处 超过50字读的时候,超出部分进行截断 当用户把所有内容删除时,失去焦点时文案设置为 “无文

  • React特征Form 单向数据流示例详解

    目录 引言 集中状态管理 双向数据流 那为何不选择双向数据流 小结 引言 今天将之前的内容做个系统整理,结合 React Form 案例, 来了解下为何React推荐单向数据流,如果采用双向管理,可能的问题 (关于React Form案例,可参考相关文章 - 学习React的特征(二) - React Form 集中状态管理 首先来看之前的React Form, 若采用单向数据流 import * as React from 'react'; const Useremail = props =>

  • react context优化四重奏教程示例

    目录 一.前言 二.用法 三.缺点 四.context优化 一重奏--使用PureComponent 二重奏--使用shouldComponentUpdate 三重奏--使用React.memo 四重奏--Provider再封装+props.children 总结 一.前言 我们在使用react的过程中,经常会遇到需要跨层级传递数据的情况.props传递数据应用在这种场景下会极度繁琐,且不利于维护,于是context应运而生 官方解释: Context 提供了一种在组件之间共享此类值的方式,而不

随机推荐