跳到主要内容

· 阅读需 2 分钟

前言

在本文中,我们将探讨 JavaScript 中一些缺失的数学方法,以及我们如何为它们编写函数。

JavaScript Math 对象包含一些非常有用且功能强大的数学运算,可用于 Web 开发,但它缺少大多数其他语言提供的许多重要运算(例如 Haskell,其中有大量运算)。

JavaScript 中缺少数学方法:求和

你可能记得在学校里,“sum”是“add”的同义词。例如,如果我们将数字 1、2 和 3 相加,它实际上意味着1 + 2 + 3。

我们的sum函数将涉及对数组中的所有值求和。

编写这个函数有两种方法:我们可以使用for循环,或者我们可以使用reduce函数。如果您想重新熟悉该reduce函数,可以阅读有关在 JavaScript 中使用 map()reduce() 的信息。

使用for循环:

function sum(array){
let total = 0
for(let count = 0; count < array.length; count++){
total = total + array[count]
}
return total
}

· 阅读需 4 分钟

前言

之前在 github 偶然发现了facebook开源的 docusaurus发现了很多不错的功能:

  • MDX 提供支持.: 节省时间并专注于文本文档。只需使用 MDX 编写文档和博客文章,Docusaurus 就会将它们构建成静态 HTML 文件,以便提供服务。借助 MDX,您甚至可以在 Markdown 中嵌入 React 组件。
  • 使用 React 构建: 通过编写 React 组件来扩展和自定义项目的布局。利用可插拔架构,设计您自己的网站,同时重用 Docusaurus 插件创建的相同数据。
  • 提供翻译功能:本地化是开箱即用的。使用 gitCrowdin 或任何其他翻译管理器来翻译您的文档并单独部署它们。
  • 文档版本控制: 支持项目所有版本的用户。文档版本控制可帮助您使文档与项目发布保持同步。
  • 支持内容搜索:让您的社区可以轻松地在您的文档中找到他们需要的内容。支持 Algolia 文档搜索。

本节将记录如何通过 docusaurus 搭建个人主页。

准备工作

安装项目脚手架

npx create-docusaurus@latest my-website classic

亦可使用typesecript

npx create-docusaurus@latest my-website classic --typescript
替换安装命令

您还可以使用首选的项目管理器初始化新项目:

npm init docusaurus

项目结构

假设您选择了经典模板并将网站命名为 my-website,您将会在新目录 my-website/ 下看到下列文件:

my-website
├── blog
│ ├── 2019-05-28-hola.md
│ ├── 2019-05-29-hello-world.md
│ └── 2020-05-30-welcome.md
├── docs
│ ├── doc1.md
│ ├── doc2.md
│ ├── doc3.md
│ └── mdx.md
├── src
│ ├── css
│ │ └── custom.css
│ └── pages
│ ├── styles.module.css
│ └── index.js
├── static
│ └── img
├── docusaurus.config.js
├── package.json
├── README.md
├── sidebars.js
└── yarn.lock

解释项目结构

  • /blog/ - 包含博客的 Markdown 文件。 详情可参考
  • /docs/ - 包含文档的 Markdown 文件。 您可在 sidebars.js 中自定义文档的侧边栏顺序。 详情可参考
  • /src/ - 如页面或自定义 React 组件一类的非文档文件。
  • /static/ - 静态目录。 此处的所有内容都将被复制进 build 文件夹的根目录中
  • /docusaurus.config.js - 站点配置文件。
  • /package.json - Docusaurus 网站是一款 React 应用程序。 您可以安装并使用任何 npm 软件包
  • /sidebars.js - sidebar 的相关配置

· 阅读需 21 分钟

课程链接:课程概览与 shell · the missing semester of your cs education (missing-semester-cn.github.io)

动机

作为计算机科学家,我们都知道计算机最擅长帮助我们完成重复性的工作。 但是我们却常常忘记这一点也适用于我们使用计算机的方式,而不仅仅是利用计算机程序去帮我们求解问题。 在从事与计算机相关的工作时,我们有很多触手可及的工具可以帮助我们更高效的解决问题。 但是我们中的大多数人实际上只利用了这些工具中的很少一部分,我们常常只是死记硬背一些如咒语般的命令, 或是当我们卡住的时候,盲目地从网上复制粘贴一些命令。

本课程意在帮你解决这一问题。

我们希望教会您如何挖掘现有工具的潜力,并向您介绍一些新的工具。也许我们还可以促使您想要去探索(甚至是去开发)更多的工具。 我们认为这是大多数计算机科学相关课程中缺少的重要一环。

课程结构

本课程包含 11 个时长在一小时左右的讲座,每一个讲座都会关注一个 尽管这些讲座之间基本上是各自独立的,但随着课程的进行,我们会假定您已经掌握了之前的内容。 每个讲座都有在线笔记供查阅,但是课上的很多内容并不会包含在笔记中。因此我们也会把课程录制下来发布到互联网上供大家观看学习。

我们希望能在这 11 个一小时讲座中涵盖大部分必须的内容,因此课程的信息密度是相当大的。为了能帮助您以自己的节奏来掌握讲座内容,每次课程都包含一组练习来帮助您掌握本节课的重点。 课后我们会安排答疑的时间来回答您的问题。如果您参加的是在线课程,可以发送邮件到 missing-semester@mit.edu 来联系我们。

由于时长的限制,我们不可能达到那些专门课程一样的细致程度,我们会适时地将您介绍一些优秀的资源,帮助您深入的理解相关的工具或主题。 但是如果您还有一些特别关注的话题,也请联系我们。

shell 是什么?

如今的计算机有着多种多样的交互接口让我们可以进行指令的的输入,从炫酷的图像用户界面(GUI),语音输入甚至是 AR/VR 都已经无处不在。 这些交互接口可以覆盖 80% 的使用场景,但是它们也从根本上限制了您的操作方式——你不能点击一个不存在的按钮或者是用语音输入一个还没有被录入的指令。 为了充分利用计算机的能力,我们不得不回到最根本的方式,使用文字接口:Shell

几乎所有您能够接触到的平台都支持某种形式的 shell,有些甚至还提供了多种 shell 供您选择。虽然它们之间有些细节上的差异,但是其核心功能都是一样的:它允许你执行程序,输入并获取某种半结构化的输出。

本节课我们会使用 Bourne Again SHell, 简称 "bash" 。 这是被最广泛使用的一种 shell,它的语法和其他的 shell 都是类似的。打开shell 提示符(您输入指令的地方),您首先需要打开 终端 。您的设备通常都已经内置了终端,或者您也可以安装一个,非常简单。

使用 shell

当您打开终端时,您会看到一个提示符,它看起来一般是这个样子的:

missing:~$ 

这是 shell 最主要的文本接口。它告诉你,你的主机名是 missing 并且您当前的工作目录("current working directory")或者说您当前所在的位置是 ~ (表示 "home")。 $ 符号表示您现在的身份不是 root 用户(稍后会介绍)。在这个提示符中,您可以输入 命令 ,命令最终会被 shell 解析。最简单的命令是执行一个程序:

missing:~$ date
Fri 10 Jan 2020 11:49:31 AM EST
missing:~$

这里,我们执行了 date 这个程序,不出意料地,它打印出了当前的日前和时间。然后,shell 等待我们输入其他命令。我们可以在执行命令的同时向程序传递 参数

missing:~$ echo hello
hello

上例中,我们让 shell 执行 echo ,同时指定参数 helloecho 程序将该参数打印出来。 shell 基于空格分割命令并进行解析,然后执行第一个单词代表的程序,并将后续的单词作为程序可以访问的参数。如果您希望传递的参数中包含空格(例如一个名为 My Photos 的文件夹),您要么用使用单引号,双引号将其包裹起来,要么使用转义符号 \ 进行处理(My\ Photos)。

但是,shell 是如何知道去哪里寻找 dateecho 的呢?其实,类似于 Python 或 Ruby,shell 是一个编程环境,所以它具备变量、条件、循环和函数(下一课进行讲解)。当你在 shell 中执行命令时,您实际上是在执行一段 shell 可以解释执行的简短代码。如果你要求 shell 执行某个指令,但是该指令并不是 shell 所了解的编程关键字,那么它会去咨询 环境变量 $PATH,它会列出当 shell 接到某条指令时,进行程序搜索的路径:

missing:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
missing:~$ which echo
/bin/echo
missing:~$ /bin/echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

当我们执行 echo 命令时,shell 了解到需要执行 echo 这个程序,随后它便会在 $PATH 中搜索由 : 所分割的一系列目录,基于名字搜索该程序。当找到该程序时便执行(假定该文件是 可执行程序,后续课程将详细讲解)。确定某个程序名代表的是哪个具体的程序,可以使用 which 程序。我们也可以绕过 $PATH,通过直接指定需要执行的程序的路径来执行该程序

在shell中导航

shell 中的路径是一组被分割的目录,在 Linux 和 macOS 上使用 / 分割,而在Windows上是 \。路径 / 代表的是系统的根目录,所有的文件夹都包括在这个路径之下,在Windows上每个盘都有一个根目录(例如: C:\)。 我们假设您在学习本课程时使用的是 Linux 文件系统。如果某个路径以 / 开头,那么它是一个 绝对路径,其他的都是 相对路径 。相对路径是指相对于当前工作目录的路径,当前工作目录可以使用 pwd 命令来获取。此外,切换目录需要使用 cd 命令。在路径中,. 表示的是当前目录,而 .. 表示上级目录:

