wenmo8 发布的文章

Java领域要说让我最服气的RPC框架当属Dubbo,原因有许多,但是最吸引我的还是它把远程调用这个事情设计得很有艺术。

1、Dubbo优点较多,我只钟情其一

1.1、优点

业内对于微服务之间调用的框架选择较多,主流是Spring Cloud的Rest方式 和 Dubbo方式,我使用Dubbo方式居多。Dubbo工业级可用,稳定又高效,深受各大公司研发同学的喜爱。

Dubbo的优点较多,比如:

  • 高性能
    :Dubbo 使用的是基于 Netty 的自定义通信协议,提供了高效的二进制数据传输,使得远程服务调用性能大幅提升。
  • 模块化设计
    :Dubbo 的架构非常模块化,主要由五大核心模块组成:远程调用模块(RPC)、集群模块、负载均衡模块、容错模块和注册中心模块。
  • 每个部件都支持多协议
    :每个部件都支持多种协议,比如注册中心,支持ZK、Redis、Nacos等等。
  • 负载均衡和容错
    :Dubbo 提供了多种容错机制,比如失败重试、失败转移等。还支持多种负载均衡,比如随机、轮询、一致性哈希等。
  • 服务注册和发现
    :Dubbo引入了注册中心的概念,实现了服务的自动注册和发现。
  • SPI 扩展机制
    :在
    背八股文
    场景下,Dubbo被提及最多的就是使用了类似Java的SPI机制,提高了扩展性,这一点仁者见仁智者见智吧。

1.2、钟情其一

但是,Dubbo最吸引人的,
半支烟
觉得反而倒是它的RPC调用。Dubbo的定位是一个RPC框架,这是它的核心和立足之地,所以Dubbo将RPC的调用过程透明化,使得开发者可以专注于业务逻辑,而不用关注底层通信问题。

一个RPC框架只有聚焦于先做好它的RPC调用过程这个模块,才会有人关注,其余的优点都是在这之后,慢慢迭代而来。

作者将RPC调用的这个过程,抽象成
一种协议消息的传输机制
,再通过
控制好线程的等待和唤醒
,来实现远程方法调用。这一设计思路真是美妙,充分体验了作者的智慧。

2、RPC简易示例

学Dubbo,首先就是要学习作者这种设计理念和思路。基于此,来实现一个简易的远程方法调用,将Dubbo的RPC过程简易化。

2.1、示例步骤

简易的RPC过程步骤如下,大致分5步,依旧使用Netty作用Socket通讯工具。

  1. 使用2个Java进程来模拟2个系统之间的调用,A进程 和 B进程。
  2. A进程的某个方法,使用网络请求调用B进程的某个方法。
  3. 然后A进程的方法就处于等待状态。
  4. 等B进程的方法执行完之后,在利用网络通知到A进程。
  5. 然后A进程的方法被唤醒,继续往下执行。

2.2、示例代码

  • B进程作为服务端,启动网络服务
public class BProcessServer {
    private final int port;
    public BProcessServer(int port) {
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new BProcessServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(port).sync();
            System.out.println("B启动了服务,端口号: " + port);
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new BProcessServer(8088).start();
    }
}
  • B进程接受网络请求参数,反序列化之后,执行对应的方法,再将执行结果返回:
