首页 > 其他分享 >Activiti使用(1)

Activiti使用(1)

时间:2023-11-04 18:56:04浏览次数:31  
标签:Activiti 流程 System 任务 实例 使用 println out

 

  1. 表的命名规范和作用

  观察创建的表,我们发现 Activiti 的表都以 act_ 开头,紧接着是表示表的用途的两个字母标识,也和 Activiti 所提供的服务的 API 对应:

  • ACT_RE:RE 表示 repository,这个前缀的表包含了流程定义和流程静态资源 (图片、规则、等等)

  • ACT_RU:RU 表示 runtime,这些表运行时,会包含流程实例、任务、变量、异步任务等流程业务进行中的数据。Activiti 只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。这样表就可以一直保持很小的体积,并且速度很快

  • ACT_HI:HI 表示 history,这些表包含一些历史数据,比如历史流程实例、变量、任务等等

  • ACT_GE:GE 表示 general,通用数据

表分类 表名 解释
一般数据    
  [ACT_GE_BYTEARRAY] 通用的流程定义和流程资源
  [ACT_GE_PROPERTY] 系统相关属性
流程历史记录    
  [ACT_HI_ACTINST] 历史的流程实例
  [ACT_HI_ATTACHMENT] 历史的流程附件
  [ACT_HI_COMMENT] 历史的说明性信息
  [ACT_HI_DETAIL] 历史的流程运行中的细节信息
  [ACT_HI_IDENTITYLINK] 历史的流程运行过程中用户关系
  [ACT_HI_PROCINST] 历史的流程实例
  [ACT_HI_TASKINST] 历史的任务实例
  [ACT_HI_VARINST] 历史的流程运行中的变量信息
流程定义表    
  [ACT_RE_DEPLOYMENT] 部署单元信息
  [ACT_RE_MODEL] 模型信息
  [ACT_RE_PROCDEF] 已部署的流程定义
运行实例表    
  [ACT_RU_EVENT_SUBSCR] 运行时事件
  [ACT_RU_EXECUTION] 运行时流程执行实例
  [ACT_RU_IDENTITYLINK] 运行时用户关系信息,存储任务节点与参与者的相关信息
  [ACT_RU_JOB] 运行时作业
  [ACT_RU_TASK] 运行时任务
  [ACT_RU_VARIABLE] 运行时变量表

 

  2. 常用服务接口

  • RepositoryService

    Activiti 的资源管理类,该服务负责部署流程定义,管理流程资源。在使用 Activiti 时,一开始需要先完成流程部署,即将使用建模工具设计的业务流程图通过 RepositoryService 进行部署

  • RuntimeService

    Activiti 的流程运行管理类,用于开始一个新的流程实例,获取关于流程执行的相关信息。流程定义用于确定一个流程中的结构和各个节点间行为,而流程实例则是对应的流程定义的一个执行,可以理解为 Java 中类和对象的关系

  • TaskService

    Activiti 的任务管理类,用于处理业务运行中的各种任务,例如查询分给用户或组的任务、创建新的任务、分配任务、确定和完成一个任务

  • HistoryService

    Activiti 的历史管理类,可以查询历史信息。执行流程时,引擎会保存很多数据,比如流程实例启动时间、任务的参与者、完成任务的时间、每个流程实例的执行路径等等。这个服务主要通过查询功能来获得这些数据

  • ManagementService

    Activiti 的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护

 3. 流程定义部署

  (1)单个文件部署

  通过调用activiti的api将流程定义的bpmn和png两个文件一个一个添加部署到activiti中,也可以将两个文件打成zip包进行部署

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProcessTest {

    @Autowired
    private RepositoryService repositoryService;

    @Test
    public void deployProcess() {
        // 流程部署
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource("process/qingjia.bpmn20.xml")
                .addClasspathResource("process/qingjia.png")
                .name("请假申请流程")
                .deploy();
        System.out.println(deploy.getId());
        System.out.println(deploy.getName());
    }
}

  (2)压缩文件部署

