wenmo8 发布的文章

Slate文档编辑器-Decorator装饰器渲染调度

在之前我们聊到了基于文档编辑器的数据结构设计,聊了聊基于
slate
实现的文档编辑器类型系统,那么当前我们来研究一下
slate
编辑器中的装饰器实现。装饰器在
slate
中是非常重要的实现,可以为我们方便地在编辑器渲染调度时处理
range
的渲染。

关于
slate
文档编辑器项目的相关文章:

Decorate


slate

decoration
是比较有趣的功能,设想一个场景,当需要实现代码块的高亮时,我们可以有几种方案来实现: 第一种方案是我们可以通过直接将代码块的内容解析的方式,解析出的关键字类别直接写入数据结构中,这样就可以直接在渲染时将高亮信息渲染出来,缺点就是会增加数据结构存储数据的大小;那么第二种方式我们就可以只存储代码信息,当需要数据高亮时也就是前端渲染时我们再将其解析出
Marks
进行渲染,但是这样的话如果存在协同我们还需要为其标记为非协同操作以及无需服务端存储的纯客户端
Op
,会稍微增加一些复杂度;那么第三种方法就是使用
decoration
,实际上可以说这里只是
slate
帮我们把第二种方法的事情做好了,可以在不改变数据结构的情况下将额外的
Marks
内容渲染出来。

当然,我们使用装饰器的场景自然不只是代码块高亮,凡是涉及到不希望在数据结构中表达却要在渲染时表现的内容,都需要使用
decoration
来实现。还有比较明显的例子就是查找能力,如果在编辑器中实现查找功能,那么我们需要将查找到的内容标记出来,这个时候我们就可以使用
decoration
来实现,否则就需要绘制虚拟的图层来完成。而如果需要实现用户态的超链接解析功能,即直接贴入连接的时候,我们希望将其自动转为超链接的节点,也可以利用装饰器来实现。

在前段时间测试
slate
官网的
search-highlighting example
时,当我搜索
adds
时,搜索的效果很好,但是当我执行跨节点的搜索时,就不能非常有效地突出显示内容了,具体信息可以查看
https://github.com/ianstormtaylor/slate/pull/5670
。这也就是说当
decoration
执行跨节点处理的时候,是存在一些问题的。例如下面的例子,当我们搜索
123
或者
12345
时,我们能够正常将标记出的
decoration
渲染出来,然而当我们搜索
123456
时,此时我们构造的
range
会是
path: [0], offset: [0-6]
,此时我们跨越了
[0]
节点进行标记,就无法正常标记内容了。

[
    { text: "12345" },
    { text: "67890" }
]

通过调用查找相关代码,我们可以看到上级的
decorate
结果会被传递到后续的渲染中,那么在本级同样会调度传递的
decorate
函数来生成新的
decorations
,并且这里需要判断如果父级的
decorations
与当前节点的
range
存在交集的话,那么内容会被继续传递下去。那么重点就在这里了,试想一下我们的场景,依旧以上述的例子中的内容为例,如果我们此时想要获取
123456
的索引,那么在
text: 12345
这个节点中肯定是不够的,我们必须要在上层数组中将所有文本节点的内容拼接起来,然后再查找才可以找到准确的索引位置。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/hooks/use-children.tsx#L21
const useChildren = (props: {
  decorations: Range[]
  // ...
}) => {
  // ...

  for (let i = 0; i < node.children.length; i++) {
    // ...
    const ds = decorate([n, p])
    for (const dec of decorations) {
      const d = Range.intersection(dec, range)
      if (d) {
        ds.push(d)
      }
    }
    // ...
  }
  // ...
}

那么此时我们就明确需要我们调用
decorate
的节点是父级元素,而父级节点传递到我们需要处理的
text
节点时,就需要
Range.intersection
来判断是否存在交集,实际上这里判断交集的策略很简单,在下面我们举了两个例子,分别是存在交集和不存在交集的情况,我们实际上只需要判断两个节点的最终状态即可。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/range.ts#L118

// start1          end1          start2          end2
// end1          start2
// end1 < start2 ===> 无交集

// start1          start2          end1          end2
// start2          end1
// start2 < end1 ===> 有交集 [start2, end1]

那么我们可以通过修改在
decorate
这部分代码中的
Range.intersection
逻辑部分来解决这个问题吗,具体来说就是当我们查找出的内容超出原本
range
的内容,则截取其需要装饰的部分,而其他部分舍弃掉,实际上这个逻辑在上边我们分析的时候已经发觉是没有问题的,也就是当我们查找
123456
的时候是能够将
12345
这部分完全展示出来的。根据前边的分析,本次循环我们的节点都在
path: [0]
,这部分代码会将
start: 0

