开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

清除C++阅读障碍 操作符 函数 数据类型 关键字

发表于 2024-04-22

c++其实语法上和java差不多,但是c++读起来却比java吃力。我认为操作符和定义好的方法名称是阻碍代码阅读的最大苦难。对较多定义好的函数不够熟悉。所以在此记录下常用的操作符和函数。如果有遇到不认识的函数可以通过man <method name>查看。
image.png

操作符

1.取地址符&和取值符*

image.png

2.->和.

  • ->通过对象的指针对象,访问结构体中的数据。
  • .直接通过对象,访问结构体中的数据。

image.png

3.sizeof

sizeof是运算符,不是函数。计算变量(或类型)所占字节空间的大小。
ps:可以通过指针地址的size来看64位还是32位。下图中指针sizeof为8个字节,也就是64位。
image.png

4.include<>和””的区别

表示编译器在搜索头文件时的顺序不同,<>表示从系统目录下开始搜索,然后再搜索PATH环境变量所列出的目录,不搜索当前目录,””是表示从当前目录开始搜索,然后是系统目录和PATH环境变量所列出的目录。
所以,系统头文件一般用<>,用户自己定义的则可以使用””,加快搜索速度。

5.#if #elif #else #endif #if defined #if !defined #ifdef #ifndef

条件编译语句,不满足条件的结构体不参与编译。

#if defined等价于#ifdef

#if !defined等价于#ifndef

后面接的symbol是指有没有定义过,即使定义的值为0,#ifdef也是true。

避免重复引入的例子:

1
2
3
4
5
c复制代码//一般使用前面两个下划线,中间一个下划线,后面两个下划线
#ifndef __TEST_H__
#define __TEST_H__
int Add(int x, int y);
#endif
1
2
c复制代码#pragma once
int Add(int x, int y);

6.struct

结构体

1
2
3
4
c复制代码struct student {
int age;
int sex;
}

7.using namespace ::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
c复制代码#include <iostream>
using namespace std;

// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{

// 调用第一个命名空间中的函数
func();
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
second_space::func();

return 0;
}

static_cast

1
2
c复制代码int i = 10;
float f = static_cast<float>(i); // 静态将int类型转换为float类型

int32_t uint32_t 及size_t

image.png

size_t主要用于计数,他就是一个unsinged int的重定义. 如sizeof函数返回值类型即为size_t。在不同位的机器中所占的位数也不同,size_t是无符号数
在不同机器中定义不同:
在32位机器中定义为:typedef unsigned int size_t; (4个字节)
在64位机器中定义为:typedef unsigned long size_t;(8个字节)

使用 size_t 来代替 int 或 unsigned 可以保证在同一个平台中,始终得到得到一个数据类型或变量的字节大小,保证了程序对该数据类型或变量的统计方式始终一致,不会因为平台的改变而出现错误。

stdint.h源码
size_t 的声明是实现相关的。它出现在一个或多个标准头文件中,比如stdio.h 和stblib.h,典型的定义如下:

1
2
3
4
c复制代码#ifndef __SIZE_T
#define __SIZE_T
typedef unsigned int size_t;
#endif

nullptr

空指针

const、constexpr

关键字 constexpr 是在 C++ 中引入的11,并在C++ 14中进行了改进。它意味着不断的表达。像 一样 const ,它可以应用于变量:当任何代码尝试修改该值时,会引发编译器错误。与 不同 const , constexpr 也可以应用于函数和类构造函数。 constexpr 指示该值或返回值是常量,并且在可能的情况下,在编译时计算。
www.cnblogs.com/ljwgis/p/13…

static、auto、extern、register

learn.microsoft.com/zh-cn/previ…

函数

  1. printf 控制台输出
  2. memset

数据类型

类型 位 范围
char 1 个字节 -128 到 127 或者 0 到 255
unsigned char 1 个字节 0 到 255
signed char 1 个字节 -128 到 127
int 4 个字节 -2147483648 到 2147483647
unsigned int 4 个字节 0 到 4294967295
signed int 4 个字节 -2147483648 到 2147483647
short int 2 个字节 -32768 到 32767
unsigned short int 2 个字节 0 到 65,535
signed short int 2 个字节 -32768 到 32767
long int 8 个字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
signed long int 8 个字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
unsigned long int 8 个字节 0 到 18,446,744,073,709,551,615
float 4 个字节 精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字)
double 8 个字节 双精度型占8 个字节(64位)内存空间,+/- 1.7e +/- 308 (~15 个数字)
long long 8 个字节 双精度型占8 个字节(64位)内存空间,表示 -9,223,372,036,854,775,807 到 9,223,372,036,854,775,807 的范围
long double 16 个字节 长双精度型 16 个字节(128位)内存空间,可提供18-19位有效数字。
wchar_t 2 或 4 个字节 1 个宽字符

注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。

关键字

C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【AI大模型应用开发】【LangSmith 生产级AI应用

发表于 2024-04-22
  • 大家好,我是同学小张,日常分享AI知识和实战案例,欢迎 点赞 + 关注 👏,持续学习,持续干货输出。

今天是LangSimth平台实战的第二篇文章。

上篇文章我们介绍了该平台的Tracing部分,它为程序提供了每一步的运行日志及监控,提供了快速调试能力以及测试数据标注和收集能力。

本文介绍该平台的数据集和测试评估部分。数据集的建立和测试评估是软件开发过程中必不可少的一部分,也是保证软件质量的重要一环。

  1. 导入本地数据集

该平台上,对于数据集的收集过程,除了上篇文章中介绍的在线标注和收集方式,还可以通过导入本地数据集的方式批量上传数据集。

以AGI课堂中的数据集例子给大家做演示。

数据集格式如下( .jsonl文件 ):outlines、user_input 以及 label字段,其中label为标注,也就是输出结果。

1
2
python复制代码{"outlines": "Assistants API\n✅1. OpenAI 给了我们更大空间\n✅2. 原生 API、GPTs、Assistants API、国产/开源大模型选型参考\n✅3. Assistants API 的主要能力\n✅4. 做一个自己的 GPT\n  1. 创建 assistant\n  2. 管理 thread\n  3. 添加 message\n  4. 开始 run\n  5. 中控调度\n  6. Function Calling\n  7. Code Interpreter\n  8. RAG", "user_input": "别进reddit的中文话题,那是最没营养的区域", "label": "N"}
{"outlines": "【神秘嘉宾】大模型时代的AI产品新挑战\n1. AI 能力演进路线\n✅2. LLMs 带来的变化\n✅3. 如何将大模型落地到实际场景中\n✅4. LLMs 存在哪些问题\n✅5. LLMs 落地三要素\n✅6. LLMs 短期、中期和长期落地方向", "user_input": "对话式交互也不是所有场景都合适", "label": "N"}

0.1 导入步骤与相关接口

(1)创建dataset,接口:create_dataset

(2)给dataset创建数据集,接口:create_examples

0.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python复制代码import json

data = []
with open('D:\GitHub\LEARN_LLM\langsmith\my_annotations.jsonl','r',encoding='utf-8') as fp:
for line in fp:
example = json.loads(line.strip())
item = {
"input": {
"outlines": example["outlines"],
"user_input": example["user_input"]
},
"expected_output": example["label"]
}
data.append(item)

from langsmith import Client

client = Client()

dataset_name = "assistant-001"

dataset = client.create_dataset(
dataset_name, #数据集名称
description="AGI课堂的标注数据", #数据集描述
)

client.create_examples(
inputs=[{"input":item["input"]} for item in data[:50]], # 只是演示,所以只上传了前50条测试数据
outputs=[{"output":item["expected_output"]} for item in data[:50]],
dataset_id=dataset.id
)

以上实现代码其实主要是调用了上述两个接口,创建了数据集和为数据集填充了测试数据。剩下的代码就是解析数据集jsonl文件格式。

0.3 运行结果

打开LangSimth,可以看到上传的数据集了

在这里插入图片描述
在这里插入图片描述

  1. 对数据集进行批量测试和评估

1.1 定义评估函数

定义一个评估函数,判断输出值是否与期望值相等,相等则评分为1,不相等则评分为0。

下面的例子使用了自定义的评估标准,要想自定义一个字符串类型的评估标准,需要继承自StringEvaluator,然后重写_evaluate_strings函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
python复制代码from langchain.evaluation import StringEvaluator
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
import re
from typing import Optional, Any

class AccuracyEvaluator(StringEvaluator):

def __init__(self):
pass

def _evaluate_strings(
self,
prediction: str,
input: Optional[str] = None,
reference: Optional[str] = None,
**kwargs: Any
) -> dict:
return {"score": int(prediction==reference)}

from langchain.evaluation import EvaluatorType
from langchain.smith import RunEvalConfig

evaluation_config = RunEvalConfig(
# 自定义评估标准
custom_evaluators=[AccuracyEvaluator()],
)

1.2 定义Chain

在这里定义你的待评估的主要数据处理流程程序,也就是你的大模型应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
python复制代码from langchain.prompts import PromptTemplate

need_answer=PromptTemplate.from_template("""
*********
你是AIGC课程的助教,你的工作是从学员的课堂交流中选择出需要老师回答的问题,加以整理以交给老师回答。

课程内容:
{outlines}
*********
学员输入:
{user_input}
*********
如果这是一个需要老师答疑的问题,回复Y,否则回复N。
只回复Y或N,不要回复其他内容。""")

model = ChatOpenAI(temperature=0,model_kwargs={"seed":42})
parser = StrOutputParser()

chain_v1 = (
{
"outlines":lambda x: x["input"]["outlines"],
"user_input":lambda x: x["input"]["user_input"],
}
| need_answer
| model
| parser
)

1.3 运行测试

运行测试的接口:arun_on_dataset,该接口需要的重要参数:

  • dataset_name:要使用的数据集名称
  • llm_or_chain_factory:使用的处理链(你要评估的程序)
  • evaluation:评估标准
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
python复制代码from langchain.smith import (
arun_on_dataset,
run_on_dataset,
)

from langsmith import Client

client = Client()

async def test_run():
dataset_name = "assistant-001"
results = await arun_on_dataset(
dataset_name=dataset_name,
llm_or_chain_factory=chain_v1,
evaluation=evaluation_config,
verbose=True,
client=client,
project_name="test-002",
tags=[
"prompt_v1",
], # 可选,自定义的标识
)
print(results)

asyncio.run(test_run())

再加一些需要的包:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码import asyncio
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema import HumanMessage
from langchain.prompts.chat import HumanMessagePromptTemplate
from langchain.prompts import ChatPromptTemplate
from langchain.evaluation import StringEvaluator
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
import re
from typing import Optional, Any

1.4 运行结果

运行日志输出如下:
在这里插入图片描述

本次测试的结果示例如下:每一条都有记录,参考结果是什么、本次测试输出结果是什么。

在这里插入图片描述

在数据集界面,还可以看到所有针对本数据集的测试信息。
在这里插入图片描述

1.5 坑

同一数据集的同一个测试只能跑一次,否则报错。也就是在同一个数据集上跑测试时,project_name参数要不同。

在这里插入图片描述

本文到这里就结束了,在本文中,我们实际使用了LangSmith平台的数据集与测试评估的部分:从数据集的创建到建立自己的评估标准,再到实际运行一个测试,得到测试结果。简单的使用,相信大家能对这一部分内容有一个全览性的认识。

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~


  • 大家好,我是同学小张,日常分享AI知识和实战案例
  • 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号也可搜【同学小张】 🙏