@Test
public void deployProcessByZip() {
    // 定义zip输入流
    InputStream inputStream = this
            .getClass()
            .getClassLoader()
            .getResourceAsStream(
                    "process/qingjia.zip");
    ZipInputStream zipInputStream = new ZipInputStream(inputStream);

    // 流程部署
    Deployment deployment = repositoryService.createDeployment()
            .addZipInputStream(zipInputStream)
            .name("请假申请流程")
            .deploy();
    System.out.println("流程部署id:" + deployment.getId());
    System.out.println("流程部署名称:" + deployment.getName());
}

流程定义部署后操作activiti的3张表如下:

act_re_deployment 流程定义部署表,每部署一次增加一条记录

act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录

act_ge_bytearray 流程资源表

 

   4. 启动流程实例

  流程定义:将bpmn文件放到activiti的三张表中,好比是java中的一个类

  流程实例:好比是java中的一个实例对象(一个流程定义可以对应多个流程实例),张三可以启动一个请假流程实例,李四也可以启动一个请假流程实例,他们互不影响

@Autowired
private RuntimeService runtimeService;

@Test
public void startUpProcess() {
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia");
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
    System.out.println("当前活动Id:" + processInstance.getActivityId());
}

操作数据表

act_hi_actinst 流程实例执行历史

act_hi_identitylink 流程的参与用户历史信息

act_hi_procinst 流程实例历史信息

act_hi_taskinst 流程任务历史信息

act_ru_execution 流程执行信息

act_ru_identitylink 流程的参与用户信息

act_ru_task 任务信息

 

   5. 查询任务

  每个节点都配置了Assignee,流程启动后,任务的负责人就可以查询自己当前需要处理的任务,查询出来的任务都是该用户的待办任务。

@Autowired
private TaskService taskService;

/**
 * 查询当前个人待执行的任务
 */
@Test
public void findPendingTaskList() {
    //任务负责人
    String assignee = "zhangsan";
    List<Task> list = taskService.createTaskQuery()
            .taskAssignee(assignee)//只查询该任务负责人的任务
            .list();
    for (Task task : list) {
        System.out.println("流程实例id:" + task.getProcessInstanceId());
        System.out.println("任务id:" + task.getId());
        System.out.println("任务负责人:" + task.getAssignee());
        System.out.println("任务名称:" + task.getName());
    }
}

说明:

流程实例id:一个流程只有一个,标识这个流程

任务id:流程每进行到某个节点,就会给这个节点分配一个任务id

输出结果如下:

流程实例id:d969f534-825e-11ed-95b4-7c57581a7819 ​ 任务id:d96c3f28-825e-11ed-95b4-7c57581a7819 ​ 任务负责人:zhangsan ​ 任务名称:张三审批

 

  6. 处理当前任务

  任务负责人查询待办任务,选择任务进行处理,完成任务,完成任务后,任务自动到下一个节点

/**
 * 完成任务
 */
@Test
public void completTask(){
    Task task = taskService.createTaskQuery()
            .taskAssignee("zhangsan")  //要查询的负责人
            .singleResult();//返回一条

    //完成任务,参数:任务id
    taskService.complete(task.getId());
}

 

 7. 查询已处理任务

@Autowired
private HistoryService historyService;

/**
 * 查询已处理历史任务
 */
@Test
public void findProcessedTaskList() {
    //张三已处理过的历史任务
    List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().taskAssignee("zhangsan").finished().list();
    for (HistoricTaskInstance historicTaskInstance : list) {
        System.out.println("流程实例id:" + historicTaskInstance.getProcessInstanceId());
        System.out.println("任务id:" + historicTaskInstance.getId());
        System.out.println("任务负责人:" + historicTaskInstance.getAssignee());
        System.out.println("任务名称:" + historicTaskInstance.getName());
    }
}

 

 8. 其他接口

/**
 * 查询流程定义
 */