missing:~$ pwd
/home/missing
missing:~$ cd /home
missing:/home$ pwd
/home
missing:/home$ cd ..
missing:/$ pwd
/
missing:/$ cd ./home
missing:/home$ pwd
/home
missing:/home$ cd missing
missing:~$ pwd
/home/missing
missing:~$ ../../bin/echo hello
hello

注意,shell 会实时显示当前的路径信息。您可以通过配置 shell 提示符来显示各种有用的信息,这一内容我们会在后面的课程中进行讨论。

一般来说,当我们运行一个程序时,如果我们没有指定路径,则该程序会在当前目录下执行。例如,我们常常会搜索文件,并在需要时创建文件。

为了查看指定目录下包含哪些文件,我们使用 ls 命令:

missing:~$ ls
missing:~$ cd ..
missing:/home$ ls
missing
missing:/home$ cd ..
missing:/$ ls
bin
boot
dev
etc
home
...

除非我们利用第一个参数指定目录,否则 ls 会打印当前目录下的文件。大多数的命令接受标记和选项(带有值的标记),它们以 - 开头,并可以改变程序的行为。通常,在执行程序时使用 -h--help 标记可以打印帮助信息,以便了解有哪些可用的标记或选项。例如,ls --help 的输出如下:

  -l                         use a long listing format
missing:~$ ls -l /home
drwxr-xr-x 1 missing users 4096 Jun 15 2019 missing

这个参数可以打印出更加详细地列出目录下文件或文件夹的信息。首先,本行第一个字符 d 表示 missing 是一个目录。然后接下来的九个字符,每三个字符构成一组。 (rwx). 它们分别代表了文件所有者(missing),用户组(users) 以及其他所有人具有的权限。其中 - 表示该用户不具备相应的权限。从上面的信息来看,只有文件所有者可以修改(w),missing 文件夹 (例如,添加或删除文件夹中的文件)。为了进入某个文件夹,用户需要具备该文件夹以及其父文件夹的“搜索”权限(以“可执行”:x)权限表示。为了列出它的包含的内容,用户必须对该文件夹具备读权限(r)。对于文件来说,权限的意义也是类似的。注意,/bin 目录下的程序在最后一组,即表示所有人的用户组中,均包含 x 权限,也就是说任何人都可以执行这些程序。

在这个阶段,还有几个趁手的命令是您需要掌握的,例如 mv(用于重命名或移动文件)、 cp(拷贝文件)以及 mkdir(新建文件夹)。

如果您想要知道关于程序参数、输入输出的信息,亦或是想要了解它们的工作方式,请试试 man 这个程序。它会接受一个程序名作为参数,然后将它的文档(用户手册)展现给您。注意,使用 q 可以退出该程序。

missing:~$ man ls

在程序间创建连接

在 shell 中,程序有两个主要的“流”:它们的输入流和输出流。 当程序尝试读取信息时,它们会从输入流中进行读取,当程序打印信息时,它们会将信息输出到输出流中。 通常,一个程序的输入输出流都是您的终端。也就是,您的键盘作为输入,显示器作为输出。 但是,我们也可以重定向这些流!

最简单的重定向是 < file> file。这两个命令可以将程序的输入输出流分别重定向到文件:

missing:~$ echo hello > hello.txt
missing:~$ cat hello.txt
hello
missing:~$ cat < hello.txt
hello
missing:~$ cat < hello.txt > hello2.txt
missing:~$ cat hello2.txt
hello

您还可以使用 >> 来向一个文件追加内容。使用管道( pipes ),我们能够更好的利用文件重定向。 | 操作符允许我们将一个程序的输出和另外一个程序的输入连接起来:

missing:~$ ls -l / | tail -n1
drwxr-xr-x 1 root root 4096 Jun 20 2019 var
missing:~$ curl --head --silent google.com | grep --ignore-case content-length | cut --delimiter=' ' -f2
219

我们会在数据清理一章中更加详细的探讨如何更好的利用管道。

一个功能全面又强大的工具

对于大多数的类 Unix 系统,有一类用户是非常特殊的,那就是:根用户(root user)。 您应该已经注意到了,在上面的输出结果中,根用户几乎不受任何限制,他可以创建、读取、更新和删除系统中的任何文件。 通常在我们并不会以根用户的身份直接登录系统,因为这样可能会因为某些错误的操作而破坏系统。 取而代之的是我们会在需要的时候使用 sudo 命令。顾名思义,它的作用是让您可以以 su(super user 或 root 的简写)的身份执行一些操作。 当您遇到拒绝访问(permission denied)的错误时,通常是因为此时您必须是根用户才能操作。然而,请再次确认您是真的要执行此操作。

有一件事情是您必须作为根用户才能做的,那就是向 sysfs 文件写入内容。系统被挂载在 /sys 下,sysfs 文件则暴露了一些内核(kernel)参数。 因此,您不需要借助任何专用的工具,就可以轻松地在运行期间配置系统内核。注意 Windows 和 macOS 没有这个文件

例如,您笔记本电脑的屏幕亮度写在 brightness 文件中,它位于

/sys/class/backlight

通过将数值写入该文件,我们可以改变屏幕的亮度。现在,蹦到您脑袋里的第一个想法可能是:

$ sudo find -L /sys/class/backlight -maxdepth 2 -name '*brightness*'
/sys/class/backlight/thinkpad_screen/brightness
$ cd /sys/class/backlight/thinkpad_screen
$ sudo echo 3 > brightness
An error occurred while redirecting file 'brightness'
open: Permission denied

出乎意料的是,我们还是得到了一个错误信息。毕竟,我们已经使用了 sudo 命令!关于 shell,有件事我们必须要知道。|>、和 < 是通过 shell 执行的,而不是被各个程序单独执行。 echo 等程序并不知道 | 的存在,它们只知道从自己的输入输出流中进行读写。 对于上面这种情况, shell (权限为您的当前用户) 在设置 sudo echo 前尝试打开 brightness 文件并写入,但是系统拒绝了 shell 的操作因为此时 shell 不是根用户。

明白这一点后,我们可以这样操作:

$ echo 3 | sudo tee brightness

因为打开 /sys 文件的是 tee 这个程序,并且该程序以 root 权限在运行,因此操作可以进行。 这样您就可以在 /sys 中愉快地玩耍了,例如修改系统中各种LED的状态(路径可能会有所不同):

$ echo 1 | sudo tee /sys/class/leds/input6::scrolllock/brightness

接下来.....

学到这里,您掌握的 shell 知识已经可以完成一些基础的任务了。您应该已经可以查找感兴趣的文件并使用大多数程序的基本功能了。 在下一场讲座中,我们会探讨如何利用 shell 及其他工具执行并自动化更复杂的任务。

课后练习

  1. /tmp 下新建一个名为 missing 的文件夹。✔️

    mkdir missing
  1. man 查看程序 touch 的使用手册。✔️

    man touch
  1. touchmissing 文件夹中新建一个叫 semester 的文件。✔️

    touch semester
  1. 将以下内容一行一行地写入semester文件:

     #!/bin/sh
    curl --head --silent https://missing.csail.mit.edu

    第一行可能有点棘手, # 在Bash中表示注释,而 ! 即使被双引号(")包裹也具有特殊的含义。 单引号(')则不一样,此处利用这一点解决输入问题。更多信息请参考 Bash quoting手册✔️

    echo \#'!'/bin/sh > semester
    echo curl --head --silent https://missing.csail.mit.edu >> semester
  1. 尝试执行这个文件。例如,将该脚本的路径(./semester)输入到您的shell中并回车。如果程序无法执行,请使用 ls命令来获取信息并理解其不能执行的原因。✔️

    提示权限不够

    执行ls -l命令

    显示-rw-r--r-- ...显然没有可执行x权限

  2. 查看 chmod 的手册(例如,使用man chmod命令)✔️

    man chmod
  1. 使用 chmod 命令改变权限,使 ./semester 能够成功执行,不要使用sh semester来执行该程序。您的shell是如何知晓这个文件需要使用sh来解析呢?更多信息请参考:shebang✔️

    chmod ugo+x semester
    ./semester
  1. 使用 |> ,将 semester 文件输出的最后更改日期信息,写入根目录下的 last-modified.txt 的文件中✔️

    ./semester | grep -i "last-modified" > /home/last-modified.txt
  2. 写一段命令来从 /sys 中获取笔记本的电量信息,或者台式机CPU的温度。注意:macOS并没有sysfs,所以mac用户可以跳过这一题✔️

    cat /sys/class/power_supply/battery/capacity

· 阅读需 15 分钟

前言

阅读本文后,你将能够了解到一下内容:

  • 定义 DNS
  • 理解 DNS 的工作方式
  • 区分递归和迭代 DNS 查找
  • 将权威性域名服务器与递归 DNS 解析器分开
  • 探索 DNS 高速缓存的工作方式

什么是 DNS?

