分类 其它 下的文章

商业模式

商业模式是帮助企业成功的“秘诀”,它通过整合企业内外部的多种要素,构建起一个全面、高效且具有独特竞争优势的运营体系。这一体系的目的是满足市场的需求,实现各利益相关者价值最大化,并确保企业的长期盈利能力。

商业模式的核心架构由三个紧密相连的环节构成:创造价值、传递价值和获取价值。

  • 创造价值:这一环节围绕客户的需求展开,提供具有吸引力的价值提案。
  • 传递价值:通过优化资源配置和实施相关策略,确保价值的有效传递。
  • 获取价值:通过精心设计的盈利机制,实现企业收益的持续增长。

这三个环节相互依赖,共同构成了商业模式的框架,它们协同工作,帮助企业在竞争激烈的市场中脱颖而出。

商业模式举例

因为互联网和数字化的发展,公司正在努力改变传统的商业模式,把生产、销售、交易、物流和支付等多个部分整合在一起,不受时间和空间的限制。

通过使用移动互联网、大数据、人工智能和云计算等新技术,公司正在改变消费者的购物习惯和生活方式。新零售的改变特别明显,线下实体门店向线上转型,而线上电商也在寻找扩大实体店市场机会。下面是一些典型的电商商业模式的例子:

  • B2C(商家对消费者):这是电商领域最为常见的模式,商家直接向消费者销售产品和服务。天猫、京东等平台是这一模式的典型代表。
  • B2B(商家对商家):在这一模式下,供应方和采购方通过电商平台完成交易,有效解决了供应链上游至中游的问题。1688等平台是这一模式的杰出代表。
  • C2C(消费者对消费者):在这种模式中,消费者可以直接与其他消费者交易。它强调产品的个性化和质量,提供类似于B2C的服务,但更注重服务。淘宝、微店等平台就是这种模式的例子。
  • C2M(消费者对工厂):这是一种消费者与制造商直接对接的模式,去除了中间环节,提供定制化的生产和消费,强调定制服务和增值服务。淘宝特价版、拼多多等平台采用了这一模式。
  • O2O(线上到线下):这一模式融合了线上信息获取和线下购买体验,是新零售商业模式的典型代表。许多传统企业正在积极探索O2O模式,以增强市场竞争力。

商业模式并非一成不变,它随着企业经营策略和市场环境的变化而变化。

商业模式画布

商业模式画布是一种广泛使用的规划商业模式的工具,由亚历山大·奥斯特瓦德(Alexander Osterwalder)首创。通过九宫格框架,企业能够将商业模式形象化,全面审视和分析其商业运作。

商业模式画布的核心要素:

  • 价值主张:企业向客户提供哪些独特的产品、服务或价值,并解决客户的哪些问题?价值主张是企业与竞争对手区分开的关键,通过创新性、性能、定制化、质量、设计、品牌、定价、成本效益、风险降低、便捷性和可用性等元素来提供价值。
  • 客户关系:企业打算与客户建立什么关系?可能的关系类型包括个性化服务、专属服务、自助服务、自动化服务、社区互动和共同创造等。
  • 客户细分:企业的目标客户群体是谁?通过识别和理解客户的不同需求及特征,企业可以对客户群体进行精准细分,如大众市场、小众市场、多边市场、区隔化市场等,更好地满足特定客户群体的需求。
  • 核心资源:企业为了支持商业活动,有哪些重要的资源?这些可能是实体资产、知识、员工、或者金融资产等。
  • 关键活动:企业需要进行哪些主要活动,来确保产品或服务的顺利运作?这些活动可能包括产品制造、问题解决、平台构建和服务网络搭建等。
  • 渠道通路:企业通过哪些途径将产品或服务传递给客户?渠道通路的构建涉及五个阶段:认知、评估、购买、交付和售后。渠道类型包括自有渠道(如实体店)、合作伙伴渠道(如分销商),同时还需考虑线上、线下和O2O等新零售渠道。
  • 合作伙伴:企业需要与哪些上游或下游的企业建立深度合作关系?合作伙伴关系可能包括战略联盟、竞争合作、新业务合作以及供应商与购买方关系。合作的实质在于资源共享和互利共赢。
  • 成本结构:企业在商业运作中是否充分考虑了成本因素?成本结构可以是成本驱动型或价值驱动型,需要考虑固定成本、变动成本、规模经济和范围经济等。
  • 收入来源:企业的主要收入途径是什么?收入的生成方式包括产品销售、使用费、订阅费、租赁费、授权费、交易费和广告费等。

下图所示为滴滴企业的商业模式画布:

价值流

价值流的相关概念包括:价值主张、价值流、价值流阶段。

价值主张

价值主张概念在商业模式画布部分已解释过。

价值主张处于商业画布的中心位置,当企业决定是否投资某个产品或服务前,首先需明确它为哪个客户群体服务?提供什么价值?以及目标客户群体能否承受产品或服务的价格?

价值流

价值流的定义:为客户创造结果的端到端活动的集合,客户可能是价值流的最终客户,也可能内部使用用户。

价值流更专注于特定的目标客户和价值主张,目标明确。同时,价值流更强调结果导向和价值增长。

通过价值流分析,我们可以很容易地看出哪些环节是增值的,哪些是不增值的。理论上,我们可以消除或减弱没有价值增长的环节,这可以避免流程过于繁重,效果不明显的情况。

价值流阶段

价值流可以进一步细分为不同的价值流阶段,其中的每个价值流阶段,都会贡献相应的价值增量,以确保客户所需整体价值的逐步实现和完整交付。价值流阶段有以下几个特征:

  • 每个流程阶段都有相应的“价值”。如果某个阶段不能增加或贡献到客户需要的价值,理论上可以放弃这个阶段。
  • 每个阶段都有进入条件。只有满足特定条件才能进入下一步。确认和保障这些条件,有利于阶段顺利实现目标。
  • 每个价值流阶段都有完成条件。设定明确的完成条件,可以快速检查是否已完成该阶段的任务,并开始下一个阶段。

我们以一个门店自提服务为例子:

  • 价值主张:让顾客享受优质的酒店服务
  • 价值流阶段:整个价值链由5个阶段组成,分别为商品浏览、下单支付、备货并通知、自提、售后。

业务能力

业务能力指的是企业开展其业务活动所必需的一系列核心技能和资源。这些能力是从业务的角度出发,为了达成特定的目标或结果,构建的特定技能或生产能力。

企业的业务能力与商业模式和价值流密切相关,因为它们直接影响了企业的业绩和价值创造。它们确保了企业战略的实施,并与客户旅程和市场环境保持一致。此外,业务能力也协调了业务需求和IT系统。

业务能力的范畴较为宏观,它有助于企业从多角度进行战略规划和业务发展。在TOGAF(开放组织架构框架)中,业务能力的实现涉及角色、流程、信息和工具的综合运用。

在其他企业管理理论中,业务能力同样被视为企业架构中的一个重要组成部分,它包括人员、组织机构、功能、流程、业务服务、数据信息、应用系统和基础设施等多个要素,并与企业的各种项目和解决方案紧密相关。

业务能力提供了一种独立于现有组织结构、业务流程、应用系统、产品/服务的业务视角,有助于企业从更高层次上理解和管理其业务。