@Test
public void findProcessDefinitionList(){
    List<ProcessDefinition> definitionList = repositoryService.createProcessDefinitionQuery()
            .orderByProcessDefinitionVersion()
            .desc()
            .list();
    //输出流程定义信息
    for (ProcessDefinition processDefinition : definitionList) {
        System.out.println("流程定义 id="+processDefinition.getId());
        System.out.println("流程定义 name="+processDefinition.getName());
        System.out.println("流程定义 key="+processDefinition.getKey());
        System.out.println("流程定义 Version="+processDefinition.getVersion());
        System.out.println("流程部署ID ="+processDefinition.getDeploymentId());
    }
}

/**
 * 删除流程定义
 */
public void deleteDeployment() {
    //部署id
    String deploymentId = "82e3bc6b-81da-11ed-8e03-7c57581a7819";
    //删除流程定义,如果该流程定义已有流程实例启动则删除时出错
    repositoryService.deleteDeployment(deploymentId);
    //设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式
    //repositoryService.deleteDeployment(deploymentId, true);
}

 

  9. 流程实例

  流程定义ProcessDefinition和流程实例ProcessInstance是Activiti重要的概念,类似于Java类和Java实例的关系

启动一个流程实例表示开始一次业务流程的运行,比如员工请假流程部署完成,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响,就好比定义一个 java 类,实例化两个对象一样,部署的流程就好比 java 类,启动一个流程实例就好比 new 一个 java 对象 

  比如我们填写一个请假单,一定会有一个请假单的唯一标识,我们通常使用这个标识来关联activiti,这个标识在activiti中称为businesskey

  BusinessKey:业务标识,通常为业务的主键,业务标识和流程标识一一对应,业务标识来源于业务系统,存储业务标识就是根据业务标识来关联查询业务系统的数据

举例:请假流程启动一个流程实例,就可以将请假单的id作为业务标识存储到activiti中,将来查询activiti的流程实例信息就可以获取请假单的id从而关联查询业务系统数据库得到请假单信息

/**
 * 启动流程实例,添加businessKey
 */
@Test
public void startUpProcessAddBusinessKey(){
    String businessKey = "1";
    // 启动流程实例,指定业务标识businessKey,也就是请假申请单id
    ProcessInstance processInstance = runtimeService.
            startProcessInstanceByKey("qingjia",businessKey);
    // 输出
    System.out.println("业务id:"+processInstance.getBusinessKey());
}

 

  10 挂起,激活流程实例

  某些情况可能由于流程变更需要将当前运行的流程暂停而不是直接删除,流程暂停后将不会执行;

  操作流程定义为挂起状态,该流程定义下面的所有流程实例全部暂停: 流程定义为挂起状态,该流程定义将不允许启动新的流程实例,同时该流程定义下所有的流程实例将全部挂起暂停执行

@Test
public void suspendProcessInstance() {
    ProcessDefinition qingjia = repositoryService.createProcessDefinitionQuery().processDefinitionKey("qingjia").singleResult();
    // 获取到当前流程定义是否为暂停状态 suspended方法为true是暂停的,suspended方法为false是运行的
    boolean suspended = qingjia.isSuspended();
    if (suspended) {
        // 暂定,那就可以激活
        // 参数1:流程定义的id  参数2:是否激活    参数3:时间点
        repositoryService.activateProcessDefinitionById(qingjia.getId(), true, null);
        System.out.println("流程定义:" + qingjia.getId() + "激活");
    } else {
        repositoryService.suspendProcessDefinitionById(qingjia.getId(), true, null);
        System.out.println("流程定义:" + qingjia.getId() + "挂起");
    }
}

  操作流程实例对象,针对单个流程执行挂起操作,某个流程实例挂起则此流程不在继续执行,完成该流程实例的当前任务将报异常;