end: 5
这部分代码截取
range
并渲染。

跨节点问题

然而我们在下一个
text range
范围内继续查找
6
这部分就没有那么简单了,因为前边我们实际上查找的
range

path: [0], offset: [0-6]
,而第二个
text
的基本
range

path: [1], offset: [0-5]
,基于上述判断条件的话我们是发现是不会存在交集的。因此如果需要在这里进行处理的话,我们就需要取得前一个
range
甚至在跨越多个节点的情况下我们需要向前遍历很多节点,当
decorations
数量比较多的情况下我们需要检查所有的节点,因为在此节点我们并不知道前一个节点是否超越了本身节点的长度,这种情况下在此处的计算量可能比较大,或许会造成性能问题。

因此我们还是从解析时构造
range
入手,当跨越节点时我们就需要将当前查找出来的内容分割为多个
range
,然后为每个
range
分别置入标记,还是以上边的数据为例,此时我们查找的结果就是
path: [0], offset: [0, 5]

path: [1], offset: [0, 1]
两部分,这种情况下我们在
Range.intersection
时就可以正常处理交集了,此时我们的
path
是完全对齐的,而即使完全将内容跨越,也就是搜索内容跨越不止一个节点时,我们也可以通过这种方式来处理。

// https://github.com/ianstormtaylor/slate/pull/5670
const texts = node.children.map(it => it.text)
const str = texts.join('')
const length = search.length
let start = str.indexOf(search)
let index = 0
let iterated = 0
while (start !== -1) {
  while (index < texts.length && start >= iterated + texts[index].length) {
    iterated = iterated + texts[index].length
    index++
  }
  let offset = start - iterated
  let remaining = length
  while (index < texts.length && remaining > 0) {
    const currentText = texts[index]
    const currentPath = [...path, index]
    const taken = Math.min(remaining, currentText.length - offset)
    ranges.push(/* 构造新的`range` */)
    remaining = remaining - taken
    if (remaining > 0) {
      iterated = iterated + currentText.length
      offset = 0
      index++
    }
  }
  start = str.indexOf(search, start + search.length)
}

此外,我们在调度装饰器的时候,需要关注在
renderLeaf
参数
RenderLeafProps
的值,因为在这里存在两种类型的文本内容,即
leaf: Text;
以及
text: Text;
基本
TextInterface
类型。而在我们通过
renderLeaf
渲染内容时,以高亮的代码块内值
mark
节点的渲染为例,我们实际渲染节点需要以
leaf
为基准而不是以
text
为基准,例如当在渲染
mark

bold
样式产生重叠时,这两种节点都需要以
leaf
为基准。

在这里的原因是,
decorations

slate
实现中是以
text
节点为基准将其拆分为多个
leaves
,然后再将
leaves
传递到
renderLeaf
中进行渲染。因此实际上在这里可以这么理解,
text
属性为原始值,而
leaf
属性为更细粒度的节点,调度
renderLeaf
的时候本身也是以
leaf
为粒度进行渲染的,当然在不使用装饰器的情况下,这两种属性的节点类型是等同的。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/components/text.tsx#L39
const leaves = SlateText.decorations(text, decorations)
const key = ReactEditor.findKey(editor, text)
const children = []

for (let i = 0; i < leaves.length; i++) {
  const leaf = leaves[i]

  children.push(
    <Leaf
    isLast={isLast && i === leaves.length - 1}
    key={`${key.id}-${i}`}
    renderPlaceholder={renderPlaceholder}
    leaf={leaf}
    text={text}
    parent={parent}
    renderLeaf={renderLeaf}
    />
  )
}

最后

在这里我们主要讨论了
slate
中的
decoration
装饰器的实现,以及在实际使用中可能会遇到的问题,主要是在跨节点的情况下,我们需要将
range
拆分为多个
range
,然后分别进行处理,并且还分析了源码来探究了相关问题的实现。那么在后边的文章中我们就主要聊一聊在
slate

Path
的表达,以及在
React
中是如何控制其内容表达与正确维护
Path
路径与
Element
内容渲染的方案。

Blog

https://github.com/WindRunnerMax/EveryDay

下面推荐5个基于.NetCore开发的CMS开源项目。

一、OrchardCore

基于ASP.NET Core 构建的、模块化和多租户应用程序框架,采用
文档数据库,非常高性能,跨平台的系统。

1、跨平台:这是基于.Net Core开发的系统,可以部署在Docker, Windows, Linux, Mac。

2、数据库:兼容MySQL、Sqlserver、Sqlite、PostgreSQL,提供文档数据库存储与查询等相关操作的API,对于CMS系统来说,这是一个非常好的方案,极大地提升性能。