域名系统 (DNS) 是互联网的电话簿。人们通过例如 nytimes.com 或 espn.com 等域名在线访问信息。Web 浏览器通过 互联网协议 (IP) 地址进行交互。DNS 将域名转换为 IP 地址,以便浏览器能够加载互联网资源。 连接到 Internet 的每个设备都有一个唯一 IP 地址,其他计算机可使用该 IP 地址查找此设备。DNS 服务器使人们无需存储例如 192.168.1.1(IPv4 中)等 IP 地址或更复杂的较新字母数字 IP 地址,例如 2400:cb00:2048:1::c629:d7a2(IPv6 中)。

DNS 如何工作?

DNS 解析过程涉及将主机名(例如 www.example.com)转换为计算机友好的 IP 地址(例如 192.168.1.1)。Internet 上的每个设备都被分配了一个 IP 地址,必须有该地址才能找到相应的 Internet 设备 - 就像使用街道地址来查找特定住所一样。当用户想要加载网页时,用户在 Web 浏览器中键入的内容(example.com)与查找 example.com 网页所需的机器友好地址之间必须进行转换。

为理解 DNS 解析过程,务必了解 DNS 查询必须通过的各种硬件设备。对于 Web 浏览器而言,DNS 查询是“在幕后”发生的,除了初始请求外,不需要从用户的计算机进行任何交互。

加载网页涉及 4 个 DNS 服务器:

  • DNS 解析器 - 该解析器可被视为被要求去图书馆的某个地方查找特定图书的图书馆员。DNS 解析器是一种服务器,旨在通过 Web 浏览器等应用程序接收客户端计算机的查询。然后,解析器一般负责发出其他请求,以便满足客户端的 DNS 查询。
  • 根域名服务器 - 根域名服务器是将人类可读的主机名转换(解析)为 IP 地址的第一步。可将其视为指向不同书架的图书馆中的索引 - 一般其作为对其他更具体位置的引用。
  • TLD 名称服务器 —— 顶级域名服务器(TLD)可看做是图书馆中一个特殊的书架。这个域名服务器是搜索特定 IP 地址的下一步,其上托管了主机名的最后一部分(例如,在 example.com 中,TLD 服务器为 “com”)。
  • 权威性域名服务器 - 可将这个最终域名服务器视为书架上的字典,其中特定名称可被转换成其定义。权威性域名服务器是域名服务器查询中的最后一站。如果权威性域名服务器能够访问请求的记录,则其会将已请求主机名的 IP 地址返回到发出初始请求的 DNS 解析器(图书管理员)。

权威性 DNS 服务器与递归 DNS 解析器之间的区别是什么?

这两个概念都是指 DNS 基础设施不可或缺的服务器(服务器组),但各自担当不同的角色,并且位于 DNS 查询管道内的不同位置。考虑二者差异的一种方式是,递归解析器位于 DNS 查询的开头,而权威性域名服务器位于末尾。

递归 DNS 解析器

递归解析器是一种计算机,其响应来自客户端的递归请求并花时间追踪 DNS 记录。为执行此操作,其发出一系列请求,直至到达用于所请求的记录的权威性 DNS 域名服务器为止(或者超时,或者如果未找到记录,则返回错误)。幸运的是,递归 DNS 解析器并不总是需要发出多个请求才能追踪响应客户端所需的记录;缓存是一种数据持久性过程,可通过在 DNS 查找中更早地服务于所请求的资源记录来为所需的请求提供捷径。

权威性 DNS 服务器

简言之,权威性 DNS 服务器是实际持有并负责 DNS 资源记录的服务器。这是位于 DNS 查找链底部的服务器,其将使用所查询的资源记录进行响应,从而最终允许发出请求的 Web 浏览器达到访问网站或其他 Web 资源所需的 IP 地址。权威性域名服务器从自身数据满足查询需求,无需查询其他来源,因为这是某些 DNS 记录的最终真实来源。 值得一提的是,在查询对象为子域(例如 foo.example.com 或 blog.example.com)的情况下,将向权威性域名服务器之后的序列添加一个附加域名服务器,其负责存储该子域的 CNAME 记录。

DNS 查找有哪些步骤?

大多数情况下,DNS 与正被转换为相应 IP 地址的域名有关。要了解此过程的工作方式,在 DNS 查找从 Web 浏览器经过 DNS 查找过程然后再返回时,跟踪 DNS 查找的路径会有所帮助。我们来看一下这些步骤。

注意:通常,DNS 查找信息将本地缓存在查询计算机内,或者远程缓存在 DNS 基础设施内。DNS 查找通常有 8 个步骤。缓存 DNS 信息时,将从 DNS 查找过程中跳过一些步骤,从而使该过程更快。以下示例概述了不缓存任何内容时的所有 8 个步骤。

DNS 查找的 8 个步骤:

  1. 用户在 Web 浏览器中键入 “example.com”,查询传输到 Internet 中,并被 DNS 递归解析器接收。

  2. 接着,解析器查询 DNS 根域名服务器(.)。

  3. 然后,根服务器使用存储其域信息的顶级域(TLD)DNS 服务器(例如 .com 或 .net)的地址响应该解析器。在搜索 example.com 时,我们的请求指向 .com TLD。

  4. 然后,解析器向 .com TLD 发出请求。

  5. TLD 服务器随后使用该域的域名服务器 example.com 的 IP 地址进行响应。

  6. 最后,递归解析器将查询发送到域的域名服务器。

  7. example.com 的 IP 地址而后从域名服务器返回解析器。

  8. 然后 DNS 解析器使用最初请求的域的 IP 地址响应 Web 浏览器。 DNS 查找的这 8 个步骤返回 example.com 的 IP 地址后,浏览器便能发出对该网页的请求:

  9. 浏览器向该 IP 地址发出 HTTP 请求。

  10. 位于该 IP 的服务器返回将在浏览器中呈现的网页(第 10 步)。

什么是 DNS 解析器?

DNS 解析器是 DNS 查找的第一站,其负责与发出初始请求的客户端打交道。解析器启动查询序列,最终使 URL 转换为必要的 IP 地址。

注意:典型的未缓存 DNS 查找将涉及递归查询和迭代查询。

务必区分递归 DNS 查询和递归 DNS 解析器。该查询是指向需要解析该查询的 DNS 解析器发出的请求。DNS 递归解析器是一种计算机,其接受递归查询并通过发出必要的请求来处理响应。

DNS 查询有哪些类型?

典型 DNS 查找中会出现三种类型的查询。通过组合使用这些查询,优化的 DNS 解析过程可缩短传输距离。在理想情况下,可以使用缓存的记录数据,从而使 DNS 域名服务器能够返回非递归查询。 3 种 DNS 查询类型:

  1. 递归查询 - 在递归查询中,DNS 客户端要求 DNS 服务器(一般为 DNS 递归解析器)将使用所请求的资源记录响应客户端,或者如果解析器无法找到该记录,则返回错误消息。
  2. 迭代查询 - 在这种情况下,DNS 客户端将允许 DNS 服务器返回其能够给出的最佳应答。如果所查询的 DNS 服务器与查询名称不匹配,则其将返回对较低级别域名空间具有权威性的 DNS 服务器的引用。然后,DNS 客户端将对引用地址进行查询。此过程继续使用查询链中的其他 DNS 服务器,直至发生错误或超时为止。
  3. 非递归查询 - 当 DNS 解析器客户端查询 DNS 服务器以获取其有权访问的记录时通常会进行此查询,因为其对该记录具有权威性,或者该记录存在于其缓存内。DNS 服务器通常会缓存 DNS 记录,以防止更多带宽消耗和上游服务器上的负载。

什么是 DNS 高速缓存?DNS 高速缓存发生在哪里?

缓存的目的是将数据临时存储在某个位置,从而提高数据请求的性能和可靠性。DNS 高速缓存涉及将数据存储在更靠近请求客户端的位置,以便能够更早地解析 DNS 查询,并且能够避免在 DNS 查找链中进一步向下的额外查询,从而缩短加载时间并减少带宽/CPU 消耗。DNS 数据可缓存到各种不同的位置上,每个位置均将存储 DNS 记录并保存由生存时间(TTL)决定的一段时间。

浏览器 DNS 缓存

现代 Web 浏览器设计为默认将 DNS 记录缓存一段时间。目的很明显;越靠近 Web 浏览器进行 DNS 缓存,为检查缓存并向 IP 地址发出正确请求而必须采取的处理步骤就越少。发出对 DNS 记录的请求时,浏览器缓存是针对所请求的记录而检查的第一个位置。

在 Chrome 浏览器中,您可以转到 chrome://net-internals/#dns 查看 DNS 缓存的状态。

操作系统(OS)级 DNS 缓存

操作系统级 DNS 解析器是 DNS 查询离开您计算机前的第二站,也是本地最后一站。操作系统内旨在处理此查询的过程通常称为“存根解析器”或 DNS 客户端。当存根解析器获取来自某个应用程序的请求时,其首先检查自己的缓存,以便查看是否有此记录。如果没有,则将本地网络外部的 DNS 查询(设置了递归标记)发送到 Internet 服务提供商(ISP)内部的 DNS 递归解析器。

与先前所有步骤一样,当 ISP 内的递归解析器收到 DNS 查询时,其还将查看所请求的主机到 IP 地址转换是否已经存储在其本地持久性层中。