@Test
public void SingleSuspendProcessInstance() {
    String processInstanceId = "8bdff984-ab53-11ed-9b17-f8e43b734677";
    ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
    //获取到当前流程定义是否为暂停状态   suspended方法为true代表为暂停   false就是运行的
    boolean suspended = processInstance.isSuspended();
    if (suspended) {
        runtimeService.activateProcessInstanceById(processInstanceId);
        System.out.println("流程实例:" + processInstanceId + "激活");
    } else {
        runtimeService.suspendProcessInstanceById(processInstanceId);
        System.out.println("流程实例:" + processInstanceId + "挂起");
    }
}

 

 11. 任务分配

  (1)固定分配

  在前面进行业务流程建模时指定固定的任务负责人,如:Assignee:zhangsan/lisi  

  (2)表达式分配

  启动流程实例,这个启动实例的方法跟之前的方法基本一致,唯一的不同是在启动时,添加了一个参数

@Test
public void deployProcess01() {
    // 流程部署
    Deployment deploy = repositoryService.createDeployment()
        .addClasspathResource("process/jiaban01.bpmn20.xml")
        .name("加班申请流程")
        .deploy();
    System.out.println(deploy.getId());
    System.out.println(deploy.getName());
}

/**
 * 启动流程实例
 */
@Test
public void startUpProcess01() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("assignee1","zhangsan");
    variables.put("assignee2","lisi");
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia01", variables);
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
}

  (3)监听器分配

  使用监听器的方式来指定负责人,那么在流程设计时就不需要指定assignee,任务监听器是发生对应的任务相关事件时执行自定义 java 逻辑 或表达式。

  Event的选项包含:Create:任务创建后触发;Assignment:任务分配后触发;Delete:任务完成后触发;All:所有事件发生都触发

  定义任务监听类,且类必须实现 org.activiti.engine.delegate.TaskListener 接口

import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;

public class MyTaskListener implements TaskListener {

    @Override
    public void notify(DelegateTask delegateTask) {
        if(delegateTask.getName().equals("经理审批")){
            //这里指定任务负责人
            delegateTask.setAssignee("zhangsan");
        } else if(delegateTask.getName().equals("人事审批")){
            //这里指定任务负责人
            delegateTask.setAssignee("lisi");
        }
    }
}
@Test
public void deployProcess03() {
    // 流程部署
    Deployment deploy = repositoryService.createDeployment()
            .addClasspathResource("process/jiaban03.bpmn20.xml")
            .name("加班申请流程")
            .deploy();
    System.out.println(deploy.getId());
    System.out.println(deploy.getName());
}

/**
 * 启动流程实例
 */
@Test
public void startUpProcess03() {
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban03");
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
}

启动流程实例,就会调用MyTaskListener监听方法

 

 12. 流程变量

  设置global变量

  流程变量的作用域是整个流程实例,如果设置的流程变量的 key 在流程实例中已存在相同的名字则后设置的变量替换前边设置的变量。

@Test
public void completTask() {
    Task task = taskService.createTaskQuery()
            .taskAssignee("zhangsan")  //要查询的负责人
            .singleResult();//返回一条

    Map<String, Object> variables = new HashMap<>();
    variables.put("assignee2", "zhao");
    //完成任务,参数:任务id
    taskService.complete(task.getId(), variables);
}

 

 13. 任务组

  (1)设置任务候选人

  在流程定义中在任务结点的assignee固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn文件中,如果要临时变更任务负责人则需要修改流程定义,系统扩展性很差,针对这种情况,我们可以给任务设置多个候选人,从候选人中选择参与者来完成任务

@Test
public void deployProcess04() {
    // 流程部署
    Deployment deploy = repositoryService.createDeployment()
            .addClasspathResource("process/jiaban04.bpmn20.xml")
            .name("请假申请流程")
            .deploy();
    System.out.println(deploy.getId());
    System.out.println(deploy.getName());

    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban04");
    System.out.println(processInstance.getId());
}

  (2)查询组任务

  指定候选人,查询该候选人当前的待办任务 候选人不能办理任务

