Elixir实战:8 容错基础知识 (3) 监督者
myzbx 2025-01-21 20:00 24 浏览
监督者是一个通用进程,用于管理系统中其他进程的生命周期。监督者进程可以启动其他进程,这些进程被视为其子进程。通过链接、监视器和退出陷阱,监督者可以检测任何子进程的可能终止,并在需要时重新启动它。
不属于监督者的进程称为工作进程。这些是提供系统实际服务的进程。您当前版本的待办事项系统仅由工作进程组成,例如待办事项缓存和待办事项服务器进程。
如果任何工作进程崩溃,可能是由于一个错误,您系统的某些部分将永远消失。这就是监督者可以提供帮助的地方。通过在监督者下运行工作进程,您可以确保失败的进程被重启,并恢复系统的服务。
要做到这一点,系统中至少需要一个监督进程。在 Elixir 中,可以使用 Supervisor 模块 (https://hexdocs.pm/elixir/Supervisor.xhtml) 来实现。通过调用 Supervisor.start_link/2 ,您可以启动监督进程,随后其工作方式如下:
- 监控进程捕获退出,然后启动子进程。
- 如果在任何时间点,子进程终止,监控进程会收到相应的退出消息并执行纠正措施,例如重启崩溃的进程。
- 如果监督进程终止,它的子进程也会被终止。
启动监督者有两种不同的方法。在基本方法中,您调用函数 Supervisor.start_link ,传递一个描述要在监督者下启动的每个子项的列表,以及一些额外的监督者选项。或者,您可以传递一个定义回调函数的模块,该函数返回这些信息。我们将从基本方法开始,稍后再解释第二种版本。
让我们向待办事项系统介绍一位监督者。图 8.3 回顾了系统中的这些过程:
- Todo.Server —允许多个客户端在单个待办事项列表上协作
- Todo.Cache —维护一个待办服务器的集合,并负责它们的创建和发现
- Todo.DatabaseWorker —对数据库执行读写操作
- Todo.Database —管理数据库工作者池并将数据库请求转发给他们
待办缓存过程是系统的入口点。当您启动缓存时,所有所需的进程都会启动,因此缓存可以被视为系统的根。现在,我们将介绍一个新的监督进程,它将监督待办缓存过程。
8.3.1 准备现有代码
在开始与主管工作之前,您需要对缓存进行一些更改。首先,您将注册缓存进程。这将允许您与该进程进行交互,而无需知道其 PID。
您还需要在启动待办缓存过程时创建一个链接。如果您希望在监视器下运行该过程,这是必需的。为什么监视器使用链接而不是监控?因为链接是双向的,因此监视器的终止意味着它的所有子进程将自动被终止。这反过来允许您正确终止系统的任何部分,而不会留下悬挂的进程。在本章和下一章中,您将看到这如何运作,当您处理更细粒度的监视时。
创建与调用进程的链接就像用 GenServer.start_link 替代 GenServer.start 一样简单。在此过程中,您还可以将相应的 Todo.Cache 接口函数重命名为 start_link 。
最后,您将使 start_link 函数接受一个参数并忽略它。这看起来有些混乱,但这使得启动一个监督过程变得稍微容易一些。原因将在稍后讨论子规范时解释。更改显示在以下列表中。
清单 8.1 待办事项缓存的变化 (supervised_todo_cache/lib/todo/cache.ex)
defmodule Todo.Cache do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def server_process(todo_list_name) do
GenServer.call(__MODULE__, {:server_process, todo_list_name})
end
def init(_) do
IO.puts("Starting to-do cache.")
...
end
...
end
重命名接口函数
以名称注册并链接到调用进程
使用注册名称的接口功能
调试消息
请注意,您还可以从 init/1 回调中调用 IO.puts/1 以进行调试。此调试表达式包含在所有其他 GenServer 回调模块中( Todo.Database 、 Todo.DatabaseWorker 和 Todo.Server )。
8.3.2 启动监督进程
通过这些更改,您可以立即尝试启动监督进程,待办缓存作为其唯一子进程。将当前文件夹更改为 supervised_todo_cache,并启动 shell ( iex -S mix )。现在,您可以启动监督者:
iex(1)> Supervisor.start_link([Todo.Cache], strategy: :one_for_one)
Starting to-do cache.
Starting database server.
Starting database worker.
Starting database worker.
Starting database worker.
启动一个监督者和待办事项缓存作为其子项
从控制台输出可以看出,调用 Supervisor.start_link/2 导致待办事项缓存开始。缓存进程随后启动了数据库进程。
让我们仔细看看 Supervisor.start_link/2 的调用:
Supervisor.start_link(
[Todo.Cache],
strategy: :one_for_one
)
子规格列表
监督者策略
如函数名称所示, Supervisor.start_link/2 启动一个监督进程并将其链接到调用者。
第一个参数是所需子项的列表。更准确地说,这个列表的每个元素都是一个子项规范,描述了如何启动和管理子项。我们稍后会详细讨论子项规范。在这种简单形式中,提供的子项规范是一个模块名称。在这种情况下,子项由 Todo.Cache 模块中的某个回调函数描述。
当监督进程启动时,它将遍历此列表并根据规范启动每个子进程。在此示例中,监督将调用 Todo.Cache.start_link/1 。一旦所有子进程启动, Supervisor.start_link/2 将返回 {:ok, supervisor_pid} 。
Supervisor.start_link/2 的第二个参数是特定于监督者的选项列表。 :strategy 选项,也称为重启策略,是必需的。此选项指定监督者应如何处理其子进程的终止。 one_ for_one 策略表示如果一个子进程终止,则应启动另一个子进程来替代它。还有其他几种策略(例如,“如果单个子进程崩溃,则重启所有子进程”),我们将在第 9 章中讨论它们。
注意 这里的重启一词使用得比较随意。从技术上讲,进程无法被重启。它只能被终止;然后,可以在其位置启动另一个由同一模块驱动的进程。新进程具有不同的 PID,并且与旧进程不共享任何状态。
无论如何,在 Supervisor.start_link/2 返回后,系统中的所有必需进程都在运行,您可以与系统进行交互。例如,您可以启动一个待办事项服务器:
iex(2)> bobs_list = Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.161.0>
缓存进程作为监督进程的子进程启动,因此我们说它是被监督的。这意味着如果缓存进程崩溃,它的监督者将重新启动它。
您可以通过引发缓存进程的崩溃快速验证这一点。首先,您需要获取缓存的 PID。如前所述,缓存现在以一个名称(它自己的模块名称)注册,因此可以借助 Process.whereis/1 轻松获取其 PID:
iex(3)> cache_pid = Process.whereis(Todo.Cache)
#PID<0.155.0>
现在,您可以使用 Process.exit/2 函数终止进程,该函数接受一个 PID 和退出原因,然后向给定进程发送相应的退出信号。退出原因可以是任意术语。在这里,您将使用原子 :kill ,它以特殊方式处理。退出原因 :kill 确保目标进程被无条件终止,即使该进程正在捕获退出。让我们看看它的实际效果:
iex(4)> Process.exit(cache_pid, :kill)
Starting to-do cache.
如您从输出中所见,过程立即重新启动。您还可以证明待办缓存现在是一个具有不同 PID 的进程:
iex(5)> Process.whereis(Todo.Cache)
#PID<0.164.0>
您可以像使用旧流程一样使用新流程:
iex(6)> bobs_list = Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.167.0>
这个简短的实验证明了一些基本的容错能力。在崩溃之后,系统自我修复并恢复了完整的服务。
名称允许流程发现
重要的是要解释为什么将待办事项缓存注册为本地名称。您应该始终记住,要与进程进行通信,您需要拥有其 PID。在第 7 章中,您使用了一种简单的方法,创建了一个进程,然后传递其 PID。这在您进入监控者领域之前是可以的。
问题在于,受监督的进程可以被重启。请记住,重启归结为用一个新进程替代旧进程——新进程有一个不同的 PID。这意味着对崩溃进程的 PID 的任何引用都变得无效,标识了一个不存在的进程。
这就是注册名称重要的原因。它们提供了一种可靠的方式来查找进程并与之交互,无论可能的进程重启。
8.3.3 子规范
要管理子进程,监督者需要一些信息,例如以下问题的答案:
- 孩子应该如何开始?
- 如果孩子终止了该怎么办?
- 应该使用什么术语来唯一标识每个孩子?
这些信息统称为子规范。回想一下,当调用 Supervisor.start_link/2 时,您发送了一份子规范列表。在其基本形态中,规范是一个映射,包含几个字段来配置子项的属性。
例如,待办事项缓存的规范可能如下所示:
%{
id: Todo.Cache,
start: {Todo.Cache, :start_link, [nil]},
}
子项的 ID
启动规范
:id 字段是一个任意术语,用于区分该子项与同一主管的其他子项。
:start 字段是形状为 {module, start_function, list_of_ arguments} 的三元组。在启动子进程时,通用监督代码将使用 apply(module, start_function, list_of_arguments) 来调用由此元组描述的函数。被调用的函数必须启动并链接该进程。
您可以省略规范中的一些其他字段,在这种情况下,将选择一些合理的默认值。我们将在第 9 章稍后讨论其中的一些。您还可以参考官方文档 https://hexdocs.pm/elixir/Supervisor.xhtml#module-child-specification 以获取更多详细信息。
无论如何,您可以将规格图直接传递给 Supervisor.start_link 。以下是一个示例:
Supervisor.start_link(
[
%{
id: Todo.Cache,
start: {Todo.Cache, :start_link, [nil]}
}
],
strategy: :one_for_one
)
这将指示主管调用 Todo.Cache.start_link(nil) 来启动子进程。请记住,您已将 Todo.Cache.start_link 更改为接受一个参数(该参数被忽略),因此您需要传递某个值(在此示例中为 nil )。
这种方法的一个问题是容易出错。如果缓存的实现发生变化,例如启动函数的签名,您需要记住在启动监控程序的代码中调整规范。
为了解决这个问题, Supervisor 允许您在子规范列表中传递一个元组 {module_name, arg} 。在这种情况下, Supervisor 将首先调用 module_name .child_spec(arg) 以获取实际的规范。此函数必须返回规范映射。然后,监督者根据返回的规范启动子进程。
Todo.Cache 模块已经定义了 child_spec/1 ,即使您没有自己编写。默认实现是由 use GenServer 注入的。因此,您也可以以以下方式启动监督者:
Supervisor.start_link(
[{Todo.Cache, nil}],
strategy: :one_for_one
)
因此, Supervisor 将调用 Todo.Cache.child_spec(nil) 并根据返回的规范启动子进程。验证注入的 child_spec/1 实现返回的内容很简单:
iex(1)> Todo.Cache.child_spec(nil)
%{id: Todo.Cache, start: {Todo.Cache, :start_link, [nil]}}
换句话说,生成的 child_spec/1 返回一个规范,该规范调用模块的 start_link/1 函数,并将参数传递给 child_spec/1 。这正是你让 Todo.Cache.start_link 接受一个参数的原因,尽管该参数被忽略:
defmodule Todo.Cache do
use GenServer
def start_link(_) do
...
end
...
end
生成默认的 child_spec/1
符合默认的 child_spec/1
通过这样做,您使 Todo.Cache 与生成的 child_spec/1 兼容,这意味着您可以将 Todo.Cache 包含在子项列表中,而无需进行任何额外的工作。
如果您不喜欢这种方法,您可以向 use GenServer 提供一些选项,以调整生成的 child_spec/1 的输出。有关更多详细信息,请参阅官方文档(https://hexdocs.pm/elixir/GenServer.xhtml#module-how-to-supervise)。如果您需要更多控制,您可以自己定义 child_spec/1 ,这将覆盖默认实现。
最后,如果您不关心传递给 child_spec/1 的参数,您可以在子规范列表中仅包含模块名称。在这种情况下, Supervisor 将向 child_spec/1 传递空列表 [] 。因此,您也可以像这样启动 Todo.Cache :
Supervisor.start_link(
[Todo.Cache],
strategy: :one_for_one
)
在进一步之前,让我们回顾一下监督者启动的工作原理。当你调用 Supervisor.start_link(child_specs, options) 时,以下情况发生:
- 新过程已启动,由 Supervisor 模块提供动力。
- 监督进程逐一遍历子规范列表,并启动每个子进程。
- 每个规范在需要时通过调用相应模块中的 child_spec/1 来解决。
- 监督者根据子规范的 :start 字段启动子进程。
8.3.4 包装监督者
到目前为止,您已经在 shell 中与 supervisor 进行了交互。但在实际应用中,您会希望在代码中使用 supervisor。就像使用 GenServer 一样,建议将 Supervisor 包装在一个模块中。
以下列表实现了您第一个监督者的模块。
清单 8.2 待办事项系统监督者 (supervised_todo_cache/lib/todo/system.ex)
defmodule Todo.System do
def start_link do
Supervisor.start_link(
[Todo.Cache],
strategy: :one_for_one
)
end
end
通过这个简单的添加,启动整个系统变得容易:
$ iex -S mix
iex(1)> Todo.System.start_link()
Starting to-do cache.
Starting database server.
Starting database worker.
Starting database worker.
Starting database worker.
名称 Todo.System 被选用来描述模块的目的。通过调用 Todo.System.start_link() ,您可以启动整个待办事项系统,包含所有必需的服务,如缓存和数据库。
8.3.5 使用回调模块
另一种启动监督者的方法是提供回调模块。这的工作方式类似于 GenServer 。您需要开发一个必须实现 init/1 函数的模块。该函数必须返回子规范的列表和其他监督者选项,例如其策略。
这里是您如何重写 Todo.System 以使用这种方法:
defmodule Todo.System do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, nil)
end
def init(_) do
Supervisor.init([Todo.Cache], strategy: :one_for_one)
end
end
包含一些常见的模板内容
使用 Todo.System 作为回调模块启动监督者
实现所需的回调函数
与 GenServer 一样,您从 use Supervisor 开始,以便在您的模块中获得一些通用的模板代码。
关键部分发生在您调用 Supervisor.start_link/2 时。您现在传递的是回调模块,而不是子规范的列表。在这种情况下,监督进程将调用 init/1 函数以提供监督规范。传递给 init/1 的参数是您传递给 Supervisor .start_link/2 的第二个参数。最后,在 init/1 中,您借助 Supervisor .init/2 函数描述监督者,传递给它子项列表和监督者选项。
前面的代码是 Supervisor.start_ link([Todo.Cache], strategy: :one_for_one) 的更复杂的等效形式。显然,您需要更多的代码行才能获得相同的效果。好的一面是,这种方法给您更多的控制。例如,如果您需要在启动子进程之前进行一些额外的初始化,您可以在 init/1 中做到。此外,回调模块在热代码重载方面更灵活,允许您修改子进程列表而无需重新启动整个监视器。
在大多数情况下,直接传递子规格列表的简单方法就足够了。此外,正如您在前面的示例中看到的,如果将 Supervisor 的使用封装在一个专用模块中,切换不同的方法就很容易。因此,在本书中,您将仅使用简单方法,而不使用回调模块。
8.3.6 连接所有过程
在这一点上,您正在监督待办缓存过程,因此您获得了一些基本的容错能力。如果缓存过程崩溃,将启动一个新过程,系统可以恢复提供服务。
然而,您当前实现中存在一个问题。当主管重新启动待办事项缓存时,您将获得一个完全独立的进程层次结构,并且会有一组与之前的待办事项服务器毫无关系的新待办事项服务器。之前的待办事项服务器将成为未使用的垃圾,仍在运行并消耗内存和 CPU 资源。
让我们演示这个问题。首先,启动系统并请求一个待办事项服务器:
iex(1)> Todo.System.start_link()
iex(2)> Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.159.0>
缓存的待办服务器在后续请求中未启动:
iex(3)> Todo.Cache.server_process("Bob's list")
#PID<0.159.0>
检查正在运行的进程数量:
iex(4)> length(Process.list())
71
现在,终止待办缓存:
iex(5)> Process.exit(Process.whereis(Todo.Cache), :kill)
Starting to-do cache.
最后,请求一个待办事项服务器用于鲍勃的列表:
iex(6)> Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.165.0>
如您所见,在您重启待办事项缓存后,检索先前获取的服务器会创建一个新进程。这并不令人惊讶,因为您终止了先前的缓存进程,这也销毁了进程状态。
当一个进程终止时,它的状态被释放,新进程以全新的状态开始。如果你想保留状态,必须自己处理;我们将在第 9 章讨论这个问题。
在缓存过程重新启动后,您将拥有一个完全新的进程,它对之前缓存的内容没有任何概念。同时,您的旧缓存结构(待处理服务器)并没有被清理。您可以通过重新检查正在运行的进程数量来看到这一点:
iex(7)> length(Process.list())
72
您有一个额外的进程,即之前为 Bob 的待办事项列表启动的待办服务器。这显然不好。终止待办缓存会破坏其状态,因此您还应该关闭所有现有的待办服务器。这样,您可以确保正确的进程终止。
要做到这一点,您必须在进程之间建立链接。每个待办事项服务器必须与缓存相连。进一步来说,您还需要将数据库服务器与待办事项缓存连接,并将数据库工作者与数据库服务器连接。这将有效确保整个结构相互链接,如图 8.4 所示。
通过链接一组相互依赖的过程,您可以确保一个过程的崩溃也会导致其依赖项崩溃。无论哪个过程崩溃,链接都确保整个结构被终止。由于这将导致缓存过程的终止,监控者会注意到这一点,并会启动一个新的系统。
通过这种方法,您可以检测系统中任何部分的错误并从中恢复,而不会留下悬挂的进程。缺点是,您允许错误产生广泛的影响。单个数据库工作者或待办服务器中的错误将导致整个结构崩溃。这远非完美,您将在第 9 章中进行改进。
目前,让我们坚持这种简单的方法并实现所需的代码。在您当前的系统中,您有一个待办事项监督者,它启动并监督缓存。您必须确保缓存与所有其他工作进程直接或间接相连。
更改很简单。您只需将项目中所有流程的 start 切换为 start_link 。在相应的模块中,您当前有如下内容:
def start(...) do
GenServer.start(...)
end
此代码片段必须转换为以下内容:
def start_link(...) do
GenServer.start_link(...)
end
当然,每个 module.start 调用必须替换为 module .start_link 。这些更改是机械的,代码在这里没有呈现。完整的解决方案位于 todo_links 文件夹中。
让我们看看新系统是如何工作的:
iex(1)> Todo.System.start_link()
iex(2)> Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
iex(3)> length(Process.list())
71
iex(4)> Process.exit(Process.whereis(Todo.Cache), :kill)
iex(5)> bobs_list = Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
iex(6)> length(Process.list())
71
终止整个过程结构
进程计数保持不变。
当您崩溃一个进程时,整个结构会被终止,并且一个新的进程会在其位置启动。链接确保相关的进程也被终止,从而保持系统的一致性。
8.3.7 重启频率
重要的是要记住,监督者不会无限期地重启子进程。监督者依赖于最大重启频率,该频率定义了在给定时间段内允许多少次重启。默认情况下,最大重启频率为 5 秒内 3 次重启。您可以通过将 :max_restarts 和 :max_seconds 选项传递给 Supervisor.start_link/2 来更改这些参数。如果超过此频率,监督者将放弃并终止自己及其所有子进程。
让我们在 shell 中验证这一点。首先,启动 supervisor:
iex(1)> Todo.System.start_link()
Starting the to-do cache.
现在,您需要频繁重启待办事项缓存进程:
iex(1)> for _ <- 1..4 do
Process.exit(Process.whereis(Todo.Cache), :kill)
Process.sleep(200)
end
在这里,您终止缓存进程并短暂休眠,允许主管重新启动该进程。这一过程重复四次,这意味着在最后一次迭代中,您将超过默认的最大重启频率(5 秒内三次重启)。
这里是输出:
Starting the to-do cache.
Starting database server.
...
** (EXIT from #PID<0.149.0>) :shutdown
重复三次
主管终止。
在超过最大重启频率后,监控程序放弃并终止,同时也关闭了子进程。
您可能会想知道这个机制的原因。当系统中的一个关键进程崩溃时,它的监控者会尝试通过启动一个新进程将其重新上线。如果这没有帮助,那么无限重启就没有意义。如果在给定的时间间隔内发生了太多次重启,很明显问题无法解决。在这种情况下,监控者能做的唯一明智的事情就是放弃并终止自己,这也会终止它的所有子进程。
该机制在所谓的监督树中发挥着重要作用,在这些树中,监督者和工作人员被组织在更深的层次结构中,这使您能够控制系统如何从错误中恢复。下一章将对此进行详细解释,您将在其中构建一个细粒度的监督树。
摘要
- 运行时错误有三种类型:抛出、错误和退出。
- 当运行时错误发生时,执行会向上移动到相应的 try 块。如果错误未被处理,进程将崩溃。
- 可以在另一个进程中检测到进程终止。为此,您可以使用链接或监视器。
- 链接是双向的——任一进程的崩溃都会传播到另一个进程。
- 默认情况下,当一个进程异常终止时,所有与之链接的进程也会终止。通过捕获退出,您可以对链接进程的崩溃做出反应并采取相应措施。
- 监督者是一个管理其他进程生命周期的进程。它可以启动、监督和重启崩溃的进程。
- Supervisor 模块用于启动监督者并与之协作。
- 一个监督者由子规范列表和监督策略定义。您可以将这些作为参数提供给 Supervisor.start_link/2 ,或者您可以实现一个回调模块。
相关推荐
- 零基础入门AI智能体:详细了解什么是变量类型、JSON结构、Markdown格式
-
当品牌跳出固有框架,以跨界联动、场景创新叩击年轻群体的兴趣点,一场关于如何在迭代中保持鲜活的探索正在展开,既藏着破圈的巧思,也映照着与新一代对话的密码。在创建AI智能体时,我们会调用插件或大模型,而在...
- C# 13模式匹配:递归模式与属性模式在真实代码中的性能影响分析
-
C#13对模式匹配的增强让复杂数据处理代码更简洁,但递归模式与属性模式的性能差异一直是开发者关注的焦点。在实际项目中,选择合适的模式不仅影响代码可读性,还可能导致执行效率的显著差异。本文结合真实测试...
- 零基础快速入门 VBA 系列 6 —— 常用对象(工作簿、工作表和区域)
-
上一节,我介绍了VBA内置函数以及如何自动打字和自动保存文件。这一节,我们来了解一下Excel常用对象。Excel常用对象Excel有很多对象,其中最常用也最重要的包括以下3个:1.Workbo...
- 不同生命数字的生肖龙!准到雷普!
-
属龙的人总在自信爆棚和自讨苦吃之间反复横跳?看完这届龙宝宝的日常我悟了。属龙的人好像天生自带矛盾体:领导力超强可人缘时好时坏,工作雷厉风行却总在爱情里翻车。关键年份的龙性格差异更大——76年龙靠谱但不...
- 仓颉编程语言基础-面向对象编程-属性(Properties)
-
属性是仓颉颉中一种强大的机制,它允许你封装对类(或接口interface、结构体struct、枚举enum、扩展extend)内部状态的访问。它看起来像一个普通的成员变量(字段),但在其背后,它通过...
- Python中class对象/属性/方法/继承/多态/魔法方法详解
-
一、基础入门:认识类和对象1.类和对象的概念在Python中,类(class)是一种抽象的概念,用于定义对象的属性和行为,而对象(也称为实例)则是类的具体表现。比如,“汽车”可以是一个类,它有...
- VBA基础入门:搞清楚对象、属性和方法就成功了一半
-
如果你刚接触VBA(VisualBasicforApplications),可能会被“对象”“属性”“方法”这些术语搞得一头雾水。但事实上,这三个概念是VBA编程的基石。只要理解它们之间的关系,...
- P.O类型文推荐|年度编推合集(一百九十五篇)
-
点击左上方关注获取更多精彩推文目录2019年度编推35篇(1V1)《悖论》作者:流苏.txt(1V1)《桂花蒸》作者:大姑娘浪.txt(1V1)《豪门浪女》作者:奚行.txt...
- Python参数传递内存大揭秘:可变对象 vs 不可变对象
-
90%的Python程序员不知道,函数参数传递中可变对象的修改竟会导致意想不到的副作用!一、参数传递的本质:对象引用传递在Python中,所有参数传递都是对象引用的传递。这意味着函数调用时传递的不是对...
- JS 开发者必看!TC39 2025 最新动向,这些新语法要火?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。TC39第...
- 2025 年值得尝试的 5 个被低估的 JavaScript 库
-
这些JavaScript库可能不会在社交媒体或HackerNews上流行起来,但它们会显著提高您的工作效率和代码质量。JavaScript不再只是框架。虽然React、Vue和Sv...
- Python自动化办公应用学习笔记30—函数的参数
-
一、函数的参数1.形参:o定义:在函数定义时,声明在函数名后面括号中的变量。o作用:它们是函数内部的占位符变量,用于接收函数被调用时传入的实际值。o生命周期:在函数被调用时创建,在函数执...
- 16种MBTI人格全解析|测完我沉默了三秒:原来我是这样的人?
-
MBTI性格测试火了这么久,你还不知道自己是哪一型?有人拿它当社交话题,有人拿它分析老板性格,还有人干脆当成择偶参考表。不废话,今天我一次性给你整理全部16种MBTI人格类型!看完你不仅能知道自己是谁...
- JS基础与高级应用: 性能优化
-
在现代Web开发中,性能优化已成为前端工程师必须掌握的核心技能之一。本文从URL输入到页面加载完成的全过程出发,深入分析了HTTP协议的演进、域名解析、代码层面性能优化以及编译与渲染的最佳实践。通过节...
- 爱思创CSP-J/S初赛模拟赛线上开赛!助力冲入2024年CSP-J/S复赛!
-
CSP-J/S组初赛模拟赛爱思创,专注信奥教育19年,2022年CSP-J/S组赛事指定考点,特邀NOIP教练,开启全真实CSP-J/S组线上初赛模拟大赛!一、比赛对象:2024年备考CSP-J/S初...
- 一周热门
- 最近发表
-
- 零基础入门AI智能体:详细了解什么是变量类型、JSON结构、Markdown格式
- C# 13模式匹配:递归模式与属性模式在真实代码中的性能影响分析
- 零基础快速入门 VBA 系列 6 —— 常用对象(工作簿、工作表和区域)
- 不同生命数字的生肖龙!准到雷普!
- 仓颉编程语言基础-面向对象编程-属性(Properties)
- Python中class对象/属性/方法/继承/多态/魔法方法详解
- VBA基础入门:搞清楚对象、属性和方法就成功了一半
- P.O类型文推荐|年度编推合集(一百九十五篇)
- Python参数传递内存大揭秘:可变对象 vs 不可变对象
- JS 开发者必看!TC39 2025 最新动向,这些新语法要火?
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)