根据其缓存中具有的记录类型,递归解析器还具有其他功能:

如果解析器没有 A 记录,但确实有针对权威性域名服务器的 NS 记录,则其将直接查询这些域名服务器,从而绕过 DNS 查询中的几个步骤。此快捷方式可防止从根和 .com 域名服务器(在我们对 example.com 的搜索中)进行查找,并且有助于更快地解析 DNS 查询。 如果解析器没有 NS 记录,它会向 TLD 服务器(本例中为 .com)发送查询,从而跳过根服务器。 万一解析器没有指向 TLD 服务器的记录,其将查询根服务器。这种情况通常在清除了 DNS 高速缓存后发生。

原文地址

· 阅读需 10 分钟

前言

在本节中,我们将了解 React 中用于 HTML 元素(例如按钮和输入元素)的事件处理程序。你将学习如何使用带有 onClick 事件的按钮,以及如何定义和使用不同类型的事件处理程序。本质上,我们将介绍三种事件处理程序:事件处理程序、内联事件处理程序和回调事件处理程序。

React中的事件处理程序

首先,我们将从 React 中针对特定 onClick 事件处理程序的按钮示例开始。这是关于如何在 React 中使用事件处理程序(也称为事件处理程序函数或处理程序)处理事件的最基本示例。按钮具有接收函数的 onClick 属性。每次触发事件时都会调用此函数(此处:单击按钮时):

import React from 'react';

function App() {
function handleClick() {
console.log('Button click ...');
}

return (
<div>
<button type="button" onClick={handleClick}>
Event Handler
</button>
</div>
);
}

对于其他属性,如 onChange(onChange 事件处理程序)和 onSubmit(onSubmit 事件处理程序),它的工作方式类似。对于初学者来说,onClick 经常不起作用,因为他们没有传递函数,而是直接在 JSX 中调用函数。例如,在下一个版本中,事件处理程序仅在第一次渲染组件时被调用一次。其他每一次单击都不会调用事件处理函数,因为函数的返回值用于 onClick 属性而不是函数本身。所以没有什么可调用的;除非函数返回另一个函数:

import React from 'react';

function App() {
function handleClick() {
console.log('Button click ...');
}

// don't do this
return (
<div>
<button type="button" onClick={handleClick()}>
Event Handler
</button>
</div>
);
}

通过使用 JavaScript 箭头函数,可以使事件处理函数更加简洁。不过,这是一个可选步骤。就个人而言,我喜欢将事件处理程序作为箭头函数:

import React from 'react';

function App() {
const handleClick = () => {
console.log('Button click ...');
};

return (
<div>
<button type="button" onClick={handleClick}>
Event Handler
</button>
</div>
);
}

但是一旦更多的事件处理程序在 React 组件中添加,通过再次给它们函数语句来使它们与其他变量更容易区分是很好的:

import React from 'react';

function App() {
const user = {
id: '123abc',
username: 'Robin Wieruch',
};

function handleUserSignIn() {
// do something
}

function handleUserSignUp() {
// do something
}

function handleUserSignOut() {
// do something
}

...
}

毕竟,onClick 事件的事件处理程序应该实现一些业务逻辑。在本例中,React 的 useState Hook 用于通过 onClick 按钮事件更新某些状态:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

function handleClick() {
setCount(count + 1);
}

return (
<div>
Count: {count}

<button type="button" onClick={handleClick}>
Increase Count
</button>
</div>
);
}

下一个示例向你展示了一个输入字段而不是一个按钮。在那里,我们使用的是始终作为第一个参数传递给事件处理函数的实际事件。该事件是来自 React 的合成事件,它本质上封装了原生 HTML 事件并在其之上添加了一些功能。每次有人使用事件的目标属性输入输入字段时,此事件都会为你提供输入字段的值:

import React from 'react';

function App() {
const [text, setText] = React.useState('');

function handleChange(event) {
setText(event.target.value);
}

return (
<div>
<input type="text" onChange={handleChange} />

{text}
</div>
);
}

以前我们没有使用过该事件,因为在我们的按钮示例中我们不需要它。在输入字段示例中,我们需要它。最后但同样重要的是,不要忘记将值传递给输入元素以使其成为受控组件:

import React from 'react';

function App() {
const [text, setText] = React.useState('');

function handleChange(event) {
setText(event.target.value);
}

return (
<div>
<input type="text" value={text} onChange={handleChange} />

{text}
</div>
);
}

简而言之,这就是事件处理程序。让我们了解 React 中更高级的处理程序。

React 中的内联事件处理程序

内联事件处理程序,也称为内联处理程序,通过直接在 JSX 中使用事件处理程序为我们提供了许多新选项:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

return (
<div>
Count: {count}

<button
type="button"
onClick={function() {
setCount(count + 1);
}}
>
Increase Count
</button>
</div>
);
}

在 JSX 中使用通用函数语句虽然很冗长。因此,JavaScript 箭头函数可以方便地定义更简洁的内联处理程序:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

return (
<div>
Count: {count}

<button
type="button"
onClick={() => setCount(count + 1)}
>
Increase Count
</button>
</div>
);
}

一般来说,开发者都是懒惰的人,所以经常使用内联事件处理程序来避免在 JSX 之外进行额外的函数声明。然而,这会将大量业务逻辑转移到 JSX 中,这使得它的可读性、可维护性和易错性降低。就个人而言,我喜欢在没有内联事件处理程序的情况下保持 JSX 干净,并在 JSX 之外声明事件处理程序。 内联处理程序也用于将参数传递给在 JSX 之外定义的更通用的处理程序:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

function handleCount(delta) {
setCount(count + delta);
}

return (
<div>
Count: {count}

<button type="button" onClick={() => handleCount(1)}>
Increase Count
</button>
<button type="button" onClick={() => handleCount(-1)}>
Decrease Count
</button>
</div>
);
}

这样,也可以并行传递事件和参数。即使在此示例中不需要它,但你肯定会在将来遇到需要该事件的一种或另一种情况(例如 React Forms 的 preventDefault ):

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

function handleCount(event, delta) {
setCount(count + delta);
}

return (
<div>
Count: {count}

<button type="button" onClick={event => handleCount(event, 1)}>
Increase Count
</button>
<button type="button" onClick={event => handleCount(event, -1)}>
Decrease Count
</button>
</div>
);
}

因此,当你需要传递事件和参数时,例如当你需要为 onClick 事件提供额外参数时,内联事件处理程序可能会为你提供帮助。然后 JSX 之外的更通用的事件处理程序可以使用这个额外的参数。

React 中的回调事件处理程序

简而言之,有回调事件处理程序或回调处理程序。当子组件需要与父组件通信时使用它们。由于 React props 只在组件树中向下传递,因此使用回调处理程序(其核心是一个函数)进行向上通信:

import React from 'react';

function App() {
const [text, setText] = React.useState('');

// 1
function handleTextChange(event) {
setText(event.target.value); // 3
}

return (
<div>
<MyInput inputValue={text} onInputChange={handleTextChange} />

{text}
</div>
);
}

// 2
function MyInput({ inputValue, onInputChange }) {
return (
<input type="text" value={inputValue} onChange={onInputChange} />
);
}

回调处理程序在某处定义 (1),在其他地方使用 (2),但回调到其定义的位置 (3)。这样,就可以从子组件到父组件进行通信。回调处理程序通过 React props 向下传递,并在调用函数时向上通信。

你已经了解了 React 的事件处理程序、内联事件处理程序和回调事件处理程序,以及如何在按钮中为它们的 onClick 事件和在输入字段中为它们的 onChange 事件使用它们。还有其他事件处理程序,例如表单元素的 onSubmit,实际上需要该事件来阻止本机浏览器行为。无论如何,所有这些事件处理程序都有其特定目的。你的目标应该是让你的代码保持可读性和可维护性,

· 阅读需 16 分钟

前言

本文主题是React事件冒泡和捕获的 。大多数 JavaScript 开发人员可能已经熟悉这个主题,因为它起源于 JavaScript 及其 DOM API。但是,在本文中,我想为 React 中的事件冒泡和捕获整理一些信息。

React 中的事件处理程序可用于侦听特定事件(例如单击事件)。我们将从 React 中的一个函数组件开始,我们使用 React 的 useState Hook 来增加一个计数器:

import * as React from 'react';

function App() {
const [count, setCount] = React.useState(0);

const handleClick = () => {
setCount(count + 1);
};

return (
<button type="button" onClick={handleClick}>
Count: {count}
</button>
);
}

export default App;

在原生 JavaScript 中,这相当于 element.addEventListener('click', handleClick);。 React 中有很多事件。下面显示了鼠标和触摸事件的事件列表:

  • touchstart
  • touchmove
  • touchend
  • mousemove
  • mousedown
  • mouseup
  • click 此特定事件列表按其执行顺序显示。因此,如果在 HTML 元素中添加了 mouseup 和 click 事件侦听器,则 mouseup 事件将在 click 事件之前触发:
import * as React from 'react';

function App() {
const handleClick = () => {
alert('click');
};

const handleMouseUp = () => {
alert('mouseup');
};

return (
<button
type="button"
onClick={handleClick}
onMouseUp={handleMouseUp}
>
Which one fires first?
</button>
);
}