本站文章一览:

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手教你写一个 headless 无头组件的单元测试、集成

发表于 2024-04-22

作者:易师傅 、github

声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

在上文,我们已经知道了如何去实现一个 headless ui 无头组件,我们都知道要想让自己的组件经受得起千锤百炼,那么一个合格的组件测试那是必不可少的;

接下来咱们就开始去实现一个 headless ui 无头组件的测试用例吧!


为了体现咱们的库的多样化,这篇文章的主要基于 Hover Card 无头组件 来实现;

其实 Hover Card 无头组件 功能和上一篇文章所讲的 Popover 无头组件 的功能类似,只能把触发事件由 click 变成了 hover 事件,其它内容几乎大差不大。

一、分析 Hover Card 无头组件功能

1)效果展示

直接上图:

8.gif

那么我们能看到其实这其中也是包括了这几个关键的子组件:

  • HoverCardRoot:根组件
  • HoverCardTrigger:触发器
  • HoverCardContent:触发内容
  • HoverCardArrow:箭头

看到这里是不是和上一篇文章所讲的 Popover 无头组件 很相似,是的,的确很相似。

2)全部组件展示

那么既然我们知道了基本的效果,那么我们看下完整的组件使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts复制代码<script setup>
import { HoverCardArrow, HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from '@yi-ui/vue'
</script>

<template>
<HoverCardRoot>
<!-- 触发器 -->
<HoverCardTrigger />
<!-- 触发内容 -->
<HoverCardPortal>
<HoverCardContent>
<div>
render content
</div>
<!-- 触发内容的箭头 -->
<HoverCardArrow />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>

能看到这就是一个完整的的Hover Card 无头组件使用;

我再介绍一下 HoverCardPortal 是干嘛的:

一个基于 Vue 内置组件 Teleport 封装的子组件,主要作用与 Vue 内置的 Teleport 作用一致

3)代码实现

因为这篇重点不在这里,如果大家硬是要了解如何实现,可以看看我上一篇文章如何实现 Popover 无头组件,相似度可以达到 80%;

我们这里主要看下代码分布即可:

image.png

接下来我们就是主要去实现 __test__ 中的代码逻辑.

二、单元测试

1)测试前准备

为了更好的测试到每一个功能点,我们需要安装一些必须的工具库:

  • vitest:这个就不解释了
  • @vitest/coverage-istanbul:检测代码覆盖率
  • @vue/test-utils:官方的底层组件测试库,用来提供给用户访问 Vue 特有的 API。先在测试文件中导入需要测试的 Vue.js 组件,再使用 mount() 方法创建一个组件实例,并可传入 props、数据等参数。
  • vitest-axe:测试库,检查程序的可访问性;可访问性测试检查用户界面是否可供每个用户轻松使用,并使我们的应用程序可用于残障人士。
  • jsdom:模拟浏览器的 Dom。
  • @testing-library/jest-dom:提供了一组自定义 jest 匹配器,可以使用它们来扩展 vitest 的常用 API(例如 toBeInTheDocument),可以使测试用例更具声明性且更易于阅读和维护。
  • @testing-library/user-event:尝试模拟触发用户与浏览器交互时在浏览器中发生的真实事件
  • @testing-library/vue:与 @vue/test-utils 类似,一个非常轻量级的专注于测试组件而不依赖于实现细节的 Vue 测试库,且包含了 @vue/test-utils 的功能。

我们推荐在应用中使用 @vue/test-utils 测试组件。@testing-library/vue 在测试带有 Suspense 的异步组件时存在问题,在使用时需要谨慎。

以上的工具库就是编写测试用例时所必须的了,下面就开始手把手的开干了~

2)vitest 使用

安装与配置:

这个在之前的文章手动实现一个无头组件库就有介绍了,就不重复赘述了

进入 __test__ 目录下新建 index.test.ts 文件:因为一般情况下,执行测试的文件名中必须包含 .test. 或 .spec. 。

常用 API 介绍:

  • test:别名又是 it,定义了一组相关的测试期望, 它接收测试名称和保存测试期望的函数。
  • describe:在当前上下文中定义一个新的测试用例,作为一组相关测试、基准以及其他嵌套的测试用例。
  • beforeEach:注册一个回调函数,在当前上下文中的每个测试运行前调用。
  • afterEach:注册一个回调函数,在当前上下文中的每个测试完成后调用。
  • beforeAll:注册一个回调函数,在开始运行当前上下文中的所有测试之前调用一次。
  • afterAll:注册一个回调函数,以便在当前上下文中所有测试运行完毕后调用一次。
  • expect:主要用于创建断言,也就是判断你写的玩意正不正确。

了解了相关介绍,我们接着往下看;

简单的案例使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ts复制代码import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
import { ref } from 'vue'

// 因为有三个测试用例,所以 beforeEach 会执行三次
const a = ref(0)
beforeEach(async () => {
a.value = 2
console.log('a 赋值为:', a.value);
})

// 只会执行一次
const b = ref(0)
beforeAll(async () => {
b.value = 3
console.log('b 赋值为:', b.value);
})

describe('一个默认的悬浮组件', () => {
it('测试两个数值相加', () => {
expect.soft(1 + 2).toBe(3) // 期望 1 + 2 等于 3,实际等于 3
})

test('测试两个数值相减', () => {
expect(2 - 1).toBe(1) // 期望 2 - 1 等于 1,实际等于 1
})

it('测试一个数值是否等于 3', () => {
expect(a.value).toBe(3) // 期望 a 等于 3,实际等于 2
})
})

// 因为有三个测试用例,所以 afterEach 会执行三次
afterEach(async () => {
a.value = 0
console.log('初始化 a 的值为:', a.value);
})

// 只会执行一次
afterAll(async () => {
b.value = 0
console.log('初始化 b 的值为:', b.value);
})

执行结果:

image.png

到这里我们就已经学会使用了 Vitest 的最最最基本的使用了;

3)Vue3 组件测试(@vue/test-utils)

因为上面我们要测试的是一个成熟的 Vue 组件,那么上面的案例很明显还不达标,所以 @vue/test-utils 库就必不可少了。

而且我们上面也简单的介绍了 @vue/test-utils 库的概念:

Vue 官方的底层组件测试库,用来提供给用户访问 Vue 特有的 API。

3.1 安装

1
bash复制代码pnpm i @vue/test-utils -Dw

3.2 常用 API 介绍与使用

@vue/test-utils 是 Vue.js 官方提供的用于编写单元测试和集成测试的工具库。下面是一些 @vue/test-utils 中常用的 API 介绍:

  1. mount: 一个常用的方法,用于挂载 Vue 组件到一个虚拟 DOM 中,并返回一个包装器。可以通过这个包装器来访问和操作组件。
  2. shallowMount: 与 mount 类似,但是它会挂载组件,但不会渲染其子组件。适用于测试一个组件而不涉及其子组件。
1
2
3
4
5
6
7
8
ts复制代码import { shallowMount, mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

// 浅挂载
const wrapperShallow = shallowMount(MyComponent);

// 完全挂载
const wrapperFull = mount(MyComponent);
  1. find: 用于查找组件中的子元素或子组件。
1
2
ts复制代码const wrapper = mount(MyComponent);
const button = wrapper.find('button');
  1. findAll: 与 find 类似,但会返回所有匹配的元素或组件。
1
2
3
ts复制代码// 查找所有段落
const wrapper = mount(MyComponent);
const all_p = wrapper.findAll('p');
  1. setData: 用于设置组件的 Data 数据。
1
2
ts复制代码const wrapper = mount(MyComponent);
wrapper.setData({ count: 10 });
  1. setProps: 用于设置组件的 props。
1
2
ts复制代码const wrapper = mount(MyComponent);
wrapper.setProps({ message: 'Hello' });
  1. attributes 和 classes

attributes 用于获取元素的属性,classes 用于获取元素的类名。

1
2
3
4
5
6
ts复制代码const wrapper = mount(MyComponent);
// 获取按钮的属性
const buttonAttributes = wrapper.find('button').attributes();

// 获取段落的类名
const paragraphClasses = wrapper.find('p').classes();
  1. emitted 和 emittedByOrder

emitted 用于检查事件是否被触发,emittedByOrder 用于获取按顺序触发的事件。

1
2
3
4
5
6
ts复制代码const wrapper = mount(MyComponent);
// 检查事件是否触发
expect(wrapper.emitted().submit).toBeTruthy();

// 获取按顺序触发的事件
const events = wrapper.emittedByOrder();
  1. trigger: 用于触发组件的事件。
1
2
ts复制代码const wrapper = mount(MyComponent);
wrapper.find('button').trigger('click');
  1. vm: 可以访问 Vue 组件实例。
1
2
ts复制代码const wrapper = mount(MyComponent);
const vm = wrapper.vm;
  1. destroy:用于卸载组件。
1
2
ts复制代码const wrapper = mount(MyComponent);
wrapper.destroy();

这些是 @vue/test-utils 中一些常用的 API,用于编写 Vue.js 组件的单元测试和集成测试。

当然还有更加高级的功能,如 contains, isVueInstance 等。

通过这些 API,我们可以很方便的编写测试用例,模拟用户操作和验证组件O不OK。

4)模拟用户行为测试(@testing-library/user-event)

我们都知道,headless ui 组件库主要是一个交互逻辑行为,所以在测试这块,我们必须要对此下重手;

其实上面的 @vue/test-utils 库能通过 trigger 触发一些基本的用户事件,例如点击鼠标移入移出等等;

但是 @vue/test-utils 是专为Vue.js设计,深度集成 Vue 特性,提供的是 Vue 组件测试一站式解决方案。


所以我们为了遵循以用户为中心的测试原则,且提供的 API 能很好模拟用户视角下的操作,鼓励测试代码与实际用户交互保持一致。

我们还需要引入@testing-library/user-event 来更逼真地模拟用户交互。

4.1安装&介绍

1
bash复制代码pnpm i @testing-library/user-event -Dw

@testing-library/user-event 是一个 JavaScript 库,专为模拟用户与网页界面之间的真实交互行为而设计。

它与 @testing-library/dom 和特定框架的 Testing Library(如@testing-library/vue)紧密结合,提供了高级的、符合浏览器行为的事件触发机制。

使用@testing-library/user-event的主要目的是确保测试代码能够精确地模拟用户在浏览器中的操作,如鼠标点击、键盘输入、拖放等,从而更有效地测试组件的交互逻辑和响应。

4.2 常见方法示例

  1. 文本输入:
* `userEvent.type(element: Element, text: string, options?: TypeOptions)`
模拟用户在指定元素上逐字符输入文本。可以传递选项(如`delay`)来控制输入速度。
  1. 鼠标点击与悬停:
* `userEvent.click(element: Element, options?: ClickOptions)`
模拟用户在指定元素上单击。可以传递选项(如`button`、`ctrlKey`等)来模拟不同的鼠标按键和修饰键状态。
* `userEvent.hover(element: Element)`
模拟用户将鼠标光标悬停在指定元素上。
  1. 键盘交互:
* `userEvent.keyboard(text: string, init?: KeyboardEventInit)`
模拟用户按下一系列键盘键。可以传递`KeyboardEventInit`对象来设置修饰键和其他属性。
  1. 选择与复选:
* `userEvent.selectOptions(select: Element, values: Array<string | Element>)`
模拟用户在下拉选择框(`<select>`元素)中选择指定的选项。
* `userEvent.check(element: Element, init?: MouseEventInit)`
模拟用户勾选一个复选框(`<input type="checkbox">`元素)。
* `userEvent.uncheck(element: Element, init?: MouseEventInit)`
模拟用户取消勾选一个复选框。
  1. 拖放操作:
* `userEvent.dragAndDrop(source: Element, target: Element, init?: DragEventInit)`
模拟用户将一个元素拖放到另一个元素上。

此外,@testing-library/user-event还提供了其他方法来模拟用户交互,如dblClick(双击)、clear(清除输入框内容)、tab(按Tab键切换焦点)等。

这些方法力求尽可能地模拟真实用户的交互过程,包括触发相关的事件序列和浏览器默认行为,使得测试更加贴近实际使用场景。

4.3 使用

新建 HoverCard.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts复制代码<template>
<div class="wrapper">测试 axe</div>

<button @click="onHandler">按钮点击</button>

<div class="hover-card" v-if="visible">
显示的内容
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)

const onHandler = () => {
console.log('点击了按钮')
visible.value = !visible.value
}
</script>

编辑 index.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ts复制代码import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
// import { axe } from 'vitest-axe'
import HoverCard from './HoverCard.vue'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import userEvent from '@testing-library/user-event'


describe('一个默认的悬浮组件', () => {
let wrapper: VueWrapper<InstanceType<typeof HoverCard>>
beforeEach(() => {
wrapper = mount(HoverCard, { attachTo: document.body })
})

it('测试 HoverCard 组件中的 click 事件', async () => {
const user = userEvent.setup()
const button = wrapper.find('button')
await user.click(button.element)
expect(wrapper.find('.hover-card').isVisible()).toBe(true)
})
})

运行结果:

image.png

5)无障碍可访问性测试(vitest-axe)

根据之前的文章介绍,我们了解到了,Headless UI 还有一个很重要的概念,那就是可访问性,所谓可访问性那就是要符合

5.1 介绍

vitest-axe 是一个用于 Vue.js 应用程序的可访问性测试工具,它基于 Axe Core 来研发的,同时是 fork 了 jest-axe 来进行二次开发,可以帮助开发人员检测应用程序中的无障碍问题。

介绍:Axe-Core 是一个流行的开源库,用于自动化检测网页的可访问性问题,确保网站符合 WAI-ARIA 和 WCAG(Web Content Accessibility Guidelines)等无障碍标准,提升残障用户使用体验。

5.2 常用 API 介绍

  1. axe:参数为待测元素(通常是DOM节点),执行 Axe Core 的检查,并返回包含检查结果的对象;然后使用Vitest 的 expect 断言 api 来验证检查结果中存在的可访问性违规。
  2. configureAxe:这个函数用于配置 Axe Core,可以设置一些选项,例如忽略某些规则等。

5.3 使用

第一步:安装

1
bash复制代码pnpm i vitest-axe -Dw

第二步:配置 vitest.setup.ts 启用无障碍测试支持

1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码import { expect } from 'vitest'
import type { AxeMatchers } from 'vitest-axe/matchers'
import * as matchers from 'vitest-axe/matchers'
import { configureAxe } from 'vitest-axe'
import "vitest-axe/extend-expect";

expect.extend(matchers)

// 拓展 .d.ts 的属性,例如 matchers 中的 toHaveNoViolations,如果你的项目没有用到 ts,可以忽略
declare module 'vitest' {
export interface Assertion extends AxeMatchers {}
export interface AsymmetricMatchersContaining extends AxeMatchers {}
}

然后在 vitest.config.ts 中添加 setupFiles: './vitest.setup.ts', 即可。

然后在 tsconfig.json 中添加 include: ['./vitest.setup.ts'], 即可。


第三步:测试文件 index.test.ts 中正式编写

首先创建一个组件 HoverCard.vue:

1
2
3
4
5
6
7
ts复制代码<template>
<div class="wrapper">测试 axe</div>
</template>

<script setup lang="ts">

</script>

然后再 index.test.ts 中引入编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ts复制代码import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
import { axe } from 'vitest-axe'
import HoverCard from './HoverCard.vue'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'

describe('一个默认的悬浮组件', () => {
let wrapper: VueWrapper<InstanceType<typeof HoverCard>>
beforeEach(() => {
// 挂载一个 vue 组件
wrapper = mount(HoverCard, { attachTo: document.body })
})

it('测试 HoverCard 组件是否渲染成功', () => {
expect(wrapper.exists()).toBe(true)
})

test('测试 HoverCard 组件是否通过了可访问性测试', async () => {
expect(await axe(wrapper.element)).toHaveNoViolations()
})
})

第四步:渲染结果

image.png

咱们会看到可访问性测试没通过,这是咋回事呀,那当然是因为我们写的 HoverCard.vue 不符合要求啦,所以接下来我们再重新改造一下。

三、编写测试组件代码

1)前提

在上面,我们介绍了 HoverCard 无头组件的基本使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts复制代码<script setup>
import { HoverCardArrow, HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from '@yi-ui/vue'
</script>

<template>
<HoverCardRoot>
<!-- 触发器 -->
<HoverCardTrigger />
<!-- 触发内容 -->
<HoverCardPortal>
<HoverCardContent>
<div>
render content
</div>
<!-- 触发内容的箭头 -->
<HoverCardArrow />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>

所以接下来,我们就根据这个来进行自定义拓展了

我们来实现一个如下的组件功能:

8.gif

2)代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ts复制代码<script setup lang="ts">
import { ref } from 'vue';
import {
HoverCardArrow,
HoverCardContent,
HoverCardPortal,
HoverCardRoot,
HoverCardTrigger,
} from '@/HoverCard';

const hoverState = ref(false);
</script>

<template>
<HoverCardRoot v-model:open="hoverState">
<HoverCardTrigger
class="inline-block cursor-pointer rounded-full shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] outline-none focus:shadow-[0_0_0_2px_white]"
>
悬浮鼠标
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent
class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] rounded-md bg-white p-5 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] data-[state=open]:transition-all"
:side-offset="5"
>
<div class="flex flex-col gap-[7px]">
<div class="flex flex-col gap-[15px]">悬浮内容1</div>
<div class="flex flex-col gap-[15px]">悬浮内容2</div>
<div class="flex flex-col gap-[15px]">悬浮内容3</div>
<div class="flex flex-col gap-[15px]">悬浮内容4</div>
</div>
<HoverCardArrow class="fill-white" :width="8" />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>

很明显我这里是使用了 tailwindcss 风格来写的样式,如果你写的话,可以不用写样式,直接使用即可;

那么接下来我们就要对这个文件来进行单元测试用例的编写了。

3)单元测试用例关联

3.1 测试组件是否渲染成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts复制代码import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
import HoverCard from './HoverCard.vue'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'


describe('一个默认的悬浮组件', () => {
let wrapper: VueWrapper<InstanceType<typeof HoverCard>>
beforeEach(() => {
wrapper = mount(HoverCard, { attachTo: document.body })
})

it('测试 HoverCard 组件是否渲染成功', () => {
expect(wrapper.exists()).toBe(true)
})
})

3.2 测试组件的可访问性测试

1
2
3
4
5
ts复制代码import { axe } from 'vitest-axe'

it('测试 HoverCard 组件是否通过了可访问性测试', async () => {
expect(await axe(wrapper.element)).toHaveNoViolations()
})

3.3 测试组件的用户模拟事件

1
2
3
4
5
6
7
ts复制代码describe('模拟 HoverCard 组件中的鼠标移入100ms后', () => {
it('是否应该通过可访问性测试', async () => {
await wrapper.find('a').trigger('mouseenter')
await sleep(100)
expect(await axe(document.body)).toHaveNoViolations()
})
})

4)运行结果

image.png

那么到这里,我们的组件单元测试就写完了,其实细看整体还是很简单的,只要弄懂几个关键库的基本使用基本就可以解决 80% 的问题了。

四、测试覆盖率

运行

1
bash复制代码pnpm coverage

运行结束后会生成一个 coverage 文件,在这里就可以看到你的测试案例的覆盖率了

image.png

打开对应的 index.html

image.png

当然覆盖率可视化不仅只有这个,比如还有 vitest/ui、storybook 等等,这些取舍就看你个人的了,大家明白即可。

总结

本文主要介绍了,headless ui 的 基本组件测试,还有从用户角度出发的 E2E测试,我相信大家都能有所收获,肯定会对 vue 组件的测试有一定的很深入的了解。

接下来的安排:

  • 文档撰写
  • 支持 Nuxt 调试
  • 打包构建

Headless UI 往期相关文章:

  1. 在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?
  2. 实战开始 🚀 在 React 和 Vue3 中使用 Headless UI 无头组件库
  3. 无头组件库既然这么火 🔥 那么我们自己手动实现一个来完成所谓的 KPI 吧
  4. 泰裤辣 🚀 原来实现一个 Popover 无头组件比传统组件简单辣么多!!

Headless UI (1).png

如果想跟我一起讨论技术吹水摸鱼, 欢迎加入前端学习群聊

如果扫码人数满了,可以扫码添加个人 vx 拉你:JeddyGong

感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏 哦 ~

关注我,带您一起搞前端 ~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2024年最新美国(区)AppleID注册教程——任何国家皆

发表于 2024-04-22

准备材料

  1. 一个国内的手机号码
  2. 一个未注册ID的邮箱
  3. 美国公民信息(有专门网站提供,见下)

接下来,以下以注册美区的AppleID为例,其它各国除了选择国家不同的话其它同样操作。

Apple ID注册

Pc或者手机注册

注册账号注意事项:

1.出生日期:建议选择大于18岁的日期,否则会有限制应用下载。

2.电子邮件:一个全新的未注册过Apple ID的邮箱。

3.手机号码:亲测,目前注册过中国区Apple ID的手机号码也是可以。

image.png

image.png

验证邮箱和电话

填写新的注册邮箱、密码、填写手机号码、输入图形验证码,其它不用理,点击继续。

随后就会它会自动跳转到Apple ID的**管理页面(可能会要加载一会儿)。进入到管理页面后,点击付款和送货,并在接下来的页面中点击添加付款方式**。

image.png

这里添加付款方式选择Fomepay的556150的卡,亲测可以绑定成功,点击获取

微信图片_20240108105643.png

绑定卡之后就可以任意消费

美区其它免税区包括:蒙大拿州(Montana)俄勒冈州(Oregon)阿拉斯加州(Alaska)特拉华州(Delaware)新罕布什尔州(New Hampshire)。

仅接着开启双重认证会再次需要 验证 一下手机号,按要求输入 验证码 。

进入App Store,点击头像框登录刚才注册的账号,会提示检查账户信息,点击检查。然后在接下来的页面打开同意开关,点击下一页继续。

在接下来的页面,直接点击**下一页**,美区的Apple ID 就已经创建完毕了!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter&Flame游戏实践#12 打砖块 - 粒

发表于 2024-04-22

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]

第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


一、 粒子系统

粒子系统 ParticleSystemComponent 是 Flame 中提供的一类具有 生命时长 的构件。具有明确生命时长的大量个体称之为 Particle。粒子系统依赖 Particle 对象进行渲染与更新:

image.png

在第一季中已经对粒子系统的 使用方式 进行过全面的介绍。本篇将基于打砖块的案例,具体介绍粒子系统的应用。

  • 《【Flutter&Flame游戏 - 拾伍】粒子系统 | ParticleSystemComponent》
  • 《【Flutter&Flame游戏 - 拾陆】粒子系统 | 粒子的种类》

1.砖块的爆炸效果

游戏是一种与用户高频交互的应用产品。用户的参与感,首要在于 游戏玩法,其次在于音效、视觉反馈交互体验。比如说小球的打击感:

  • [1] 靠撞击时的 反弹 这种现实物理经验,让用户在感知上体验打击感。
  • [2] 通过需求撞击时发出的音效,如用户在听觉上体验打击感。
  • [3] 通过添加砖块爆炸效果,在视觉上进一步加强打击感。

如下所示,当撞击或者射击等方式使砖块消失时,通过序列帧动画展示爆炸效果,爆炸完后就会消失。所以,这是一种大量、具有生命时长的构件。可以通过粒子系统来完成:

撞击爆炸 射击爆炸
128.gif 127.gif

Particle 的派生体系中,有 SpriteAnimationParticle 粒子可用于序列帧动画的播放。

image.png

之前硬币的序列帧动画,是通过一整张序列帧图片加载的资源:

image.png

这里来看一下,如何使用一张张的序列帧图片进行动画,如下所示,爆炸的序列帧是 14 张图片:

image.png

序列帧动画,主要是构建 SpriteAnimation 对象。可以通过 SpriteAnimation.spriteList 构造基于 Sprite 列表完成任务。其中 Sprite 列表可以根据文件名遍历得到:

注: 游戏启动时需要将图片先加载进 loader 中,extraImages来收集零散的图片资源。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码---->[lib/bricks/07/bricks_game.dart]----
final List<Sprite> spriteList = [];
for(int i=1;i<=14;i++){
String name = 'TCSY_000${i.toString().padLeft(2,'0')}.png';
spriteList.add(game.loader[name]);
}

SpriteAnimation sa = SpriteAnimation.spriteList(
spriteList,
stepTime: 0.05,
loop: false
);

在世界中定义一个 showBoomParticle 方法,在指定的位置添加爆炸粒子。通过 add 方法加入 ParticleSystemComponent 组件。粒子系统构件会在lifespan 秒后自动移除,想要播放一次恰好移除,可以根据序列帧数量控制生命时长。

1
2
3
4
5
6
7
8
9
10
11
dart复制代码void showBoomParticle(Vector2 position) {
/// 同上...
add(
ParticleSystemComponent(
position: position,
particle: SpriteAnimationParticle(
lifespan: spriteList.length * 0.05,
animation: sa,
)),
);
}

注:默认情况下,序列帧的 stepTime*数量 会对和 lifespan 对齐,源码中可以看出,会通过生命时长修改动画的 stepTime。如果不希望对齐 ,可以将alignAnimationTime 置为 false:

image.png


最后一步是何处触发 showBoomParticle 方法。其实添加爆炸效果的时机很明确,我们也在之前封装过砖块销毁后统一操作的入口 PlayWorld#onBrickWillRemove :

1
2
3
4
5
dart复制代码void onBrickWillRemove(Brick brick) {
Vector2 brickCenter = brick.absolutePosition + brick.size / 2;
/// 略其他...
showBoomParticle(brickCenter);
}

到这里,我们就基于 ParticleSystemComponent 实现了爆炸粒子的处理。下面继续看一下其他粒子的使用,进一步优化打砖块的视觉表现:


2. 小球的路径展示

接下来基于粒子系统,实现如下所示的小球轨迹的展示。思路其实很简单,在小球运行时不断生成圆形的粒子,在一定时长之后消失即可:

展示小球路径 两个小球
126.gif 125.gif

运动路径的粒子在小球中产生,使用在 Ball 中增加一个 showPathParticle 方法,在当前小球的位置增加一个维持 1 秒的圆形粒子 CircleParticle,作为 ParticleSystemComponent 中的粒子,加入到世界中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码---->[lib/bricks/07/heroes/ball.dart]----
void showPathParticle() {
CircleParticle circleParticle = CircleParticle(
radius: 4,
lifespan: 1,
paint: Paint()..color = Colors.white.withOpacity(0.2),
);

final ParticleSystemComponent psc = ParticleSystemComponent(
position: position - Vector2(0, size.y / 2),
particle: circleParticle,
);
game.world.add(psc);
}

然后,只要在小球更新的回调中不断添加粒子即可。由于 update 方法更新的频率很高,可以通过 _timeRecord 记录一下经过的时间,来限制 showPathParticle 触发的频率:

1
2
3
4
5
6
7
8
9
10
11
dart复制代码double _timeRecord = 0;
@override
void update(double dt) {
super.update(dt);
_timeRecord += dt;
if (game.status == GameStatus.playing && _timeRecord > 0.06) {
showPathParticle();
_timeRecord = 0;
}
position += v * dt;
}

3. 小球死亡场景优化

如下所示,底部增加闪电网的序列帧动画表示小球死亡的底线(如下左图)。另外小球死亡时,展示死亡粒子动画(如下右图)。这样可以避免小球死亡时突然消失和出现。

底部闪电网 小球死亡粒子动画
115.gif 129.gif

底部的闪电网通过 DiedLine 组件展示:

  • [1]. 它继承自 SpriteAnimationComponent 构建,展示序列帧动画。
  • [2]. 它混入 CollisionCallbacks 支持碰撞检测,当 onCollisionStart 监听到碰撞者是小球时,将小球移除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
dart复制代码--->[lib/bricks/07/heroes/died_line.dart]---
class DiedLine extends SpriteAnimationComponent
with HasGameRef<BricksGame>, CollisionCallbacks {
@override
FutureOr<void> onLoad() {
final List<Sprite> spriteList = [];
for (int i = 1; i <= 4; i++) {
String name = 'lightning${i.toString().padLeft(2, '0')}.png';
spriteList.add(game.loader[name]);
}

animation = SpriteAnimation.spriteList(
spriteList,
stepTime: 0.1,
loop: true,
);
size = Vector2(kViewPort.width, 40);

position = Vector2(0, kViewPort.height - height - 40);

add(RectangleHitbox());
return super.onLoad();
}

@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
if (other is Ball) {
other.removeFromParent();
}
super.onCollisionStart(intersectionPoints, other);
}
}

最后小球在死亡时开启粒子动画,这和砖块被击碎时类似。在小球死亡后,通过 showDieParticle 方法添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码--->[lib/bricks/07/heroes/ball.dart]---
void showDieParticle(Vector2 position) {
final List<Sprite> spriteList = [];
for (int i = 1; i <= 12; i++) {
String name = 'died_${i.toString().padLeft(4, '0')}.png';
spriteList.add(game.loader[name]);
}
SpriteAnimation sa = SpriteAnimation.spriteList(spriteList, stepTime: 0.1, loop: false);
game.world.add(
ParticleSystemComponent(
position: position - Vector2(0, 80),
particle: SpriteAnimationParticle(
lifespan: spriteList.length * 0.05,
animation: sa,
)),
);
}

二、Loading 界面与加载资源

一般游戏会在开始时先加载图片、配置数据数据等资源。展示 Loading 界面让玩家看到加载资源的进度,加载完成后才进入游戏。

Loading 20% Loading 80%
image.png image.png

1.资源加载器

目前打砖块的资源加载主要在 BricksGame#onLoad 中,包括本地配置的初始化、加载关卡数据、加载图片的异步任务。我们可以将这些任务得到的数据,统一通过资源管理器来封装处理。

image.png

资源管理器将作为可持久化的数据资源仓库,BricksGame 依赖资源管理器访问数据。资源管理器由于在应用过程中始终存在,而且只需要单一实例,可以通过单例模式进行维护。如下所示,定义 ResManager 持有 GameConfigManager、GoodsManager、List<Level>、TextureLoader 等需要异步加载的资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dart复制代码class ResManager {

ResManager._();
static ResManager instance = ResManager._();

late SharedPreferences sp;
late GameConfigManager configManager;
GoodsManager goodsManager = GoodsManager();
List<Level> _levels = [];
List<Level> get levels => _levels;
TextureLoader loader = TextureLoader();

void load() async{
//将BricksGame#onLoad加载数据逻辑迁移到这里。
}

2.异步任务的加载进度

包括图片加载在内,目前有非常多的加载任务,如何在定义进度加载规则,以及通知外界加载的进度变化。是资源管理器的要点。我们可以使用 Stream 在每个异步任务完成后,通过 Stream 通知外界进度变化。当加载完毕,该流完成任务,进行关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dart复制代码final StreamController<double> _progressCtrl = StreamController();

Stream<double> get loadStream => _progressCtrl.stream;

void load() async{
sp = await SharedPreferences.getInstance();
_progressCtrl.add(0.1);
configManager = GameConfigManager(sp);
configManager.loadConfig(sp);
await loadLevels();
_progressCtrl.add(0.2);
await loader.load(
'assets/images/break_bricks/break_bricks.json',
'break_bricks/break_bricks.png',
extra: extraImages,
loadingCallBack: (total,cur){
_progressCtrl.add(0.8*(cur/total));
}
);
await goodsManager.loadGoods();
_progressCtrl.add(1);
_progressCtrl.close();
}

其中加载图片是确定个数的异步任务,我们可以通过回调函数来通知外界图片记载的进度变化。如下所示,定义 LoadProgressCallBack 函数类型进行表示:

1
java复制代码typedef LoadProgressCallBack = void Function(int total, int cur);

image.png


3.Loading 界面中加载进度使用

此时,应用打开后可以先展示 AssetsLoadingPage 界面。

image.png

在 AssetsLoadingPage 状态类初始化中触发资源管理器加载数据,并监听 loadStream 触发更新通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dart复制代码class AssetsLoadingPage extends StatefulWidget {
const AssetsLoadingPage({Key? key}) : super(key: key);

@override
State<AssetsLoadingPage> createState() => _AssetsLoadingPageState();
}

class _AssetsLoadingPageState extends State<AssetsLoadingPage> {

bool get isLoading => _progress != 1;
double _progress = 0;

@override
void initState() {
super.initState();
ResManager.instance.load();
ResManager.instance.loadStream.listen(_onLoading);
}

当监听到进度变化时,如果进度小于 1 ,更新进度值,触发界面更新,来展示当前进度值。当进度为 1 时,触发跳转到游戏主界面。

1
2
3
4
5
6
7
8
9
10
11
dart复制代码void _onLoading(double progress) {
if (progress == 1) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const PlatformAdapterApp(),
),
);
}
_progress = progress;
setState(() {});
}

到这里,打砖块游戏的内容就基本结束。虽然是个小游戏,但麻雀虽小五脏俱全,其中整合了商店、背包、道具、金币、关卡、设置等游戏的常见的模块。大家也可以在此基础上进行进一步地拓展。


三、各平台应用打包

最后,我们将把打砖块的这个游戏在各个平台进行打包,这样就可以分享给其他人玩耍。Flutter应用可以产出原生级的Andriod、iOS、Windows、MacOS、Linux、web 六大主流平台应用。游戏自然也可以一套代码,完成六大平台的应用构建。


1. 构建 web 应用