@Test
public void findGroupTaskList() {
    //查询组任务
    List<Task> list = taskService.createTaskQuery()
            .taskCandidateUser("zhangsan01")//根据候选人查询
            .list();
    for (Task task : list) {
        System.out.println("----------------------------");
        System.out.println("流程实例id:" + task.getProcessInstanceId());
        System.out.println("任务id:" + task.getId());
        System.out.println("任务负责人:" + task.getAssignee());
        System.out.println("任务名称:" + task.getName());
    }
}

  (3)拾取(claim)任务

  该组任务的所有候选人都能拾取 将候选人的组任务,变成个人任务,原来的候选人就变成了该任务的负责人 如果拾取后不想办理该任务 需要将已经拾取。张三01拾取任务了,张三02就不能拾取了

@Test
public void claimTask(){
    //拾取任务,即使该用户不是候选人也能拾取(建议拾取时校验是否有资格)
    //校验该用户有没有拾取任务的资格
    Task task = taskService.createTaskQuery()
            .taskCandidateUser("zhangsan01")//根据候选人查询
            .singleResult();
    if(task!=null){
        //拾取任务
        taskService.claim(taskId, "zhangsan01");
        System.out.println("任务拾取成功");
    }
}

  (4)查询个人任务

  查询方式同个人任务部分,根据assignee查询用户负责的个人任务,办理个人任务

@Test
public void findGroupPendingTaskList() {
    //任务负责人
    String assignee = "zhangsan01";
    List<Task> list = taskService.createTaskQuery()
            .taskAssignee(assignee)//只查询该任务负责人的任务
            .list();
    for (Task task : list) {
        System.out.println("流程实例id:" + task.getProcessInstanceId());
        System.out.println("任务id:" + task.getId());
        System.out.println("任务负责人:" + task.getAssignee());
        System.out.println("任务名称:" + task.getName());
    }
}

  (5)办理个人任务

@Test
public void completGroupTask() {
    Task task = taskService.createTaskQuery()
            .taskAssignee("zhangsan01")  //要查询的负责人
            .singleResult();//返回一条
    taskService.complete(task.getId());
}

  (6)归还组任务

  如果个人不想办理该组任务,可以归还组任务,归还后该用户不再是该任务的负责人

@Test
public void assigneeToGroupTask() {
    String taskId = "d96c3f28-825e-11ed-95b4-7c57581a7819";
    // 任务负责人
    String userId = "zhangsan01";
    // 校验userId是否是taskId的负责人,如果是负责人才可以归还组任务
    Task task = taskService
            .createTaskQuery()
            .taskId(taskId)
            .taskAssignee(userId)
            .singleResult();
    if (task != null) {
        // 如果设置为null,归还组任务,该 任务没有负责人
        taskService.setAssignee(taskId, null);
    }
}

  

  14. 任务交接

  任务交接,任务负责人将任务交给其它候选人办理该任务

@Test
public void assigneeToCandidateUser() {
    // 当前待办任务
    String taskId = "d96c3f28-825e-11ed-95b4-7c57581a7819";
    // 校验zhangsan01是否是taskId的负责人,如果是负责人才可以归还组任务
    Task task = taskService
            .createTaskQuery()
            .taskId(taskId)
            .taskAssignee("zhangsan01")
            .singleResult();
    if (task != null) {
        // 将此任务交给其它候选人zhangsan02办理该 任务
        taskService.setAssignee(taskId, "zhangsan02");
    }
}

 

  15. 网关

  (1)排他网关

  只有一条路径会被选择,当你的流程出现这样的场景:请假申请,两天以内,部门经理审批流程就结束了,两天以上需要总经理直接审批,这个时候就需要排他网关

  (2)并行网关

  所有路径会被同时选择,当出现这样的场景:请假申请开始,需要部门经理和总经理都审批,两者没有前后需要两个人全部审批才能进入下个节点人事审批。这个时候就需要并行网关,与排他网关的主要区别是,并行网关不会解析条件,即使顺序流中定义了条件,也会被忽略

  (3)包含网关

  可以同时执行多条线路,也可以在网关上设置条件,可以看做是排他网关和并行网关的结合体;

  当出现这样的场景:请假申请大于等于2天需要由部门总经理审批,小于2天由部门经理审批,请假申请必须经过人事经理审批。这个时候就需要包含网关