export default App;

在某些情况下,你可能希望在另一个事件触发时阻止其中一个事件。例如,当触摸事件发生并被处理时,你可能希望阻止所有点击事件。

无论如何,在前面的示例中,所有事件都发生在同一个 HTML 元素上。关于事件冒泡或捕获还没有什么可看的。接下来,让我们探索使用多个 HTML 元素的事件冒泡:

import * as React from 'react';

function App() {
const [count, setCount] = React.useState(0);

const handleCount = () => {
setCount((state) => state + 1);
};

return (
<div onClick={handleCount}>
<button type="button" onClick={handleCount}>
Count: {count}
</button>
</div>
);
}

export default App;

在这个例子中,按钮似乎被点击了两次,因为计数器增加了 2 而不是 1。然而,发生的事情是包装容器元素也调用了它的事件处理程序。进入在 React 中事件冒泡。

React 中的事件冒泡

下面的示例显示了两个具有相同样式的 HTML 元素。为了简单起见,我们在这里使用内联样式,但是,你可以随意使用更复杂的方式来设置你的 React 应用程序的样式。

无论如何,让我们进入事件冒泡的话题。如你所见,在下一个示例中,只有外部容器元素侦听单击事件,而不是内部容器元素。但无论你是单击外部元素还是内部元素,都会触发事件处理程序:

import * as React from 'react';

const style = {
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleClick = () => {
alert('click');
};

return (
<div style={style} onClick={handleClick}>
<div style={style}>Click Me</div>
</div>
);
}

export default App;

在 JavaScript 中,这个原理称为事件冒泡。每当在 HTML 元素(例如内部 HTML 元素)上发生事件时,它就会开始运行通过该特定元素的处理程序,然后是其父 HTML 元素(例如外部 HTML 元素,它实际上在其中找到侦听处理程序)的处理程序,然后一直向上遍历每个祖先 HTML 元素,直到它到达文档的根。 在下一个示例中尝试一下,当单击内部 HTML 元素时,两个事件处理程序都会被触发。如果单击外部 HTML 元素,则仅触发外部元素的事件处理程序:

import * as React from 'react';

const style = {
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleOuterClick = () => {
alert('outer click');
};

const handleInnerClick = () => {
alert('inner click');
};

return (
<div style={style} onClick={handleOuterClick}>
<div style={style} onClick={handleInnerClick}>
Click Me
</div>
</div>
);
}

export default App;

换句话说,事件从它们的起源开始冒泡整个文档。通过 React 的 useEffect Hook 在文档上添加一个事件监听器,自己验证这种行为:

import * as React from 'react';

const style = {
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleOuterClick = () => {
alert('outer click');
};

const handleInnerClick = () => {
alert('inner click');
};

React.useEffect(() => {
const handleDocumentClick = () => {
alert('document click');
};

document.addEventListener('click', handleDocumentClick);

return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, []);

return (
<div style={style} onClick={handleOuterClick}>
<div style={style} onClick={handleInnerClick}>
Click Me
</div>
</div>
);
}

export default App;

因此,如果一个事件从其交互元素中冒出整个文档,那么在某些情况下如何停止冒泡呢?在 React 中输入 stopPropagation

React stopPropagation

stopPropagation() 方法是 DOM API 的原生方法。由于 React 将事件包装到称为合成事件的 React 版本中,因此该 API 仍然可用于 React 事件,它还可以用于停止事件的传播:

import * as React from 'react';

function App() {
const [count, setCount] = React.useState(0);

const handleCount = (event) => {
setCount((state) => state + 1);

event.stopPropagation();
};

return (
<div onClick={handleCount}>
<button type="button" onClick={handleCount}>
Count: {count}
</button>
</div>
);
}

export default App;

我们通过对事件使用 stopPropagation() 方法扩展了前面的示例之一。这样,当按钮被点击时,事件不会冒泡,也不会触发周围容器元素的事件处理程序。 反之,当容器元素被显式点击时(在这种情况下不太可能没有任何进一步的样式),只有容器的事件处理程序会触发。这里容器元素上的 stopPropagation() 有点多余,因为它上面没有事件处理程序。

最佳时机:默认情况下不要停止事件传播。例如,如果你将在项目中的每个按钮上使用 stopPropagation(),但稍后你想在文档级别跟踪用户点击,你将不会再收到这些事件。默认情况下使用 stopPropagation() 往往会导致错误,因此仅在必要时使用它。

当停止事件传播有意义时,让我们看看更复杂的场景。例如,可能有一个可点击的标题,可以将用户从任何页面导航到主页,但是,在标题内有一个按钮可以让用户从应用程序中注销。两个元素都应该是可点击的,不会相互干扰:

import * as React from 'react';

const styleHeader = {
padding: '10px',
border: '1px solid black',
boxSizing: 'border-box',
width: '100%',
display: 'flex',
justifyContent: 'space-between',
};

function App() {
const [isActive, setActive] = React.useState(false);

const handleHeaderClick = () => {
alert('header click (e.g. navigate to home page)');
};

const handleButtonClick = (event) => {
alert('button click (e.g. log out user)');

if (isActive) {
event.stopPropagation();
}
};

return (
<>
<div style={styleHeader} onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>

<button type="button" onClick={() => setActive(!isActive)}>
Stop Propagation: {isActive.toString()}
</button>
</>
);
}

export default App;

在不停止传播的情况下,注销按钮将触发它自己的事件处理程序,但也会触发标题上的事件处理程序,因为事件会冒泡到它上面。当 stopPropagation() 被激活时,单击注销按钮不会导致标题上的冒泡事件,因为该事件被阻止冒泡。

总之,只要有一个带有处理程序的元素嵌套在另一个带有处理程序的元素中,两者都在侦听相同的事件(这里:单击事件),使用 stopPropagation() 将有助于将事件委托给正确的处理程序(通过防止他们从冒泡)。

target && currentTarget

当单击带有监听器(事件处理程序)的 HTML 元素时,你可以访问它的事件(在 React 中它是合成事件)。在其他属性中,事件可以访问表示导致事件的元素的目标属性。因此,如果按钮具有事件处理程序并且用户单击此按钮,则该事件将以按钮元素作为目标。

即使这个事件冒泡到另一个事件处理程序,如果一个嵌套元素导致了这个事件,那么目标仍然由这个嵌套元素表示。因此,在所有处理程序中,事件的目标都不会改变。

从事件处理程序到事件处理程序的变化是事件的 currentTarget,因为它表示实际事件处理程序正在运行的元素:

import * as React from 'react';

const style = {
display: 'block',
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleDivClick = (event) => {
alert(`
<div /> \n
event.target: ${event.target} \n
event.currentTarget: ${event.currentTarget}
`);
};

const handleSpanClick = (event) => {
alert(`
<span /> \n
event.target: ${event.target} \n
event.currentTarget: ${event.currentTarget}
`);
};

return (
<div style={style} onClick={handleDivClick}>
<span style={style} onClick={handleSpanClick}>
Click Me
</span>
</div>
);
}

export default App;

通常你将与事件的目标进行交互,例如停止事件的传播或阻止默认行为。但是,有时你希望从正在运行的事件处理程序访问元素,因此你可以改用 currentTarget。

React 中的事件捕获

当谈到 JavaScript 中的事件冒泡时,不得不提的是存在事件捕获的概念。实际上两者都是依次发生的:当用户与元素交互时,DOM API 会向下遍历文档(捕获阶段)到目标元素(目标阶段),然后 DOM API 才会再次向上遍历(冒泡阶段)。

在某些情况下,你可能希望在捕获阶段中的事件到达冒泡阶段之前对其进行拦截。然后,你可以使用 onClickCapture 而不是 onClick 来处理 JSX 中的单击事件,或者使用 addEventListener() 方法的第三个参数来激活在捕获阶段而不是冒泡阶段的监听:

function App() {
const handleOuterClick = () => {
alert('outer click');
};

const handleInnerClick = () => {
alert('inner click');
};

React.useEffect(() => {
const handleDocumentClick = () => {
alert('document click');
};

document.addEventListener(
'click',
handleDocumentClick,
true
);

return () => {
document.removeEventListener(
'click',
handleDocumentClick,
true
);
};
}, []);

return (
<div style={style} onClickCapture={handleOuterClick}>
<div style={style} onClickCapture={handleInnerClick}>
Click Me
</div>
</div>
);
}

谈到“某些情况”有点含糊。因此,让我们回到前面的示例,其中我们将注销按钮嵌套在标题元素中。如果单击该按钮以不触发标头的事件处理程序,则该按钮将停止事件的传播。现在,如果你想通过在顶级文档级别引入分析跟踪来扩展此示例,你可以验证自己对于单击按钮,你不会收到分析跟踪,而只会收到标题,因为按钮阻止了从冒泡到文档的事件:

function App() {
const handleHeaderClick = () => {
alert('header click (e.g. navigate to home page)');
};

const handleButtonClick = (event) => {
alert('button click (e.g. log out user)');

// important: stops event from appearing
// in the document's event handler
event.stopPropagation();
};

React.useEffect(() => {
const handleDocumentClick = (event) => {
alert(`
document clicked - \n
run analytics for clicked element: ${event.target}
`);
};

document.addEventListener('click', handleDocumentClick);

return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, []);

return (
<>
<div style={styleHeader} onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>
</>
);
}

凭借我们对捕获阶段的了解,我们可以在实际用户交互冒泡之前对事件进行分析跟踪。在这种情况下,我们通过将第三个参数设置为 true(使用捕获阶段而不是冒泡阶段)在文档上添加事件侦听器:

function App() {
const handleHeaderClick = () => {
alert('header click (e.g. navigate to home page)');
};

const handleButtonClick = (event) => {
alert('button click (e.g. log out user)');

// important: stops event from appearing
// in the document's event handler
event.stopPropagation();
};

React.useEffect(() => {
const handleDocumentClick = (event) => {
alert(`
document clicked - \n
run analytics for clicked element: ${event.target}
`);
};

document.addEventListener(
'click',
handleDocumentClick,
true
);

return () => {
document.removeEventListener(
'click',
handleDocumentClick,
true
);
};
}, []);

return (
<>
<div style={styleHeader} onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>
</>
);
}

当点击注销按钮时,捕获阶段从上到下遍历所有处理程序,从而触发文档级别的处理程序进行分析跟踪。然后它向下遍历元素到目标(此处:按钮),因为没有其他事件处理程序正在侦听捕获阶段(例如,通过使用 onClickCapture 代替)。从那里,事件冒泡并触发按钮的事件处理程序,阻止事件传播到标题的事件处理程序。

在日常工作中,大多数开发人员使用冒泡阶段通过使用事件处理程序来拦截事件,并使用 stopPropagation() 方法阻止事件传播。因此,在开发人员的脑海中,总是有事件模型在 HTML 树中冒泡。然而,正如某些边缘情况所示,了解捕获阶段也是有意义的。

理解 JavaScript 中的事件冒泡对于在 React 中使用它至关重要。每当你有一个复杂的页面,其中带有事件处理程序的伪按钮被包装到其他伪按钮中时,就无法绕过本机 stopPropagation 方法。但是,请谨慎使用它,而不是默认使用它,否则从长远来看,你可能会遇到错误。

· 阅读需 8 分钟

前言

浅比较在 React 开发中无处不在。它在不同的流程中起着关键作用,也可以在 React 组件生命周期的多个地方找到。类组件是否应该更新的机制,React hooks 的依赖数组,通过 React.memo 进行记忆等等。

如果你曾经阅读过 React 的官方文档,你很可能已经经常看到浅比较这个术语。因此,本文将研究浅比较的概念,它到底是什么,它是如何工作的。

什么是浅比较?

理解浅层比较最直接的方法是深入研究它的实现。相应的代码可以在共享子包的 React Github 项目中找到。例如下面的实现:

import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';

/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}

return true;
}