3、可视化:支持可视化编辑网站模板。

4、模板引擎:支持 Liquid 模板语言的。

5、自定义查询:可通过配置SQL、Lucene 自定义模块,或者对外提供API。

6、扩展性:这是一个多租户系统,可通过一次部署托管多个网站。

7、GraphQL:提供灵活的GraphQL API,可以供外部系统调用。

Star:7.5K⭐

项目地址:

https://github.com/OrchardCMS/OrchardCore

图片

二、Umbraco CMS

开发者团队是来源于丹麦,经过多年的发展,已经成为全球比较知名并被广泛使用的CMS,它以友好的用户体验和高度可定制性而出名,非常适合用于开发各种类型网站项目,比如开发企业网站、电子商务系统、企业管理系统等。

1、操作简单:具有非常直观的用户界面,方便管理员轻松地管理和发布内容;

2、高度可定制性:提供了各种视图组件方便创建各种页面,同时提供了API和模板引擎方便开发者来创建独特的网站布局和设计,以满足复杂的业务需求;

3、内容管理:提供强大的内容管理功能,包括版本控制、内容发布、媒体库管理,通过以上功能,让用户客户轻松管理各种内容类型,从文本、图片、多媒体文件等;

4、多语言支持:支持包含中文在内的20几个国家语言包,方便用户构建全球化的网站;

5、权限控制:具有灵活的权限控制功能,管理员可以根据业务需求创建不同的用户角色权限;

6、搜索功能:内置了强大的搜索功能,方便用户快速搜索和筛选网站内容,提供了全文搜索、标签搜索、分类搜索等多种方式,还提供搜索结果的排序等功能;

7、集成第三方解决方案:集成了多种第三方插件,比如SEO工具、营销插件等,方便企业通过集成第三方插件和模块,来扩展网站的功能;

8、社区:拥有一个非常活跃的用户社区,相关问题都可以在上面交流分享、寻找问题解决方案等。

Star:4.6K⭐

项目地址:

https://github.com/umbraco/Umbraco-CMS

图片

三、Piranha CMS

一个轻量级且跨平台的CMS库,专为.NET 8设计。

该项目提供多种模板,具备CMS基本功能,也有空模板方便从头开始构建新网站,甚至可以作为移动应用的后端。

是一个完全解耦的CMS,意味着我们可以使用任何技术以任何方式构建的应用程序。

1、基于.Net 8开发。

2、数据库支持:SQL数据库(本地和Azure)、SQLite、MySQL、PostgreSQL和Azure Cosmos DB等。

3、技术栈:Asp.net Mvc Core、EntityFrameworkCore、JQuery、Bootstrap。

Star:2K⭐

项目地址:

https://github.com/PiranhaCMS/piranha.core

图片

四、Cofoundry

采用代码优先开发、具备可扩展且灵活的架构、简单易用的内容管理系统。

1、基于.Net 8开发。

2、数据库支持:SqlServer (Express) 2016+, 或者 Azure SQL。

3、技术栈:Asp.net Mvc Core、EntityFrameworkCore、JQuery、Bootstrap。

Star:841⭐

项目地址:

https://github.com/cofoundry-cms/cofoundry

图片

五、MixCoreCMS

基于.NET Core框架的开源内容管理系统(CMS),提供了丰富的的基础功能和插件,包括文章发布、图片管理、文件管理、评论管理、标签管理等,可以满足大部分网站的需求。

集成了Google Analytics分析,以及友好的Seo功能,非常适合用于创建企业网站、内容系统、个人博客,也可以用于开发企业管理类型的系统。

1、数据库:支持MySQL, SQL Server, PostgreSQL, SQLite;

2、基于ASP.Net Core 6 / Dotnet Core 6、SignalR、Angular 和 Bootstrap开发。

3、支持多语言版本;

4、支持跨平台部署,可以在Windows、Linux、Mac等平台上运行;

5、支持界面皮肤个性化定制。

Star:828⭐

项目地址:

https://github.com/mixcore/mix.core

图片

- End -

更多开源项目:
https://github.com/bianchenglequ/NetCodeTop

一:背景

1. 讲故事

准备明年把
.NET高级调试的训练营
的课程进行重构,采用案例引导式,而CPU爆高类有不少是程序员在写代码的时候不注意时间复杂度,在数据量稍微大一点的情况直接幻化成了死循环,时间复杂度这东西是学校的数据结构课程,有心的朋友在写多层循环的时候脑子里面一定要过一遍,今天就给大家带一篇此类案例,也算是继续丰富我的新课程体系。