public class BProcessServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        String reqData = msg.toString(CharsetUtil.UTF_8);
        System.out.println("B进程接受到了请求数据: " + reqData);

        executeMethod(ctx);
    }

    /**
     * 执行方法
     *
     * @param ctx
     * @throws InterruptedException
     */
    private void executeMethod(ChannelHandlerContext ctx) throws InterruptedException {
        // TODO 将请求消息按照某种规则解析成方法名、方法参数等,其实就是反序列化的过程。
        System.out.println("对接受的数据做反序列化,然后开始执行 消息体里指定的方法...");

        // 模拟方法执行
        Thread.sleep(2000);
        System.out.println("执行完毕,返回结果...");

        // 将结果 通知给 A 进程
        ByteBuf dataByteBuf = ctx.alloc().buffer().writeBytes("Task completed".getBytes(CharsetUtil.UTF_8));
        ctx.writeAndFlush(dataByteBuf);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  • A进程启动Netty客户端,建立与B进程的通信,然后发起远程调用,处于等待状态。
public class AProcessClient {

    private final String host;
    private final int port;
    private final Object lock = new Object();  // 监视器对象

    public AProcessClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new AProcessClientHandler(lock));
                        }
                    });

            ChannelFuture future = bootstrap.connect(host, port).sync();
            System.out.println("A进程与B进程建立了通信连接");

            Channel channel = future.channel();

            // 发起远程调用
            callRemoteMethod(channel);

            channel.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    /**
     * 执行方法
     *
     * @param channel
     * @throws InterruptedException
     */
    private void callRemoteMethod(Channel channel) throws InterruptedException {
        //TODO 此处需要将调用的方法和参数,按照协议进行序列化。这次暂且省去此过程。
        System.out.println("A进程将 请求的方法和参数 进行序列化,然后向B进程发起网络调用...");

        ByteBuf dataByteBuf = channel.alloc().buffer().writeBytes("Start call method".getBytes(CharsetUtil.UTF_8));

        channel.writeAndFlush(dataByteBuf);

        // 使用wait等待B进程通知
        synchronized (lock) {
            System.out.println("A进程等待B进程的响应...");
            lock.wait();  // 等待通知
        }

        System.out.println("A进程收到了B进程的响应通知,继续往下...");
    }

    public static void main(String[] args) throws InterruptedException {
        new AProcessClient("localhost", 8088).start();
    }
}
  • A进程接受B进程的响应,同时被唤醒,然后以上
    lock.wait()
    以后的代码得以继续执行。