web 平台应用,本身也是可以在各个平台通过浏览器访问的。其特点是无需安装包,可以直接通过浏览器访问。

flutter build web

该命令构建出的产物在 build/web 文件夹下,可以把它部署到服务器中,index.html 是它的访问入口:

image.png

没有服务器的朋友,可以将其作为网站部署到 gitee page 或者 github page 中。如下所示:

image.png

如下是部署到的访问链接, web 中游戏的性能和设备本身有关。这里桌面端也可以轻松地跑到 120 FPS:

toly1994328.gitee.io/game_box/

image.png


2. 构建 windows 应用

Flutter 可以将应用打包为 windows 平台的可执行文件,也就是 .exe。

flutter build windows

该命令构建出的产物在 build/windows/runner/Release 文件夹下。将其压缩分享给其他人,就可以在 windows 操作系统中进行游戏。你也可以通过其他工具打包成安装文件,这点在以后单独介绍。

image.png

目前在我的小破本上可以跑到 140 + 的 FPS :

image.png


3. 构建 Macos 应用

Flutter 可以将应用打包为 MacOS 平台的可执行文件:

flutter build macos

该命令构建出的产物在 build/macos/Build/Products/Release 文件夹下。将其压缩分享给其他人,就可以在 Android 操作系统中安装进行游戏:

image.png

10年前的老 mac 本表示,我还能再战几年:也能维持在 60 FPS:

image.png


4. 构建 Android 应用

Flutter 可以将应用打包为 Android 平台的可执行文件,也就是 .apk。下面是打包 android-arm64 架构的 apk 包命令:

flutter build apk –target-platform android-arm64 –split-per-abi

该命令构建出的产物在 build/app/outputs/flutter-apk 文件夹下。将其压缩分享给其他人,就可以在 Android 操作系统中安装进行游戏:

image.png

安装后,四年前的 Android 旧设备,也能轻松保持 60 FPS。这表示 Flutter & Flame 的性能还有很大的压榨空间。

image.png


最后 iOS 和 Linux 平台类似,目前没有相关设备,暂时就不打包了。

iOS 打包应用: flutter build ios

Linux 打包应用: flutter build linux

到这里,打砖块游戏就告一段落,我们也得到了相关的成果。接下来,我们将继续前进,去往下一类别的游戏,进一步探讨 Flutter 在游戏方面的潜能。敬请期待 ~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

神奇的 SQL 之层级 → 为什么 GROUP BY 之后不

发表于 2024-04-22

开心一刻

老公酷爱网络游戏,老婆无奈

老婆告诫老公:你玩就玩了,但是千万不可以在游戏里找老婆,不然,哼哼…

老公嘴角露出了微笑:放心吧亲爱的,我绝对不会在游戏里找老婆的,因为我有老公!

老婆:…

真变态.jpg

GROUP BY 后 SELECT 列的限制

标准 SQL 规定,在对表进行聚合查询的时候,只能在 SELECT 子句中写下面 3 种内容:

1
2
3
html复制代码通过 GROUP BY 子句指定的聚合键
聚合函数(SUM 、AVG 等)
常量

我们来看个例子
我们有学生班级表 tbl_student_class 以及如下数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码DROP TABLE IF EXISTS tbl_student_class;
CREATE TABLE tbl_student_class (
id int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
sno varchar(12) NOT NULL COMMENT '学号',
cno varchar(5) NOT NULL COMMENT '班级号',
cname varchar(20) NOT NULL COMMENT '班级名',
PRIMARY KEY (id)
) COMMENT='学生班级表';

-- ----------------------------
-- Records of tbl_student_class
-- ----------------------------
INSERT INTO tbl_student_class VALUES ('1', '20190607001', '0607', '影视7班');
INSERT INTO tbl_student_class VALUES ('2', '20190607002', '0607', '影视7班');
INSERT INTO tbl_student_class VALUES ('3', '20190608003', '0608', '影视8班');
INSERT INTO tbl_student_class VALUES ('4', '20190608004', '0608', '影视8班');
INSERT INTO tbl_student_class VALUES ('5', '20190609005', '0609', '影视9班');
INSERT INTO tbl_student_class VALUES ('6', '20190609006', '0609', '影视9班');

我们想统计各个班(班级号、班级名)一共有多少人、以及最大的学号

这个 SQL 该如何写?

可是有人会想了,cno 和 cname 本来就是一对一,cno 一旦确定,cname 也就确定了,那 SQL 是不是可以这么写?

1
2
3
sql复制代码SELECT cno,cname,count(sno),MAX(sno) 
FROM tbl_student_class
GROUP BY cno;

可你执行下,会发现报错了

1
sql复制代码[Err] 1055 - Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.tbl_student_class.cname' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

提示信息:SELECT 列表中的第二个表达式 cname 不在 GROUP BY 的子句中,同时它也不是聚合函数;这与 sql 模式:ONLY_FULL_GROUP_BY 不相容

此时你们脑海中是不是冒出一个这样的问题:为什么 GROUP BY 后,SELECT 子句不能直接引用原表(不在 GROUP BY 子句)中的列 ?

非常好,有这个问题就说明你们已经着了我的道了

装逼.jpg

SQL 模式

MySQL 服务器可以在不同的 SQL 模式下运行,并且可以针对不同的客户端以不同的方式应用这些模式,具体取决于 sql_mode 系统变量的值

DBA 可以设置全局 sql_mode 以匹配站点服务器操作要求,并且每个应用程序可以将其会话 sql_mode 设置为其自己的要求

SQL 模式会影响 MySQL 支持的 SQL 语法以及它执行的 数据验证检查,这使得在不同环境中使用MySQL 以及将 MySQL 与其他数据库服务器一起使用变得更加容易

更多详情请查阅官网:Server SQL Modes

MySQL 版本不同,内容会略有不同(包括默认值),查阅的时候注意与自身的 MySQL 版本保持一致

SQL 模式主要分两类:语法支持类 和 数据检查类

语法支持类

常用的如下

ONLY_FULL_GROUP_BY:对于 GROUP BY 聚合操作,如果在 SELECT 中的列、HAVING 或者 ORDER BY 子句的列,没有在 GROUP BY 中出现,那么这个 SQL 是不合法的