前些天有位朋友找到我,说他们的网站会有CPU瞬高的情况,在网上找相关资料最终找到我这边,想让我帮忙分析下咋回事?像这种CPU瞬高,比较好的方式就是用procdump自动化的抓取,万不可手工去抓,接下来就上 windbg 分析吧。

二:WinDbg分析

1. 为什么会CPU爆高

以终为始,先看看CPU是否真的高,可以用
!tp

!cpuid
命令观察,这里稍微提一下,为什么要用
!cpuid
看看CPU的能力呢?这是因为我曾经分析过只有 2core 的CPU。尼玛,只有2个core,还分析个毛线哈,干脆把机器关了,这样CPU就不高了。。。自此以后我就留了一个心眼,输出参考如下:


0:033> !tp
CPU utilization: 100%
Worker Thread: Total: 5 Running: 5 Idle: 0 MaxLimit: 32767 MinLimit: 4
Work Request in Queue: 11
    Unknown Function: 00007ffbfaa417d0  Context: 00000283733c3718
    Unknown Function: 00007ffbfaa417d0  Context: 0000027f26f50cb0
    Unknown Function: 00007ffbfaa417d0  Context: 0000028377199f58
    AsyncTimerCallbackCompletion TimerInfo@0000028371c46820
    AsyncTimerCallbackCompletion TimerInfo@0000028371d06800
    Unknown Function: 00007ffbfaa417d0  Context: 00000283756d3248
    Unknown Function: 00007ffbfaa417d0  Context: 0000027f26f63578
    Unknown Function: 00007ffbfaa417d0  Context: 00000283733d0160
    Unknown Function: 00007ffbfaa417d0  Context: 00000283756a72d8
    Unknown Function: 00007ffbfaa417d0  Context: 00000283771a6828
    Unknown Function: 00007ffbfaa417d0  Context: 000002837719d1f8
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 3 Free: 2 MaxFree: 8 CurrentLimit: 2 MaxLimit: 1000 MinLimit: 4
0:033> !cpuid
CP  F/M/S  Manufacturer     MHz
 0  6,106,6  <unavailable>   2800
 1  6,106,6  <unavailable>   2800
 2  6,106,6  <unavailable>   2800
 3  6,106,6  <unavailable>   2800

从卦中可以看出当前线程池队列稍有积压,5个托管线程全部被打满,并且当前机器是4个核,看样子是有4个线程在满负荷跑呀。。。

2. 线程都在干什么

为了追踪线程都在干什么?使用
~*e !clrstack
观察各个线程的调用栈,结合程序的瞬高特性,捕获了一个相对来说高度可疑的代码,参考如下:


OS Thread Id: 0x2f00 (33)
        Child SP               IP Call Site
000000f2a42fd508 00007ffbfd72b0a7 System.String.Equals(System.String, System.String) [f:\dd\ndp\clr\src\BCL\system\string.cs @ 647]
000000f2a42fd510 00007ffba1715a0b xxx.StockAsyncDbTask+c__DisplayClass4_1.b__6(xxx.GoodsInfo)
000000f2a42fd540 00007ffba118c6ca System.Linq.Enumerable.FirstOrDefault[[System.__Canon, mscorlib]](System.Collections.Generic.IEnumerable`1<System.__Canon>, System.Func`2<System.__Canon,Boolean>)
000000f2a42fd5b0 00007ffba1716008 xxx.StockAsyncDbTask+c__DisplayClass4_0.b__2(xxx.GoodsInfo)
000000f2a42fd670 00007ffbfd720505 System.Collections.Generic.List`1[[System.__Canon, mscorlib]].ForEach(System.Action`1<System.__Canon>) [f:\dd\ndp\clr\src\BCL\system\collections\generic\list.cs @ 553]
000000f2a42fd6c0 00007ffba1349e7e xxx.SaveStockToDb()
000000f2a42fd760 00007ffba13487ed xxx.DoWork()
000000f2a42fd7b0 00007ffba1348631 xxx.QuartzScheduler.QuartzJob.Quartz.IJob.Execute(Quartz.IJobExecutionContext)
000000f2a42fd8b0 00007ffba0f8ca12 Quartz.Core.JobRunShell+d__9.MoveNext()
000000f2a42fdb80 00007ffba0f83150 System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start[[Quartz.Core.JobRunShell+d__9, Quartz]](d__9 ByRef) [f:\dd\ndp\clr\src\BCL\system\runtime\compilerservices\AsyncMethodBuilder.cs @ 322]
000000f2a42fdc30 00007ffba0f8309d Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken)
000000f2a42fdd30 00007ffba0f829f4 Quartz.Core.QuartzSchedulerThread+c__DisplayClass28_0.b__0()
000000f2a42fdd60 00007ffbfd7abe4e System.Threading.Tasks.Task`1[[System.__Canon, mscorlib]].InnerInvoke() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Future.cs @ 680]
000000f2a42fddb0 00007ffbfd7aaf27 System.Threading.Tasks.Task.Execute() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498]
000000f2a42fddf0 00007ffbfd73df12 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980]
000000f2a42fdec0 00007ffbfd73dd95 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928]
000000f2a42fdef0 00007ffbfd7ab1e1 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827]
000000f2a42fdfa0 00007ffbfd7aa8c1 System.Threading.Tasks.Task.ExecuteEntry(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767]
000000f2a42fdfe0 00007ffbfd708e46 System.Threading.ThreadPoolWorkQueue.Dispatch() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820]

根据卦中的显示找到了问题方法,为了保护客户隐私,这里稍微会模糊一下,主要是看下复杂度的骨架结构。

从卦象看里面至少包含了3层for循环,所以时间复杂度是 O(N3) 次方,学过数据结构和算法的朋友应该知道,这个复杂度不得了,要逆天了。。。

3. O(N3) 是祸根吗?

要想知道 O(N3) 是不是祸根,得要看有没有给它不停的施肥翻土,可以找找相关的集合,使用
!dso
命令观察即可。


0:033> !dso
OS Thread Id: 0x2f00 (33)
RSP/REG          Object           Name
rbx              0000028227b4be70 xxx.GoodsInfo
000000F2A42FD520 0000028029c02038 System.Collections.Generic.List`1[[xxx.GoodsInfo, xxx.Model]]
...
000000F2A42FD708 0000027f277d3de8 System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[xxx_DataGrab, xxx.Data]]