在业务架构体系中,业务能力的关键在于系统化地表现企业的核心业务功能。

以电商业务为例,常见的业务能力包括店铺管理、商品管理、会员营销、订单处理、物流、支付结算、售后服务等方面的综合管理。这些管理活动汇集而成的能力,构成了企业高层业务能力,并且可以进一步细分为多个子业务能力。

例如,订单处理能力可以细分为平台订单管理、自营订单管理、第三方订单管理、订单来源追踪、订单分拆处理等子能力。

业务流程

业务流程是一系列逻辑上相关联的业务活动,它们组合起来以达成特定的业务结果。在业务架构的设计阶段,业务流程扮演着至关重要的角色,它不仅关系到企业资源的有效利用,也直接影响到企业IT架构中应用功能的设计和系统整合的具体需求。

业务流程是价值流概念的进一步展开,它将价值流中的概念细化为可操作的流程。

业务流程与业务能力的区别:

  • 业务能力:关注企业核心业务的能力和结果,不涉及具体的流程分解。
  • 业务流程:专注于流程本身,面向特定场景,通过活动组合解决具体问题,是企业日常运作的关键。
  • 业务流程涵盖关键业务活动,如销售、市场推广、生产、采购和客户服务等,以及执行这些活动的角色和他们之间的互动。同时,业务流程还需遵循行业规范、专业标准和企业内部规章。

业务流程可以进一步细化为不同层级,包括主流程和子流程。它们是连接不同业务部门的纽带,端到端流程往往跨越多个部门或业务能力领域,实现价值的增加。

以电商系统为例,用户交易流程是一个相对标准化的流程,通常包括以下环节:

  • 商品选择
    :用户浏览电商平台,选择想要购买的商品,并添加到购物车。
  • 购物车确认
    :用户查看购物车中的商品,可以修改数量或删除不想购买的商品。
  • 结算
    :用户选择“结算”选项,准备进行支付。在此步骤,用户还可以选择或添加配送地址。
  • 支付
    :用户选择支付方式(如信用卡、支付宝、微信支付等),输入必要的支付信息进行支付。
  • 订单确认
    :支付完成后,系统生成订单,确认购买的商品和支付详情,并通常通过邮件或短信形式发送订单确认信息给用户。
  • 物流处理
    :订单信息传递给仓库,开始打包和发货流程。
  • 发货与跟踪
    :商品发货后,用户可以通过订单系统跟踪物流状态。
  • 确认收货
    :用户收到货物,并确认收货。

业务流程可以进一步细化为不同层次,提供更具体的管理和执行指南。通过分层方法,企业能够确保业务流程设计与价值流的每个环节紧密相连,从而在不同层级上实现价值的最大化。通常包括以下几个层次:

  • 流程类别
    :大类别,如采购、销售、生产等。
  • 流程组
    :在同一类别下的相关流程集合,如订单处理流程组可能包括订单接收、订单确认、订单履行等。
  • 流程
    :具体的操作步骤,例如订单确认流程可能包括接收订单、审核订单、确认库存、生成配送单等步骤。
  • 子流程
    :流程中的更详细步骤,如审核订单可能细化为验证客户资料、检查支付状态等。
  • 任务
    :最基本的操作单元,具体到个人的具体工作,如输入客户订单数据、打印配送单等。

组织架构

组织架构是按照企业战略来设定和安排部门和岗位,形成稳定且科学的管理体系。这个体系保证企业能适应业务需求并支持企业发展。

组织架构对于业务架构至关重要。在梳理业务流程时,必须根据业务流程的运作规律和处理逻辑,在流程的各个节点上安排合适的人员,确保组织的灵活性和明确的责任分配。

同时,业务架构也需要考虑组织的业务需求和发展,对部门的岗位设置、人员配置、角色定义、权限分配、职责明确以及考核机制进行清晰的规划,保障业务流程中每个环节的顺利运作。

下图所示为一个中小连锁企业的组织架构图。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

十,Spring Boot 的内容协商的详细剖析(附+Debug调试说明)

@


1. 基本介绍

根据客户端接收能力不同,SpringBoot返回不同媒体类型的数据。

比如:客户端 Http请求
Accept:application/xml
则返回xml数据,客户端 Http 请求
Accept:application/json
则返回json数据。

关于 内容协商的 类是:
AbstractJackson2HttpMessageConverter.java
这个类

在这里插入图片描述

在这里插入图片描述

2. 准备工作

导入相关的
jar
依赖。

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_jsonxml</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
    <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

准备好,测试的 Bean 类,两个,分别为 Car,Monster 类,这里我使用了
lombok
插件自动生成 set和 get方法。关于这部分的内容大家可以移步至:✏️✏️✏️
六,Spring Boot 容器中 Lombok 插件的详细使用,简化配置,提高开发效率_lombok的getter和setter怎么用-CSDN博客

在这里插入图片描述

package com.rainbowsea.bean;


import lombok.Data;


@Data
public class Car {
    private String name;
    private Double price;

}

package com.rainbowsea.bean;


import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;


@Component
@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Monster {
    private Integer id;
    private String name;
    private Boolean isMarried;
    private Integer age;
    private Date birth;
    private Car car;
    private String[] skill;
    private List<String> hobby;
    private Map<String,Object> wife;
    private Set<Double> salaries;
    private Map<String,List<Car>> cars;

}

相关的 Controller 控制器,处理相关的请求路径映射

在这里插入图片描述

package com.rainbowsea.controller;


import com.rainbowsea.bean.Car;
import com.rainbowsea.bean.Monster;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

@Controller
public class ResponseController {


    // 返回 Monster 数据-要求以JSON格式返回
    @GetMapping("/get/monster")
    @ResponseBody
    public Monster getMonster() {

        // monster 对象是从DB数据库获取-这里老师模拟一个monster对象
        Monster monster = new Monster();
        monster.setId(100);
        monster.setName("奔波霸");
        monster.setAge(200);
        monster.setIsMarried(false);
        monster.setBirth(new Date());
        Car car = new Car();
        car.setName("奔驰");
        car.setPrice(222.2);
        monster.setCar(car);

        return monster;
    }

}

项目的/应用程序的启动场景

在这里插入图片描述

package com.rainbowsea;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication  // 标注项目的启动场景
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

运行程序,打开浏览器输入:
http://localhost:8080/get/monster
。运行测试

在这里插入图片描述

3. 内容协商的本质

从上述运行结果的返回来看,显示的是
JSON
格式的数据。

为什么显示的是 JSON格式的数据。显示返回的是什么样的格式的数据是由:

请求头当中的
Accept
属性的值所决定的。

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,
/
;q=0.8