public class AProcessClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private final Object lock;

    public AProcessClientHandler(Object lock) {
        this.lock = lock;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        String resData = msg.toString(CharsetUtil.UTF_8);
        System.out.println("A进程接受到了响应数据: " + resData);

        // B 进程任务完成,使用 notify 唤醒等待的线程
        synchronized (lock) {
            lock.notify();  // 唤醒 A 进程
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

3、总结

Dubbo的优秀设计思路有许多,我只钟情其一,那就是RPC的调用过程。以上是一个简易的RPC远程调用的示例,用于理解Dubbo的原理和源码,希望对你有帮助!

本篇完结!欢迎 关注、加V(yclxiao)交流、全网可搜(程序员半支烟)

原文链接:
https://mp.weixin.qq.com/s/J0fzDH-iqGnnnjqaXMLs-A

SQL Server 中的 NUL 设备/NIL设备


在 SQL Server 中,有一个特殊的设备叫做 NUL(注意,不是 NULL),它类似于文件系统中的“黑洞”。NUL 设备类似于 Linux 系统中的 /dev/null,所有写入到 NUL 的数据都会被直接丢弃。

我们可以利用这个特性,在不需要实际生成备份文件的情况下进行测试或模拟备份操作。


完整备份的常规操作
通常情况下,我们对某个数据库进行完整备份时,可以使用以下最简单的 SQL 语句:

BACKUP DATABASE AdventureWorks2012 TO DISK = N'AdventureWorks2012.BAK'

执行后,SQL Server 会显示备份的结果

已为数据库 'AdventureWorks2012',文件 'AdventureWorks2012' (位于文件 1 上)处理了 24328页。
已为数据库
'AdventureWorks2012',文件 'AdventureWorks2012_log' (位于文件 1 上)处理了 2页。BACKUP DATABASE 成功处理了 24330 页,花费 4.103 秒(46.326 MB/秒)。



备份文件将存储在实例的默认备份路径中。可以通过以下 SQL 查询来确认该路径:

DECLARE @BackupDest VARCHAR(200)EXEC master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'BackupDirectory', @BackupDestOUTPUT;SELECT @BackupDest;



使用 NUL 设备进行备份
如果不想实际生成备份文件,但需要测试备份操作或了解备份时间等,可以将备份数据写入 NUL 设备。
SQL Server 支持两种方式将备份写入 NUL 设备:使用 NUL 或 NUL:。

BACKUP DATABASE AdventureWorks2012 TO DISK = N'NUL' 
GO
BACKUP DATABASE AdventureWorks2012 TO DISK = N'NUL:'
GO

执行后,SQL Server 同样会显示备份的结果

已为数据库 'AdventureWorks2012',文件 'AdventureWorks2012' (位于文件 1 上)处理了 24328页。
已为数据库
'AdventureWorks2012',文件 'AdventureWorks2012_log' (位于文件 1 上)处理了 2页。BACKUP DATABASE 成功处理了 24330 页,花费 1.499 秒(126.803 MB/秒)。


NUL 设备还可以通过备份判断磁盘的读吞吐量,即使你备份至多个NUL设备。备份至NUL设备主要是读取数据,写数据并没有写入具体的磁盘中。

BACKUP DATABASE AdventureWorks2012 TO DISK = N'NUL',DISK = N'NUL',DISK = N'NUL',DISK = N'NUL' ,DISK = N'NUL'



总结
备份至 NUL 设备的主要用途包括:
1、备份吞吐量验证:NUL 设备可以用来测试数据库的
读 IOPS 性能
,帮助验证备份时的读写吞吐量。
2、没有日志备份导致磁盘满:如果日志文件长期未备份而变得非常大导致磁盘空间不足,可以通过将
日志备份写入 NUL 设备
来快速释放日志空间,之后再进行日志文件收缩或者设置简单模式收缩文件。
3、在高可用配置:特别是 SQL Server2016或者以上版本, AlwaysOn AG 场景中,使用
automatic seeding 功能
配置可用性数据库前需要备份,可将数据库备份至NUL设备后再初始化同步。
4、COPY_ONLY 备份:在不希望中断备份链(不truncate日志文件里的日志)但需要在生产环境中进行测试时,可以使用
COPY_ONLY 参数
来创建临时备份,避免对生产环境的备份链产生影响。

需要注意的是,备份至 NUL 设备时的读操作对性能的影响与实际备份相似,因此在生产环境中进行测试时,仍需谨慎评估其影响。



参考文章
https://www.modb.pro/db/452106
https://blog.csdn.net/lnotime/article/details/104847946
https://cloud.tencent.com.cn/developer/information/%E6%AD%A3%E5%9C%A8%E8%BF%98%E5%8E%9F%E6%95%B0%E6%8D%AE%E5%BA%93SQL%20Server%20-%E6%95%B0%E6%8D%AE%E4%B8%BAnull%E4%B8%8D%E8%83%BD%E5%AF%B9null%E5%80%BC%E8%B0%83%E7%94%A8%E6%AD%A4%E6%96%B9%E6%B3%95%E6%88%96%E5%B1%9E%E6%80%A7


本文版权归作者所有,未经作者同意不得转载。

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

写在前面:

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

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

本篇我们从各个方面来介绍
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》—— 法老

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

运行的效果如下

先引入一下我们需要的库

在nuget上面搜一下"expression.Drawing",安装一下这个包

我们再创建一个Window,引入一下这个包的命名空间

我们设置一下这个加载动画呈现的窗体的样式

        xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing"mc:Ignorable="d"WindowStyle="None"ResizeMode="NoResize"Background="#4C858585"WindowStartupLocation="CenterScreen"AllowsTransparency="True"Loaded="Window_Loaded"Title="Window1" Height="400" Width="400"

然后我们要用到我们添加的库里面的一个圆弧控件

        <ed:Arc x:Name="arc" Height="100" Width="100" StrokeThickness="50" Panel.ZIndex="1"StartAngle="-240" EndAngle="-300" Stretch="None"ArcThicknessUnit="Pixel">
            <ed:Arc.Stroke>
                <RadialGradientBrush GradientOrigin="0.3,0.3" RadiusX="0.7" RadiusY="0.7">
                    <GradientStop Color="#FFFFF00E" Offset="1"/>
                    <GradientStop Color="White" Offset="0"/>
                </RadialGradientBrush>
            </ed:Arc.Stroke>
        </ed:Arc>

背景色设置成一个圆形渐变的原因是为了让这个控件看起来像一个球体,而不是一个圆,得到的效果如下:

再到下面放几个圆球

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Width="200" Margin="200,0,0,0">
    <StackPanel x:Name="stackPanel" Orientation="Horizontal" RenderTransformOrigin="0.5,0.5">
    <!--这里是为了后面的动画用,因为我是用blend添加的动画,所以生成的代码如下--> <StackPanel.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </StackPanel.RenderTransform> <Ellipse/> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> </StackPanel> </StackPanel>

资源里面添加一下这些球的大小和颜色

        <Style TargetType="{x:Type Ellipse}">
            <Setter Property="Height" Value="20"/>
            <Setter Property="Width" Value="20"/>
            <Setter Property="Fill" Value="#FFFF5800"/>
        </Style>

就会得到下面这个样子

我们再添加一下动画,让这两部分动起来

        <Storyboard x:Key="Storyboard1" RepeatBehavior="Forever" AutoReverse="True">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="arc" Storyboard.TargetProperty="(ed:Arc.EndAngle)">
                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-300"/>
                <EasingDoubleKeyFrame KeyTime="00:00:0.25" Value="-271"/>
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="arc" Storyboard.TargetProperty="(ed:Arc.StartAngle)">
                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-240"/>
                <EasingDoubleKeyFrame KeyTime="00:00:0.25" Value="-270"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="Storyboard2" RepeatBehavior="Forever">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="stackPanel" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="00:00:01" Value="-78"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

还需要把这个动画也动起来,在window的loaded事件里面添加下面的代码

        private void Window_Loaded(objectsender, RoutedEventArgs e)
{
var b = FindResource("Storyboard1") asStoryboard;
b.Begin();
var c = FindResource("Storyboard2") asStoryboard;
c.Begin();
}

这个时候运行这个窗体,就会发现已经实现了吃豆豆的效果,但是因为动画是一直在循环,小豆子一直往左边移动的时候,移动到一个位置就不会移动了,动画在重置的时候会让

动画看起来有一个延迟感,我们可以给这豆子的父级添加一个clip,让动画视觉上看起来是连续的(这里可以对比一下添加clip和不添加的效果的区别)

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Width="200" Margin="200,0,0,0">
<StackPanel.Clip>
<RectangleGeometry>
<RectangleGeometry.Rect>
<Rect X="0" Y="0" Width="150" Height="40"/>
</RectangleGeometry.Rect>
</RectangleGeometry>
</StackPanel.Clip>
<StackPanel x:Name="stackPanel" Orientation="Horizontal" RenderTransformOrigin="0.5,0.5"> <!--这里是为了后面的动画用,因为我是用blend添加的动画,所以生成的代码如下--> <StackPanel.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </StackPanel.RenderTransform> <Ellipse/> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> </StackPanel> </StackPanel>

然后就是怎么把这个效果运用到我们的项目中

我们再添加一个window2,在里面添加一个button

button的click事件代码如下

我们运行window2,点击一下按钮,就会发现3秒钟以后,吃豆豆的动画就消失了,也就是数据加载完成,吃豆豆就不显示了

如果项目里面有很多的地方都要用到这个动画,我们可以添加一个类来专门做这个动画的事情

代码里面的Action就是我们需要耗时的一些操作

所以我们的button的click里面的代码就变成了下面这段代码

好了,到这里这个动画的实现就结束了

项目github地址:
bearhanQ/WPFFramework: Share some experience (github.com)

QQ技术交流群:332035933;