0:033> !do 0000027f277d3de8
Name:        System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[xxx_DataGrab,xxx.Data]]
MethodTable: 00007ffba12b82f8
EEClass:     00007ffbfd345c10
Size:        80(0x50) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffbfd1d8538  4001887        8       System.Int32[]  0 instance 0000027f27d51328 buckets
00007ffbfe422618  4001888       10 ...non, mscorlib]][]  0 instance 0000027f27d51350 entries
00007ffbfd1d85a0  4001889       38         System.Int32  1 instance                1 count
00007ffbfd1d85a0  400188a       3c         System.Int32  1 instance                1 version
00007ffbfd1d85a0  400188b       40         System.Int32  1 instance               -1 freeList
00007ffbfd1d85a0  400188c       44         System.Int32  1 instance                0 freeCount
00007ffbfd1c7790  400188d       18 ...Canon, mscorlib]]  0 instance 00000282274c1978 comparer
00007ffbfd1c57c0  400188e       20 ...Canon, mscorlib]]  0 instance 0000027f27cfc630 keys
00007ffbfd1eef60  400188f       28 ...Canon, mscorlib]]  0 instance 0000000000000000 values
00007ffbfd1d5dd8  4001890       30        System.Object  0 instance 0000000000000000 _syncRoot

0:033> !do 0000028029c02038
Name:        System.Collections.Generic.List`1[[xxx, xxx.Model]]
MethodTable: 00007ffba126e830
EEClass:     00007ffbfd362af8
Size:        40(0x28) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffbfd1ee250  40018a0        8     System.__Canon[]  0 instance 00000283278195b0 _items
00007ffbfd1d85a0  40018a1       18         System.Int32  1 instance            21863 _size
00007ffbfd1d85a0  40018a2       1c         System.Int32  1 instance                0 _version
00007ffbfd1d5dd8  40018a3       10        System.Object  0 instance 0000000000000000 _syncRoot
00007ffbfd1ee250  40018a4        8     System.__Canon[]  0   static  <no information>

从卦中可以看到第一层的dictionary只有1条记录,第二层的 List 有高达 2.1w 数据,第三层的 dbStocks 在线程栈没有找到,我也懒得找到了,起码发现了第二层的 list 确实比较大,加上数据的佐证,基本上就找到了问题所在,也满足程序的瞬高的现象。

4. 解决方案

知道了复杂度高,优化的方向就是降低时间复杂度,将 O(N3) 降低到 O(N),方法就是在深层循环之前提前用 Dictionary 或者 HashSet 来预存数据,将后面的 for 循环变成字段的key查找,而key查找则是 O(1)。

为了让大家有个宏观概念,我让 chatgpt 给我生成一个 O(N2) 到 O(N) 的例子,参考代码如下:


public class GoodsInfo
{
    public int Spid { get; set; }
    // 其他属性...
}
 
public class Stock
{
    public int Spid { get; set; }
    // 其他属性...
}
 
public class Optimizer
{
    // 原始O(N^2)复杂度的查找方法
    public List<GoodsInfo> FindMatchingGoodsInfoO_N2(List<GoodsInfo> goodsInfos, List<Stock> stocks, int targetSpid)
    {
        List<GoodsInfo> result = new List<GoodsInfo>();
        foreach (var goodsInfo in goodsInfos)
        {
            foreach (var stock in stocks)
            {
                if (goodsInfo.Spid == stock.Spid && stock.Spid == targetSpid)
                {
                    result.Add(goodsInfo);
                }
            }
        }
        return result;
    }
 
    // 优化后的O(N)复杂度的查找方法
    public List<GoodsInfo> FindMatchingGoodsInfoO_N(List<GoodsInfo> goodsInfos, List<Stock> stocks, int targetSpid)
    {
        HashSet<int> stockSpids = new HashSet<int>(stocks.Select(s => s.Spid));
        List<GoodsInfo> result = new List<GoodsInfo>();
 
        foreach (var goodsInfo in goodsInfos)
        {
            if (stockSpids.Contains(goodsInfo.Spid) && goodsInfo.Spid == targetSpid)
            {
                result.Add(goodsInfo);
            }
        }
 
        return result;
    }
}

可以看到 chatgpt 很聪明,用 HashSet 来化煞。

三:总结

说实话像这种生产事故,我以前在公司的项目中也会偶发的遇到,都是赶时间,加班加点写出来的代码,只想把功能写出来早点下班,复杂度高?后面再说吧。。。代码写的太好,容易被老板优化。。。
图片名称

“程序怎么运行不了,不应该啊?”

“程序怎么能运行了,不应该啊!”

这句话是不是让程序员朋友们的 DNA 动了呢?今天鱼皮分享一些新手程序员常犯的小 Bug,很多是我自己或者网友们的亲身经历,相信绝大多数程序员都写过这些 Bug~

程序员经典小 Bug

1、标点符号错误

刚学编程语言的很多同学应该都被这个错误折磨过,比如在代码中使用中文逗号(

)或引号(
“”
),结果就导致了编译错误。

// 使用了中文逗号,编译会报错
Map<String, Integermap new HashMap<>();
map.put("key1", 1);

我之前就遇到过一位同学,把类似上面的代码拍了个照,然后问我哪里有错,我当时快把眼珠子瞪出来了,也没发现问题:

结果后面他自己发现问题了,我知道真相后直接红温了。

其实这类 Bug 很好自己解决,开发工具都会给出提示的,只不过由于新手不知道要去看错误信息罢了。

2、更新数据没指定范围

现在的数据库操作框架封装得太好了,以至于很多同学都不怎么自己写 SQL,查询语句可能还写过一点儿,但更新语句基本上没写过。这就导致了很多低级问题,比如在更新或删除数据时,忘记加上 WHERE 条件。像之前我分享过,我们团队一位同学更新某个用户权限的时候,不小心把所有用户的权限都刷成了 “管理员”。

UPDATE orders SET role 'admin';

一般有经验的开发者看到数据更新或删除操作,就条件反射想到要加 WHERE 条件:

UPDATE orders SET role 'admin'
WHERE id = xxx

企业中通常也会给数据库加上限制,防止范围更新和删除。

3、资源忘记释放

在开发中,文件、数据库连接、内存、网络连接都属于资源,如果打开了资源没有释放,就有可能因为资源泄露导致程序崩溃,很多线上 Bug 都是这么来的。

比如打开一个文件,却没有关闭:

public void readFile(String path) throws IOException {
   FileReader reader new FileReader(path);
   char[] buffer new char[1024];
   reader.read(buffer);
   // 忘记关闭文件
}

平时要养成好的习惯,只要打开了资源,都要看看有没有 close 方法。如果有的话,再确认该资源会不会自动关闭;如果不会自动关闭,就要手动释放资源。

在 Java 中,可以在 finally 块中、或者使用
try-with-resources
语法自动释放资源:

public void readFile(String path) throws IOException {
   try (FileReader reader new FileReader(path)) {
       char[] buffer new char[1024];
       reader.read(buffer);
  }
}

4、明文存储隐私数据

这也是一类低级错误,比如在数据库中明文存储用户的密码:

INSERT INTO users (usernamepassword)
VALUES ('admin''123456');

好好好,这下管理员爽翻了!

标准做法是,使用哈希算法 + 盐值加密存储密码:

String hashedPassword BCrypt.hashpw("123456", BCrypt.gensalt());

虽然这个错误很低级,但可千万别小看它。某公司因为明文存储密码被处罚了 9100 万欧元!

类似的错误还有直接从前端明文发送密码给后端,虽然可以通过 HTTPS 协议增强安全性,但 HTTPS 只保证传输加密,服务端和客户端仍能看到密码明文,攻击者可能通过日志窃取密码。

5、前端存储秘钥

这也是一类低级错误,经常出现于调用第三方 API 的时候。

比如需要调用一个第三方天气服务 API,为了省事,前端直接将秘钥写到了 JS 代码中:

// 第三方 API 秘钥
const API_KEY "yupi123456";
// 调用 API
async function getWeather(city) {
 const url `https://codefather.cn/weather?city=${city}&apikey=${API_KEY}`;
 const response await fetch(url);
 const data await response.json();
 console.log(data);
}