ANSI_QUOTES:启用后,不能用双引号来引用字符串,因为它被解释为识别符,作用与 一样。设置它以后,update t set f1=””...,会报Unknown column ‘’ in field list` 这样的语法错误

PIPES_AS_CONCAT:将 || 视为字符串的连接操作符而非 或 运算符,这和 Oracle 数据库是一样的,也和字符串的拼接函数 CONCAT() 相类似

NO_TABLE_OPTIONS:使用 SHOW CREATE TABLE 时不会输出 MySQL 特有的语法部分,如 ENGINE ,这个在使用 mysqldump 跨 DB 种类迁移的时候需要考虑

NO_AUTO_CREATE_USER:字面意思不自动创建用户;在给 MySQL 用户授权时,我们习惯使用 GRANT ... ON ... TO dbuser 顺道一起创建用户,设置该选项后就与 Oracle 操作类似,授权之前必须先建立用户

数据检查类

常用的如下

NO_ZERO_DATE:是否允许 0000-00-00 作为有效日期,也依赖于严格SQL模式是否启用

1
2
3
html复制代码NO_ZERO_DATE 没启用,0000-00-00 无效,插入不会产生警告
NO_ZERO_DATE 被启用,0000-00-00 有效,插入产生警告
NO_ZERO_DATE 和 严格模式 都启用,0000-00-00 无效,insert 会报错,但是 INSERT IGNORE 和 UPDATE IGNORE 认为 0000-00-00 有效(会产生警告)

NO_ZERO_IN_DATE:是否允许年部分为非0但月或日部分为0的日期(简称 0部分日期,例如:2010-00-01 或 2010-01-00)作为有效日期,同样也依赖于严格SQL模式是否启用

1
2
3
4
html复制代码NO_ZERO_IN_DATE 没启用,0部分日期有效,插入不会产生警告
NO_ZERO_IN_DATE 被启用,0部分日期有效被当做 0000-00-00,插入产生警告
NO_ZERO_DATE 和 严格模式 都启用,0部分日期无效,insert 会报错,但是 INSERT IGNORE 和 UPDATE IGNORE 会将 0部分日期 当做 0000-00-00(产生警告)
至于当做 0000-00-00 后,该如何处理,则往上看 NO_ZERO_DATE

NO_ENGINE_SUBSTITUTION:使用 ALTER TABLE 或 CREATE TABLE 指定 ENGINE 时, 需要的存储引擎被禁用或未编译,该如何处理

1
2
html复制代码启用时,直接抛出错误
禁用时,CREATE 用默认的存储引擎替代,ATLER 不进行更改,并抛出警告

STRICT_TRANS_TABLES:设置它,表示启用严格模式;注意 STRICT_TRANS_TABLES 不是几种策略的组合,单独指 INSERT、UPDATE 出现少值或无效值该如何处理

1
2
3
html复制代码前面提到的把 '' 传给 int,严格模式下非法,若启用非严格模式则变成 0,产生一个warning
Out Of Range,变成插入最大边界值
当要插入的新行中,不包含其定义中没有显式 DEFAULT 子句的非NULL列的值时,该列缺少值

默认模式

当我们没有修改配置文件的情况下,MySQL 是有自己的默认模式的

版本不同,默认模式也不同

1
2
3
4
5
sql复制代码-- 查看 MySQL 版本
SELECT VERSION();

-- 查看 sql_mode
SELECT @@sql_mode;

sql_mode.gif

我们可以看到,5.7.21 的默认模式包含:

1
sql复制代码ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

而第一个:ONLY_FULL_GROUP_BY 就会约束:当我们进行聚合查询的时候,SELECT 的列不能直接包含非 GROUP BY 子句中的列

那如果我们去掉该模式呢 ?

宽松模式.gif

我们发现,之前报错的 SQL

1
2
3
sql复制代码SELECT cno,cname,count(sno),MAX(sno) 
FROM tbl_student_class
GROUP BY cno;

能正常执行了,但是一般情况下推荐开启 ONLY_FULL_GROUP_BY,线上环境往往是开启的

虽然案例中,ONLY_FULL_GROUP_BY 是否开启,结果都是对的,那是因为 cno 与 cname 唯一对应的,如果 cno 与 cname 不是唯一对应,那么在未开启的情况下,cname 的值是随机的,这就会造成难以排查的问题,有兴趣的可以去试试

那为什么会有 ONLY_FULL_GROUP_BY 模式呢 ?

问的好,说明你们着道很深了

装大逼.gif

阶

阶(order)是用来区分集合或谓词的阶数的概念

谓词逻辑中,根据输入值的阶数对谓词进行分类,= 或者 BETWEEEN 等输入值为一行的谓词叫作 一阶谓词 ,而像 EXISTS 这样输入值为行的集合的谓词叫作 二阶谓词 (HAVING 的输入值也是集合,但它不是谓词);以此类推,三阶谓词=输入值为 集合的集合 的谓词,四阶谓词=输入值为 集合的集合的集合 的谓词

但是 SQL 里并不会出现三阶以上的情况,所以不用太在意

简单点如下图

阶.png

谈到了阶,就不得不谈下集合论

集合论是 SQL 语言的根基,因为它的这个特性,SQL 也被称为面向集合语言

只有从集合的角度来思考,才能明白 SQL 的强大威力

通过上图,相信大家也都能看到,这里不做更深入的讲解了,有兴趣的可以去查相关资料

为什么聚合后不能再引用原表中的列

很多人都知道聚合查询的限制,但是很少有人能正确地理解为什么会有这样的约束

表 tbl_student_class 中的 cname 存储的是每位学生的班级信息,但需要注意的是,这里的 cname 只是每个学生的属性,并不是小组的属性

而 GROUP BY 又是聚合操作,操作的对象就是由多个学生组成的小组

因此,小组的属性只能是平均或者总和等统计性质的属性,如下图

小组.png

询问每个学生的 cname 是可以的,但是询问由多个学生组成的小组的 cname 就没有意义了

对于小组来说,只有 一共多少学生 或者 最大学号是多少 这样的问法才是有意义的

强行将适用于个体的属性套用于团体之上,纯粹是一种分类错误

而 GROUP BY 的作用是将一个个元素划分成若干个子集,使用 GROUP BY 聚合之后,SQL 的操作对象便由 0 阶的 行 变为了 1 阶的 行的集合 ,此时,行的属性便不能使用了

SQL 的世界其实是层级分明的等级社会,将低阶概念的属性用在高阶概念上会导致秩序的混乱,这是不允许的

此时我相信大家都明白:为什么聚合后不能再引用原表中的列

640 (13).jpg

单元素集合也是集合

现在的集合论认为 单元素集合 是一种正常的集合

单元素集合 和 空集 一样,主要是为了保持理论的完整性而定义的

因此对于以集合论为基础的 SQL 来说,当然也需要严格地区分 元素 和 单元素集合

因此,元素 a 和集合 {a} 之间存在着非常醒目的层级差别:a ≠ {a}

这两个层级的区别分别对应着 SQL 中的 WHERE子句 和 HAVING子句 的区别

WHERE子句 用于处理 行 这种 0 阶的对象,而 HAVING子句 用来处理 集合 这种 1 阶的对象

总结

1、SQL 严格区分层级,包括谓词逻辑中的层级(EXISTS),也包括集合论中的层级(GROUP BY)

2、有了层级区分,那么适用于个体上的属性就不适用于团体了,这也就是:为什么聚合查询的 SELECT 子句中不能直接引用原表中的列 的原因

3、一般来说,单元素集合 的属性和其 唯一元素 的属性是一样的,这种只包含一个元素的集合让人觉得似乎没有必要特意地当成集合来看待,但是为了保持理论的完整性,我们还是要严格区分 元素 和 单元素集合

4、层级是 SQL 标准,而不是针对具体的某个数据库,文中只是用 MySQL 做的案例演示,而非是 MySQL 特有的内容

参考

《SQL基础教程》

《SQL进阶教程》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Swift 中的泛型 Swift 泛型

发表于 2024-04-21

Swift 泛型

hudson 译 原文

Swift 允许我们创建不与任何特定具体类型绑定的泛型类型、协议和函数,与满足一组给定要求的任何类型一起使用。

作为一种强类型安全的语言,泛型是 Swift 的一个核心特性,对于 Swift 的许多方面都非常重要 —— 包括其标准库,后者大量使用了泛型。只需看看一些基本数据结构,比如 Array 和 Dictionary,这两者都是泛型的。

泛型使得同一种类型、协议或函数能够针对大量用例进行特化。例如,由于 Array 是泛型的,它允许为任何类型创建专门的实例 —— 比如字符串:

1
2
3
4
5
6
7
8
swift复制代码var array = ["One", "Two", "Three"]
array.append("Four")

// 由于上面的数组是专门针对字符串的,因此不能插入其他类型的值:
array.append(5)

// 当我们从数组中取出一个元素时,我们仍然可以像正常的字符串一样对待它,因为我们具有完整的类型安全性。
let characterCount = array[0].count

要创建自己的泛型,只需定义泛型类型是什么,以及可选的附加约束。例如, 下面创建一个 Container 类型,它可以包含任何值,以及一个日期:

1
2
3
4
swift复制代码struct Container<Value> {
var value: Value
var date: Date
}

就像能够创建特定的Array和Dictionary一样,我们也可以为任何类型的值创建特定Container,比如字符串或整数:

1
2
swift复制代码let stringContainer = Container(value: "Message", date: Date())
let intContainer = Container(value: 7, date: Date())

请注意,在上面不需要指定要将 Container特定的哪些具体类型 —— Swift 的类型推断会自动推断出stringContainer 是 Container<String> 实例,而 intContainer 是Container<Int> 实例。

当编写可能适用于许多不同类型的代码时,泛型尤其有用。例如,我们可能会使用上面的 Container 来实现一个泛型的 Cache,它可以为任何类型的键存储任何类型的值。在这种情况下,还需要添加了一个约束:Key 需要遵循Hashable,以便可以将其用于字典 —— 就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
swift复制代码class Cache<Key: Hashable, Value> {
private var values = [Key: Container<Value>]()

func insert(_ value: Value, forKey key: Key) {
let expirationDate = Date().addingTimeInterval(1000)

values[key] = Container(
value: value,
date: expirationDate
)
}

func value(forKey key: Key) -> Value? {
guard let container = values[key] else {
return nil
}

// 如果容器的日期是过去的,那么值已经过期,我们将其从缓存中删除。
guard container.date > Date() else {
values[key] = nil
return nil
}

return container.value
}
}

有了以上内容,现在可以为任何类型创建类型安全的缓存 —— 例如用户或搜索结果:

1
2
3
4
5
6
7
8
9
swift复制代码class UserManager {
private var cachedUsers = Cache<User.ID, User>()
...
}

class SearchController {
private var cachedResults = Cache<Query, [SearchResult]>()
...
}

上面的代码中,我们确实需要指定 Cache 具体化的类型,因为编译器无法从调用点推断出该信息。

单个函数无论在何处定义也可以是泛型的。例如,下面代码扩展 String(它不是泛型类型),以添加一个泛型函数,该函数允许轻松地增加数组中所有 Identifiable 值的 ID:

1
2
3
4
5
6
7
swift复制代码extension String {
mutating func appendIDs<T: Identifiable>(of values: [T]) {
for value in values {
append(" \(value.id)")
}
}
}

甚至协议也可以是泛型的!实际上,上面的 Identifiable 协议就是一个例子,因为它使用关联类型来使其能够用任何类型的 ID 类型进行具体化—— 就像这样:

1
2
3
4
5
swift复制代码protocol Identifiable {
associatedtype ID: Equatable & CustomStringConvertible

var id: ID { get }
}

上面的方法使得每个符合 Identifiable 协议的类型都可以决定它想要使用的 ID 类型 —— 同时仍然能够充分利用我们为 Identifiable 类型编写的所有泛型代码(比如上面的 String 扩展)。

例如,这里是 Article 类型使用 UUID 值作为 ID,而 Tag 类型可能只是使用整数(作为ID):

1
2
3
4
5
6
7
8
9
10
swift复制代码struct Article: Identifiable {
let id: UUID
var title: String
var body: String
}

struct Tag: Identifiable {
let id: Int
var name: String
}

上面的技术非常有用。例如与另一个系统(如服务器端后端)兼容时,需要某些数据模型使用特定类型的 ID 。

再次,上述代码中,编译器将完成大部分繁重的工作,因为它将根据每个类型的 id 属性自动推断出 Article.ID 表示 UUID,Tag.ID 表示 Int —— 现在,Article 和 Tag 都可以传递给接受符合 Identifiable 的值的任何函数,同时仍然保持不同的类型,甚至使用自己的不同类型的标识符。

这就是泛型的强大之处,它使我们能够编写更容易重用的代码,同时仍然能够进行局部具体化。算法、数据结构和实用工具通常是泛型的最佳候选 —— 因为它们通常只需要其使用类型满足一定的要求,而不是与特定的具体类型绑定。

感谢阅读!🚀

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript的this绑定

发表于 2024-04-21

内容总览


一.直接调用(独立函数调用/全局调用)


😂直接调用顾名思义就是直接进行函数的调用,怎么直接调用哪? 我们常见的直接调用分为两种情况,如下:

1
2
3
4
5
scss复制代码function foo () {
console.log(this)
}
//进行函数调用
foo()

这是最常见的一种情况,这种情况this在非严格模式的情况下指向的是全局对象,这种调用方式,我们称之为独立函数调用,或者称之为全局调用。

还有另外一种情况是在对象中定义,然后赋值给一个变量,然后单独对这个赋值后的函数进行调用,依然是独立函数调用或者称之为全局调用,非严格模式下指向的是window。

1
2
3
4
5
6
7
8
js复制代码let obj = {
name: "zpj",
foo: function () {
console.log(this)
}
}
let bar = obj.foo
bar()

这种写法,依然属于独立函数调用,指向的依然是window或者说全局对象。

🚨注意:独立函数调用(全局调用)this的指向在严格模式下并不指向window而是指向undefined所以我们在使用的时候千万不要使用this来代替window,而是直接使用window。

二.对象绑定


😈通过对象绑定比较容易理解,因为我们在平时会经常用到,当我们在如下的这种方式进行的时候会指向调用它的对象obj;

1
2
3
4
5
6
7
8
9
js复制代码let obj={
name:"aaa",
foo:function(){
console.log(this);
}
}
obj.foo()

// { name: 'aaa', foo: [Function: foo] }

三.new绑定


🦊在讲解new绑定之前,我们先来看下当我们在new一个对象的时候到底做了什么事情。

  1. 创建新的空对象。
  2. 将this指向这个空对象。
  3. 指向函数体中的代码。
  4. 没有显示返回空对象的时候默认返回这个对象。

😶‍🌫️从第二步我们知道,当我们进行new操作的时候会将this绑定到实例化的这个对象上面。

1
2
3
4
5
6
7
8
9
js复制代码function foo (name) {
this.name = name
console.log(this.name)
}
let bar = new foo("zzz")
console.log(bar)

// zzz
// foo { name: 'zzz' }

通过上述的代码我们可以看到当我们进行对象实例化的时候函数内部的this指向的是实例化的这个对象。

四.this指向绑定事件的元素


1
2
3
4
5
6
7
8
js复制代码<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>
1
2
3
4
5
6
7
8
js复制代码// this 是绑定事件的元素
// target 是触发事件的元素 和 srcElememnt 等价
let colorList = document.getElementById("color-list");
colorList.addEventListener("click", function (event) {
console.log('this:', this);
console.log('target:', event.target);
console.log('srcElement:', event.srcElement);
})

🤡有些时候我们会遇到一些困扰,比如在div节点的事件函数内部,有一个局部的 callback 方法,该方法被作为普通函数调用时,callback 内部的this是指向全局对象 window的.

1
html复制代码<div id="div1">我是一个div</div>
1
2
3
4
5
6
7
8
js复制代码window.id = 'window';
document.getElementById('div1').onclick = function(){
console.log(this.id); // div1
const callback = function(){
console.log(this.id); // 因为是普通函数调用,所以 this 指向 window
}
callback();
}

此时有一种简单的解决方案,可以用一个变量保存 div节点的引用,如下:

1
2
3
4
5
6
7
8
9
js复制代码window.id = 'window';
document.getElementById('div1').onclick = function(){
console.log(this.id); // div1
const that = this; // 保存当前 this 的指向
const callback = function(){
console.log(that.id); // div1
}
callback();
}

五.显式绑定(call/apply)


🥴有的时候this的指向并不能如我们所愿,这个时候我们需要手动去更改this的指向,来满足我们的需求,其实在JavaScript中给我们提供了能够更改this的绑定,首先我们看下call和apply。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码function foo(name,age){
console.log(this);
console.log(name,age);
}
const obj = {
name:"zs",
age:12,
}
foo.call(obj,'ls',30)

// { name: 'zs', age: 12 }
// ls 30

通过call函数我们可以看到我们可以手动的将this绑定到我们新定义的对象上面来,并且通过call单个传参的方式将参数传递给了这个函数,实现了函数的调用和this的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码function foo (name, age) {
console.log(this)
console.log(name, age)
}
const obj = {
name: "zs",
age: 12,
}
foo.apply(obj, ['nnn', 45])

// { name: 'zs', age: 12 }
// ls 30

我们会发现我们使用apply的方式进行绑定的修改结果依然如此,差别在于他们的传参方式不同,call是单个的方式进行传参的,而apply是通过数组的方式传参的。

六.bind函数的显式绑定


🐻bind的绑定和call和apply的使用有些差别,使用bind会生成一个新的函数,这个新函数我们称之为BF绑定函数,我们需要手动对这个函数进行调用。

1
2
3
4
5
6
7
8
9
10
js复制代码function foo(){
console.log("foo",this)
}

let obj = {
name:"why"
}

let bar = foo.bind(obj)
bar()

七.内置函数的调用绑定思考


😂我们在开发中会用到很多内置的函数,比如定时器setTimeout 这个时候我们需要靠经验来判断当前的this指向因为有些东西根本不是我们来调用的,而是函数内部调用的,我们根本不知道他们做了什么,这些内容需要我们自己总结一下,内容如下。

  1. 定时器内部的函数this指向window
1
2
3
4
5
6
js复制代码setTimeout(function () {
console.log(this, "1")
}, 500)
setTimeout(() => {
console.log(this, "2")
}, 500)

  1. 按钮的点击事件,指向事件发起的对象,也就是绑定事件的元素。
1
2
3
4
js复制代码let box = document.querySelector(".test")
box.onclick = function () {
console.log(this)
}

  1. forEach中的this也是指向window
1
2
3
4
js复制代码const array = [1, 2, 3, 4, 5, 6]
array.forEach(item => {
console.log(this)
})

八.绑定优先级的比较


🥴我们前面了解的都是一些独立的规则,但是实际的情况往往是比较复杂的,可能涉及到多个绑定一起使用的情况,这个时候我们就需要研究一下不同函数之间调用的优先级。

  1. 直接调用(默认绑定)的优先级是最低的。
  2. 显式绑定优先级高于对象绑定(隐式绑定)。
1
2
3
4
5
6
7
8
9
10
11
12
js复制代码function foo () {
console.log(this.name)
}
let obj = {
name: "zzz",
bar: foo
}
obj.bar.call({
name: "aaa"
})

// aaa
  1. new绑定优先级高于对象绑定(隐式绑定)的优先级
1
2
3
4
5
6
7
8
9
10
11
js复制代码function foo (name) {
this.name = name
console.log(this.name)
}
let obj = {
name: "zzz",
bar: foo
}
new obj.bar("ccc")

// ccc
  1. new绑定不能和call与apply一起使用,new绑定的优先级比bind高。
1
2
3
4
5
6
7
8
9
js复制代码function foo (name) {
this.name = name
console.log(this.name)
}

let bar = foo.bind("zzz")
new bar("ccc")

// ccc
  1. bind和apply的优先级,bind的优先级更高,也高于call因为call和apply使用方法一样。
1
2
3
4
5
6
7
8
js复制代码function foo () {
console.log(this)
}

let bar = foo.bind("zzz")
bar.call("aaa")

// zzz

九.绑定规则之外


🥴其实在上述的绑定规则之外还有许多我们有时候按照规则难以理解的情况,我们来总结下有哪些情况。

  1. 在显式绑定当中如果传入null/undefined这个绑定会被忽略,使用默认规则,严格模式能够绑定。
1
2
3
4
5
6
7
8
9
js复制代码function foo () {
console.log(this)
}

foo.apply(null)
foo.apply(undefined)

// window
// window
  1. 创建一个函数的间接引用,使用函数的默认绑定规则,指向window(了解)
1
2
3
4
5
6
7
8
9
10
11
12
js复制代码var obj1 = {
name:"obj1",
foo:function(){
console.log("foo",this)
}
}
var obj2={
name:"obj2"
};
(obj2.foo = obj1.foo)()

// window

十.箭头函数的使用


😂我们之前使用函数的方式是这样的。

1
2
3
4
js复制代码// 普通函数方式
function foo1(){}
// 函数表达式方式
let foo2 = function(){}

🐣箭头函数的写法

1
2
3
4
5
6
7
js复制代码// 箭头函数的完整写法
// 1.()函数的参数
// 2.{}函数体
let foo3 = (name,age)=>{
console.log(name);
console.log(age);
}

😙箭头函数与普通函数的区别:箭头函数中没有this和arguments并且不能作为构造函数使用。

🤡箭头函数的优化方式:

  1. 当一个参数的时候可以省略函数参数的小括号。
1
2
3
4
js复制代码let name = ["abc","bca","nba"];
name.forEach(item=>{
console.log(item);
})
  1. 如果函数体中只有一行执行代码{}可以省略,但是一行代码中不能带return;
1
2
js复制代码let name = ["a","b","c"];
name.filter(item=> console.log(item))
  1. 如果函数体中只有一行代码,那么这行代码的返回值会作为整个函数的返回值。
1
2
js复制代码let name = ["a","b","c"];
name.filter(item=>item==='a')
  1. 如果默认返回值是一个对象,那么这个对象必须加小括号
1
js复制代码let arr = ()=>({name:"zzz",age:12})

💗箭头函数使用案例:使用所有nums的所有平方和的值。

1
js复制代码let nums = [20,30,11,15,111]
1
2
3
4
5
6
7
8
9
10
11
12
js复制代码/*
*1.首先调用filter函数然后返回
*2.然后调用map函数然后返回
*3.然后调用reduce计算后返回。
*/
let nums = [20, 30, 11, 15, 111]
let num = nums.filter(item => item % 2 === 0)
.map(item => item * item)
.reduce((pre, cur) => pre + cur, 0)
console.log(num)

// 1300

十一.箭头函数中的this


😁箭头函数中是没有this的,所以this如果在箭头函数中的this就是上级作用域的this。

1
2
3
4
js复制代码let bar =()=>{
console.log(this)
}
// window

👽因为没有this的原因,箭头函数中使用显式绑定也是无法绑定过去的。

1
2
3
4
5
js复制代码let bar =()=>{
console.log(this)
}
bar.call({name:"zzz"})
// window

😭我们再来看下一个案例来明白下this的查找规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码let obj = {
name: "obj",
foo: function () {
let bar = () => {
console.log(this)
}
return bar
}
}
let fn = obj.foo()
fn.call("bbb")

// obj
  1. 首先调用foo函数返回bar定义的函数,箭头函数没有this。
  2. 根据作用域的查找规则,会向上层找this,foo函数中是有作用域的。
  3. 使用call函数是无法更改掉this的。
  4. 所以打印出来this的指向是obj函数。

十二.this常见面试题解析


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
js复制代码const o1 = {
text: 'o1',
fn: function () {
return this.text;
}
}

const o2 = {
text: 'o2',
fn: function () {
return o1.fn();
}
}

const o3 = {
text: 'o3',
fn: function () {
var fn = o1.fn;
return fn();
}
}

console.log(o1.fn()); // o1
console.log(o2.fn()); // o1
console.log(o3.fn()); // undefined

👽解析:首先o1.fn是一个对象调用,所以this指向的是这个对象o1,o2.fn()单调用的时候返回的是o1.fn因此指向的仍然是o1,第三个o1.fn进行了重新赋值然后调用,独立函数调用指向undefined。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码var name = "window";
var person = {
name:"person",
sayName:function(){
console.log(this.name);
}
}
function sayName(){
var sss = person.sayName;
sss(); // window
person.sayName(); // person
(person.sayName)(); // person 等价于 person.sayName()
(b=person.sayName)(); // window
}
sayName();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
js复制代码var name = 'window'
var person1 = {
name:'person1'
foo1:function(){
console.log(this.name)
},
foo2:()=>console.log(this.name),
foo3:function(){
return function(){
console.log(this.name)
}
},
foo4:function(){
return ()=>{
console.log(this.name)
}
}
}

var person2 = {name:'person2'}
person1.foo1(); // person1
person1.foo1.call(person2); // person2

person1.foo2() // window
person1.foo2.call(person2); // window

person1.foo3()(); //window 独立函数调用
person1.foo3.call(person2)() // window // 默认调用
person1.foo3().call(person2) // person2
person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
js复制代码var name = "window"
function Person(name){
this.name = name
this.foo1 = function(){
console.log(this.name)
}
this.foo2 = ()=>console.log(this.name)
this.foo3 = function(){
return function(){
console.log(this.name)
}
}
this.foo4 = function(){
return ()=>{
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1() // person1
person1.foo1.call(person2) //person2
person1.foo2() // person1
person1.foo2.call(person2) // person1
person1.foo3()() // window
person1.foo3.call(perosn2)() // window
person1.foo3().call(person2) // person2
person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码var name = "window"
function Person(name){
this.name = name
this.obj = {
name:'obj',
foo:function(){
return function(){
console.log(this.name)
}
},
foo2:function(){
return ()=>{
console.log(this.name)
}
}
}
}

var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()() // window 独立函数调用
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2
person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

作用域和作用域链

发表于 2024-04-21

一.什么是作用域


🎯作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性,换句话说,作用域决定了代码区块中变量和其他资源的可见性,可能这两句话并不好理解,我们先来看个例子:

1
2
3
4
5
js复制代码function outFun2() {
var inVariable = "内层变量2";
}
outFun2();
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

😭从上面的例子可以体会到作用域的概念,变量 inVariable 在全局作用域没有声明,所以在全局作用域下取值会报错。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。ES6之前 JavaScript没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了“块级作用域”,可通过新增命令 let 和 const 来体现。

二.全局作用域和函数作用域


😁在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域。

  1. 最外层函数和在最外层函数外面定义的变量拥有全局作用域。
1
2
3
4
5
6
7
8
9
10
11
12
js复制代码var outVariable = "我是最外层变量"; //最外层变量
function outFun() { //最外层函数
var inVariable = "内层变量";
function innerFun() { //内层函数
console.log(inVariable);
}
innerFun();
}
console.log(outVariable); // 我是最外层变量
outFun(); // 内层变量
console.log(inVariable); // inVariable is not defined
innerFun(); // innerFun is not defined
  1. 所有未定义直接赋值的变量自动声明为拥有全局作用域。
1
2
3
4
5
6
7
js复制代码function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined
  1. 所有 window 对象的属性拥有全局作用域,一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.top 等等,全局作用域有个弊端:如果我们写了很多行 JS代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会污染全局命名空间, 容易引起命名冲突。
1
2
3
4
5
js复制代码// 张三写的代码中
var data = {a: 100}

// 李四写的代码中
var data = {x: true}

这就是为何 jQuery、Zepto等库的源码,所有的代码都会放在 (function(){….})( ) 中,因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。


👽函数作用域:函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。

1
2
3
4
5
6
7
8
9
js复制代码function doSomething(){
var stuName="zhangsan";
function innerSay(){
console.log(stuName);
}
innerSay();
}
console.log(stuName); // 脚本错误
innerSay(); // 脚本错误

作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行,我们看个例子,用泡泡来比喻作用域可能好理解一点:

最后输出的结果为 2、4、12,泡泡 1 是全局作用域,有标识符 foo,泡泡 2 是作用域 foo,有标识符 a、bar、b,泡泡 3 是作用域 bar,仅有标识符 c,值得注意的是:块语句(大括号“{ }”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

1
2
3
4
5
js复制代码if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'

JS 的初学者经常需要花点时间才能习惯变量提升,而如果不理解这种特有行为,就可能导致 bug 。正因为如此, ES6引入了块级作用域,让变量的生命周期更加可控。

三.块级作用域


🐣块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问,块级作用域在如下情况被创建:在一个函数内部,在一个代码块(由一对花括号包裹)内部。

🎯let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  1. 声明变量不会提升到代码块顶部:let、const 声明并不会被提升到当前代码块的顶部,因此你需要手动将let、const 声明放置到顶部,以便让变量在整个代码块内部可用。
1
2
3
4
5
6
7
8
9
10
js复制代码function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
  1. 禁止重复声明:如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:
1
2
js复制代码var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared

👽在本例中, count 变量被声明了两次:一次使用 var ,另一次使用 let,因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。但如果在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误。

1
2
3
4
5
6
js复制代码var count = 30;
// 不会抛出错误
if (condition) {
let count = 40;
// 其他代码
}

循环中的绑定块作用域的妙用:开发者可能最希望实现 for 循环的块级作用域了,因为可以把声明的计数器变量限制在循环内。例如,以下代码在 JS 经常见到:

1
2
3
4
5
6
7
8
9
10
js复制代码<button>测试1</button>
<button>测试2</button>
<button>测试3</button>

var btns = document.getElementsByTagName('button')
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log('第' + (i + 1) + '个')
}
}

我们要实现这样的一个需求: 点击某个按钮, 提示”点击的是第 n 个按钮”。

此处我们先不考虑事件代理,万万没想到,点击任意一个按钮,后台都是弹出“第四个”。

这是因为 i 是全局变量,执行到点击事件时,此时 i 的值为 3。

那该如何修改,最简单的是用 let 声明 i

1
2
3
4
5
js复制代码for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log('第' + (i + 1) + '个')
}
}

四.作用域链


📆首先认识一下什么叫做自由变量 。如下代码中,console.log(a) 要得到 a 变量,但是在当前的作用域中没有定义 a(可对比一下 b)。当前作用域没有定义的变量,这成为自由变量 。自由变量的值如何得到 ?需要向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)

1
2
3
4
5
6
7
js复制代码var a = 100
function fn() {
var b = 200
console.log(a) // 这里的 a 在这里就是一个自由变量
console.log(b)
}
fn()

😭什么是作用域链哪?如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码var a = 100
function f1() {
var b = 200
function f2() {
var c = 300
console.log(a) // 100 自由变量,顺作用域链向父作用域找
console.log(b) // 200 自由变量,顺作用域链向父作用域找
console.log(c) // 300 本作用域的变量
}
f2()
}
f1()

😁关于自由变量的取值:在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取 ?要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。所以,不要在用以上说法了。相比而言,用这句话描述会更加贴切:要到创建这个函数的那个域”。作用域中取值,这里强调的是“创建”,而不是“调用”,切记切记,其实这就是所谓的”静态作用域”。再来看一个例子:

1
2
3
4
5
6
7
8
js复制代码const food = "rice";
const eat = function () {
console.log(`eat ${food}`);
};
(function () {
const food = "noodle";
eat(); // eat rice
})();

在本示例中,最终打印的结果为 eat rice。因为对于 eat( ) 函数来说,创建该函数时它的父级上下文为全局上下文,所以 food 的值为 rice。如果我们将代码稍作修改,改成如下:

1
2
3
4
5
6
7
8
js复制代码const food = "rice";
(function () {
const food = "noodle";
const eat = function () {
console.log(`eat ${food}`);
};
eat(); // eat noodle
})();

这个时候,打印出来的值就为 eat noodle。因为对于 eat( ) 函数来讲,创建它的时候父级上下文为 IIFE,所以 food 的值为 noodle。

五.作用域与作用域链


👽许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。我们知道 JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样

  1. 解释阶段:词法分析,语法分析,作用域规则确定。
  2. 执行阶段:创建执行上下文,执行函数代码,垃圾回收。

🚨JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是 this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。作用域和执行上下文之间最大的区别是:执行上下文在运行时确定,随时可能改变,作用域在定义时就确定,并且不会改变。


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在生产压力下加速正则表达式

发表于 2024-04-21

如果你能分离正则表达式方法并使用基准测试来检查比较,那么如果你真的需要的话,你可以加速正则表达式。

译自 How to Speed up Regular Expressions under Production Pressure,作者 David Eastman。

在指出使用正则表达式的优势时,我感到有些内疚,因为我在多篇文章中提到了它的优势,却从未提及它的运行速度可能很慢。

在许多应用案例中,正则表达式的速度并不是问题。它只是通过表单捕获一些问题。但是,当速度很重要时,你突然会变成一名侦探,寻找时间杀手。这会迫使你找出哪些代码片段效率低下,但在生产压力下不得不加快速度是一种高难度行为。

我将使用 C# 示例,但最重要的是,你通常必须注意如何在任何你使用的语言中使用正则表达式,并且编译正则表达式等选项可能会有所帮助。

在我比较执行速度时,我必须使用某种基准工具来进行有效的比较。幸运的是,BenchmarkDotNet 已经存在。它适用于控制台应用程序,这正是我们所需要的。

我将继续使用 Visual Studio Code,因为它更适合创建和显示项目,而无需解决方案。为了加快速度,我将使用模板。

打开 Warp,我首先运行以下步骤:

这只是使用可用的 benchmark template 为我们设置一个名为 BenchmarkRegex 的项目,以设置合适的项目框架。我们可以看到生成的位于目录中的文件:

然后,我们可以使用 code . 命令启动 VS Code.

但首先,让我们考虑一下我在之前的文章中运行的一些正则表达式任务。我们使用了一个棘手的模式,它使用交替和 环视(lookaround) 来证明 “i before e except after c” 在英语中经常被打破:

上面的模式通过查找不带 “c” 的 “cie” 或 “ei” 来查找破坏性示例。请注意,环视是正则表达式中可能在不同实现中表现不同的函数之一,应谨慎使用。在这种情况下,我们使用否定后顾 (?<!c) 来确认 “ei” 前面没有 “c”,但不会消耗该 “c”。阅读文章了解更多详情。

我们可以将此示例文本和模式直接放入我们的新模板文件 Benchmark.cs 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码using System; 
using System.Text.RegularExpressions;
using BenchmarkDotNet;
using BenchmarkDotNet.Attributes;
namespace BenchmarkRegex
{
public class Benchmarks
{
private const string Pattern = @"(cie|(?<!c)ei)";
private const string GoodText = "Good: ceiling, receipt, deceive, chief, field, believe.";
private const string BadText = "Bad: species, science, sufficient, seize, vein, weird.";
static bool printMeOnce = false;

[Benchmark]
public void Scenario1()
{
// Implement your benchmark here var
f = Regex.IsMatch(GoodText + BadText, Pattern);
if (!printMeOnce) foreach (Match match in Regex.Matches(GoodText+BadText, Pattern, RegexOptions.None))
Console.WriteLine("Found '{0}' at position {1}", match.Value, match.Index);
printMeOnce = true;
}
}
}

首先,我们检查匹配是否有效,以及它是否捕获了六个案例。

我们只能对发布模式下的控制台应用程序进行基准测试,这很好,因此我们可以在 Warp 命令行中运行 dotnet run -C Release。很快,在日志中,我们得到了六个案例被捕获的确认:

最后,我们得到了基准:

好的,太棒了。当然,我们现在需要回到我们的主题,即加速正则表达式。因此,第一个也是相当明显的方法就是使模式 静态化。既然我们已经确认了模式有效,我们就可以放弃打印输出,毕竟,这使得基准测试非常慢!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码.. 
private const string Pattern = @"(cie|(?<!c)ei)";
private static readonly string StaticPattern = @"(cie|(?<!c)ei)";
..
[Benchmark] public void Scenario1()
{
// Implement your benchmark here
Regex.Matches(GoodText+BadText, Pattern, RegexOptions.None);
}

[Benchmark] public void Scenario2()
{
// Implement your benchmark here
Regex.Matches(GoodText+BadText, StaticPattern, RegexOptions.None);
}
..

因此,我们大致期望第二个场景会快一些。事实确实如此:

(是的,在不打印的情况下,我们处于纳秒级范围。)

现在我们已经测试了基准测试,我们可以测试编译选项:

1
2
3
4
5
6
7
8
9
csharp复制代码private const string Pattern = @"(cie|(?<!c)ei)"; 
private static readonly string StaticPattern = @"(cie|(?<!c)ei)";
private static readonly Regex CompiledRegex = new(Pattern, RegexOptions.Compiled);
..
[Benchmark] public void Scenario3()
{
CompiledRegex.Matches(GoodText+BadText);
}
..

那么,这个基准测试如何?

嗯,大约一半。但这并不是一个明确的结论。在其中和周围发生着许多事情,你需要了解。

当你第一次开始使用 C# 时,你可能还记得了解到它被转换为中间语言(IL 或 MSIL),然后通过即时(JIT)编译编译成操作系统的本机格式。(在 C# 于 2000 年发布时,这似乎有点无关紧要,因为 Microsoft 与 Windows 紧密绑定。)

然而,Regex 会生成自己的节点、解析树和操作,然后将其转换为 IL。请记住,Regex 是比 .NET 更古老的技术——大约早了半个世纪。这在一定程度上解释了为什么在处理它时有特殊规则。

如果没有 Compile 标志,则会将实例化的 Regex 对象解释为上述一组内部操作。当调用对象上的方法(如Match)时,才会将这些操作代码转换为 IL,以便 JIT 编译器可以执行它们。如果进行的 Regex 调用很少,这是可以的。如果 Regex 定义是静态的,则操作代码会得到缓存。默认情况下,最近使用的 15 个操作代码会被缓存。如果你确实使用了许多模式,可以使用Regex.CacheSize属性来更改此设置。

如果使用了 Compile 标志,预编译的正则表达式会增加启动时间,但执行单个模式匹配方法的速度会更快。如果你反复使用某些模式,这是有用的。

你可以创建一个 Regex 对象和模式,对其进行编译,然后将其保存到独立程序集中。你可以调用Regex.CompileToAssembly方法来编译并保存它。但是,在设计时考虑这一点是有意义的,因为你要将应用程序切分成单独的程序集。

总之,明智的认识是,Regex 根本不应在时间关键区域中使用。如果你运行的表达式很少,最好以通常的解释方式完成。如果你经常运行相同的模式,请使用 Compile 标志或将它们放在单独的程序集中。最终,如果你可以隔离 Regex 方法并使用基准测试来检查比较,你就可以在行动中抓住时间杀手。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…363738…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%