这过程中发生了很多事情,所以让我们将其拆分并逐步执行该功能。

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
}

从函数定义开始,函数接受两个将相互比较的实体。与 TypeScript 不同,此代码使用 Flow 作为类型检查系统。两个函数参数都是使用特殊的混合 Flow 类型键入的,类似于 TypeScript 的未知数。它表明参数可以是任何类型的值,该函数将找出其余的并使其工作。

import is from './objectIs';

function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
// ...
}

其次,首先使用来自 React 内部对象的 is 函数将函数参数相互比较。导入的函数只不过是 JavaScript 的 Object.is 函数的 polyfill 版本。这个比较函数基本上等同于常见的 === 运算符,但有两个例外:

  • Object.is 认为相反的有符号零(+0 和 -0)不相等,而 === 认为它们相等。
  • Object.is 认为 Number.NaN 和 NaN 相等,而 === 认为它们不相等。

基本上,第一个条件语句处理所有简单的情况:如果两个函数参数具有相同的值,对于原始类型,或引用相同的对象,对于数组和对象,那么它们被认为是浅比较相等的。

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}

// ...
}

在处理了两个函数参数值相等或引用同一个对象的所有简单情况之后,我们想要进入更复杂的结构(对象和数组)。但是,如果任何一个参数是原始值,前面的条件语句仍然可以给我们留下原始值。

因此,为了确保我们从现在开始处理两个复杂的结构,代码会检查任一参数是否不是对象类型或等于 null。前一个检查确保我们正在处理对象或数组,而后一个检查是过滤掉空值,因为它们的类型也是对象。如果任一条件成立,我们肯定是在处理不相等的参数(否则前面的条件语句会将它们过滤掉),因此浅比较返回 false。

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// ...
}

现在可以确定我们只处理数组和对象,我们可以专注于浅比较这些数据结构。为此,我们必须深入研究复杂数据结构的值,并在两个函数参数之间进行比较。

但在我们这样做之前,我们可以通过一个简单的检查来确保两个参数具有相同数量的值。如果不是,则通过浅层比较可以保证它们不相等,这可以节省我们一些精力。为此,我们使用参数的键。对于对象,键数组将由实际键组成,而对于数组,键数组将由字符串中原始数组中占用的索引组成。

import hasOwnProperty from './hasOwnProperty';

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...

// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}

return true;
}

作为最后一步,我们按键迭代两个函数参数的值,并逐个验证它们以确定它们是否等效。为此,代码使用在上一步中生成的键数组,使用 hasOwnProperty 检查键是否实际上是参数的属性,并使用与比较值之前相同的 Object.is 函数。

如果事实证明任何键在两个参数之间没有等效值,那么通过浅比较可以肯定它们不相等。因此,我们缩短了 for 循环,并从 shallowEqual 函数中返回 false。如果所有值都相等,那么我们可以通过浅比较调用函数参数相等并从函数返回 true。

一些有趣的点

现在我们了解了浅层比较及其背后的实现,我们可以从这些知识中学到一些有趣的东西:

  • 浅比较不使用严格相等、=== 运算符,而是使用 Object.is 函数。
  • 通过浅比较,空对象和数组是等价的。
  • 通过浅比较,以索引为键的对象等效于在各个索引处具有相同值的数组。例如。 { 0: 2, 1: 3 } 等价于 [2, 3]
  • 由于 Object.is 优于 === 的使用,通过浅比较,+0 和 -0 不等价,NaN 和 Number.NaN 也不等价。- 如果它们在复杂结构内进行比较,这也适用。 虽然两个内联创建的对象(或数组)通过浅比较相等({} 和 [] 浅相等),但具有嵌套内联对象的内联对象不相等({ someKey: {} } 和 { someKey: [] } 不浅平等的)。

· 阅读需 11 分钟

前言

在 React 18 中发布的另一个重要特性是 Suspense。如果你在之前使用过 React ,那么你就会知道 Suspense 功能并不是特别新。早在 2018 年,Suspense 作为 React 16.6 版的一部分作为实验性功能发布。然后,它主要针对与React.lazy。

但是现在,有了 React 18,Suspense 的正式发布就在我们面前。伴随着并发渲染的发布,Suspense 的真正威力终于解锁了。Suspense 和并发渲染之间的交互为改善用户体验开辟了广阔的世界。

但就像所有功能一样,就像并发渲染一样,从基础开始很重要。Suspense 到底是什么?为什么我们首先需要Suspense ?Suspense 如何解决这个问题?有什么好处?为了帮助你理解这些基础知识,本文将详细介绍这些问题,并为你提供有关 Suspense 的知识基础。

什么是Suspense?

本质上,Suspense 是 React 开发人员向 React 指示组件正在等待数据准备好的一种机制。然后 React 知道它应该等待该数据被获取。同时,将向用户显示一个反馈,并且 React 将继续渲染应用程序的其余部分。数据准备好后,React 会返回到那个特定的 UI 并相应地更新它。

从根本上说,这听起来与 React 开发人员必须实现数据获取流程的当前方式没有太大区别:使用某种状态来指示组件是否仍在等待数据,useEffect开始获取数据,显示加载状态基于数据的状态,并在数据准备好后更新 UI。

但在实践中,Suspense 使这在技术上完全不同。与上面提到的数据获取流程相反,Suspense 与 React 深度集成,允许开发人员更直观地编排加载状态,并避免竞争条件。为了更好地理解这些细节,了解我们为什么需要 Suspense 很重要。

为什么我们需要Suspense?

在没有 Suspense 的情况下,实现数据获取流程的主要方法有两种:渲染时获取渲染后获取。但是,这些传统的数据获取流程存在一些问题。要了解 Suspense,我们必须深入研究这些流程的问题和局限性。

渲染时获取

useEffect大多数人将使用和状态变量来实现前面提到的数据获取流程。这意味着只有在组件呈现时才开始获取数据。所有数据获取都发生在组件的副作用和生命周期方法中。

这种方法的主要问题是:组件仅在渲染时触发数据获取,异步特性迫使组件必须等待其他组件的数据请求。

假设我们有一个ComponentA获取一些数据并具有加载状态的组件。在内部,ComponentA还呈现另一个组件ComponentB,该组件也自己执行一些数据获取。但是由于数据获取的实现方式,ComponentB只有在渲染时才开始获取数据。这意味着它必须等到ComponentA完成获取数据然后渲染ComponentB。

这导致了瀑布式方法,其中组件之间的数据获取顺序发生,这实质上意味着它们相互阻塞。