这样一来,用户直接打开 F12 控制台,就能看到你的秘钥了,即使对 JS 代码加密混淆,也能轻而易举被找到。

所有前端的内容都是不安全的。
如果有调用第三方 API 的需求,最好还是通过后端进行转发。

6、忘记区分环境

刚在企业中接触多环境的同学,可能会不小心把测试环境的代码或配置部署到生产环境。

比如 Java 项目使用
application.yml
文件来管理配置,测试代码时,我先把数据库改为测试库:

spring:
datasource:
  url: jdbc:mysql://localhost:3306/dev_db

结果上线前,忘了把配置改回来,导致线上环境找不到这个数据库或者因为网络隔离无法连接。

标准的做法是,通过配置文件后缀区分多环境,在启动项目时指定对应的环境值即可。比如
application-dev.yml
表示开发环境、
application-prod.yml
表示生产环境。

7、强行合并或推送代码

我见过一些急性子的同学,在提交或推送代码的时候遇到了代码冲突,觉得麻烦就强行合并或推送了。

# 忽略代码冲突,强行合并
git merge branch-feature --strategy-option=theirs

# 强行推送,覆盖远程代码
git push --force

此举可谓图一时之快,但后患无穷矣。

很快你的同事就会找上门:我的码呢?

你的领导也会找上门:没通过审核的代码怎么就推到主分支了?