在这里插入图片描述

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
xml 为 0.9 优先级更高
*/*(其它的数据类型,包括 json格式的字符) 为 0.8 稍微低一点。

而这里之所以显示的
JSON
格式的数据,是因为在我们pom.xml 文件中导入的 spring boot 依赖当中包含了 json 数据格式的
jar
依赖。

在这里插入图片描述

而没有 xml 的数据格式的
jar
依赖,自然就是优先为 json 你有的数据格式的jar依赖为准了。

我们可以 Debug 看看。

我们在
AbstractJackson2HttpMessageConverter.java
类当中的
writeInternal()
的方法当中打上
断点

在这里插入图片描述

在这里插入图片描述

直到走到这里,我们查看 generator的值:发现是 JSON。这就没错了,我们仅仅是spring boot 框架种自行配置了 json 数据格式的依赖,而没有其它的数据格式的依赖,自然就使用我们有的数据格式的了。

下面我们导入
xml
数据格式的依赖,再进行一个测试

在这里插入图片描述

<!--        引入 处理xml 的依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_jsonxml</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
    <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--        引入 处理xml 的依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>

    </dependencies>

</project>

重新运行程序,打开浏览器输入:
http://localhost:8080/get/monster
。运行测试
在这里插入图片描述

这回返回的就是 xml 格式的数据了,因为我们这里配置了 json和 xml 两种格式的依赖,但是,xml数据格式的优先级为0.9比 json 的优先级更高,所以显示的是 xml 格式的数据类型。

同样我们也进行一个 Debug 断点调试。还是一样的。

在这里插入图片描述

在这里插入图片描述

我们前端显示的也是 xml 格式的数据。
在这里插入图片描述


4. 内容协商:注意事项和使用细节

  1. Postman 可以通过修改 Accept 的值,来返回不同的数据格式。感兴趣的大家可以自行去下载,试试。官网地址:
    https://www.postman.com/

在这里插入图片描述

  1. 对于浏览器,我们无法修改其 Accept的值,怎么办?解决方案:开启支持基于请求参数的内容协商功能

  2. 修改
    application.yaml
    (必须在类路径"resources"下才行),开启基于请求参数的内容协商功能。

在这里插入图片描述

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true # 开启基于请求参数的内容协商功能
      #?format=json

其实修改的就是:WebMvcProperties类当中的 内部类 Contentnegotiation的 favorParameter 的值。

在这里插入图片描述

在这里插入图片描述

配置好以后,重新启动程序,打开浏览器,注意需要在我们地址后面加上
?format=xxx
,xxx 就是你要转换显示为什么样格式的数据的值。比如:

  • ?format=json 就是在前端以 json格式的数据展示。
  • ?format=xml 就是在前端以 xml 格式的数据展示。

注意:参数format是规定好的,在开启请求参数的内容协商功能后,SpringBoot底层 ParameterContentNegotiationStrategy 会通过 format 来接收参数,然后返回对应的媒体类型/数据格式,当然format=xxx,这个xxx媒体类型/数据格式是SpringBoot可以
处理的才行,不能乱写。

也注意是英文的
?

在这里插入图片描述

在这里插入图片描述

其实这个
format
的值是:ParameterContentNegotiationStrategy 类当中的 parameterName 属性值。

在这里插入图片描述

这个我们也可以修改这个值,指定一个内容协商的参数名,就不再是默认的 format而是我们自己定义的一个参数名了。

还是在
application.yaml
文件当中配置,增加上一个属性。

这里我们指定**内容协商的参数名为,
rainbowsea

在这里插入图片描述

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true # 开启基于请求参数的内容协商功能
      #?format=json
      parameter-name: rainbowsea # 指定一个内容协商的参数名,就不再是默认的 format而是
      # ?rainbowsea=json ,默认的失效了

重新启动程序,浏览器运行测试:

在这里插入图片描述

需要注意的是: 我们自己指定一个内容协商的参数名,修改掉了默认的 format 的参数了,就不再是默认的 format而是,rainbowsea=json ,默认的(format 就不可以再用了,已经失效了)失效了。

在这里插入图片描述

默认的 format 已经失效了, 无论我们配置=json,还是 xml ,都返回的是 xml 格式类型的数据

。因为 xml 优先级是0.9 比较高的,所以返回的就是默认优先级高的 xml 的格式的了。

5. 总结:

  1. 内容协商就是:根据客户端接收能力不同,SpringBoot返回不同媒体类型的数据。

    比如:客户端 Http请求
    Accept:application/xml
    则返回xml数据,客户端 Http 请求
    Accept:application/json
    则返回json数据。

  2. 内容协商的返回值是由:

  3. 请求头当中的
    Accept
    属性的值所决定的。Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,
    /
    ;q=0.8

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
xml 为 0.9 优先级更高
*/*(其它的数据类型,包括 json格式的字符) 为 0.8 稍微低一点。
  1. 参数format是规定好的,在开启请求参数的内容协商功能后,SpringBoot底层 ParameterContentNegotiationStrategy 会通过 format 来接收参数,然后返回对应的媒体类型/数据格式,当然format=xxx,这个xxx媒体类型/数据格式是SpringBoot可以
    处理的才行,不能乱写。也注意是英文的
    ?
  2. 我们自己指定一个内容协商的参数名,修改掉了默认的 format 的参数了,就不再是默认的 format而是,rainbowsea=json ,默认的(format 就不可以再用了,已经失效了)失效了。

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

一:背景

1. 讲故事

前段时间有位朋友找到我,说他的窗体程序在客户这边出现了卡死,让我帮忙看下怎么回事?dump也生成了,既然有dump了那就上 windbg 分析吧。

二:WinDbg 分析

1. 为什么会卡死

窗体程序的卡死,入口门槛很低,后续往下分析就不一定了,不管怎么说先用 !clrstack 看下主线程,输出如下:


0:000> !clrstack
OS Thread Id: 0x3118 (0)
        Child SP               IP Call Site
000000c478afd1d8 00007ffc284e9a84 [HelperMethodFrame_1OBJ: 000000c478afd1d8] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
000000c478afd300 00007ffbf2cc19ac System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 243]
000000c478afd330 00007ffbf2cc197f System.Threading.WaitHandle.WaitOne(Int32, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 194]
000000c478afd370 00007ffbf1421904 System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
000000c478afd3e0 00007ffbf0c8e2f4 System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
000000c478afd520 00007ffbf1425124 System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
000000c478afd590 00007ffb995d6fe8 DevComponents.DotNetBar.StyleManager.OnColorTintChanged(System.Drawing.Color, System.Drawing.Color)
000000c478afd5f0 00007ffb995d69ff DevComponents.DotNetBar.StyleManager.set_ColorTint(System.Drawing.Color)
000000c478afd680 00007ffb995d694c DevComponents.DotNetBar.StyleManager.set_ManagerColorTint(System.Drawing.Color)
...
000000c478afd6b0 00007ffb995d50f9 xxx.MarkInspectPadControl.InitializeComponent()

有经验的朋友看到上面的卦象相信就知道咋事情了,即有工作线程创建了用户控件导致的,而且这个控件貌似和 DevComponents 有关,接下来的常规套路就是挖一下 WindowsFormsSynchronizationContext 对象看看到底是哪一个线程创建的,使用 !dso 即可。


0:000> !dso
OS Thread Id: 0x3118 (0)
RSP/REG          Object           Name
000000C478AFCF98 000002093b9143c0 System.Windows.Forms.WindowsFormsSynchronizationContext
...
0:000> !do poi(20939c91588)
Name:        System.Threading.Thread
MethodTable: 00007ffbf2769580
EEClass:     00007ffbf288c658
Size:        96(0x60) bytes
00007ffbf276aaf8  4001934       4c         System.Int32  1 instance                1 m_ManagedThreadId