function ComponentA() {
const [data, setData] = useState(null);

useEffect(() => {
fetchAwesomeData().then(data => setData(data));
}, []);

if (user === null) {
return <p>Loading data...</p>;
}

return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
);
}

function ComponentB() {
const [data, setData] = useState(null);

useEffect(() => {
fetchGreatData().then(data => setData(data));
}, []);

return data === null ? <h2>Loading data...</h2> : <SomeComponent data={data} />;
}

渲染后获取

为了防止组件之间数据获取的顺序阻塞,一种替代方法是尽早开始所有数据获取。因此,与其让组件负责处理渲染时的数据获取,而且数据请求都单独发生,而是在树开始渲染之前启动所有请求。

这种方法的优点是所有数据请求都是一起发起的,因此ComponentB不必等待ComponentA完成。这解决了组件顺序阻塞彼此数据流的问题。但是,它引入了另一个问题,我们必须等待所有数据请求完成,然后才能为用户呈现任何内容。可以想象,这不是最佳体验。

// Start fetching before rendering the entire tree
function fetchAllData() {
return Promise.all([
fetchAwesomeData(),
fetchGreatData()
]).then(([awesomeData, greatData]) => ({
awesomeData,
greatData
}))
}

const promise = fetchAllData();

function ComponentA() {
const [awesomeData, setAwesomeData] = useState(null);
const [greatData, setGreatData] = useState(null);

useEffect(() => {
promise.then(({ awesomeData, greatData }) => {
setAwesomeData(awesomeData);
setGreatData(greatData);
});
}, []);

if (user === null) {
return <p>Loading data...</p>;
}

return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
);
}

function ComponentB({data}) {
return data === null ? <h2>Loading data...</h2> : <SomeComponent data={data} />;
}

Suspense 如何解决数据获取问题?

从本质上讲,fetch-on-render fetch-then-render 的主要问题归结为我们试图强制同步两个不同的流程,即数据获取流程和 React 生命周期。借助 Suspense,我们获得了一种不同类型的数据获取方法,即所谓的 render-as-you-fetch 方法。

const specialSuspenseResource = fetchAllDataSuspense();

function App() {
return (
<Suspense fallback={<h1>Loading data...</h1>}>
<ComponentA />
<Suspense fallback={<h2>Loading data...</h2>}>
<ComponentB />
</Suspense>
</Suspense>
);
}

function ComponentA() {
const data = specialSuspenseResource.awesomeData.read();
return <h1>{data.title}</h1>;
}

function ComponentB() {
const data = specialSuspenseResource.greatData.read();
return <SomeComponent data={data} />;
}

与之前实现的不同之处在于它允许组件在 React 到达它的那一刻启动数据获取。这甚至发生在组件渲染之前,并且 React 并没有就此停止。然后它继续评估组件的子树,并在等待数据获取完成时继续尝试渲染它。

这意味着 Suspense 不会阻塞渲染,这意味着子组件不必等待父组件完成后再发起其数据获取请求。React 尝试尽可能多地渲染,同时启动适当的数据获取请求。请求完成后,React 将重新访问相应的组件并使用新接收的数据相应地更新 UI。

Suspense有什么好处?

  • 尽早开始获取数据。Suspense 引入的 render-as-you-fetch 方法最大和最直接的好处是数据获取尽早启动。这意味着用户必须等待的时间更少,应用程序更快,这对任何前端应用程序都是普遍有益的。
  • 更直观的加载状态。使用 Suspense,组件不必再包含大量的 if 语句或单独跟踪状态来实现加载状态。相反,加载状态被集成到它所属的组件本身中。这使得组件更直观,通过保持加载代码接近相关代码,并且更可重用,因为加载状态包含在组件中。
  • 避免竞争条件。我没有在本文中深入讨论的现有数据获取实现的问题之一是竞争条件。在某些情况下,传统的 fetch-on-render 和 fetch-then-render 实现可能会导致竞争条件,具体取决于时间、用户输入和参数化数据请求等不同因素。主要的潜在问题是我们试图强制同步两个不同的进程,React 和数据获取。但是使用 Suspense,这可以更优雅、更集成地完成,从而避免了上述问题。
  • 更集成的错误处理。使用 Suspense,我们基本上已经为数据请求流创建了边界。最重要的是,由于 Suspense 使其与组件代码的集成更加直观,它允许 React 开发人员还为 React 代码和数据请求实现更集成的错误处理。

总结

React Suspense 已经被关注了 3 年多。但是随着 React 18 的发布,官方发布的时间越来越近了。除了并发渲染,它将是作为 React 版本的一部分发布的最大功能之一。就其本身而言,它可以将数据获取和加载状态实现提升到一个新的直观和优雅水平。

为了帮助你了解 Suspense 的基础知识,本文介绍了几个对其很重要的问题和方面。这涉及到 Suspense 是什么,为什么我们首先需要像 Suspense 这样的东西,它如何解决某些数据获取问题以及 Suspense 带来的所有好处。

· 阅读需 5 分钟

前言

React 18 支持对状态更新的自动批处理支持。 这有助于避免在 PromisesetTimeoutsetInterval、原生事件处理程序以及react事件处理程序中多次渲染状态更新。 因此,由于自动批处理,我们在React 应用程序中获得了开箱即用的性能改进。

什么是(自动)批处理?

批处理是将多个状态更新分组为单个更新的过程。 如果我们有多个调用来设置组件的状态,React 会将这些更新组合在一个更新调用(称为批处理)中,从而导致组件的一次重新渲染。 当 React 自动计算出这一点并批量更新状态时,它被称为自动批处理,下面让我们通过探究更新状态的各种方式来看看它是如何工作的?

React 17 及之前的事件处理程序的状态更新

让我们举个例子来了解在 React 17 中事件处理程序上的状态更新时渲染是如何发生的?

const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);

const handleClick = () => {
setCount(count + 1);
setShowModal((prev) => !prev);
// React renders once at the end (that's batching)
};

console.log('rendered component');

return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}

现在,我们可以看到有两个状态更新调用。 一个用于 setCount,另一个用于 setShowModal。 但是,react 确保最后只调用一个渲染。 如果更新不是批处理的,它会以不成熟方式渲染组件,导致 UI 闪烁。 即我们希望我们的组件仅在更新计数和更新 showModal 标志后才呈现。

React 17 及之前的 Promise 和原生事件处理程序的状态更新

自动批处理不适用于promise/非react处理程序(如 setTimeoutsetInterval 等)中的状态更新。

const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);

const handleClick = () => {
console.log('fetch called');

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => {
setCount(count + 1); // Re-render is called
setShowModal((prev) => !prev); // Re-render is called
})
};

console.log('rendered component');

return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me now</button>
</div>
);
}

通常,我们调用 API 请求来获取某些内容并根据 API 请求的响应在回调中执行状态更新。 正如我们在上面的示例中看到的,有 2 次调用来设置回调中的状态,导致 2 次重新渲染。 这是一个性能瓶颈。 这可能会导致 UI 闪烁,从而呈现部分状态更新的结果。

React 18+ 中 Promise 和原生事件处理程序的状态更新

React 通过为 PromisesetTimeout 和 setInterval、原生事件处理程序以及默认的 react 事件处理程序中的状态更新提供自动批处理支持来解决此问题。 注意:对于这个例子,我们已经更新了如下所示的 reactreact-dom 库版本。

https://unpkg.com/react@18.0.0-beta-24dd07bd2-20211208/umd/react.development.js
https://unpkg.com/react-dom@18.0.0-beta-24dd07bd2-20211208/umd/react-dom.development.js

如果我们采用与上面给出的相同示例,我们可以在下面看到渲染的数量。

const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);

const handleClick = () => {
console.log('fetch called');

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => {
setCount(count + 1);
setShowModal((prev) => !prev);
// React 18 renders once at the end (that's automatic batching)
})
};

console.log('rendered component');

return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}

注意:我们需要将渲染升级为createRoot。 对于上面给出的示例,我们渲染了 Counter 组件,如下所示。 我们使用 ReactDOM.createRoot 创建了根,然后在其上渲染了 Counter 组件。

· 阅读需 14 分钟

前言

目前 React 生态系统中最大的话题是 React 18 及其备受期待的并发渲染功能的完整发布。 2021 年 6 月,React 团队宣布了 React 18 的计划以及即将发生的事情。 2021年 12 月,React Conf 2021 的主题是所有新发布的并发渲染功能。

与 React 18 一起发布的几个新 API 允许用户充分利用 React 的并发渲染功能。 这些hook是:

本文将介绍这三个新 API、它们的用例、它们解决的问题、添加它们的原因以及它们如何集成到并发渲染领域。

the-usesyncexternalstore-hook

在 React v16.14.0 中引入的用于适应并发渲染的 API 之一是 useMutableSource,它旨在允许 React 组件在并发渲染期间安全有效地与外部可变源集成。

Hook 将附加到数据源,等待更改,并相应地安排更新。 所有这一切都会以一种防止撕裂的方式发生,即当出现视觉不一致时,因为同一状态有多个值。

这对于新的并发渲染特性来说是一个特别突出的问题,因为状态流可以很快地交织在一起。 然而,采用 useMutableSource 被证明是困难的,原因如下:

  1. Hook 是异步的 Hook 不知道如果选择器函数的结果值发生变化,它是否可以重用它。 唯一的解决方案是重新订阅提供的数据源并再次检索快照,这可能会导致性能问题,因为它发生在每次渲染上。