所以遇到代码冲突之后,一定要仔细处理冲突,不要强行合并或推送,除非你能接受这么做的最坏结果。

对于管理者,最好在代码管理平台中开启保护分支,禁止成员把未审核通过的代码直接推送到主分支。

8、提交敏感信息

很多朋友的数据保护意识是比较差的,尤其是刚接触 Git 代码提交的同学,可能一不小心,就把包含了数据库账号密码的配置文件提交到 GitHub 等开源平台了,开源精神令人感动。

不信的话,你可以在 GitHub 搜索和秘钥有关的关键词,一抓一大把。

我自己也经历过这事,曾经提供了一个免费的图床给编程导航的同学使用,结果有不止一个人把我的图床秘钥开源到了 GitHub 上。

好在有些大厂的云服务会自动检测你有没有将秘钥提交到开源平台,如果出现了,会给你发送邮件。

解决这个问题的方法也很简单,我们可以准备两套配置文件,一套开源,一套自用,在 Git 中忽略掉自用配置文件的提交即可。


OK 就分享到这里,大家还见过哪些常见的、或者 “有点儿东西” 的 Bug 呢?欢迎评论区分享~

更多编程学习资源

ChangeSpeed
类是
Manim
库中用于修改动画速度的类。

它提供了一种灵活的方式来控制动画的播放速度,使动画在不同时间段内以不同的速度播放,从而创造出更加丰富多样的动画效果。

比如,在创建包含多个元素动画的场景中,通过
ChangeSpeed
可以精确控制不同元素在不同时间点的移动速度,实现复杂的动画节奏编排。

1. 动画概述

与之前介绍的那些动画类不同,
ChangeSpeed
不是为元素实现某种具体的动画效果,

它的作用对象是动画本身,它的主要特点有:

  1. 灵活的速度调整
    :可以指定动画在不同时间对应的速度因子,也就是在动画的不同阶段设置不同的速度
  2. 与多种动画类型结合
    :不仅可以用于修改单个动画的速度,还能处理
    AnimationGroup
    类型的动画,将多个动画组合成一个整体进行速度控制
  3. 支持速率函数覆盖
    :通过
    rate_func
    参数可以覆盖传入动画的速率函数,从而进一步自定义动画的速度变化曲线

主要参数有:

参数名称 类型 说明
anim Animation 需要修改速度的动画对象
speedinfo dict 指定在不同时间点的动画速度
rate_func func 覆盖传入动画的速率函数,在修改速度之前应用
affects_speed_updaters bool 决定是否影响通过
add_updater
方法添加的更新器的速度