按照剧本的话 WindowsFormsSynchronizationContext 应该会有2个,但这里只有1个,这一个还是主线程的同步上下文,这就完犊子了。。。完全不按照剧本走,这也是真实dump分析的复杂性,那到底是谁创建的呢? 天要绝人之路吗?

2. 出路在哪里

所有东西的落地都在汇编里,而汇编又在方法里,所以突破口就是寻找线程栈中的方法,接下来到
System.Windows.Forms.Control.MarshaledInvoke
方法里看一看可有什么大货,简化后如下:


private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
{
    bool flag = false;
    if (SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, Handle), out var _) == SafeNativeMethods.GetCurrentThreadId() && synchronous)
    {
        flag = true;
    }
    ThreadMethodEntry threadMethodEntry = new ThreadMethodEntry(caller, this, method, args, synchronous, executionContext);
    lock (threadCallbackList)
    {
        if (threadCallbackMessage == 0)
        {
            threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
        }
        threadCallbackList.Enqueue(threadMethodEntry);
    }
    if (flag)
    {
        InvokeMarshaledCallbacks();
    }
    else
    {
        UnsafeNativeMethods.PostMessage(new HandleRef(this, Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
    }
    if (synchronous)
    {
        if (!threadMethodEntry.IsCompleted)
        {
            WaitForWaitHandle(threadMethodEntry.AsyncWaitHandle);
        }
        return threadMethodEntry.retVal;
    }
    return threadMethodEntry;
}

从卦中的代码来看,这个
SafeNativeMethods.GetWindowThreadProcessId
方法是关键,它可以拿到这个窗口创建的
processid

threadid
,接下来观察下简化后的汇编代码。


0:000> !U /d 00007ffbf0c8e2f4
preJIT generated code
System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
Begin 00007ffbf0c8dec0, size 4e9
00007ffb`f0c8dec0 55              push    rbp
00007ffb`f0c8dec1 4157            push    r15
00007ffb`f0c8dec3 4156            push    r14
00007ffb`f0c8dec5 4155            push    r13
00007ffb`f0c8dec7 4154            push    r12
00007ffb`f0c8dec9 57              push    rdi
00007ffb`f0c8deca 56              push    rsi
00007ffb`f0c8decb 53              push    rbx
00007ffb`f0c8decc 4881ecf8000000  sub     rsp,0F8h
00007ffb`f0c8ded3 488dac2430010000 lea     rbp,[rsp+130h]
...
00007ffb`f0c8dff0 488d55b0        lea     rdx,[rbp-50h]
00007ffb`f0c8dff4 ff151e1eddff    call    qword ptr [System_Windows_Forms_ni+0x8fe18 (00007ffb`f0a5fe18)] (System.Windows.Forms.SafeNativeMethods.GetWindowThreadProcessId(System.Runtime.InteropServices.HandleRef, Int32 ByRef), mdToken: 00000000060033c4)
00007ffb`f0c8dffa 448bf0          mov     r14d,eax

根据卦中的汇编以及x64调用协定,
lea rdx,[rbp-50h]
就是我们的 processid,同时
mov r14d,eax
中的 r14d 就是我们的 threadid,突破口已找到,接下来就是深挖了。

3. 如何挖出进程ID和线程ID

有一点要知道 000000c478afd520 和 MarshaledInvoke 方法的 rsp 隔了一个 0x8,同时方法中影响 rsp 的 push 和 sub 都要计算进去,这里就不赘述了,具体可以参考文章:
https://www.cnblogs.com/huangxincheng/p/17250240.html
简单计算后如下:


0:000> ? 000000c478afd520-0x8-(0n8*0n8)-0xF8+0x130
Evaluate expression: 843838379280 = 000000c4`78afd510
0:000> dp 000000c4`78afd510-0x50 L1
000000c4`78afd4c0  00000000`000029dc

0:000> r r14
r14=000000c478afcf14
0:000> dp 000000c478afcf14 L1
000000c4`78afcf14  00000000`00000080

从卦中可以看到
processid=29dc ,threadid=0x80
,这东西是何方神圣呢,我们用 ~ 来找它的真身吧。

0:000> ~
...
  18  Id: 29dc.80 Suspend: 0 Teb: 000000c4`7890d000 Unfrozen
...

0:018> k
 # Child-SP          RetAddr               Call Site
00 000000c4`7a2ffcc8 00007ffc`28028ba3     ntdll!NtWaitForSingleObject+0x14
01 000000c4`7a2ffcd0 00007ffb`fa651cf8     KERNELBASE!WaitForSingleObjectEx+0x93
02 000000c4`7a2ffd70 00007ffb`fa652a51     wpfgfx_v0400!CPartitionManager::GetWork+0x17b
03 000000c4`7a2ffdc0 00007ffb`fa67a2fb     wpfgfx_v0400!CPartitionThread::Run+0x21
04 000000c4`7a2ffdf0 00007ffc`2a037bd4     wpfgfx_v0400!CPartitionThread::ThreadMain+0x2b
05 000000c4`7a2ffe20 00007ffc`2a76ced1     kernel32!BaseThreadInitThunk+0x14
06 000000c4`7a2ffe50 00000000`00000000     ntdll!RtlUserThreadStart+0x21

现在有点傻傻分不清了,怎么 winform 里还有 wpf 的渲染线程,有可能是 DevComponents 这种第三方控件在底层引入的吧。到这里路子又被堵死了,接下来该往哪里走呢?三步一回头,继续看主线程上的方法代码吧。

4. 在源码中寻找答案

虽然在两条路上的突围都失败了,但可以明显的看到离真相真的越来越近,也收获到了大量的作战信息,通过上面的
set_ManagerColorTint
方法的反编译,参考如下:


private void InitializeComponent()
{
    this.styleManager1.ManagerColorTint = System.Drawing.Color.Black;
}

[Description("Indicates color current style is tinted with.")]
[Category("Appearance")]
public Color ManagerColorTint
{
    get
    {
        return ColorTint;
    }
    set
    {
        ColorTint = value;
    }
}

看到源码之后太无语了,其实就是一个简单的
颜色赋值
,根据前面的探索
styleManager1
是由渲染线程创建的,所以主线程对它的赋值自然是得不到渲染线程的反馈。

那这个问题该怎么办呢?大概是如下两种吧。

  1. 重点关注 styleManager1 控件,用排除法观察程序运行状况。
  2. 看文档是否用了错误的方式使用 styleManager1 控件。

三:总结

这次生产事故还是挺有意思的,为什么 WinForm 中可以存在
CPartitionThread
渲染线程,最后还祸在其身,给我几百例dump分析之旅中添加了一笔色彩!

图片名称

写在前面:

大家好,我是
山里看瓜
,该系列文章是为了帮助大家不管面试还是开发对前端的一些基本但是很重要的知识点认识更加深入和全面。

想写这个系列文章的初衷是:我发现前端的很多基本知识,使用起来很简单,定义看起来也很简单。很多人你在问他相关问题的时候,他也能说上几句。但是为什么用?怎么用会更好?原理是什么?让你实现你怎么做?这些问题很多人都是一知半解,某些知识点我本人也是如此,只知道去用,甚至有时候都不知道为什么用,更别说原理,秉承的原则就是程序跟我要么有一个能跑,至于怎么跑那雨我无瓜...

本篇我们从各个方面来介绍
Promise
,把一些你知道的不知道的点全都梳理一遍,让你面试中讲得透彻,表现亮眼;让你在开发中使用更加知根知底。

我们将从以下几个方面进行讲解学习:

Promise
是什么?

定义

  1. mdn描述:一个
    Promise
    是一个代理,它代表一个在创建
    promise
    时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。
  2. 总结来说:
    Promise
    是ES6规范提出的一个技术,用于在JavaScript中进行异步编程(读写文件、数据库、请求、定时器)的解决方案,原来的方案是使用回调嵌套的方式,容易造成回到地狱(后面我们会说到)。
  3. 具体来说:
    • 语法上来说:
      Promise
      是一个构造函数
    • 功能上来说:
      Promise
      对象用来封装一个异步操作并可以获取其成功或者失败的值
    • promise 中既可以异步任务,也可以时同步任务

Promise
的状态

状态是 promise 实例对象中的一个属性:PromiseState,该属性有三种值:

  1. 待定(pending):初始状态,既没有被兑现,也没有被拒绝,待定状态。
  2. 完成(resolved / fullfiled ):意味着操作成功完成。
  3. 失败(rejected):意味着异步操作失败。

状态变化有且只有两种情况:

  1. pending
    变为
    resolved / fullfiled
  2. pending
    变为
    rejected

状态变化说明:

  • 有且只会有这两种变化情况,并且 promise 对象的状态只会改变一次,从 pending 改变为成功或者失败状态。
  • 无论状态变为成功或者失败,始终都会有一个结果数据(跟是否有返回无关)。
  • 执行成功后的结果只一般称为
    value
    ,执行失败的结果值一般称为
    reason

Promise
的结果

Promise 的结果属性:

  • 我们在实例对象身上能看到
    PromiseResult
    这个属性,它保存着异步任务执行成功或者失败的结果。

怎么修改 promise 的结果?

  • resolve 函数:修改为成功状态(fullfiled)。
  • reject 函数:修改为失败状态(rejected)。

Promise
的工作流程

为什么要用
Promise
?

  1. 让我们在处理异步操作时,能够更加灵活地指定回调函数。
    • 以前回调函数方式,必须在启动异步任务前就指定回调函数;
    • promise:启动异步任务 ——> 返回 promise 对象 ——> 给 promise 对象绑定回调;
    • promise方式,我们甚至可以在异步任务结束后再指定,并且可以指定多个回调。
  2. 支持链式调用,可以解决回调地狱问题。
    • 回调地狱:回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调执行的条件。
    • 回调地狱的缺点:不利于阅读,不利于异常处理,维护起来比较复杂。

如何使用
Promise
—— 方法参数详细说明

Promise
构造器函数:Promise(executor) {}

  • executor 函数:执行器 (resolve, reject) => {}
    • resolve 函数: 内部定义成功时我们调用的函数 value => {}
    • reject 函数:内部定义失败时我们调用的函数 reason => {}
  • 说明:executor 会在 Promise 内部立即同步调用,异步操作在执行器中执行,
    即执行器函数并不是异步执行

Promise
原型方法

  1. Promise.prototype.then 方法:(onResolved, onRejected) => {}
    • onResolved 函数:成功的回调函数 (value) => {}
    • onRejected 函数:失败的回调函数 (reason) => {}
    • then()总是返回一个新的promise
    • 新promise的结果状态由then指定的回调函数执行的结果决定
      • 抛出错误
      • 返回失败的promise
      • 返回成功的promise
      • 返回其它任何值
  • 说明:这两个方法用于指定得到成功 value 的成功的回调和得到失败 reason 的失败的回调
  • then 方法返回一个新的 promise对象
  1. Promise.prototype.catch 方法:(onRejected) => {}
    • onRejected 函数:失败的回调函数 (reason) => {}

Promise 构造函数本身的方法

  1. Promise.resolve 方法:(value) => {}
    • value:成功的数据或 promise 对象
    • 如果传递的参数为 非 promise 对象,则返回的结果为成功 promise 对象
    • 如果传入的参数为 Promise 对象,则参数的结果决定了 resolve 的结果
  • 说明:返回一个成功/失败的 promise 对象
let p1 = Promise.resolve(520)
let p2 = Promise.resolve(new Promise((resolve, reject) => {
  resolve('OK')
}))  // 这时 p2 状态为成功,成功的值为 'OK'Ï
  1. Promise.reject 方法:(reason) =>{}
    • reason:失败的原因
  • 说明:返回一个失败的 promise 对象
let p = Promise.reject(520)  // 无论传入的是什么,返回的都是一个失败的promise 对象
// 传入什么,失败的结果就是什么
  1. Promise.all 方法:(promises) => {}
    • promises:包含 n 个 promise 的数组
    • 批量/一次性发送多个异步请求
    • 当都成功时, 返回的promise才成功
    • 一旦有一个失败的, 返回的promise就失败了
  • 说明:返回一个新的 promise,只有所有的 promise 都成功时才成功,只要有一个失败了就直接失败
    • 成功的结果时每一个 promise 对象成功结果组成的数组(有顺序)
    • 失败的结果是在这个数组中失败的那个 promise 对象失败的结果
let p1 = new Promise((resolve, reject) => {
  resolve('OK')
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Success')

const result = Promise.all([p1, p2, p3])
  1. Promise.race 方法:(promises) => {}
    • promises:包含 n 个 promise 的数组
    • race:赛跑/比赛
    • 说明:返回一个新的promise,第一个完成的 promise 的结果状态就是最终的结果状态
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK')
  }, 1000)
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Success')

const result = Promise.race([p1, p2, p3])  // =>结果为 p2 的结果,因为p2 先改变状态

Promise
在开发中比较常用的技巧

我们在实际开发中,经常遇到需要发多个请求获取数据,当这些请求之前并没有相互依赖时,我们使用正常的
Promise
方式去请求或者使用
async
await
方式请求,都是顺序执行,一个请求在前一个请求完成之后发起,这样非常的低效率,并且性能和体验都非常的差,我们依赖请求数据的页面部分会有长时间空白,用户体验非常差。

这时候我们可以使用
Promise.all()
+
async、await
来同时并发请求,这样请求就可以同时发起,实现一个并行发出的效果。

Promise.all(promises)
方法的结果是一个包含所有异步操作的结果数组,能够一一对应上 promises 数组中的异步操作。

以下是一个简单示例:

// 请求接口数据的方法
const getApiData = async () {
	const [res1, res2, res3] = await Promise.all(
		[
			Api.getData1(),
			Api.getData2(),
			Api.getData3(),
		]
	)
}

几个注意点:

  • 函数内部使用
    await
    时,函数必须使用 async 关键字;
  • 只使用一个
    await
    ,给
    Promise.all()
    使用;
  • 内部请求不要加 await 关键字,否则还是会顺序请求,不能实现并行发起;
  • 因为
    Promise.all()
    的结果是对应内部异步操作的数组,我们可以直接通过数组解构,获取每个请求的结果,方便后续针对请求值做操作。

Promise
风格方法封装举例

  1. fs 模块封装
function mineReadFile (path) {
  return new Promise((resolve, reject) => {
    // 读取文件
    require('fs').readFile(path, (err, data) => {
      // 判断
      if (err) reject(err)
      // 成功
      resolve(data)
    })
  })
}

// 调用
mineReadFile("/file/test.txt").then(value => {
		console.log(value)
	}, reason => {
		console.log(reason)
	});
  1. Ajax 请求封装
function sendAJAX(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    // 设置响应数据格式
    xhr.responseType = 'json'
    xhr.open('GET', url)
    xhr.send();
    // 处理结果
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 判断成功
        if (xhr.status >= 200 && xhr.status < 300) {
          // 成功的结果
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
  })
}

 // 调用
sendAJAX('https://api.apiopen.top/getJoke')
.then(value => {
  console.log(value)
}, reason => {
  console.warn(reason)
})

手写
Promise

开始手写之前我们需要先搞清楚 promise 的几个关键问题:

  1. 如何改变 promise 的状态?


    • resolve(value):如果当前是 pending 就会变为 resolved
    • reject(reason):如果当前是 pending 就会变为 rejected
    • 抛出异常:如果当前是 pending 就会变为 rejected
  2. 一个 promise 指定(then方法)多个成功/失败回调函数,都会调用吗?


    • 当 promise 改变为对应状态时都会调用

    let p = new Promise((resolve, reject) => {
      resolve('ok')  // 这里状态改变了,所以下边两个回调都会执行,如果状态不改变,下面的回调都不执行
    })
    
    // 指定回调 - 1
    p.then(value => {
      console.log(value)
    })
    
    // 指定回调 - 2
    p.then(value => {
      alert(value)
    })
    
    
  3. 改变 promise 状态和指定回调函数的顺序是什么样的,谁先执行,谁后执行?

    问题简单描述:promise 代码在运行时,resolve/reject改变状态先执行,还是 then 方法指定回调先执行?


    • 都有可能,正常情况下是先指定回调再改变状态,但也可以先改变状态再指定回调


      • 当执行器函数中的任务是一个同步任务(直接调 resolve()/reject()) 的时候,先改变 promise 状态,再去指定回调函数*

      • 当执行器函数中的任务是一个异步任务的时候,then 方法先执行(指定回调),改变状态后执行

        // 这时是hen 方法先执行(指定回调),改变状态后执行
        let p = new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('OK')
          }, 1000)
        })
        
        p.then(value => {
          console.log(value)
        })
        
    • 如何先改状态再指定回调?


      • 在执行器中直接调用 resolve()/reject()
      • 延迟更长时间才调用 then()
    • 什么时候才能得到数据(回调函数什么时候执行)?


      • 如果先指定的回调,那当状态发生改变时(调用resolve()/reject()时),回调函数就会调用,得到数据
      • 如果先改变的状态,那当指定函数时(then 方法),回调函数就会调用,得到数据
  4. promise.then() 返回的新 promise 的结果状态有什么决定?


    • 简单表达:由 then() 指定的回调函数执行的结果决定
    • 详细表达:
      • 如果抛出异常,新 promise 变为 rejected,reason 为抛出的异常
      • 如果返回的是非 promise 的任意值,新 promise 变为 resolved,value 为返回的值
      • 如果返回的时另一个新的 promise,此 promise 的结果就会成为新 promise的结果
  5. promise 如何串联多个操作任务?


    • promise 的 then() 返回一个新的promise,可以看成 then() 的链式调用
    • 通过 then 的链式调用串联多个同步/异步任务
  6. promise异常穿透?


    • 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调,
    • 前面任何操作除了异常,都会传到最后失败的回调中处理
  7. 中断 promise 链


    • 当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数
    • 办法:在回调函数中返回一个 pending 状态的 promise 对象

    let p = new Promise((resolve, reject) => {
    	setTimeout(() => {
            resolve('OK')
        }, 1000)
    })
    
    p.then(value => {
        console.log(111)
        return new Promise(() => {})
    }).then(value => {
        console.log(222)
    })
    

函数方式:封装成一个构造函数

function Promise(executor) {
	// 添加属性
	this.PromiseState = 'pending'
	this.PromiseResult = null
	// 声明属性  因为实例对象不能直接调用onResolve跟onReject 所以下面then中需要先保存在callback里面
	this.callbacks = []
	// 保存实例对象的 this 的值
	const self = this //  常见的变量名有self _this that

	// resolve 函数
	function resolve(data) {
		// 判断状态
		if (self.PromiseState !== 'pending') return
		// console.log(this)  => 这里的this指向window,下面用this的话时直接修改的window
		// 1. 修改对象的状态 (PromiseState)
		self.PromiseState = 'fulfilled'
		// 2. 设置对象结果值 (PromiseResult)
		self.PromiseResult = data
		// 调用成功的回调函数
		setTimeout(() => {
			self.callbacks.forEach((item) => {
				item.onResolved(data)
			})
		})
	}

	// reject 函数
	function reject(data) {
		// 判断状态
		if (self.PromiseState !== 'pending') return
		// 1. 修改对象的状态 (PromiseState)
		self.PromiseState = 'rejected'
		// 2. 设置对象结果值 (PromiseResult)
		self.PromiseResult = data
		// 调用失败的回调函数
		setTimeout(() => {
			self.callbacks.forEach((item) => {
				item.onRejected(data)
			})
		})
	}
	try {
		// 同步调用【执行器函数】
		executor(resolve, reject)
	} catch (e) {
		// 修改 promise 对象状态
		reject(e)
	}
}

// 添加 then 方法
Promise.prototype.then = function (onResolved, onRejected) {
	const self = this
	// 判断回调函数参数
	if (typeof onRejected !== 'function') {
		onRejected = (reason) => {
			throw reason
		}
	}
	if (typeof onResolved !== 'function') {
		onResolved = (value) => value
	}
	return new Promise((resolve, reject) => {
		// 封装函数
		function callback(type) {
			try {
				// 获取回调函数的执行结果
				let result = type(self.PromiseResult)
				// 判断
				if (result instanceof Promise) {
					result.then(
						(v) => {
							resolve(v)
						},
						(r) => {
							reject(r)
						}
					)
				} else {
					// 结果的对象状态为 【成功】
					resolve(result)
				}
			} catch (e) {
				reject(e)
			}
		}
		// 调用回调函数  根据 PromiseState 去调用
		if (this.PromiseState === 'fulfilled') {
			setTimeout(() => {
				callback(onResolved)
			})
		}
		if (this.PromiseState === 'rejected') {
			setTimeout(() => {
				callback(onRejected)
			})
		}
		// 判断 pending 状态
		if (this.PromiseState === 'pending') {
			// 保存回调函数
			this.callbacks.push({
				onResolved: function () {
					callback(onResolved)
				},
				onRejected: function () {
					callback(onRejected)
				},
			})
		}
	})
}

// 添加 catch 方法
Promise.prototype.catch = function (onRejected) {
	return this.then(undefined, onRejected)
}

// 添加 resolve 方法
Promise.resolve = function (value) {
	return new Promise((resolve, reject) => {
		if (value instanceof Promise) {
			value.then(
				(v) => {
					resolve(v)
				},
				(r) => {
					reject(r)
				}
			)
		} else {
			// 状态设置为成功
			resolve(value)
		}
	})
}

// 添加 reject 方法
Promise.reject = function (reason) {
	return new Promise((resolve, reject) => {
		reject(reason)
	})
}

// 添加 all 方法
Promise.all = function (promises) {
	// 声明变量
	let count = 0 // 计数
	let arr = [] // 结果数组
	// 遍历
	return new Promise((resolve, reject) => {
		for (let i = 0; i < promises.length; i++) {
			promises[i].then(
				(v) => {
					// 得知对象的状态是成功
					// 每个promise对象成功都加 1
					count++
					// 将当前每个promise对象成功的结果都存入到数组中
					arr[i] = v
					// 判断
					if (count === promises.length) {
						// 修改状态
						resolve(arr)
					}
				},
				(r) => {
					reject(r)
				}
			)
		}
	})
}

// 添加 race 方法
Promise.race = function (promises) {
	return new Promise((resolve, reject) => {
		for (var i = 0; i < promises.length; i++) {
			promises[i].then(
				(v) => {
					// 修改返回对象的状态为成功
					resolve(v)
				},
				(r) => {
					// 修改返回对象的状态为成功
					reject(r)
				}
			)
		}
	})
}

class 类的方式:封装成一个类

// 封装成类
class Promise {
	//构造方法
	constructor(executor) {
		// 添加属性
		this.PromiseState = 'pending'
		this.PromiseResult = null
		// 声明属性  因为实例对象不能直接调用onResolve跟onReject 所以下面then中需要先保存在callback里面
		this.callbacks = []
		// 保存实例对象的 this 的值
		const self = this //  常见的变量名有self _this that

		// resolve 函数
		function resolve(data) {
			// 判断状态
			if (self.PromiseState !== 'pending') return
			// console.log(this)  => 这里的this指向window,下面用this的话时直接修改的window
			// 1. 修改对象的状态 (PromiseState)
			self.PromiseState = 'fulfilled'
			// 2. 设置对象结果值 (PromiseResult)
			self.PromiseResult = data
			// 调用成功的回调函数
			setTimeout(() => {
				self.callbacks.forEach((item) => {
					item.onResolved(data)
				})
			})
		}

		// reject 函数
		function reject(data) {
			// 判断状态
			if (self.PromiseState !== 'pending') return
			// 1. 修改对象的状态 (PromiseState)
			self.PromiseState = 'rejected'
			// 2. 设置对象结果值 (PromiseResult)
			self.PromiseResult = data
			// 调用失败的回调函数
			setTimeout(() => {
				self.callbacks.forEach((item) => {
					item.onRejected(data)
				})
			})
		}
		try {
			// 同步调用【执行器函数】
			executor(resolve, reject)
		} catch (e) {
			// 修改 promise 对象状态
			reject(e)
		}
	}

	// then 方法封装
	then(onResolved, onRejected) {
		const self = this
		// 判断回调函数参数
		if (typeof onRejected !== 'function') {
			onRejected = (reason) => {
				throw reason
			}
		}
		if (typeof onResolved !== 'function') {
			onResolved = (value) => value
		}
		return new Promise((resolve, reject) => {
			// 封装函数
			function callback(type) {
				try {
					// 获取回调函数的执行结果
					let result = type(self.PromiseResult)
					// 判断
					if (result instanceof Promise) {
						result.then(
							(v) => {
								resolve(v)
							},
							(r) => {
								reject(r)
							}
						)
					} else {
						// 结果的对象状态为 【成功】
						resolve(result)
					}
				} catch (e) {
					reject(e)
				}
			}
			// 调用回调函数  根据 PromiseState 去调用
			if (this.PromiseState === 'fulfilled') {
				setTimeout(() => {
					callback(onResolved)
				})
			}
			if (this.PromiseState === 'rejected') {
				setTimeout(() => {
					callback(onRejected)
				})
			}
			// 判断 pending 状态
			if (this.PromiseState === 'pending') {
				// 保存回调函数
				this.callbacks.push({
					onResolved: function () {
						callback(onResolved)
					},
					onRejected: function () {
						callback(onRejected)
					},
				})
			}
		})
	}

	// catch 方法
	catch(onRejected) {
		return this.then(undefined, onRejected)
	}

	// resolve 方法
	static resolve(value) {
		return new Promise((resolve, reject) => {
			if (value instanceof Promise) {
				value.then(
					(v) => {
						resolve(v)
					},
					(r) => {
						reject(r)
					}
				)
			} else {
				// 状态设置为成功
				resolve(value)
			}
		})
	}

	// reject 方法
	static reject(reason) {
		return new Promise((resolve, reject) => {
			reject(reason)
		})
	}

	// all 方法
	static all(promises) {
		// 声明变量
		let count = 0 // 计数
		let arr = [] // 结果数组
		// 遍历
		return new Promise((resolve, reject) => {
			for (let i = 0; i < promises.length; i++) {
				promises[i].then(
					(v) => {
						// 得知对象的状态是成功
						// 每个promise对象成功都加 1
						count++
						// 将当前每个promise对象成功的结果都存入到数组中
						arr[i] = v
						// 判断
						if (count === promises.length) {
							// 修改状态
							resolve(arr)
						}
					},
					(r) => {
						reject(r)
					}
				)
			}
		})
	}

	//race 方法
	static race(promises) {
		return new Promise((resolve, reject) => {
			for (var i = 0; i < promises.length; i++) {
				promises[i].then(
					(v) => {
						// 修改返回对象的状态为成功
						resolve(v)
					},
					(r) => {
						// 修改返回对象的状态为成功
						reject(r)
					}
				)
			}
		})
	}
}

async

await

  • async/await是消灭异步回调的终极武器(以同步的流程,书写异步的代码)
  • 作用: 简化promise对象的使用, 不用再使用then/catch来指定回调函数
  • 但和Promise并不互斥
  • 反而, 两者相辅相成
  • 执行async函数, 返回promise对象
  • await相当于promise的then
  • try...catch可捕获异常, 相当于promise的catch

async 函数

  1. 函数的返回值为 promise 对象
  2. promise 对象的结果由 async 函数执行的返回值决定

await 表达式

  1. await 右侧的表达式一般为 promise 对象,但也可以时其它的值
  2. 如果表达式是 promise 对象,await 返回的是 promise 成功的值
  3. 如果表达式是其它值,直接将此值作为 await 的返回值Ï

async 和 await结合使用示例:

// resource 1.html 2.html 3.html

const fs = require('fs')

// 回调函数的方式
fs.readFile('./resousrce/1.html', (err, data1) => {
  if (err) throw err
  fs.readFile('./resousrce/2.html', (err, data2) => {
  	if (err) throw err
    fs.readFile('./resousrce/3.html', (err, data3) => {
  		if (err) throw err
      console.log(data1 + data2 + data3)
		})
	})
})
// resource 1.html 2.html 3.html

const fs = require('fs')
const util = require('util')
const mineReadFile = util.pomiseify(fs.readFile)

// async 与 await 结合
async function main() {
  try {
    // 读取第一个文件的内容
  	let data1 = await mineReadFile('./resourse/1.html')
 	 	let data2 = await mineReadFile('./resourse/2.html')
  	let data3 = await mineReadFile('./resourse/3.html')
  
  	console.log(data1 + data2 + data3)
  }catch(e) {
    console.log(e)
  }
}
main()
// async 与 await 结合发送 Ajax 请求
function sendAJAX(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    // 设置响应数据格式
    xhr.responseType = 'json'
    xhr.open('GET', url)
    xhr.send();
    // 处理结果
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 判断成功
        if (xhr.status >= 200 && xhr.status < 300) {
          // 成功的结果
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
  })
}

// 段子接口地址:https://api.apiopen.top/getJoke
let btn = document.querySelector('#btn')

btn.addEventListener('click', async function () {
  // 获取段子信息
  let duanzi = await sendAJAX('https://api.apiopen.top/getJoke')
  console.log(duanzi)
})

写在后面

前端的东西其实很多并不难,只是很多人很少去深究,去全面了解,大家都只是学个大概,会用就行;

本系列文章将会全面深入的带你重新夯实前端基础,把一些重要且常用的知识点深入讲解;

希望看完文章的你能有所收货,使用起来更加轻松,面试更加自如亮眼;

我相信能看到这里的人呢,都是想进步想成长的小伙伴,希望在工作小伙伴的升职加薪,在找工作的小伙伴面试顺利,收割offer;

对你有帮助的话给作者点点关注吧,你的支持就是我创作的动力!Peace and love~~

音乐分享

不知道有没有喜欢说唱的小伙伴,作者有个想法,每期文章最后分享一首觉得不错的说唱,当然你觉得好听的歌曲也可以评论区分享给大家。

本期歌曲:《ghost face》—— 法老

  • 最喜欢的一句词:一个穷孩子生活在有钱人的城市,尝试用精神去对抗物质。

1、前言

在数字化时代,自动化工具成为了提升工作效率和生产力的重要手段。Python作为一种广泛使用的编程语言,以其强大的功能和易用性受到许多开发者的青睐。 而今天给大家推荐一款开源的自动化脚本工具:
AutoKey

结合Python的强大编程能力与AutoKey的任务自动化特性,用户可以高效地完成各种复杂的操作。比如在日常办公工作中,我们经常会遇到许多重复性的任务,如数据录入、文件整理、邮件发送等。这些任务不仅耗时,而且容易出错。为了提高工作效率,我们可以利用Python和AutoKey两款工具,实现办公自动化。从而释放双手,让计算机为我们做出更多的工作。

2、AutoKey介绍

AutoKey
是一个适用于 Linux 和 X11 系统的开源的桌面自动化工具,能够根据预设的文本或键盘快捷键触发相应的操作,主要用于提高用户的工作效率。通过结合 Python 脚本,你可以创建自定义的键盘快捷方式、文本替换以及复杂的自动化任务。对于需要经常性输入固定文本、启动复杂命令的用户,AutoKey 提供了非常强大的功能。

3、AutoKey主要功能和适用场景

AutoKey主要可通过热键(快捷键)触发文本、命令、脚本或复杂的宏来执行任务。

3.1 主要功能

它的核心功能主要包括以下几个方面:

1、文本输入自动化:

  • 自动输入:可以预先录制或编写文本序列,然后通过热键触发这些文本的输入,从而快速完成重复性文本输入任务。
  • 替换:自动替换文本中的特定模式或关键字,例如,将所有的电子邮件地址替换为链接。
  • 自动更正:在输入过程中自动更正拼写错误。

2、键盘和鼠标操作自动化:

  • 宏录制:记录键盘和鼠标操作,创建可重复使用的宏。
  • 宏执行:通过热键或触发条件执行宏,实现复杂的操作自动化。
  • 模拟点击:自动执行鼠标点击操作。

3、命令和脚本执行:

  • 命令执行:通过热键执行系统命令,如打开文件、运行程序等。
  • 脚本执行:支持多种脚本语言,如Python、Lua等,可以编写复杂的自动化脚本。

4、定时任务:

  • 定时执行:设置特定时间点或时间间隔执行任务,如定时检查邮件、定时提醒等。

5、系统集成:

  • 系统通知:通过AutoKey发送系统通知,提醒用户执行某些操作。
  • 与其他应用集成:可以与其他应用程序如浏览器、文本编辑器等集成,扩展其功能。

6、跨平台支持:

  • AutoKey 支持多个操作系统,包括Linux、macOS和Windows,用户可以在不同的平台上使用。

7、用户界面:

  • 图形用户界面:AutoKey 提供了一个图形用户界面,方便用户创建和管理自动化任务。
  • 命令行界面:AutoKey 也提供命令行工具,允许用户通过命令行进行操作。

AutoKey 的核心功能使其成为提高工作效率和减少重复性工作的一种强大工具,尤其适合那些需要频繁输入或执行相同操作的用户。通过AutoKey,用户可以定制自己的工作流程,从而更加专注于任务的核心内容。

3.2 适用场景

AutoKey 的应用场景非常广泛,以下是一些典型例子:

  • 重复性操作
    : 比如填写表单、复制粘贴文本、发送邮件等,使用 AutoKey 可以自动完成这些任务,节省时间和精力。
  • 自动化操作流程
    : 比如将网页上的数据提取到表格中、自动生成报告等,使用 AutoKey 可以将这些流程自动化,提高效率。
  • 游戏升级辅助
    : 在游戏中,可使用 AutoKey自动完成一些重复性的操作,例如刷怪、采集等。
  • 个人日常工作
    : 通过 AutoKey 可以将一些个人工作流程自动化,例如整理文件、管理时间、快速搜索信息等,提高工作效率。

4、AutoKey安装、使用

1、安装

pip3 install autokey

不同安装方式可参考:
https://github.com/autokey/autokey/wiki/Installing

2、以下是一些简单的 AutoKey 脚本示例:

# 复制当前窗口的标题到剪贴板
import autokey

window = autokey.Window.active()
title = window.title()
autokey.Clipboard.set_text(title)

3、假设我们想要自动化登录到一个网站的操作。我们可以通过以下Python代码来实现:

# 导入autokey模块
import autokey

# 定义一个函数来执行登录操作
def login_to_website():
    # 启动浏览器并打开网站
    autokey.run_command("open -a /Applications/Google Chrome.app https://www.example.com")
    # 等待页面加载
    autokey.sleep(5)
    # 输入用户名和密码
    autokey.type_string("username")
    autokey.press("tab")
    autokey.type_string("password")
    # 按下回车键进行登录
    autokey.press("enter")

# 设置触发关键字
hotkey = autokey.Hotkey()
hotkey.set_keyboard_shortcut("ctrl+shift+l")
hotkey.set_function(login_to_website)

# 保存脚本
autokey.script.save()

在这个例子中,我们定义了一个名为login_to_website的函数,它会启动浏览器,打开指定的网址,输入用户名和密码,然后登录。我们为这个函数设置了一个快捷键ctrl+shift+l,这样每当我们在任何地方按下这个快捷键组合时,都会触发这个登录操作。

4、更多官方使用API可查阅:

https://autokey.github.io/index.html

5、小结

通过结合Python的强大功能和AutoKey的自动化特性,我们可以构建出强大的自动化脚本来解决日常任务。无论是简单的文本输入,还是复杂的网页操作,Python和AutoKey都能提供有效的解决方案。随着自动化技术的不断进步,这类技术组合将会在提升工作效率方面发挥更大的作用。

项目地址

https://github.com/autokey/autokey