对于用户和库(如 Redux),这意味着他们必须记住项目中的每个选择器,并且无法内联定义选择器函数,因为它们的引用不稳定。

  1. 它必须处理外部状态 最初的实现也有缺陷,因为它必须处理 React 之外的状态。 这意味着由于其可变性,状态可能随时更改。

因为 React 试图以异步方式解决这个问题,这有时会导致 UI 的可见部分被替换为备用,从而导致次优的用户体验。

所有这一切都使得库维护者的迁移变得痛苦,并且对开发人员和用户来说都是次优的体验。

使用 useSyncExternalStore 解决这些问题

为了解决这些问题,React 团队更改了底层实现并将 Hook 重命名为 useSyncExternalStore 以正确反映其行为。 这些变化包括:

  • 每次选择器(用于快照)更改时都不会重新订阅外部源——相反,React 将比较选择器的结果值,而不是选择器函数,以决定是否再次检索快照,以便用户可以定义 选择器内联而不会对性能产生负面影响
  • 每当外部存储发生更改时,生成的更新现在始终是同步的,这可以防止 UI 被替换为回退

唯一的要求是 getSnapshot Hook 参数的结果值需要是引用稳定的。 React 在内部使用它来确定是否需要检索新快照,因此它需要是不可变值或记忆/缓存对象。

为了方便起见,React 将提供一个附加版本的 Hook,它自动支持对 getSnapshot 的结果值的记忆。

如何使用 useSyncExternalStore

// Code illustrating the usage of `useSyncExternalStore`.
// Source: <https://github.com/reactwg/react-18/discussions/86>

import {useSyncExternalStore} from 'react';

// React will also publish a backwards-compatible shim
// It will prefer the native API, when available
import {useSyncExternalStore} from 'use-sync-external-store/shim';

// Basic usage. getSnapshot must return a cached/memoized result
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);

// Code illustrating the usage of the memoized version.
// Source: <https://github.com/reactwg/react-18/discussions/86>

// Name of API is not final
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';

const selection = useSyncExternalStoreWithSelector(
store.subscribe,
store.getSnapshot,
getServerSnapshot,
selector,
isEqual
);

The useId Hook

在服务器端运行 React

长期以来,一个 React 项目只在客户端运行。简而言之,这意味着所有代码都被发送到用户的浏览器(客户端),然后浏览器负责向用户呈现和显示应用程序。

React 作为一个整体一直在向服务器端渲染(SSR)领域扩展。在 SSR 中,服务器负责根据 React 代码生成 HTML 结构。而不是所有的 React 代码,只有 HTML 被发送到浏览器。

然后,浏览器只负责采用该结构并通过渲染组件、在其上添加 CSS 并将 JavaScript 附加到它来使其具有交互性。这个过程称为水合作用。

hydration 最重要的要求是服务器和客户端生成的 HTML 结构必须匹配。如果他们不这样做,浏览器就无法确定它应该对结构的特定部分做什么,这会导致不正确地呈现或非交互式 UI。

这在依赖于标识符的特性中尤为突出,因为它们必须在两边都匹配,例如在生成唯一样式类名称和可访问性标识符时。

useID Hook 的演进

为了解决这个问题,React 最初引入了 useOpaqueIdentifier Hook,但不幸的是,它也存在一些问题:

在不同的环境中,Hooks 会产生不同的输出(不透明):

服务器端:它会产生一个字符串 客户端:它会产生一个特殊的对象,必须直接传递给 DOM 属性 这意味着 Hook 只能生成一个标识符,并且不可能动态生成新的 ID,因为它必须遵守 Hook 的规则。 因此,如果您的组件需要 X 个不同的标识符,它必须在不同的时间调用 Hook X,这在实践中显然不能很好地扩展。

// Code illustrating the way `useOpaqueIdentifier` handles the need for N identifiers in a single component, namely calling the hook N times.
// Source: <https://github.com/facebook/react/pull/17322#issuecomment-613104823>

function App() {
const tabIdOne = React.unstable_useOpaqueIdentifier();
const panelIdOne = React.unstable_useOpaqueIdentifier();
const tabIdTwo = React.unstable_useOpaqueIdentifier();
const panelIdTwo = React.unstable_useOpaqueIdentifier();

return (
<React.Fragment>
<Tabs defaultValue="one">
<div role="tablist">
<Tab id={tabIdOne} panelId={panelIdOne} value="one">
One
</Tab>
<Tab id={tabIdTwo} panelId={panelIdTwo} value="one">
One
</Tab>
</div>
<TabPanel id={panelIdOne} tabId={tabIdOne} value="one">
Content One
</TabPanel>
<TabPanel id={panelIdTwo} tabId={tabIdTwo} value="two">
Content Two
</TabPanel>
</Tabs>
</React.Fragment>
);
}

某些可访问性 API(如 aria-labelledby)可以通过空格分隔的列表接受多个标识符,但由于 Hook 的输出被格式化为不透明的数据类型,它总是必须直接附加到 DOM 属性。这意味着无法正确使用上述可访问性 API。

为了解决这个问题,实现已更改并重命名为 useId。这个新的 Hook API 在 SSR 和 hydration 期间生成稳定的标识符以避免不匹配。在服务器渲染的内容之外,它回退到一个全局计数器。

与使用 useOpaqueIdentifier 创建不透明数据类型(服务器中的特殊对象和客户端中的字符串)不同,useId Hook 会在两侧生成非透明字符串。

这意味着如果我们需要 X 个不同的 ID,就没有必要再调用 Hook X 次了。相反,组件可以调用 useId 一次并将其用作整个组件所需的标识符的基础(例如,使用后缀),因为它只是一个字符串。这解决了 useOpaqueIdentifier 中存在的两个问题。

如何使用 useID

下面的代码示例说明了如何根据我们上面讨论的内容使用 useId。 因为 React 生成的 ID 是全局唯一的,并且后缀是本地唯一的,所以动态创建的 ID 也是全局唯一的——因此不会导致任何水合不匹配。

// Code illustrating the improved way in which `useId` handles the need for N identifiers in a single component, namely calling the hook once and creating them dynamically.
// Source: <https://github.com/reactwg/react-18/discussions/111>

function NameFields() {
const id = useId();
return (
<div>
<label htmlFor={id + '-firstName'}>First Name</label>
<div>
<input id={id + '-firstName'} type="text" />
</div>
<label htmlFor={id + '-lastName'}>Last Name</label>
<div>
<input id={id + '-lastName'} type="text" />
</div>
</div>
);
}

The useInsertionEffect Hook . CSS-in-JS 库的问题

最后一个将在 React 18 中添加的 Hook——我们将在这里讨论——是 useInsertionEffect。这个与其他的略有不同,因为它的唯一目的对于动态生成新规则并在文档中插入带有 <style> 标记的 CSS-in-JS 库很重要。

在某些场景下,<style>标签需要在客户端生成或编辑,如果不仔细处理,可能会导致并发渲染的性能问题。这是因为在添加或删除 CSS 规则时,浏览器必须检查这些规则是否适用于现有树。它必须重新计算所有样式规则并重新应用它们——而不仅仅是改变的规则。如果 React 发现另一个组件也生成了新规则,那么同样的过程将再次发生。

这实际上意味着在 React 渲染时,必须针对每一帧的所有 DOM 节点重新计算 CSS 规则。虽然你很有可能不会遇到这个问题,但它的规模并不大。

从理论上讲,有一些方法主要与时间有关。这个时间问题的最佳解决方案是在对 DOM 进行所有其他更改的同时生成这些标签,就像 React 库那样。最重要的是,它应该发生在任何尝试访问布局之前以及所有内容呈现给浏览器进行绘制之前。

这听起来像是 useLayoutEffect 可以解决的问题,但问题是同一个 Hook 将用于读取布局和插入样式规则。这可能会导致不希望的行为,例如在一次通过中多次计算布局或读取不正确的布局。

useInsertionEffect 如何解决并发渲染问题

为了解决这个问题,React 团队引入了 useInsertionEffect Hook。 它与 useLayoutEffect Hook 非常相似,但它无法访问 DOM 节点的 refs。

这意味着只能插入样式规则。 它的主要用例是插入全局 DOM 节点,如 <style> 或 SVGs <defs>。 由于这仅与在客户端生成标签有关,因此 Hook 不会在服务器上运行。

// Code illustrating the way `useInsertionEffect` is used.
// Source: <https://github.com/reactwg/react-18/discussions/110>

function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}

function Component() {
let className = useCSS(rule);
return <div className={className} />;
}

React 18 最令人期待的特性是它的并发渲染特性。 随着团队的宣布,我们收到了新的 API,允许用户根据他们的用例采用并发渲染功能。 虽然有些是全新的,但有些是基于社区反馈的先前 API 的改进版本。

在本文中,我们介绍了三个最新的 API,即 useSyncExternalStore、useId 和 useInsertionEffect Hooks。 我们查看了它们的用例、它们解决的问题、与以前的版本相比为什么需要进行某些更改,以及它们用于并发渲染的目的。

React 18 充满了新功能,绝对值得期待!