标签:Activiti,流程,System,任务,实例,使用,println,out
From: https://www.cnblogs.com/homle/p/17809563.html

相关文章

  • Vmware网络配置与Xshare使用
    1Vmware网络配置1.1安装完后Vmware提供了几种网络连接方式,分别是Bridged(桥接模式)、NAT(网络地址转换模式)、Host-Only(仅主机模式) 1.2桥接模式桥接模式就是将主机网卡与虚拟机的网卡利用虚拟网桥进行通信。在桥接的作用下,类似于把物理主机虚拟为一个交换机,所有桥接设置的虚......
  • 结构体的定义和基础使用
    结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,是C语言中一种重要的数据类型。结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。结构体通常用来表示类型不同但是又相关的若干数据。定义:structperson{charname[30];//姓名......
  • MariaDB(MySQL)的常用命令3 【使用通配符过滤】
    第8章使用通配符过滤LIKE操作符百分号(%)通配符(匹配多个字符,类似?)SELECT*FROMstudentsWHEREemailLIKE'%@163.com';-下划线(_)通配符(匹配单个字符,类似*)SELECT*FROMstudentsWHEREnameLIKE'张_';Tips:1.查找的字符串,可能是大小......
  • Golang使用mqtt
    示例使用使用EMQX提供的免费公共MQTT服务器,该服务基于EMQX的MQTT物联网云平台创建。服务器接入信息如下:Broker:broker.emqx.ioTCPPort:1883WebsocketPort:8083golang代码如下packagemainimport( "fmt" mqtt"github.com/eclipse/paho.mqtt.golang" "time......
  • 如何安装使用cv::plot
    cv::plot是OpenCV的一个模块,用于绘制2D图像。要使用cv::plot,你需要首先安装OpenCV库。以下是在Ubuntu系统上安装OpenCV的步骤:1.更新你的包列表:sudoapt-getupdate2.安装OpenCV:sudoapt-getinstalllibopencv-dev在Windows系统上,你可以从OpenCV的官方网站下载预编译的库,并......
  • Python 异常处理:try、except、else 和 finally 的使用指南
    异常处理当发生错误(或我们称之为异常)时,Python通常会停止执行并生成错误消息。try块用于测试一段代码是否存在错误。except块用于处理错误。else块用于在没有错误时执行代码。finally块用于无论try和except块的结果如何都要执行的代码。可以使用try语句来处理这些......
  • 如何使用Event事件对异步线程进行阻塞和放行?
    //定义信号事件staticAutoResetEventautoResetEvent=newAutoResetEvent(false);//定义要异步执行的方法staticvoidA()    {        for(inti=0;i<10;i++)        {            autoResetEvent.WaitOne();//阻塞等待信号......
  • Python 异常处理:try、except、else 和 finally 的使用指南
    异常处理当发生错误(或我们称之为异常)时,Python通常会停止执行并生成错误消息。try块用于测试一段代码是否存在错误。except块用于处理错误。else块用于在没有错误时执行代码。finally块用于无论try和except块的结果如何都要执行的代码。可以使用try语句来处理这些......
  • Linux 下使用串口的简易教程
    1、检查串口的配置。查看串口的波特率、数据位、校验位等可以使用以下命令: $stty-F/dev/ttyS02、修改串口的配置。使用stty命令可以修改串口的波特率、数据位、校验位等选项,例如,将串口波特率修改为115200,按如下方式操作:$stty-F/dev/ttyS01152003、stty命令功......
  • 前端javasript——forEach、map、filter和reduce的使用场景
    (文章目录)⭐前言大家好,我是yma16,不止前端,本文分享关于前端javasript——forEach、map、filter、reduce区别与使用。自我介绍前端->全栈开发,csdn内容合伙人,2023csdn新星计划Node赛道Top1,csdn2023新星计划vue3+ts赛道导师,阿里云社区专家博主,华为云享专家,前端技术栈:vue2v......