speedinfo
参数是一个字典,

它的
key
是动画运行时间的百分比(0 到 1 之间的浮点数),
value
是对应的速度因子。

affects_speed_updaters
参数一般在做联动动画时使用的,用来决定是否影响联动动画的速度。

ChangeSpeed
还有一些方法,可以用于全面控制动画的执行流程、时间相关属性和相关对象的处理。

名称 说明
add_updater 用于将速度变化应用于更新器
begin 在动画开始时被调用时,进行初始化操作
finish 动画结束时被调用
update_mobjects 更新相关对象,如起始对象和(对于变换动画)目标对象
clean_up_from_scene 在动画结束后清理场景
get_scaled_total_time 返回动画所花费的时间
interpolate 在动画的每一帧被调用,用于设置动画的进度

2. 使用示例

下面通过示例来演示如何使用
ChangeSpeed
类来丰富动画的表现效果。

2.1. 变速移动的点

此示例在场景中创建了两个点,然后使用
ChangeSpeed
类来控制其移动动画。

通过
speedinfo
参数,设置第一个先减速移动,然后再加速移动,最后再次减速移动。

第二点与第一个点的速度变化正好相反,并且两个点在运动过程中都采用线性速率函数。

d1 = Dot(color=BLUE)
d2 = Dot(color=RED)
self.add(d1, d2)

# 先慢后快再慢
self.play(
    ChangeSpeed(
        d1.animate(run_time=2).shift(RIGHT * 5),
        speedinfo={0.2: 0.5, 0.3: 2, 0.8: 0.5},
        rate_func=linear,
    ),
)

# 先快后慢再快
self.play(
    ChangeSpeed(
        d2.animate(run_time=2).shift(LEFT * 5),
        speedinfo={0.2: 2, 0.3: 0.5, 0.8: 2},
        rate_func=linear,
    ),
)

2.2. 变速旋转的正方形

先添加了一个正方形,然后利用
ChangeSpeed
类来调整其旋转动画。

根据
speedinfo
设定,正方形在动画开始的前
30%
时间内慢速旋转(速度因子为
0.5
),之后在
70%~100%
时间内快速旋转(速度因子为
2
),

同时使用平滑的速率函数,产生了变速旋转的动画效果。

square = Square()

# 正方形先慢速旋转 然后快速旋转
self.play(
    ChangeSpeed(
        Rotate(square, angle=PI, run_time=3),
        speedinfo={0.3: 0.5, 0.7: 2},
        rate_func=smooth,
    )
)

2.3. 结合更新器的变速缩放

首先创建了一个圆,并定义了一个更新器函数,使圆在每一帧渲染时按一定比例持续缩放。

接着通过
ChangeSpeed
类的静态方法
add_updater
将更新器应用到圆上,并根据
speedinfo
参数,使得更新器在动画中间部分速度变慢,从而实现了圆的变速缩放效果。

注意
,这里要设置
affects_speed_updaters=True
,否则圆会匀速放大,使得
speedinfo
参数无效。

circle = Circle()
self.add(circle)

# 定义一个更新器函数,使圆持续缩放
def update_circle(circle, dt):
    circle.scale(1 + 0.1 * dt)

# 将更新器添加到圆上,并使用ChangeSpeed控制更新器的速度
ChangeSpeed.add_updater(circle, update_circle)
self.play(
    ChangeSpeed(
        Wait(4),
        speedinfo={0.3: 2, 0.4: 0.4, 0.7: 0.4, 1: 2},
        affects_speed_updaters=True,
    )
)

2.4. 动画组中元素的不同速度

在场景中添加了两条水平方向且一上一下的直线,通过
ChangeSpeed
类作用于
AnimationGroup
来实现不同速度的动画效果。

因为
speedinfo
参数只能设置一种速度因子,为了使两条直线的变色速度不一样,这里使用了一个技巧,设置两条直线变色的
run_time
不一样。

这样,即使它们的
speedinfo
参数一样,最终还是以不同节奏来变色。

l1 = Line(LEFT * 3, RIGHT * 3).shift(UP)
l2 = Line(LEFT * 3, RIGHT * 3).shift(DOWN)
self.add(l1, l2)

# 创建一个动画组,使两条线以不同的速度变色
self.play(
    ChangeSpeed(
        AnimationGroup(
            l1.animate(run_time=2).set_color(RED),
            l2.animate(run_time=4).set_color(BLUE),
        ),
        speedinfo={
            0.3: 1,
            0.6: 0.5,
            1: 1,
        },
        rate_func=linear,
    )
)

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
change_speed.py
),

下载地址:
完整代码
(访问密码: 6872)