easycodesniper

blog by chen qiyi

Prompt

前言

有效地与大型语言模型交流已成为一种关键技能。在openAI 的官方文档中已经给出了最佳实践——如何精心构建问题(prompt engineering),以便从这些大模型中获得最准确、最相关的答案。正确认识并运用这些技巧。

在我的这篇文章中,将总结概括如何编写优秀的 prompt

编写详细的指示 - 大模型不是你肚子里的蛔虫

大模型无法读懂你的想法,它只能根据你给他的信息来返回结果。因此,你需要确保你的 prompt 包含足够的信息,以便大模型理解你的意图。

大模型不会嫌弃你啰嗦,你对问题描述的越详细,你越有可能得到想要的结果。

以下是一些编写 prompt 的策略:

1. 详细描述你的问题,才能得到更相关的回答

为了获得高度相关的回复,请确保请求提供了任何重要的细节或上下文。否则你就把它留给模型去猜你的意思了。

Worse Prompt: 帮我编写一段代码来计算斐波那契数列

得到的回复:

Better Prompt: 写一个 JavaScript 函数来正确高效地计算斐波那契数列,并在关键步骤处给出注释,以解释每个部分的作用以及为什么要这么写

得到的回复:

2. 给大模型一个人设,也给用户一个人设

可以指定大模型在回复时的人设,一个合适且恰当的人设能让大模型更好地响应你的问题。

Worse Prompt: 向我解释一下计算机网络的 OSI 模型

得到的回复:

Better Prompt(给大模型一个人设): 你是一个网络工程师,向我解释一下计算机网络的 OSI 模型

得到的回复:

Better Prompt(给用户一个人设): 我是一个 10 岁的孩子,向我解释一下计算机网络的 OSI 模型

得到的回复:

3. 使用分隔符清楚的指示提问的不同部分

分隔符例如三引号、XML 标记、章节标题等,可以帮助区分文本的不同部分。

Better Prompt:

1
2
3
4
5
6
7
8
9
10
11
12
示例 1: 使用XML标记作为分隔符

你将获得关于同一主题的两篇文章(用XML标记分隔)。首先总结每篇文章的论点。然后指出哪一个论点更站得住脚并解释原因。

<article> insert first article here </article>
<article> insert second article here </article>

示例 2: 使用三引号作为分隔符

总结用三引号分隔的文本

"""insert text here"""

4. 指定完成任务所需的步骤

指定完成任务所需的步骤,大模型会按照步骤执行,而不是直接给出最终答案。这可以确保大模型理解你的意图,并正确地执行任务。

Worse Prompt: 请帮我写一篇关于 xxx 的热点新闻的解读

Better Prompt:

1
2
3
4
请你按照下列步骤来给我撰写一篇关于xxx的热点新闻的解读
Step 1: 检索关于xxx的新闻,选取热度较高的3篇新闻
Step 2: 对3篇新闻进行摘要,并分析其核心观点和原因,先形成解读的目录框架
Step 3: 根据目录框架,对3篇新闻进行详细解读,并解释其观点和原因

5. 向大模型提供例子

Worse Prompt: 我是一个前端初学者,请给我推荐一篇关于 css 的文章

得到的回复:

Better Prompt:

1
2
3
4
5
6
7
我是一个前端初学者,请给我推荐一篇关于css的文章,按照 四等号 中的示例回复我:

====
文章名称:xxx
文章链接:xxx
推荐理由:xxx
====

得到的回复:

6. 指定大模型返回的长度

你可以要求模型生成具有给定目标长度的输出,目标输出长度可以根据单词、句子、段落、项目符号等的数量来指定。

但是请注意,指示模型生成特定数量的单词并不能达到高精度。模型可以更可靠地生成具有特定数量的段落或要点的输出。

Better Prompt:

1
2
3
示例 1: 用三引号分隔的文本在50个单词内进行总结。
示例 2: 用两段话总结用三引号分隔的文本。
示例 3: 用3个要点总结三引号分隔的文本。

向大模型提供参考文本

大模型可以通过自由地发挥来回答你的提问,特别是当被问及深奥的话题或引用和 url 时,这就会导致大模型回复给用户了一个假答案

为大模型提供参考文本可以限制它的过度自由发挥,同时也减少得到假答案的可能性

如果我们可以为模型提供与当前查询相关的可信信息,那么我们可以指示模型使用提供的信息来组成它的答案。

Better Prompt:

1
2
3
4
5
使用三引号分隔的文章回答问题。如果在文章中找不到答案,就写“我找不到答案”。

"""insert article here"""

问题:xxx

考虑到所有模型都有有限的上下文窗口,我们需要一些方法来动态查找与所问问题相关的信息 — 这就是需要用到 知识库

将复杂的任务拆分为更简单的子任务

正如将复杂系统分解为一组模块化组件是软件工程中的良好实践一样,提交给语言模型的任务也是如此。复杂任务往往比简单任务的错误率更高。

此外,复杂的任务通常可以重新定义为简单任务的工作流,其中早期任务的输出用于构造后续任务的输入。

对于需要大量独立指令来处理不同情况的任务,首先对查询类型进行分类,并使用该分类来确定需要执行哪些指令。还可以递归地应用此过程,将任务分解为一系列阶段。这可以通过定义固定类别和硬编码与处理给定类别中的任务相关的指令来实现。

Better Prompt:

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

// 先对用户查询进行分类,类似于定义不同的功能分支

您将获得客户服务查询。将每个查询划分为主要类别和次要类别。根据主要类别和次要类别给出你的回复。
主要类别:前端 和 后端
次要类别:
前端次要类别:ui设计 和 接口调用
后端次要类别:bug修复 和 接口开发

// 根据分类,给出对应的回复

当用户询问你的上下文涉及「ui设计」时,请按照以下步骤为用户提供帮助:
- Step 1:询问用户 ui 的类型,例如:按钮、输入框、弹窗等
- Step 2: 询问用户要设计的 ui 的大致样式,例如尺寸、颜色、字体等
- Step 3: 根据用户要求给出ui设计的html 和 css代码,并询问用户是否对此满意
- Step 4: 如果用户满意,则 接着询问用户该ui是否需要设计JS逻辑;否则,重复步骤 1 和 2
- Step 5: 如果用户需要设计JS逻辑,则根据用户需求给出JS代码;如果用户不需要设计JS逻辑,则结束

当用户询问你的上下文涉及「接口调用」时,请按照以下步骤为用户提供帮助:
- Step 1: 询问用户接口类型、接口url 和 接口参数
- Step 2: 询问用户是否指定某个工具库进行网络请求,如果用户未指定,则默认使用axios来编写请求
- Step 3: 根据用户需求给出请求代码,并询问用户是否对此满意
- Step 4: 如果用户满意,则 结束;否则,重复步骤 1 和 2

当用户询问你的上下文涉及「bug修复」时,请按照以下步骤为用户提供帮助:
- Step 1: 询问用户bug的位置、bug描述和bug截图
- Step 2: 根据用户需求给出修复代码,并询问用户是否对此满意
- Step 3: 如果用户满意,则 结束;否则,重复步骤 1 和 2

当用户询问你的上下文涉及「接口开发」时,请按照以下步骤为用户提供帮助:
- Step 1: 询问用户接口类型、接口url 和 接口参数
- Step 2: 询问用户使用什么编程语言或者框架来编写接口代码,如果用户未指定,则默认使用Java
- Step 3: 根据用户需求给出接口代码,并询问用户是否对此满意
- Step 4: 如果用户满意,则 结束;否则,重复步骤 1 和 2

如果你已经理解上述内容,请回复我「已理解」,我会在下一条问题中咨询你问题,请根据上述内容进行匹配和回答。

使用这种方法的好处就在于:

通过发出特殊字符串来指示模型,以指示对话状态何时发生变化。这使我们能够将系统转换为状态机,其中状态决定注入哪些指令。通过跟踪状态,与该状态相关的指令,以及可选的允许从该状态转换的状态。

得到的回复:

给模型时间去「思考」

当试图立即回答问题时,模型会犯更多的推理错误,而不是花时间找出答案。在回答问题之前询问“思维链”可以帮助模型更可靠地推理出正确的答案。

1. 在匆忙得出结论之前,指示模型自己找出解决方案

当我们明确地指示模型在得出结论之前从第一原则进行推理时,我们会得到更好的结果。例如,假设我们需要一个模型来评估学生对数学问题的解决方案。最明显的解决方法就是简单地询问模型学生的解决方案是否正确。

来看下面的例子:

Worse Prompt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户:

判断学生的答案是否正确。

问题陈述:我正在建造一个太阳能装置,我需要帮助来解决财务问题。
-土地成本为每平方英尺100美元
-我能以每平方英尺250美元的价格买到太阳能电池板
-我谈了一份维护合同,每年要花我10万美元,每平方英尺还要额外花10美元
第一年运营的总成本是多少作为平方英尺数的函数?

学生解答:设x为装置面积,单位为平方英尺。
1. 土地成本:100倍
2. 太阳能电池板成本:250倍
3. 维护费用:10万+ 100倍
总成本:100x + 250x + 100000 + 100x = 450x + 100000


大模型回复: 解决方案正确!

但是这个学生的答案其实是不正确的!

在我们可以通过提示模型首先生成自己的解决方案来让模型成功地注意到这一点,即让模型先自己进行思考。

Better Prompt

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
用户:

首先想出你自己解决这个问题的办法。然后将你的解决方案与学生的解决方案进行比较,并评估学生的解决方案是否正确。在你自己做了这道题之前,不要判断这个学生的答案是否正确。

问题陈述:我正在建造一个太阳能装置,我需要帮助来解决财务问题。
-土地成本为每平方英尺100美元
-我能以每平方英尺250美元的价格买到太阳能电池板
-我谈了一份维护合同,每年要花我10万美元,每平方英尺还要额外花10美元
第一年运营的总成本是多少作为平方英尺数的函数?

学生解答:设x为装置面积,单位为平方英尺。
1. 土地成本:100倍
2. 太阳能电池板成本:250倍
3. 维护费用:10万+ 100倍
总成本:100x + 250x + 100000 + 100x = 450x + 100000


大模型回复:

设x为装置面积,单位为平方英尺。
1. 土地成本:100倍
2. 太阳能电池板成本:250倍
3. 维护费用:10万+ 10倍
总成本:100x + 250x + 100000 + 10x = 360x + 100000

这个学生的答案不正确。他们在维修成本计算中犯了一个错误,用100倍代替10倍。第一年运营的正确总成本是360x + 100,000。

2. 使用内心独白或查询序列来隐藏模型的推理过程

前面的策略表明,在回答特定问题之前,模型有时对问题进行详细的推理是很重要的。

对于某些应用程序,模型用于得出最终答案的推理过程可能不适合与用户共享。例如,在辅导应用程序中,我们可能希望鼓励学生自己找出答案,但模型对学生解决方案的推理过程可能会向学生揭示答案。

内心独白是一种可以用来缓解这种情况的策略。内心独白的思想是指示模型将对用户隐藏的部分输出放入结构化格式中,以便于解析它们。然后,在将输出呈现给用户之前,对输出进行解析,只显示想要给用户看到的部分。

Better Prompt

1
2
3
4
5
6
7
8
9
10
11
12
13
你作为一个专业和资深数学老师,判断学生的答案是否正确。

按照以下步骤回答用户查询,先不要输出任何三引号(""")中的内容,等执行完所有步骤时再根据步骤的要求输出应该输出的内容。

Step 1: 首先找出你自己解决问题的方法。不要依赖学生的答案,因为它可能是错误的。将此步骤的所有工作用三引号(""")括起来。

Step 2: 将你的解决方案与学生的解决方案进行比较,并评估学生的解决方案是否正确。将此步骤的所有工作用三引号(""")括起来。

Step 3: 如果学生犯了一个错误,确定你可以给学生什么提示而不泄露答案。将此步骤的所有工作用三引号(""")括起来。

Step 4: 如果学生犯了错误,只给学生提供上一步的提示,不要输出三引号中的内容。不要写“Step 4: .....”,而要写“提示: ...”。

如果你已经理解上述内容,请回复我「已理解」,我会在下一条问题中给出数学问题 和 学生的解决方案,请根据上述内容进行匹配和回答。

测试 Prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户:

问题陈述:我正在建造一个太阳能装置,我需要帮助来解决财务问题。
-土地成本为每平方英尺100美元
-我能以每平方英尺250美元的价格买到太阳能电池板
-我谈了一份维护合同,每年要花我10万美元,每平方英尺还要额外花10美元
第一年运营的总成本是多少作为平方英尺数的函数?

学生解答:设x为装置面积,单位为平方英尺。
1. 土地成本:100倍
2. 太阳能电池板成本:250倍
3. 维护费用:10万+ 100倍
总成本:100x + 250x + 100000 + 100x = 450x + 100000


大模型:

提示: 请检查维护合同中变量成本的计算,确保每平方英尺的额外费用被正确计算。

React-query

查询 queries

对于query的定义,在官方文档中是这么说的:
“A query is a declarative dependency on an asynchronous source of data that is tied to a unique key” 翻译过来是:查询是对绑定到唯一键的异步数据源的声明性依赖项

我对于query的理解是:将一个唯一的key(unique key) 与 一个获取数据的方法 进行绑定

你可以在组件中或者hook中,使用 useQuery 来订阅一个查询

useQuery 至少需要接受两个参数:

  • key:该查询的唯一键值
  • fn:一个返回Promise的函数,能解析出数据或者抛出错误(即具体的请求数据的函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useQuery } from '@tanstack/react-query'

function App() {
// 具体获取数据的函数
const getUserInfo = async () => {
const data = await axios('/api/useInfo')
return data
}

// 订阅一个查询
const userInfo = useQuery({
queryKey: ['useData'],
queryFn: getUserInfo
})
}

这个唯一的key值,将在内部用于重新获取、缓存和在整个项目中共享该查询的信息

useQuery的返回结果包含了你需要用到的绝大部分信息,这也正是使用react-query的一个好处,可以提高开发效率

1
2
3
4
5
6
//  接上面例子

const { isLoading, isError, isSuccess, data, error, status } = useQuery({
queryKey: ['useData'],
queryFn: getUserInfo
})

首先是结果中会返回一些比较重要的状态:

  • 布尔值isLoading 或者 status === 'loading' 表示查询暂时还没有数据
  • 布尔值isError 或者 status === 'error' 表示查询遇到了错误
  • 布尔值isSuccess 或者 status === 'success' 表示查询成功,数据可用

可以看出 status 和 isLoading、isError、isSuccess的作用是差不多的,关键在于你习惯于使用谁来进行判断

然后是一些重要的信息:

  • error 如果查询处于isError状态,可以获取到错误的具体信息
  • data 如果查询处于isSuccess状态,可以获取到请求的数据

下面是一个简单的例子,来整体演示使用useQuery

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
import { useQuery } from '@tanstack/react-query'

function App() {
// 具体获取数据的函数
const getUserInfo = async () => {
const data = await axios('/api/useInfo')
return data
}

// 订阅一个查询
const { isLoading, isError, isSuccess, data, error, status } = useQuery({
queryKey: ['useData'],
queryFn: getUserInfo
})

if (status === "loading") {
return <span>Loading...</span>;
}

if (status === "error") {
return <span>Error: {error.message}</span>;
}

return (
<div>
<div>userName: {data.userName}</div>
<div>age: {data.age}</div>
</div>
)
}

查询键值 queryKey

在react-query内部基于queryKey来管理查询缓存

queryKey必须是一个数组,对数组内部的结构没有过多的限制,该数组可以简单的由一个或多个字符串构成,也可以是包含许多嵌套对象的数组。

  1. 最简单的形式
1
2
3
4
useQuery({ 
queryKey: ['userData'],
...
})
  1. 复杂的形式

当查询需要更多的信息来唯一的描述数据时,数组可以是 字符串 加上 任意数量的可序列化对象的形式

常见的场景有:需要传递参数来进行查询

1
2
3
4
5
6
7
8
9
10
11
// 根据 userId 查询数据
let userId = 'xxx'
useQuery({
queryKey: ['useInfo', userId],
...
})

// 查询特定type的数据
useQuery({
queryKey: ['xxxx', { type: 'xxx' }]
})

注意:
queryKey的散列是确定的,即 顶层数组中各个key不同的排列顺序会被认为是不同的查询键值

1
2
3
4
5
6
7
// 以下两个的查询键值不相等,因为顶层数组中各key的排列顺序不同
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})

// 以下两个的查询键值是相等的,顶层数组中各key的排列顺序相同,嵌套对象中的排列顺序不影响
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})

查询函数 queryFn

官方文档对queryFn的定义:A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.

即:queryFn可以是任何形式的函数,必须返回一个promise,并且返回的promise应该 给出数据 或者 抛出错误

  1. queryFn的参数

queryFn默认接受一个queryFunctionContext参数,该参数是一个对象,主要包含以下属性:

  • queryKey:即整个queryKey数组
  • pageParam:在 无限查询 中会使用到,包含查询当前页所使用的参数
  • signal:用作 查询取消
  • meta:可以填写任意关于该查询的额外信息

由上可知,queryKey不仅可以用于唯一的标识查询,也可以作为参数传递给queryFn

1
2
3
4
5
6
7
8
9
10
useQuery({  
queryKey: ['useData', useId],
queryFn: getUserData
})

const getUserData = async ({ queryKey }) => {
const [ _key, useId ] = queryKey
const data = await fetch('/api/useData' + useId)
return data
}
  1. 抛出和处理错误

为了使 React Query 确定查询错误,查询函数的错误必须抛出或返回rejected Promise。查询函数中引发的任何错误都将被持久化在查询的error状态中

并行查询

并行查询即并行的执行多个查询,或者说同时执行的查询

  1. 如果需要并行查询的数量较少且固定,可以使用手动的并行查询
1
2
3
4
5
6
7
function App () {
// 下面的查询将自动地并行执行
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams });
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects });
...
}

在 React 的 suspense 模式下使用 React Query 时,这种并行模式不起作用。 因为第一个查询将在内部抛出 Promise,并且将在其他查询运行之前挂起组件。 此时建议使用 useQueries hook

  1. 使用useQueries进行动态并行查询

如果需要执行的查询数量不固定,即在每次渲染之间都会变化,那就不能进行手动查询了

useQueries接受一组作为查询配置的对象,并以数组形式返回查询的结果:

1
2
3
4
5
6
7
8
9
10
function App({ users }) {
const userQueries = useQueries({
queries: users.map((user) => {
return {
queryKey: ["user", user.id],
queryFn: () => fetchUserById(user.id),
};
}),
});
}

有依赖的查询

有依赖的查询 或者说 按顺序排列的查询,即当前查询是否执行(或何时执行)依赖于前一个查询的结果

可以使用enable配置项来告诉query何时可以运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 先执行这个query来拿到user的数据
const { data } = useQuery({
queryKey: ["user", email],
queryFn: getUserByEmail,
});

const userId = data?.id;

// 此query依赖于 user query 的结果
const { data } = useQuery({
queryKey: ["projects", userId],
queryFn: getProjectsByUser,
// 直到 userId 存在,查询才会被执行
enabled: !!userId,
});

QueryClient

使用QueryClient可以创建一个query客户端,来与query缓存联系起来

1
2
3
4
5
6
7
8
9
10
import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient({
// 全局设置query的一些配置
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})

QueryClient可传入一个对象,其中包含三个参数(三个参数都是可选的):

  • defaultOptions:为使用该client的所有查询(query)和修改(mutation)设置默认的配置项
  • queryCache:该client所连接的query缓存
  • mutationCache:该client所连接的mutation缓存

配置项

  1. refetchOnWindowFocus

如果用户在短暂离开窗口后回来时,数据被标记为过时的,react-query会在后台自动请求新的数据

可以使用该配置项在全局或者单个查询中禁用该功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全局
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})

// 单个查询
useQuery({
queryKey: ["useData"],
queryFn: getUserData,
refetchOnWindowFocus: false,
})
  1. enabled

可以为单个查询配置enabled = false来禁用自动查询

enabled = false时:

  • 如果查询已经缓存了数据,将以status === 'success'进行初始化
  • 如果查询没有缓存数据,将以status === 'loading'进行初始化
  • 该查询不会在挂载时自动获取数据、不会在后台重新获取数据
  • 将忽略客户端的invalidateQueriesrefetchQueries调用
  • useQuery返回的 refetch 可用于手动触发查询以进行数据获取
1
2
3
4
5
6
7
8
// 手动触发查询
const { isError, data, error, refetch } = useQuery({
queryKey: ["useData"],
queryFn: getUserData,
enabled: false,
})

<button onClick={() => refetch()} >手动触发查询</button>

永久性的禁用查询并不是你使用react-query的理由,你可能更多的需要进行惰性查询

  1. retry

useQuery查询失败时,如果该查询的请求未达到最大连续重试次数(默认 3 次),那么react-query将自动重试该查询。

可以在全局或者单个查询上配置重试逻辑

  • retry = false 将禁用重试 && retry = true 将无限次重试
  • retry = 5 设置为一个数字,表示最大重试次数
  • retry =(failureCount,error)=> ... 允许基于请求失败的原因进行自定义逻辑

QueryClient的一些api

  1. queryClient.fetchQuery

该方法是一个异步的方法,用于触发查询并将结果缓存,通常用于预加载数据。

参数

fetchQuery接受的参数和useQuery相同,即 queryKey、queryFn、option(可选)

返回值

调用fetchQuery将返回一个promise对象,包含了请求错误的信息 或者 成功时的数据

当调用fetchQuery时会执行如下步骤:

  • 检查缓存中是否已有与提供的 queryKey 对应的缓存数据
  • 如果缓存中有数据,并且数据是新鲜的,那么fetchQuery将返回这些数据。
  • 如果缓存中没有数据,或者数据已过时,那么将执行 queryFn,并将结果数据放入缓存中。

使用示例

演示用户在导航到用户详情页时,使用fetchQuery预加载用户数据

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
43
44
45
46
47
48
49
50
import { QueryClient } from 'react-query';

// 创建一个 QueryClient 实例
const queryClient = new QueryClient();

// 一个函数,用于获取用户数据
const fetchUserById = async (userId) => {
const response = await fetch(`api/users/${userId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

// 假设这是一个事件处理函数,当用户点击时触发
const handleUserLinkClick = (userId) => {
// 使用 fetchQuery 预加载用户数据
queryClient.fetchQuery(['user', userId], () => fetchUserById(userId));

// 在这里,我们可能会进行路由导航操作
navigate(`/users/${userId}`);
};

// 用户详情组件
const UserDetails = ({ userId }) => {
// 使用 useQuery 钩子查询用户数据
const { data, isLoading, error } = useQuery(
['user', userId],
() => fetchUserById(userId),
{
// 如果数据已经在 fetchQuery 中预加载,这里会立刻拿到缓存数据
staleTime: Infinity, // 你可以根据需要设置合适的新鲜度
}
);

if (isLoading) {
return <span>Loading...</span>;
}

if (error) {
return <span>Error: {error.message}</span>;
}

return (
<div>
<h1>{data.name}</h1>
{/* ... 其他用户信息 */}
</div>
);
};
  1. queryClient.prefetchQuery

该方法是一个异步的方法,用于在后台提前获取数据并将其存储在缓存中,以便在未来某个时刻需要时能够立即使用

prefetchQuery的工作方式与fetchQuery大致相同,但prefetchQuery更关注于数据的预加载,它通常不返回数据,只是单纯地将数据预加载到缓存中。同时,prefetchQuery通常在数据需要之前调用,而不是在渲染组件时调用

  1. queryClient.getQueryData

该方法是一个同步方法,用于返回已存在的查询的缓存数据,如果没有缓存数据则返回undefined

参数

只需要传入 queryKey 即可

返回值

如果缓存存在则返回数据,否则返回undefined

使用示例

const data = queryClient.getQueryData(queryKey)

更多

其他更多的queryClient api 可以前往官网查看

主动查询失败 Query Invalidation

查询会在过时之后自动重新查询,但是在很多时候,由于修改了某些数据,你能明确的知道数据已经是过时的了(即使它还没有到默认的过时时间)。

这个时候可以调用QueryClientinvalidateQueries方法来明确的告诉react-query数据已经过时了,并重新查询新的数据

简单的例子:

1
2
3
4
5
6
7
8
9
10
import { useQuery, useQueryClient } from "@tanstack/react-query"

// 获取 queryClient
const queryClient = useQueryClient()

// 使缓存中的每个查询都无效
queryClient.invalidateQueries()

// 无效以 userData 开头的键值的查询
queryClient.invalidateQueries({ queryKey: ["userData"] })

对于传入queryKey使查询失效的精确度,也有多种方式进行控制

  1. 传入特定的(或者完整)的queryKey
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
queryClient.invalidateQueries({
queryKey: ["userData", { userName: "easy code sniper" }],
})

// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData", { userName: "easy code sniper" }],
queryFn: getUserData,
})

// 该查询不会被无效
const userQuery = useQuery({
queryKey: ["userData"],
queryFn: getUserData,
})
  1. 使用exact配置,表示只想使指定的queryKey对应的查询失效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
queryClient.invalidateQueries({
queryKey: ["userData"],
exact: true,
})

// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData"],
queryFn: getUserData,
})

// 该查询不会被无效
const userQuery = useQuery({
queryKey: ["userData", { userName: "easy code sniper" }],
queryFn: getUserData,
})
  1. 自定义更精细化的查询失效

可以将 predicate 函数传递给invalidateQueries方法。 此函数将从查询缓存中接收每个Query实例,并允许你返回 true 或 false 来确定是否使该查询无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === "userData" && query.queryKey[1]?.age >= 18,
});

// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData", { age: 20 }],
queryFn: getUserData,
});

// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData", { age: 10 }],
queryFn: getUserData,
});

修改 Mutations

对于mutations的定义,在官方文档中是这么说的:
“mutations are typically used to create/update/delete data or perform server side-effects.”

即:用于创建、删除、更新数据或者执行服务器命令等操作

你可以在组件或者自定义hook中使用 useMutation 来修改数据

简单示例:修改用户的信息

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
function App() {
const mutation = useMutation({
mutationFn: (userInfo) => {
return axios.post("/updateUserInfo", userInfo);
},
});

return (
<div>
{mutation.isLoading ? (
"updating ..."
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}

{mutation.isSuccess ? <div>updated!</div> : null}

<button
onClick={() => {
mutation.mutate({ userName: 'easy code sniper' });
}}
>
修改用户信息
</button>
</>
)}
</div>
);
}

由上面的例子可以看出,你可以通过调用mutation方法来给mutationFn传入参数

useMutation和查询一样也会返回一些状态和信息:

状态:

  • 布尔值isLoading 或者 status === 'loading' 表示修改正在进行
  • 布尔值isError 或者 status === 'error' 表示修改遇到了错误
  • 布尔值isSuccess 或者 status === 'success' 表示修改成功,数据可用

信息:

  • error 如果修改处于isError状态,可以获取到错误的具体信息
  • data 如果修改处于isSuccess状态,可以获取到数据

注意: mutate函数是一个异步函数,在React16及以前版本,你不能在事件回调中直接使用它。你需要将mutate包装在另一个函数中

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
// 在React16及之前的版本,这将无法正常工作
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (event) => {
event.preventDefault()
return fetch("/api", new FormData(event.target))
},
})

return <form onSubmit={mutation.mutate}>...</form>
}

// 这将正常工作
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (formData) => {
return fetch("/api", formData)
},
})
const onSubmit = (event) => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}

return <form onSubmit={onSubmit}>...</form>
}

副作用

useMutation最便利,也是最有用的功能可能就在于它能定义一些副作用配置,这些配置允许在其生命周期的任何阶段快速而简单地产生副作用。

一个最常见的例子就是,在修改数据之后能自动的重新获取最新的数据(如果你经历过一些开发,那你一定会对这个功能感到兴奋🚀~~)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
useMutation({
mutationFn: updateUserInfo,
onMutate: (variables) => {
// 修改即将发生!
},
onError: (error, variables, context) => {
// 错误触发!
},
onSuccess: (data, variables, context) => {
// 成功时触发
},
onSettled: (data, error, variables, context) => {
// 错误或成功……这并不重要
},
})

除了在useMutation中可以配置这些副作用项外,还可以在调用mutate函数时配置组件特定的副作用,支持的配置项包括:onSuccessonErroronSettled

1
2
3
4
5
6
7
8
mutate(userInfo, {
onSuccess: (data, variables, context) => {
},
onError: (error, variables, context) => {
},
onSettled: (data, error, variables, context) => {
},
})

注意: 如果组件在修改完成之前就被卸载了,那使用mutate配置的组件特定的副作用将不被运行

修改导致的查询失败

在开发中非常常见的是,当应用中的一个对数据的修改成功时,很有可能在你的应用中有相关的查询需要作废,并需要重新获取数据来解释修改所产生的新变化

假设我们调用const mutation = useMutation({ mutationFn: updateUserInfo })对用户信息进行了修改,那么势必导致现在渲染的数据是过时的。我们希望所有对userInfo的查询都失效,并重新获取最新的数据

我们可以使用useMutation的副作用onSuccess配置 和QueryClient的invalidateQueries函数来实现

1
2
3
4
5
6
7
8
9
10
11
import { useMutation, useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

// 当此修改成功时,将所有带有useData查询键值的查询都无效
const mutation = useMutation({
mutationFn: updateUserInfo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["userData"] })
},
})

通过修改的数据更新查询内容

当在处理更新某些数据时,新的数据往往会在更新的响应中自动返回,我们可以利用修改函数返回的对象,并使用 Query Client 的 setQueryData 方法立即用新数据更新现有的查询,而不是去触发新的数据获取,浪费对已有数据的网络调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: updateUserInfo,
onSuccess: (data) => {
queryClient.setQueryData(["userData", { userName: 'cqy' }], data);
},
});

mutation.mutate({
userName: 'cqy',
age: 22,
});

// 下面的查询将被更新为成功的修改响应
const { status, data, error } = useQuery({
queryKey: ["useData", { userName: 'cqy' }],
queryFn: fetchTodoById,
});

useInfiniteQuery

useInfiniteQuery钩子用于处理无限滚动或分页场景,它可以用来逐页加载数据,并且可以无缝地集成更多数据加载到现有数据集中。

参数

useInfiniteQuery接受以下参数:

  • queryKey
  • queryFn
  • options (可选): 一个配置对象,包含以下属性:
    • getNextPageParam: 一个函数,用于从最后一页的数据中获取下一页的 pageParam
    • getPreviousPageParam: 一个函数,用于从第一页的数据中获取上一页的 pageParam
    • 其他 React Query 提供的所有配置选项,如 staleTime, cacheTime, onSuccess, onError 等

getNextPageParam

getNextPageParam用于定义如何从获取到的数据中提取分页参数,以便加载下一页的数据。

getNextPageParam函数接收两个参数:

  • lastPage: 当前查询返回的最后一页数据。
  • allPages: 当前已经加载的所有页面组成的数组。

函数的返回值应该是一个值,这个值将作为下一个 pageParam 参数传递给 queryFn 函数以获取下一页数据。

示例:

1
2
3
4
5
6
7
8
9
const getNextPageParam = (lastPage, allPages) => {
// 如果 API 有一个 'nextPage' 字段,就返回它来获取下一页
return lastPage.nextPage;
};

const getNextPageParam = (lastPage, allPages) => {
// 如果 API 使用游标,并且存在一个 'nextCursor' 字段,就返回它
return lastPage.nextCursor;
};

getPreviousPageParam

getPreviousPageParam用于获取前一页数据,在实现双向无限滚动或分页时特别有用。

getPreviousPageParam函数接收两个参数:

  • firstPage: 当前查询返回的第一页数据。
  • allPages: 目前已经加载的所有页面数据的数组。
    这个函数的返回值应该是一个标识,表示用于 queryFn 函数获取前一页数据时所需的 pageParam

示例:

1
2
3
4
5
6
7
8
9
const getPreviousPageParam = (firstPage, allPages) => {
// 如果 API 有一个 'prevPage' 字段,就返回它来获取前一页
return firstPage.prevPage;
};

const getPreviousPageParam = (firstPage, allPages) => {
// 如果 API 使用游标,并且存在一个 'prevCursor' 字段,就返回它
return firstPage.prevCursor;
};

返回值

useInfiniteQuery返回一个对象,其中包含以下属性:

  • status: 查询的状态(’loading’, ‘error’, ‘success’, ‘idle’)。
  • data: 包含每一页数据的数组。
  • error: 查询失败时的错误对象。
  • fetchNextPage: 函数,用于获取下一页数据。
  • fetchPreviousPage: 函数,用于获取上一页数据。
  • hasNextPage: 布尔值,表示是否还有下一页数据。
  • hasPreviousPage: 布尔值,表示是否还有上一页数据。
  • isFetchingNextPage: 布尔值,表示是否正在获取下一页数据。
  • isFetchingPreviousPage: 布尔值,表示是否正在获取上一页数据。
  • isFetching: 布尔值,表示是否正在进行查询(包括初始查询和后续的分页查询)。
  • isLoading: 布尔值,表示是否正在进行初始查询。
  • isRefetching: 布尔值,表示是否正在重新获取数据。
  • 其他 React Query 钩子通常返回的属性,如 refetch, remove 等。

data

useInfiniteQuery返回的data与常规的useQuery不同,因为它需要处理多个“页”的数据,而不是单个数据集。

data 是一个对象,它包含以下属性:

  • pages: 一个数组,其中的每一个元素代表了每一页加载的数据。这些数据按照加载的顺序排列。例如,data.pages[0] 是第一页的数据,data.pages[1] 是第二页的数据,依此类推。
  • pageParams: 这个数组与 pages 数组有相同的长度,包含了获取每一页数据时 queryFn 函数使用的参数。通常这些参数是由 getNextPageParamgetPreviousPageParam 函数提供的。

使用示例

下面是一个使用 useInfiniteQuery 来实现无限滚动加载文章列表的例子:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useInfiniteQuery } from 'react-query';

// 分页获取文章的函数
const fetchArticles = async ({ pageParam = 1 }) => {
const res = await fetch(`/api/articles?page=${pageParam}`);
return res.json();
};

// 组件内部
const ArticlesList = () => {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery('articles', fetchArticles, {
getNextPageParam: (lastPage, pages) => {
if (lastPage.nextPage) {
return lastPage.nextPage;
} else {
return undefined;
}
},
});

if (status === 'loading') return <p>Loading...</p>;
if (status === 'error') return <p>Error: {error.message}</p>;

return (
<>
{data.pages.map((page, i) => (
<React.Fragment key={i}>
{page.articles.map(article => (
<p key={article.id}>{article.title}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
</>
);
};

useMutation

useMutation 钩子是用来处理异步逻辑(如创建、更新或删除数据)的,这些逻辑会引起数据的变化。与useQueryuseInfiniteQuery不同的是,useMutation不是用来获取数据的,而是用来修改数据

参数

useMutation 接受以下参数:

  • mutationFn (必须): 一个函数,它执行异步逻辑(比如API调用)。这个函数接受你传递给mutate函数的变量。
  • options (可选): 一个配置对象,可以包含如下属性:
    • onMutate: 在 mutationFn 执行之前调用的函数,用于执行乐观更新或返回回滚函数的数据。
    • onSuccess: 当 mutationFn 成功完成时调用的函数。
    • onError: 当 mutationFn 执行失败时调用的函数。
    • onSettled: 无论 mutationFn 成功还是失败都会调用的函数。
    • 以及其他选项

返回值

useMutation 返回一个对象,其中包含以下属性和方法:

  • mutate: 一个函数,你可以用它来触发异步逻辑(mutationFn)的执行。
  • mutateAsync: 类似于mutate,但是返回一个Promise,可以用于async/await。
  • data: mutationFn成功解析的数据。
  • error: 如果mutationFn抛出错误,这里会包含错误对象。
  • isLoading: 如果mutationFn正在执行,这里会是true。
  • isSuccess: 如果mutationFn成功完成,这里会是true。
  • isError: 如果mutationFn执行失败,这里会是true。
  • status: mutation的状态(idle、loading、success、error)。

读JS红宝书

for-in 和 for-of

for-in

for-in语句用于枚举对象中的非符号键属性,for-in语句不能保证返回对象属性的顺序

语法:

1
2
3
for ( const key in obj ) {
// ...
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = {
name: 'cqy',
age: 11,
hobby: ['play', 'code', 'sleep'],
family: {
mom: 'mother',
father: 'father'
},
isMale: true,
[Symbol()]: 'tag'
}

for( const key in obj ) {
console.log( key );
}

// name
// age
// hobby
// family
// isMale

for-of

for-of语句用于遍历可迭代对象的属性,会按照可迭代对象的next()方法产生值的顺序迭代元素

语法:

1
2
3
for ( const key of iterator ) {
// ...
}

使用示例:

1
2
3
for ( const el of [1,2,3,4] ) {
console.log(el)
}

执行上下文与作用域

变量或函数的上下文决定了它们可以访问哪些数据,每个上下文都有一个关联的变量对象,这个上下文中定义的所有变量和函数都存在于这个对象上。

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。

上下文栈:
每个函数调用都有自己的上下文。当代码执行流进入函数,函数的上下文被推到一个上下文栈上。在函数执行完后,上下文栈会弹出该函数上下文,将控制权还给之前的执行上下文。

作用域链:
上下文中的代码在执行时,会创建 变量对象 的一个 作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

正在执行的上下文的 变量对象 始终位于作用域链的最前端(即最先被访问)

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索从作用域链的最前端开始,逐级往后,直到找到标识符。

原始值包装类型

为了方便操作原始值,ES提供了3种特殊的引用类型:BooleanNumberString

每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法

1
2
3
4
5
let name = 'easy code sniper'

let arr = name.split(' ')

console.log(arr); // [ 'easy', 'code', 'sniper' ]

在这里 name 是一个字符串类型的变量,是一个原始值。但却可以调用split方法。原始值本身不是对象,因此逻辑上不应该有方法。

name原始值能够调用split方法,是后台进行了许多处理。在以读模式(读取变量保存的值)访问字符串值的任何时候:

  • 后台会创建一个String类型的实例
  • 调用实例上的特定方法
  • 销毁实例

可以将上述3步想象成执行了如下3行代码:

1
2
3
let name = new String('easy code sniper')
let arr = name.split(' ')
name = null

这种行为可以让我们在原始值上调用对象的方法。对于布尔值和数值,也是执行以上步骤,只不过使用的是BooleanNumber包装类型而已。

这种自动创建的原始值包装对象只存在于访问它的那行代码执行期间,这意味着不能在运行时给原始值添加属性和方法

Array.from 和 Array.of

Array.from

Array.from用于将 类数组结构 转换为 数组。第一个参数是一个类数组对象(任何可迭代的结构),或者有一个length属性和可索引元素的结构

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 字符串会被拆分为单字符数组
*/
console.log(Array.from('cqy')); // [ 'c', 'q', 'y' ]

/**
* 对现有数组进行浅拷贝
*/
let arr1 = [1, 2, { name: 'cqy' }]

let arr2 = Array.from(arr1)

arr2[2].name = 'easy code sniper'

console.log(arr1); // [ 1, 2, { name: 'easy code sniper' } ]

/**
* 将arguments转化为数组
*/
function func() {
console.log(Array.from(arguments));
}

func(1,2,3) // [ 1, 2, 3 ]

Array.from()还接受 第二个可选的映射函数参数,新数组的每一项都会调用这个映射函数,并将每一项作为参数传入映射函数。
还可以接受 第三个可选参数 ,用于指定映射函数中this的值(箭头函数中不适用

1
2
let arr = [1, 2, 3, 4]
let arr1 = Array.from(arr, function(x) { return x * this.num }, { num: 2 }) // [ 2, 4, 6, 8 ]

Array.of

Array.of用于将一组参数转换为数组实例

数组迭代方法

ES为数组定义了5个迭代方法。每个方法接收两个参数:

  • 以每一项为参数运行的函数,该函数接收3个参数:数组元素、索引、数组本身
  • 可选的 作为函数运行上下文的作用域对象(影响函数中的this的值)

5个迭代方法如下:

  • every:对数组的每一项都运行传入的函数,如果每一项函数都返回true,则这个方法返回true
  • filter:对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回
  • forEach:对数组每一项都运行传入的函数,没有返回值
  • map:对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
  • some:对数组每一项都运行传入的函数,如果有一项函数返回true,则这个方法返回true

数组归并方法

ES为数组定义了2个归并方法:reduce()reduceRight()。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。

reduce()方法从数组第一项开始遍历到最后一项,而reduceRight()从最后一项开始遍历到第一项

这两个方法都接收两个参数:

  • 对每一项都会运行的 归并函数
    • 归并函数接收4个参数:上一个归并值、当前项、当前项索引、数组本身
    • 归并函数的返回值会作为下一次调用本归并函数的第一个参数(如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的 第一个参数是数组的第一项 ,第二个参数是数组的第二项)
  • 可选的 以之为归并起点的初始值
1
2
3
4
5
let arr = [1,2,3,4,5]

let sum = arr.reduce((prev, current, index, array) => prev + current)

console.log(sum); // 15

迭代器

循环是迭代机制的基础,它可以指定迭代的次数,以及每次迭代要执行什么操作。

但循环又不适用于所有的数据结构。首先数组可以通过[]操作符取得特定索引位置上的项,这并不适用于所有数据结构;其次通过递增索引来访问数据是特定于数组的方式,并不适用于其他具有隐式顺序的数据结构

Array.prototype.forEach方法向通用迭代需求迈进了一步,解决了单独记录索引和通过数组对象取得值的问题。但是无法标识迭代何时终止,且回调结构也比较笨拙

迭代器模式

迭代器模式把有些结构称为「可迭代对象(iterable)」,因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费

可迭代对象是抽象的说法,可以将它理解成数组、集合类型的对象 或者 具有类似数组行为的其他数据结构。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。

迭代器(iterator)是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,并且迭代器会暴露用于迭代 可迭代对象 的API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值

可迭代对象必须暴露一个属性作为「默认迭代器」,这个属性使用特殊的Symbol.iterator作为键。这个默认迭代器属性引用一个迭代器工厂函数,调用这个工厂函数返回一个新迭代器

很多内置类型都实现了Iterable接口:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments对象
  • NodeList等DOM集合类型

检查是否存在默认迭代器属性:

1
2
3
4
5
6
7
8
9
10
11
12
// 没有实现迭代器工厂函数
let num = 1
console.log(num[Symbol.iterable]) // undefined

// 实现了迭代器工厂函数
let str = 'cqy'
console.log(str[Symbol.iterator]); // [Function: [Symbol.iterator]]

//调用这个工厂函数会生成一个迭代器
let str = 'cqy'
console.log(str[Symbol.iterator]()); // Object [String Iterator] {}

在实际开发中,不需要显式调用这个工厂函数来生成迭代器,一些原生语言结构会在后台调用可迭代对象的工厂函数

  • for-of循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合、映射
  • Promise.all()Promise.race()
  • yield*操作符

迭代器API使用next()方法在可迭代对象中遍历数据,每次成功调用next(),都会返回一个IteratorResult对象,其中包括:迭代器返回的下一个值valuedone状态(done为true 表示「耗尽」,即没有下一个值了)

1
2
3
4
5
6
7
8
let arr = ['aa', 'bb', 'cc']

let iter = arr[Symbol.iterator]()

console.log(iter.next()); // { value: 'aa', done: false }
console.log(iter.next()); // { value: 'bb', done: false }
console.log(iter.next()); // { value: 'cc', done: false }
console.log(iter.next()); // { value: undefined, done: true }

每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会单独的遍历可迭代对象

自定义迭代器

任何实现Iterator接口的对象都可以作为迭代器使用,下面的例子中的Counter类只能被迭代一定的次数

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
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit // 限制迭代的次数
}

next() {
if(this.count <= this.limit) {
return { done: false, value: this.count++ }
} else {
return { done: true, value: undefined }
}
}

[Symbol.iterator]() { // 返回实例,实例中定义了next方法用于迭代
return this
}
}

let counter = new Counter(3)

// 当使用for-of循环counter时,后台会去查找counter身上是否有 [Symbol.iterator]() 工厂函数,并调用它,
for(const i of counter) {
console.log(i); // 1 2 3
}

这个类实现了Iterator接口,但是它的每个实例只能被迭代一次。第二次迭代没有输出的原因是,第一次迭代已经使count === limit,所以第二次迭代不会返回任何内容

1
2
3
4
5
6
7
8
for(const i of counter) {
console.log(i); // 1 2 3
}


for(const i of counter) {
console.log(i); // 没有输出
}

要实现可以创建多个迭代器,必须美创建一个迭代器就对应一个新计数器。基于此需求,可以把计数器变量放到闭包中,通过闭包返回迭代器

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
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit
}

[Symbol.iterator]() {
let count = 1
let limit = this.limit

return {
next() {
if(count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
}
}
}
}

let counter = new Counter(3)

for(const i of counter) {
console.log(i); // 1 2 3
}

for(const i of counter) {
console.log(i); // 1 2 3
}

提前终止迭代器

提前终止迭代也是一个常见的需求,for-of循环可以通过break、continue、return或者throw提前退出,在退出时会寻找实现Iterator接口的对象上是否有return()方法。如果对象上有这个方法,在退出时就会调用这个方法

return()方法必须返回一个有效的IteratorResult对象,可以只简单的返回{ done: true }

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
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit
}

[Symbol.iterator]() {
let count = 1
let limit = this.limit

return {
next() {
if(count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
},

return() { // 自定义提前退出迭代方法
console.log('Exiting iter');
return { done: true }
}
}
}
}

let counter = new Counter(3)

for(const i of counter) {
if(i > 1) {
break
}
console.log(i);
}
// 1
// Exiting iter

生成器

生成器是 ES6 新增的结构,可以让函数拥有在函数块内暂停和恢复代码执行的能力

生成器的形式是一个函数,函数名称前面加一个星号( * )表示它是一个生成器,标识生成器函数的星号( * )不受两侧空格的影响:

1
2
3
4
5
6
7
function * generatorFn() {}
function* generatorFn() {}
function *generatorFn() {}

let generatorFn = function* () {}

console.log(generatorFn); // [GeneratorFunction: generatorFn]

调用生成器函数会产生一个生成器对象,生成器对象一开始处于暂停执行的状态。

生成器对象也实现了Iterator接口,因此具有next()方法,调用这个方法会让生成器开始或恢复执行

next()方法的返回值类似于迭代器,有一个done属性和一个value属性。函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done: true状态

1
2
3
4
5
6
7
function * generatorFn() {}

console.log(generatorFn()); // Object [Generator] {}

console.log(generatorFn().next); // [Function: next]

console.log(generatorFn().next()); // { value: undefined, done: true }

value属性是生成器函数的返回值,默认是undefined,可以通过生成器函数的返回值来指定

生成器函数只会在初次调用next()方法后开始执行

1
2
3
4
5
6
7
function * generatorFn() {
return 'easy code sniper'
}

console.log(generatorFn()); // 不会打印 easy code sniper

console.log(generatorFn().next()); // { value: 'easy code sniper', done: true }

yield中断执行

yield关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行

yield关键字有点像函数的中间返回语句,它生成的值会出现在next()方法返回的对象中

与 return 的区别:通过yield关键字退出的生成器函数处于done: false状态;通过return关键字退出的生成器函数处于done: true状态

1
2
3
4
5
6
7
8
9
10
11
12
function * generatorFn() {
yield 'cqy';
yield 'kyrie';
return 'easy code sniper'
}

let generatorObj = generatorFn()

console.log(generatorObj.next()); // { value: 'cqy', done: false }
console.log(generatorObj.next()); // { value: 'kyrie', done: false }
console.log(generatorObj.next()); // { value: 'easy code sniper', done: true }

显式的调用next()方法用处不大,但是可以在需要自定义迭代对象时,把生成器对象当作可迭代对象使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
function * generatorFn() {
yield 'cqy';
yield 'kyrie';
yield 'easy code sniper'
}

for(const item of generatorFn()) {
console.log(item);
}

// cqy
// kyrie
// easy code sniper

理解对象

JS使用一些内部特性来描述对象的属性的特征,对象的属性分两种:数据属性 和 访问器属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有4个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下是 true
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下是 true
  • [[Value]]:属性实际的值。默认是 undefined

使用Object.defineProperty()方法可以修改属性的默认特性。

该方法接收3个参数;要给其添加属性的对象、属性名称 和 一个描述符对象(描述符对象上的属性可以包含:configurable、enumerable、writable、value)

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {}

// 为 obj 创建一个 name 属性,并配置为不可修改
Object.defineProperty(obj, 'name', {
value: 'easy code sniper',
writable: false
})

console.log(obj.name); // easy code sniper

obj.name = 'cqy'

console.log(obj.name); // easy code sniper

访问器属性不包含数据值。它包含一个获取函数(getter)和一个设置函数(setter)。在读取访问器属性时,会调用getter,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用setter函数并传入新值,这个函数必须对数据做出修改

访问器属性有4个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下是 true
  • [[Get]]:获取函数
  • [[Set]]:设置函数

使用Object.defineProperty()方法可以定义访问器属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义 _name 为 伪私有变量,通过 name 属性去访问和设置 _name 属性
let obj = {
_name: 'easy code sniper'
}

Object.defineProperty(obj, 'name', {
get() {
return this._name
},
set(newValue) {
this._name = newValue
}
})

console.log(obj.name); // easy code sniper

obj.name = 'cqy'

console.log(obj.name); //cqy

理解原型

构造函数 和 原型对象

只要创建一个函数,就会按照规则为该函数创建一个prototype属性,该属性指向原型对象

所有的原型对象自动获得一个名为constructor属性,指回与之关联的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}

console.log(Person.prototype);

// 输出
// {
// constructor: f Person(),
// __proto__: Object
// }

console.log(Person.prototype.constructor); // f Person()

console.log(Person.prototype.constructor === Person); // true

实例 和 原型对象

每次调用构造函数创建一个实例,实例内部的[[Prototype]]就会被赋值为 构造函数的原型对象

实例通过__proto__属性可以访问到构造函数的原型

关键在于理解:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有

1
2
3
4
5
6
7
function Person() {}

let person = new Person()

console.log(person.__proto__ === Person.prototype); // true

console.log(person.__proto__.constructor === Person); // true

isPrototypeOf()可以确定两个对象之间的关系。本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回 true

1
console.log(Person.prototype.isPrototypeOf(person)) // true

Object.getPrototypeOf()方法可以返回参数的内部特性[[Prototype]]

1
console.log(Object.getPrototypeOf(person) == Person.prototype)

Object.setPrototypeOf(obj, prototype)方法可以向实例的私有特性[[Prototype]]写入一个新值,即重写原型

1
2
3
4
5
6
7
8
9
function Person() {}

let person = new Person()

function Pig() {}

Object.setPrototypeOf(person, Pig)

console.log(Object.getPrototypeOf(person)); // [Function: Pig]

in 操作符
in 操作符会在可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上

1
2
3
4
5
6
7
8
function Person() {}

Person.prototype.name = 'cqy'

let person = new Person()

console.log(person.hasOwnProperty('name')); // false
console.log('name' in person); // true

classnames源码阅读

前言

classnames 是一个JavaScript工具库,用于 有条件地 将不同的class类名组合在一起

用法

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
classNames('foo', 'bar'); // => 'foo bar'

classNames('foo', { bar: true }); // => 'foo bar'

classNames({ 'foo-bar': true }); // => 'foo-bar'

classNames({ 'foo-bar': false }); // => ''

classNames({ foo: true }, { bar: true }); // => 'foo bar'

classNames({ foo: true, bar: true }); // => 'foo bar'

// 多种不同类型的参数
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// 假值对应的类名都会被忽略
classNames(null, false, 'bar', undefined, 0, { baz: null }, ''); // => 'bar'

// 支持动态类名
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });

// 在React中的使用:将classNames赋值给元素的className属性即可
export default function App() {

return (
<div className={classNames(...)}></div>
)
}

源码解析

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 获取对象的hasOwnProperty方法,该方法用于判断某个属性是否是对象本身的,而不是继承自原型链
const hasOwn = {}.hasOwnProperty;

export default function classNames () {
let classes = '';

for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];
if (arg) {
classes = appendClass(classes, parseValue(arg));
}
}

return classes;
}

function parseValue (arg) {
// 如果是字符串,直接返回
if (typeof arg === 'string') {
return arg;
}
// 如果既不是字符串,也不是对象和数组,就返回空字符串
if (typeof arg !== 'object') {
return '';
}
// 如果是数组,调用classNames来返回一个组合好的结果字符串
if (Array.isArray(arg)) {
return classNames.apply(null, arg);
}
// arg.toString !== Object.prototype.toString 表示arg的toString方法不是继承自Object上
/**
* !arg.toString.toString().includes('[native code]')
* 如果arg有toString方法,进一步检查这个方法是否是原生的
* 方法是将 arg.toString 方法转换成字符串,然后检查它是否包含字符串 '[native code]'。如果一个函数的字符串表示中包含 '[native code]',这通常意味着函数是原生提供的,而不是用户定义的。
*/
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
return arg.toString();
}

let classes = '';

// 如果arg是对象
for (const key in arg) {
// hasOwn 判断key是否是arg对象本身的属性,而不是继承自原型链的属性
// arg[key] 判断key对应的值是否存在,且不为false
if (hasOwn.call(arg, key) && arg[key]) {
classes = appendClass(classes, key);
}
}

return classes;
}

function appendClass (value, newClass) {
if (!newClass) {
return value;
}

return value ? (value + ' ' + newClass) : newClass;
}

react-use源码阅读

前言

React的开发离不开hooks,在社区中也有各种的hooks工具库。本篇阅读的就是react-use工具库

希望通过阅读源码,加深自己对于React Hook的理解。

useCustomCompareEffect

React的useEffect比较依赖项的规则是Object.is(),它大部分情况下和===的效果是相同的
useCustomCompareEffectHook 用于当依赖项是对象或者数组时自定义依赖项比较方法,定义更加复杂的比较逻辑

使用示例:

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
import { useState } from "react";
import useCustomCompareEffect from "./hooks/useCustomCompareEffect";

function App() {

const [obj, setObj] = useState({
name: 'kyrie',
age: 21
})
// 自定义比较函数,只有当name值发生变化是重新渲染
const depsEqual = (newDeps, prevDeps) => {
return (prevDeps[0]?.name === newDeps[0]?.name) ? true : false
}

useCustomCompareEffect(() => {
console.log('🚀~~ useCustomCompareEffect working !');
}, depsEqual, [obj])

return (
<div>
<p>name: { obj.name }</p>
<p>age: { obj.age }</p>
<button onClick={() => setObj({ ...obj, name: 'easy code sniper' })}>修改name</button>
<button onClick={() => setObj({ ...obj, age: 30 })}>修改age</button>
</div>
)
}

export default App;

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { DependencyList, EffectCallback, useEffect, useRef } from 'react';

// 判断是否是基本类型值,是基本类型就返回true
// Object():给Object()函数传递一个值,如果是对象或者数组,则返回该值的引用;如果是基本类型值,则把该基本类型值包装成为对应的对象类型
const isPrimitive = (val: any) => val !== Object(val);

// 自定义比较函数有两个参数:旧的deps 和 新的deps ,该函数需要返回一个bool值来表示依赖是否发生变化
type DepsEqualFnType<TDeps extends DependencyList> = (prevDeps: TDeps, nextDeps: TDeps) => boolean;

/**
* 接受三个参数
* effect:副作用函数
* deps:依赖项
* depsEqual:自定义比较函数,该函数有两个参数,前一个依赖项 和 当前依赖项,要求返回一个boolean来表示依赖项是否发生了变化
*/
const useCustomCompareEffect = <TDeps extends DependencyList>(
effect: EffectCallback,
deps: TDeps,
depsEqual: DepsEqualFnType<TDeps>
) => {
// 生产环境下的一些警告提示,可以忽略
if (process.env.NODE_ENV !== 'production') {
// deps依赖必须有
if (!(deps instanceof Array) || !deps.length) {
console.warn(
'`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'
);
}
// 如果每一项依赖都是基本类型,那使用useEffect即可
if (deps.every(isPrimitive)) {
console.warn(
'`useCustomCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}
// depsEqual 必须是一个函数
if (typeof depsEqual !== 'function') {
console.warn(
'`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list'
);
}
}
//定义一个ref来存储依赖项
const ref = useRef<TDeps | undefined>(undefined);

/*
* !ref.current 表示初次渲染
* !depsEqual(deps, ref.current) 根据自定义的比较函数来判断依赖是否发生变化
*/
if (!ref.current || !depsEqual(deps, ref.current)) {
ref.current = deps; // 当初次渲染时,或者依赖项发生变化时,ref存储新的依赖
}

// useEffect依赖的并不是deps,实际依赖的是ref.current
// 只有在depsEqual函数判断依赖项发生了变化,并更新ref.current之后,useEffect才会重新执行effect
useEffect(effect, ref.current);
};

export default useCustomCompareEffect;

useShallowCompareEffect

React的useEffect比较依赖项的规则是Object.is(),它大部分情况下和===的效果是相同的
useShallowCompareEffectHook 会对每个依赖项进行浅比较,而不是引用相等

浅比较
通常是检查对象的顶层属性是否具有相同的值和相同的引用,对于嵌套对象不进行深入比较

源码及解析如下:

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
import { DependencyList, EffectCallback } from 'react';
import { equal as isShallowEqual } from 'fast-shallow-equal';
import useCustomCompareEffect from './useCustomCompareEffect';

// 判断是否是基本类型值
// 给Object()函数传递一个值,如果是对象或者数组,则返回该值的引用;如果是基本类型值,则把该基本类型值包装成为对应的对象类型
const isPrimitive = (val: any) => val !== Object(val);

// 调用第三方库,实现依赖项的浅比较
const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList) =>
prevDeps.every((dep, index) => isShallowEqual(dep, nextDeps[index]));

/**
* 接受两个参数
* effect:副作用函数
* deps:依赖项
*/
const useShallowCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
// 生产环境下的一些警告提示,可以忽略
if (process.env.NODE_ENV !== 'production') {
if (!(deps instanceof Array) || !deps.length) {
console.warn(
'`useShallowCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'
);
}

if (deps.every(isPrimitive)) {
console.warn(
'`useShallowCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}
}

// 调用 useCustomCompareEffect , 将shallowEqualDepsList作为自定义比较方法
// 实际上就是对 useCustomCompareEffect 的又一层封装
useCustomCompareEffect(effect, deps, shallowEqualDepsList);
};

export default useShallowCompareEffect;

useEvent

用于在特定的事件目标(如 window、document 或任何实现了特定事件监听器接口的对象)上添加和移除事件监听器,它旨在 在不同类型的事件目标上 提供一个统一的 API

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { useEffect } from 'react';
// isBrower 判断是否是浏览器环境
// on off 是封装好的浏览器事件监听器添加和移除方法
import { isBrowser, off, on } from './misc/util';


// ListenerType1 和 ListenerType2 接口定义了两种不同的事件监听模式

export interface ListenerType1 {
addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);

removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
}

export interface ListenerType2 {
on(name: string, handler: (event?: any) => void, ...args: any[]);

off(name: string, handler: (event?: any) => void, ...args: any[]);
}

export type UseEventTarget = ListenerType1 | ListenerType2;

// isListenerType1 和 isListenerType2 用来判断目标事件对象是否有 addEventListener 或 on 属性来实现的

const isListenerType1 = (target: any): target is ListenerType1 => {
return !!target.addEventListener; // !! 任何真值都会被转化成true,假值都会被转化成false
};
const isListenerType2 = (target: any): target is ListenerType2 => {
return !!target.on;
};

const defaultTarget = isBrowser ? window : null;

/**
* 接受四个参数;
* name:事件名称
* handler:事件处理函数
* target:事件监听器的目标对象,默认值是 defaultTarget
* options:可选参数,用于传递给事件监听器的选项
*/
const useEvent = (
name,
handler,
target,
options?
) => {
useEffect(() => {
if (!handler || !target) { // 如果没有传入事件处理函数或者事件对象,不做任何处理直接返回
return;
}
if (isListenerType1(target)) { //如果目标对象实现的是addEventListener模式,将它手动代理成on模式来添加监听器
on(target, name, handler, options);
} else if (isListenerType2(target)) { //如果目标对象实现的是on模式,直接用on模式添加监听器
target.on(name, handler, options);
}
return () => { // 在组件销毁时删除事件监听
if (isListenerType1(target)) {
off(target, name, handler, options);
} else if (isListenerType2(target)) {
target.off(name, handler, options);
}
};
}, [name, handler, target, JSON.stringify(options)]);
};

export default useEvent;

onoff函数的封装:

on函数接受一个可能为null的对象obj和一个参数列表args。参数列表可以是任何addEventListener方法接受的参数。函数内部会检查obj是否存在并且是否有addEventListener方法。如果条件满足,它会调用obj.addEventListener并传入args参数。
这个函数的主要作用是检查对象是否存在并且可以添加事件监听器,然后按照给定的参数调用addEventListener方法。这样做可以避免直接在组件内部调用addEventListener时可能遇到的null或undefined对象错误。

off函数同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function on<T extends Window | Document | HTMLElement | EventTarget>(
obj: T | null,
...args: Parameters<T['addEventListener']> | [string, Function | null, ...any]
): void {
if (obj && obj.addEventListener) {
obj.addEventListener(...(args as Parameters<HTMLElement['addEventListener']>));
}
}

export function off<T extends Window | Document | HTMLElement | EventTarget>(
obj: T | null,
...args: Parameters<T['removeEventListener']> | [string, Function | null, ...any]
): void {
if (obj && obj.removeEventListener) {
obj.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>));
}
}

useGeolocation

该hook用于获取和追踪用户的地理位置,基于浏览器内置对象navigator实现

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import useGeolocation from "./hooks/useGeolocation";


function App() {

let geolocation = useGeolocation()

console.log(geolocation);

return (
<div className="App">
</div>
);
}

export default App;

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import { useEffect, useState } from 'react';

// 定义获取位置错误的接口
export interface IGeolocationPositionError {
readonly code: number;
readonly message: string;
readonly PERMISSION_DENIED: number;
readonly POSITION_UNAVAILABLE: number;
readonly TIMEOUT: number;
}

// 定义当前位置信息的接口
export interface GeoLocationSensorState {
loading: boolean; //位置信息是否正在加载中
accuracy: number | null; //获取的位置的精确度
altitude: number | null; //相对于海平面的高度
altitudeAccuracy: number | null; //高度的精确度
heading: number | null; //设备移动方向
latitude: number | null; //纬度
longitude: number | null; //经度
speed: number | null; //速度
timestamp: number | null; //这些位置数据的时间戳
error?: Error | IGeolocationPositionError; //在获取地理位置信息时发生错误,这里会存储错误信息
}

export interface PositionOptions {
enableHighAccuracy?: boolean; // 是否需要高精度的位置信息
maximumAge?: number; // 能接受多旧的位置信息
timeout?: number; //获取位置信息的最长等待时间
}

const useGeolocation = (options?: PositionOptions): GeoLocationSensorState => {
const [state, setState] = useState<GeoLocationSensorState>({
loading: true,
accuracy: null,
altitude: null,
altitudeAccuracy: null,
heading: null,
latitude: null,
longitude: null,
speed: null,
timestamp: Date.now(),
});
let mounted = true; // 表示组件是否挂载
let watchId: any; // 调用navigator.geolocation.watchPosition返回的id,用于在组件卸载时清除监听

// 位置信息请求成功的回调函数,navigator.geolocation.getCurrentPosition会在成功时传给该函数一个Positon对象,包含了位置信息
const onEvent = (event: any) => {
if (mounted) {
setState({
loading: false,
accuracy: event.coords.accuracy,
altitude: event.coords.altitude,
altitudeAccuracy: event.coords.altitudeAccuracy,
heading: event.coords.heading,
latitude: event.coords.latitude,
longitude: event.coords.longitude,
speed: event.coords.speed,
timestamp: event.timestamp,
});
}
};

// 位置信息请求失败的回调函数,navigator.geolocation.getCurrentPosition会在失败时传给该函数一个error对象,包含了错误信息
const onEventError = (error: IGeolocationPositionError) =>
mounted && setState((oldState) => ({ ...oldState, loading: false, error }));


/**
* navigator.geolocation.getCurrentPosition用于获取当前地理位置信息
* 接受三个参数:成功的回调、错误时的回调(可选)、选项对象PositionOptions(可选)
*
* navigator.geolocation.watchPosition用于注册一个监听器,在用户的设备地理位置发生变化时被调用。该方法返回一个id,用于取消监听
* 接受三个参数:成功的回调、错误时的回调(可选)、选项对象PositionOptions(可选)
*
* navigator.geolocation.clearWatch(watchId)用于取消监听,传入监听器id
*/
useEffect(() => {
navigator.geolocation.getCurrentPosition(onEvent, onEventError, options);
watchId = navigator.geolocation.watchPosition(onEvent, onEventError, options);

return () => {
mounted = false;
navigator.geolocation.clearWatch(watchId);
};
}, []);

return state;
};

export default useGeolocation;

navigator对象 是 浏览器内置对象,它提供了关于用户浏览器的信息

useHover

useHover提供一种简单的方式来跟踪鼠标悬停状态。这个钩子接受一个 React 元素或者一个返回 React 元素的函数,并返回一个数组,包含一个带有悬停事件处理的克隆元素以及一个表示悬停状态(true 或 false)的布尔值。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import useHover from "./hooks/useHover";


function App() {

const [ el, state ] = useHover(
<div
onMouseEnter={() => console.log('🚀~~ enter')}
onMouseLeave={() => console.log('🚀~~ leave')}
>我是一个div</div>
)

return (
<div className="App">
{el}
{state && <p>Hovered !</p>}
</div>
);
}

export default App;

源码及解析如下:

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
43
import * as React from 'react';

// noop 不执行任何操作的空函数 () => {}
import { noop } from './misc/util';

const { useState } = React;

//定义Element接口,表示Element类型可以是一个React元素,也可以是一个函数,该函数接受一个boolean类型的参数,并返回React元素
export type Element = ((state: boolean) => React.ReactElement<any>) | React.ReactElement<any>;

// useHover 接受一个Element类型的参数
const useHover = (element: Element): [React.ReactElement<any>, boolean] => {
// state状态用于表示是否处于鼠标悬停状态
const [state, setState] = useState(false);

// 接受一个原始事件处理函数,返回一个新的事件处理函数。
// 新的事件处理函数首先调用传入的原始事件处理函数(如果没有提供,则调用 noop),然后通过 setState 更新悬停状态。
const onMouseEnter = (originalOnMouseEnter?: any) => (event: any) => {
(originalOnMouseEnter || noop)(event);
setState(true);
};
const onMouseLeave = (originalOnMouseLeave?: any) => (event: any) => {
(originalOnMouseLeave || noop)(event);
setState(false);
};

// 传入的element是一个函数,则将代表是否hover的状态state传入来返回对应的React元素
if (typeof element === 'function') {
element = element(state);
}

// element.props.onMouseEnter 和 element.props.onMouseLeave 是原始元素上可能存在的鼠标悬停和离开函数
const el = React.cloneElement(element, {
onMouseEnter: onMouseEnter(element.props.onMouseEnter),
onMouseLeave: onMouseLeave(element.props.onMouseLeave),
});

// 返回克隆的React元素 以及 表示悬浮状态的state
return [el, state];
};

export default useHover;

React.cloneElement()

它允许你克隆一个React元素,并且可以选择性的传入props、子元素以及key。它的作用是基于已有的React元素创建一个新的元素,默认保留原有元素的props、内部状态和行为

默认保留原有元素的props、内部状态和行为
因为在默认情况下,React只是浅克隆了原有元素,并传入新的props。在不改变key的情况下,React会视这两个组件为同一个实例,这就意味着,即使属性可能有所改变,但由于组件类型和 key 没有变化,React的重用逻辑会保留组件实例及其内部状态。

使用React.cloneElement可以很方便地扩展或修改组件的子元素,而不需要创建一个全新的组件。

1
2
3
4
5
React.cloneElement(
element, // 想要克隆的React对象
props, // 可选参数。传入一个对象,包含要添加或者覆盖的新属性
[...children] // 可选参数。可以传入任意数量的子节点来替换被克隆元素的子节点
)

React.cloneElement 使用示例:

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
import React from 'react';

const ListItem = ({ children, onClick }) => {
return <li onClick={onClick}>{children}</li>;
};

const List = ({ items }) => {
return (
<ul>
{items.map((item, index) => {
// 克隆ListItem,并为每个item添加特定的onClick处理器
return React.cloneElement(<ListItem>{item.text}</ListItem>, {
key: index, // 必须提供key,特别是在map循环中
onClick: () => alert(`Item ${index + 1} clicked!`), // 绑定了一个弹窗事件
});
})}
</ul>
);
};

// 使用List组件
const App = () => {
const items = [
{ text: 'Item 1' },
{ text: 'Item 2' },
{ text: 'Item 3' },
];

return <List items={items} />;
};

export default App;

useHoverDirty

useHoverDirty提供一种方式来检测用户是否悬浮在某个元素上

useHover接受一个React元素或者一个函数,而useHoverDirty接受React ref
useHover通过设置React元素的onMouseOveronMouseOut事件,而useHoverDirty通过设置DOM元素的onMouseOveronMouseOut事件

使用示例:

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
import React, { useRef } from 'react';
import useHoverDirty from './useHoverDirty';

const HoverComponent = () => {
// 创建一个ref来引用我们想要检测悬停的DOM元素
const hoverRef = useRef(null);
// 使用我们的自定义Hook来获取悬停状态
const isHovered = useHoverDirty(hoverRef);

// 根据悬停状态动态设置样式
const style = {
width: '200px',
height: '200px',
backgroundColor: isHovered ? 'blue' : 'red',
// 当鼠标悬停时背景色变蓝,否则为红色
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background-color 0.3s'
};

return (
<div ref={hoverRef} style={style}>
{isHovered ? 'Hovering' : 'Not Hovering'}
</div>
);
};

export default HoverComponent;

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { RefObject, useEffect, useState } from 'react';
import { off, on } from './misc/util';

/**
* ref: 这是一个React ref对象,指向要监听悬浮事件的DOM元素。使用ref是因为我们需要直接操作DOM元素来绑定和解绑事件监听器。
* enabled: 这是一个可选参数,默认为true。它允许使用者启用或禁用悬浮监听功能。这可以用来在某些条件下动态开启或关闭事件监听。
*/
const useHoverDirty = (ref: RefObject<Element>, enabled: boolean = true) => {

// 开发模式下的警告,可以忽略
if (process.env.NODE_ENV === 'development') {
if (typeof ref !== 'object' || typeof ref.current === 'undefined') {
console.error('useHoverDirty expects a single ref argument.');
}
}

// 判断是否处于悬浮状态的state
const [value, setValue] = useState(false);

useEffect(() => {
const onMouseOver = () => setValue(true);
const onMouseOut = () => setValue(false);

if (enabled && ref && ref.current) {
on(ref.current, 'mouseover', onMouseOver);
on(ref.current, 'mouseout', onMouseOut);
}

const { current } = ref;

return () => {
if (enabled && current) {
off(current, 'mouseover', onMouseOver);
off(current, 'mouseout', onMouseOut);
}
};
}, [enabled, ref]);

return value;
};

export default useHoverDirty;


// on函数 和 off函数 的封装
export function on<T extends Window | Document | HTMLElement | EventTarget>(
obj: T | null,
...args: Parameters<T['addEventListener']> | [string, Function | null, ...any]
): void {
if (obj && obj.addEventListener) {
obj.addEventListener(...(args as Parameters<HTMLElement['addEventListener']>));
}
}

export function off<T extends Window | Document | HTMLElement | EventTarget>(
obj: T | null,
...args: Parameters<T['removeEventListener']> | [string, Function | null, ...any]
): void {
if (obj && obj.removeEventListener) {
obj.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>));
}
}

useIntersection

useIntersection使用了浏览器的 Intersection Observer API 来异步地检测目标元素与其祖先元素或顶级文档视口的交叉状态的变化。

Intersection Observer API 是一个浏览器API,它允许你异步观察一个元素与其祖先元素或全局视口的交集变化。能够高效得获取元素是否进入或离开视口的信息。

相关概念

  • root(根): 想要检查目标元素与之相交的容器元素,如果未指定或为null,默认为浏览器视口
  • rootMargin(根边界): 类似于CSS的margin属性。它允许你指定一个在根元素的外围形成的边框区域,用于增加或减少用于检查交集的区域大小
  • threshold(阈值): 一个数字或由多个数字组成的数组,指定了观察者的回调函数被执行的条件。例如,如果阈值是0.5,则目标元素有50%进入根元素时,观察者的回调函数将被执行

IntersectionObserver()方法是一个构造函数,用于创建一个新的 IntersectionObserver 对象。
这个构造函数接受两个参数:一个回调函数和一个可选的配置对象。

  • 回调函数:当被观察的元素进入或退出交集区域时,回调函数会被执行。这个函数接受两个参数:

    • entries: 一个 IntersectionObserverEntry 对象数组,每个对象都描述了一个被观察元素的交集状态
    • observer: 对应的 IntersectionObserver 实例,允许你访问观察者的属性或调用其方法,如 disconnect 或 unobserve
  • 配置对象:包含三个属性

    • root
    • rootMargin
    • threshold

Intersection Observer

  • root: 想要检查目标元素与之相交的容器元素,如果未指定或为null,默认为浏览器视口
  • rootMargin: 类似于CSS的margin属性。它允许你指定一个在根元素的外围形成的边框区域,用于增加或减少用于检查交集的区域大小
  • thresholds: 一个数字或由多个数字组成的数组,指定了观察者的回调函数被执行的条件。例如,如果阈值是0.5,则目标元素有50%进入根元素时,观察者的回调函数将被执行
  • disconnect(): 停止观察所有目标
  • observe(target): 开始观察一个目标元素
  • takeRecords: 返回所有目标的IntersectionObserverEntry对象数组
  • unobserve(targrt): 停止观察一个目标元素

Intersection Observer Entry

  • boundingClientRect: 返回目标元素的矩形信息,用于计算与视口的交集
  • intersectionRatio: 目标元素可见部分占总体的比例
  • intersectionRect: 目标元素与视口交叉部分的矩形信息
  • isIntersecting: 目标元素是否与根元素相交
  • rootBounds: 根元素的矩形信息
  • target: 目标元素
  • time: 观察到交叉状态变化时的时间戳

使用场景

  • 懒加载:当图片或其他资源进入视口时才加载它们,以此节省带宽和提高页面加载速度
  • 无限滚动: 当用户滚动到页面底部时,自动加载更多内容

源码及解析如下:

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
import { RefObject, useEffect, useState } from 'react';
/**
* useIntersection接受两个参数
* ref: 一个ref引用对象,指向要观察的目标元素
* options: 配置对象,用于初始化Intersection Observer的选项,比如 root、rootMargin 和 threshold
*/
const useIntersection = (
ref: RefObject<HTMLElement>,
options: IntersectionObserverInit
): IntersectionObserverEntry | null => {
// 创建 intersectionObserverEntry 状态,用于存储 intersectionObserverEntry 对象信息
const [intersectionObserverEntry, setIntersectionObserverEntry] =
useState<IntersectionObserverEntry | null>(null);

useEffect(() => {
// ref是否存在,浏览器是否支持IntersectionObserver
if (ref.current && typeof IntersectionObserver === 'function') {
// handle函数用于更新 intersectionObserverEntry 状态
const handler = (entries: IntersectionObserverEntry[]) => {
setIntersectionObserverEntry(entries[0]);
};

// 创建一个监听器对象
const observer = new IntersectionObserver(handler, options);
// 监听目标元素
observer.observe(ref.current);

// 组件销毁时取消监听,并清空 intersectionObserverEntry 状态
return () => {
setIntersectionObserverEntry(null);
observer.disconnect();
};
}
return () => {};
}, [ref.current, options.threshold, options.root, options.rootMargin]);

return intersectionObserverEntry;
};

export default useIntersection;

使用示例

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
import React, { useRef } from "react";
import useIntersection from "./hooks/useIntersection";


function App() {

const intersectionRef = useRef(null)

const intersection = useIntersection(intersectionRef, {
root: null,
rootMargin: '0px',
threshold: 1
})

console.log(intersection);

return (
<div style={{ width: '200px', height: '200px', backgroundColor: 'yellow', overflowY: 'scroll' }}>
<div style={{ width: '100%', height: '200px' }} ></div>
<div style={{ width: '100px', height: '50px', backgroundColor: 'skyblue' }} ref={intersectionRef}>
{
intersection && (intersection.intersectionRatio < 1)
? 'Obscured'
: 'Full in view'
}
</div>
</div>
)
}

export default App;

useKey

useKey用于设置监听键盘事件

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {useKey} from 'react-use';

const Demo = () => {
const [count, set] = useState(0);
const increment = () => set(count => ++count);
useKey('ArrowUp', increment);

return (
<div>
Press arrow up: {count}
</div>
);
};

源码及解析如下;

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { DependencyList, useMemo } from 'react';
import useEvent, { UseEventOptions, UseEventTarget } from './useEvent';
import { noop } from './misc/util';

export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = null | undefined | string | ((event: KeyboardEvent) => boolean);
export type Handler = (event: KeyboardEvent) => void;

export interface UseKeyOptions<T extends UseEventTarget> {
event?: 'keydown' | 'keypress' | 'keyup';
target?: T | null;
options?: UseEventOptions<T>;
}
// createKeyPredicate接受一个参数keyFilter,该参数可以是一个判断函数(用于判断何时符合条件)、一个字符串(指定的keyboard) 或者 undefined/null
// 然后 createKeyPredicate 根据不同的参数来创建判断函数
// 如果传入的是函数,就返回这个函数
// 如果传入的是字符串(特定的keyboard),则将它包装成一个函数,当event.key === 字符串 时返回true,表示符合条件触发回调函数
// 如果传入的是undefined/null,则包装一个固定返回false的函数,表示不符合条件不触发回调函数
const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
typeof keyFilter === 'function'
? keyFilter
: typeof keyFilter === 'string'
? (event: KeyboardEvent) => event.key === keyFilter
: keyFilter
? () => true
: () => false;
/**
*
* @param key 指定触发回调的键
* @param fn 要触发的回调函数
* @param opts 配置选项
* @param deps 依赖项
*/
const useKey = <T extends UseEventTarget>(
key: KeyFilter,
fn: Handler = noop,
opts: UseKeyOptions<T> = {},
deps: DependencyList = [key]
) => {
// event 指定是哪种键盘事件(keydown,keyup,keypress)
// target 绑定事件的目标对象
// options 传递给useEvent的配置项
const { event = 'keydown', target, options } = opts;

// 包装后的回调函数
const useMemoHandler = useMemo(() => {
// 根据传入的key值创建一个判断函数
const predicate: KeyPredicate = createKeyPredicate(key);
// handlerEvent 是浏览器事件对象,当向监听器添加事件处理函数时,浏览器会自动传入事件对象作为参数
// 我们可以通过event参数访问到事件的详细信息,如被按下的键是什么(event.key)
const handler: Handler = (handlerEvent) => {
// 判断是否符合触发回调的条件
if (predicate(handlerEvent)) {
// 如果符合,就执行用户传入的回调函数
return fn(handlerEvent);
}
};
return handler;
}, deps);

// 调用useEvent来为目标对象绑定事件
useEvent(event, useMemoHandler, target, options);
};

export default useKey;

useLongPress

useLongPress用于设置长按之后触发的回调

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useCallback, useRef } from 'react';
import { off, on } from './misc/util';

interface Options {
isPreventDefault?: boolean;
delay?: number;
}

const isTouchEvent = (ev: Event): ev is TouchEvent => {
// 检查浏览器传入的事件对象中是否有touches属性,即是否触发了touches事件
return 'touches' in ev;
};

const preventDefault = (ev: Event) => {
if (!isTouchEvent(ev)) return;

// 当触摸点个数小于2个时,阻止默认事件
if (ev.touches.length < 2 && ev.preventDefault) {
ev.preventDefault();
}
};
/**
*
* @param callback 长按之后的回调函数
* @param options 配置项对象(可选),其中包括是否阻止默认事件,延迟时间等
*/
const useLongPress = (
callback: (e: TouchEvent | MouseEvent) => void,
{ isPreventDefault = true, delay = 300 }: Options = {}
) => {
// 用于缓存 定时器id 和 目标对象
const timeout = useRef<ReturnType<typeof setTimeout>>();
const target = useRef<EventTarget>();

// 封装监听触摸事件的函数
const start = useCallback(
(event: TouchEvent | MouseEvent) => {
// 绑定触摸事件,并缓存目标对象
if (isPreventDefault && event.target) {
on(event.target, 'touchend', preventDefault, { passive: false });
target.current = event.target;
}
// 缓存定时器id
timeout.current = setTimeout(() => callback(event), delay);
},
[callback, delay, isPreventDefault]
);

// 封装解除监听事件的函数
const clear = useCallback(() => {
timeout.current && clearTimeout(timeout.current);

if (isPreventDefault && target.current) {
off(target.current, 'touchend', preventDefault);
}
}, [isPreventDefault]);

// 返回的对象可以直接解构赋值给目标元素
return {
onMouseDown: (e: any) => start(e),
onTouchStart: (e: any) => start(e),
onMouseUp: clear,
onMouseLeave: clear,
onTouchEnd: clear,
} as const;
};

export default useLongPress;

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useLongPress } from './useLongPress';

const Demo = () => {
const onLongPress = () => {
console.log('calls callback after long pressing 300ms');
};

const defaultOptions = {
isPreventDefault: true,
delay: 300,
};
const longPressEvent = useLongPress(onLongPress, defaultOptions);

// 直接解构赋值给元素
return <button {...longPressEvent}>useLongPress</button>;
};

useMeasure

useMeasure用于测量一个DOM元素的尺寸和位置,并在尺寸或位置变化时更新这些信息。这个Hook依赖于ResizeObserver API,它允许你监听一个元素的大小变化。

ResizeObserver是一个强大的Web API,允许开发者监听HTML元素的尺寸变化。

基本用法

使用ResizeObserver创建一个observer实例,并给它提供一个回调函数,该函数会在被观察元素的尺寸位置发生变化时被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
// 从 entry.contentRect 中获取元素的尺寸位置信息
const { width, height } = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${width}px x ${height}px`);
}
});

// 假设有一个元素是这样的:<div id="myElement"></div>
const myElement = document.getElementById('myElement');

// 开始观察myElement元素
resizeObserver.observe(myElement);


// 停止观察某一个元素
resizeObserver.unobserve(myElement);

// 停止观察所有元素并释放资源
resizeObserver.disconnect();

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { useMemo, useState, useEffect } from 'react';
import { isBrowser, noop } from './misc/util';

export type UseMeasureRect = Pick<
DOMRectReadOnly,
'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
>;
export type UseMeasureRef<E extends Element = Element> = (element: E) => void;
export type UseMeasureResult<E extends Element = Element> = [UseMeasureRef<E>, UseMeasureRect];

// 定义默认设置
const defaultState: UseMeasureRect = {
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
left: 0,
bottom: 0,
right: 0,
};

function useMeasure<E extends Element = Element>(): UseMeasureResult<E> {
// element用于存储对DOM元素的引用
const [element, ref] = useState<E | null>(null);
// rect用于存储测量的结果
const [rect, setRect] = useState<UseMeasureRect>(defaultState);

// 创建一个监听器函数,当被监听元素尺寸位置发生变化时,设置并返回新的位置尺寸信息
const observer = useMemo(
() =>
new (window as any).ResizeObserver((entries) => {
if (entries[0]) {
const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
setRect({ x, y, width, height, top, left, bottom, right });
}
}),
[]
);

useEffect(() => {
if (!element) return;
observer.observe(element);
return () => {
observer.disconnect();
};
}, [element]);

// 返回ref回调函数 可以将它绑定到元素的ref属性上,React会在组件挂载时将DOM元素作为参数传递给该ref回调函数
// 以及 rect对象(包含了元素的尺寸和位置信息)
return [ref, rect];
}

// 浏览器兼容性处理
export default isBrowser && typeof (window as any).ResizeObserver !== 'undefined'
? useMeasure
: ((() => [noop, defaultState]) as typeof useMeasure);

useMouse

useMouse用于动态跟踪鼠标的位置

源码及解析如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { RefObject, useEffect } from 'react';

import useRafState from './useRafState';
import { off, on } from './misc/util';

export interface State {
docX: number; // 鼠标的X坐标相对于整个文档(document)的位置
docY: number; // 鼠标的Y坐标相对于整个文档的位置
posX: number; // 目标元素的X坐标相对于其定位上下文(positioning context)的位置
posY: number; // 目标元素的Y坐标相对于其定位上下文的位置
elX: number; // 鼠标的X坐标相对于目标元素(offset parent)的位置
elY: number; // 鼠标的Y坐标相对于目标元素(offset parent)的位置
elH: number; // 目标元素自身的高度
elW: number; // 目标元素自身的宽度
}

const useMouse = (ref: RefObject<Element>): State => {
const [state, setState] = useRafState<State>({
docX: 0,
docY: 0,
posX: 0,
posY: 0,
elX: 0,
elY: 0,
elH: 0,
elW: 0,
});

useEffect(() => {
const moveHandler = (event: MouseEvent) => {
// 如果已经绑定了元素的ref属性
if (ref && ref.current) {
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
// window.pageXOffset 表示当前文档水平滚动的距离
const posX = left + window.pageXOffset;
const posY = top + window.pageYOffset;
const elX = event.pageX - posX;
const elY = event.pageY - posY;

setState({
docX: event.pageX,
docY: event.pageY,
posX,
posY,
elX,
elY,
elH,
elW,
});
}
};

on(document, 'mousemove', moveHandler);

return () => {
off(document, 'mousemove', moveHandler);
};
}, [ref]);

return state;
};

export default useMouse;

usePageLeave

usePageLeave 用于当鼠标离开页面时触发一个回调

event.relatedTargetevent.toElement

浏览器默认事件对象中的属性,relatedTarget属性是一个事件属性,它在某些特定的事件中提供了关于事件的额外上下文。这个属性特别用于鼠标事件,比如mouseover和mouseout,以及焦点事件,比如focusin和focusout。

relatedTarget属性引用了与事件相关的一个DOM元素

对于鼠标事件

  • 在mouseover事件中,relatedTarget属性引用的是鼠标刚刚离开的那个元素,即鼠标指针之前所在的元素。
  • 在mouseout事件中,relatedTarget属性引用的是鼠标即将移动到的那个元素,即鼠标指针即将进入的元素。

对于焦点事件

在focusin(或focus)事件中,relatedTarget属性引用的是失去焦点的元素,即焦点从哪个元素移开。
在focusout(或blur)事件中,relatedTarget属性引用的是即将获得焦点的元素,即焦点将要移到哪个元素上。

toElement的作用与relatedTarget相似,特别是在mouseover和mouseout事件上,用于兼容旧版本浏览器

源码及解析如下:

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
import { useEffect } from 'react';
import { off, on } from './misc/util';

const usePageLeave = (onPageLeave, args = []) => {
useEffect(() => {
if (!onPageLeave) {
return;
}

const handler = (event) => {
event = event ? event : (window.event as any);
// from 保存了鼠标即将移动到的元素
const from = event.relatedTarget || event.toElement;
// 鼠标是否离开浏览器文档页面时触发回调
if (!from || (from as any).nodeName === 'HTML') {
onPageLeave();
}
};

on(document, 'mouseout', handler);
return () => {
off(document, 'mouseout', handler);
};
}, args);
};

export default usePageLeave;

useClickAway

useClickAway用于当用户在目标元素外部单击时触发回调

源码及解析如下:

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
43
44
import { RefObject, useEffect, useRef } from 'react';
import { off, on } from './misc/util';

const defaultEvents = ['mousedown', 'touchstart'];

/**
* ref:目标对象的ref引用
* onClickAway:传入的回调函数
* events:需要绑定的事件类型
*/
const useClickAway = <E extends Event = Event>(
ref: RefObject<HTMLElement | null>,
onClickAway: (event: E) => void,
events: string[] = defaultEvents
) => {
// 缓存传入的回调函数
const savedCallback = useRef(onClickAway);

useEffect(() => {
savedCallback.current = onClickAway;
}, [onClickAway]);

useEffect(() => {
const handler = (event) => {
const { current: el } = ref;
// el 是对DOM元素的引用
// 如果 el存在 并且 当前触发事件的元素不是el及其子孙节点 则 执行传入的回调函数
// contains 方法是用来测试一个节点是否是另一个节点的后代。如果目标元素在 el 之外,contains 方法会返回 false,取反后为 true
el && !el.contains(event.target) && savedCallback.current(event);
};
// 绑定用户传入的所有事件类型
for (const eventName of events) {
on(document, eventName, handler);
}
return () => {
for (const eventName of events) {
off(document, eventName, handler);
}
};
}, [events, ref]);
};

export default useClickAway;

useContext

context

在开发中,父组件可能需要通过多层中间组件将 props 传递给某个子孙组件,这个传递 props 的过程就会变得十分冗长

context可以在组件树中不需要props 传递数据,而是将数据直达所需的组件

context可以让父组件为其内部的整个组件树人提供数据

使用context

使用context需要三个步骤:

  • 创建一个context
  • 在需要数据的组件中使用创建的context
  • 在指定数据的组件中提供context

创建一个context

新建一个 js 文件,在其中创建constext并导出他

使用createContext()来创建一个 context,需要给他传入一个默认值

1
2
3
import { createContext } from "react";

export const MyContext = createContext(1); // 创建并导出一个context

使用context

在需要用到数据的组件中,引入useContextHook 以及 创建的context

1
2
3
4
5
6
7
import { useContext } from 'react';
import { MyContext } from './MyContext.js';

function Son() [
const context = useContext(MyContext) // useContext 告诉 React 想要读取 myContext
// 现在,得到的context就是myContext的默认值
]

提供context

在父组件中使用context provider将子组件包裹起来,就可以提供context给他们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useState } from 'react';
import { MyContext } from './MyContext.js';

function Parent() {

let [ name, setName ] = useState('easy code sniper')

return (
<div>
<MyContext.Provider value={name}>
<MyContext.Provider>
</div>
)
}

这告诉 React:“如果在Parent组件中的任何子组件请求MyContext,给他们这个name。”
组件会使用 UI 树中在它上层最近的那个MyContext.Provider传递过来的值。

useContext

语法:const value = useContext(SomeContext)

  • SomeContext就是先前用createContext创建的 context,context 本身不包含信息,它只代表你可以提供或从组件中读取的信息类型。

返回值

useContext为调用它的组件返回 context 的值。它的返回值被确定为传递给树中调用组件上方最近的SmeContext.Providervalue
如果没有这样的 provider,那么返回值将会是为创建该 context 传递给createContextdefaultValue。返回的值始终是最新的,如果 context 发生变化,React 会自动重新渲染读取 context 的组件。

设计模式浅谈

多态

多态的含义是:同一操作作用于不同对象上,可以产生不同的解释和不同的执行结果。也就是说,给不同的对象发送同一个消息,对象会做出不同的反馈

下面的代码就体现着多态性,当我们分别向程序员和老师发出工作的消息时,他们根据此作出了不同的反应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let work = function (person) {
if(person instanceof Coder) {
console.log('coding')
} else if (person instanceof Teacher) {
console.log('teaching')
}
}

const Coder = function (){}
const Teacher = function (){}

work(new Coder()) // coding
work(new Teacher()) // teaching

但这样的多态性无法令人满意,如果现在要新增一个司机类型,那我们就需要修改work函数的代码。修改的代码越多,就存在越多的危险,并且work函数也会随着类型的变多成为一个巨大的函数

多态背后的思想是:将做什么谁去做怎么做分离开来。
在上面的例子中,人都会工作,但是不同的人怎么工作是不同的。把不变的部分隔离出来,把可变的部分封装起来,这就给予了我们扩展程序的能力。

通过下面的改动,将不变的部分隔离出来,那就是所有人都会工作。然后把可变的部分各自封装起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let work = function (person) {
if(person.work instanceof Function) {
person.work()
}
}

const Coder = function (){}
Coder.prototype.work = () => {
console.log('coding')
}
const Teacher = function (){}
Teacher.prototype.work = () => {
console.log('teaching')
}

work(new Coder())
work(new Teacher())

与静态语言类型不同的是,JavaScript的变量类型在运行期是可变的。一个JavaScript对象,既可以是Coder类型,也可以是Teacher类型。这就意味着JavaScript对象的多态性是与生俱来的

封装

封装的目的是将信息隐藏,封装可以是对任何形式内容的封装。也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等

封装使得对象内部的变化对其他对象而言是透明的,其他对象也不关心它的内部实现。

封装变化

通过封装变化的方式,把系统中稳定不变的部分和易于变化的部分隔离开来,我们只需要替换易于变化的部分。这可以很大程度上保证程序的稳定性和可扩展性

原型模式

在以类为中心的面向对象编程语言中,对象总是从类中创建出来的。而在原型编程的思想中,类不是必须的,对象也未必需要从类中创建出来。一个对象是通过克隆另一个对象所得到的。

原型模式的关键在于通过克隆来创建对象,即语言本身是否提供了clone方法。在ES5中提供了Object.create方法来克隆对象

1
2
3
4
5
6
7
8
9
let Person = function() {
this.name = 'easy code sniper'
this.age = 22
}

let person = new Person()

let clonePerson = Object.create( person )
console.log(clonePerson) // { name: 'easy code sniper', age: 22 }

JavaScript中的原型继承

JavaScript遵循原型编程的基本规则:

  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 对象会记住它的原型
  • 如果对象无法响应某个请求,它会把这个请求委托给自己的原型

1.所有的数据都是对象

JavaScript有两种类型机制:基本类型 和 对象类型

基于一切都应该是对象的本意(除了undefined之外),基本类型数据可以通过包装类的方式变成对象类型数据

JavaScript中的根对象是Object.prototype对象,Object.prototype对象是一个空的对象。在JS中遇到的每个对象,实际都是从Object.prototype对象克隆而来的

2.要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它

在JavaScript中我们不需要关心克隆的细节,只是显式地调用let obj = new Object()let obj = {},引擎内部会从Object.prototype上面克隆一个对象出来

JavaScript的函数集可以作为普通函数被调用,也可以作为构造器被调用。当使用new运算符来调用函数时,此时的函数就是一个构造器。用new运算符来创建对象的过程,实际上也只是先克隆Object.prototype对象,再进行一些其他额外操作的过程

模拟new创建对象:

1
2
3
4
5
6
7
8
let objectFactory = function() {
let obj = new Object() // 先克隆一个空对象
Constructor = [].shift.call( arguments ) // 取出参数中的第一项,即外部传入的构造器
obj.__proto__ = Constructor.prototype // 指向正常的原型
let ret = Constructor.apply(obj, arguments) // 基于剩余的arguments给obj设置属性

return typeof ret === 'object' ? ret : obj // 确保构造器总是返回一个对象
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name
}

Person.prototype.getName = function() {
return this.name
}

let a = objectFactory(Person, 'easy code sniper')

console.log( a.name ); // easy code sniper

console.log( a.getName() ); // easy code sniper

3.对象会记住它的原型

JavaScript给对象提供一个__proto__属性,某个对象的__proto__属性默认会指向它的构造器的原型对象,即Constructor.prototype

__proto__就是 对象 和 对象构造器的原型 联系起来的纽带。这就是我们在objectFactory函数中需要手动给obj对象设置正确的__proto__指向

this 的指向

this 的指向大致可以分为 4 种情况:

  • 作为对象的方法调用
  • 作为普通函数调用
  • 构造器调用
  • callapply 调用

作为对象的方法调用

当函数作为对象的方法被调用时,this 指向该对象

1
2
3
4
5
6
7
8
9
let obj = {
name: "easy code sniper",
getName: function () {
console.log(this === obj); // true
console.log(this.name); // easy code sniper
},
};

obj.getName();

作为普通函数调用

当函数作为普通函数被调用时,this 指向全局对象。在浏览器中,全局对象就是 window;在 Node 环境中,全局对象就是 globalThis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
globalThis.name = "easy code sniper";

function func() {
console.log(this.name);
}

func(); // easy code sniper

// 或者

globalThis.name = "cqy";

let obj = {
name: "easy code sniper",
getName: function () {
console.log(this === obj); // false
console.log(this.name); // cqy
},
};

let func = obj.getName;

func();

构造器调用

除了一些内置函数,大部分 JS 函数都可以当作构造器使用。当使用 new 运算符调用函数时,该函数总是返回一个对象,通常情况下,构造器里的 this 就指向返回的对象

1
2
3
4
5
6
7
let Func = function() {
this.name = 'easy code sniper'
}

let obj = new Func()

console.log(obj.name); // easy code sniper

如果构造器显式的返回一个对象,那么返回的将会是这个对象,this也会指向这个对象

1
2
3
4
5
6
7
8
9
10
let Func = function() {
this.name = 'easy code sniper'
return {
name: 'cqy'
}
}

let obj = new Func()

console.log(obj.name); // cqy

call 和 apply

Function.prototype.callFunction.prototype.apply是定义在Function的原型上的两个方法,用于动态的改变this指向

call和apply的作用一模一样,区别仅在于传入参数的形式不同:

  • apply接受两个参数,第一个参数指定函数体内this的指向,第二个参数会作为参数传递给被调用的函数,类型为数组或类数组
  • call传入参数数量不固定,第一个参数指定函数体内this的指向,从第二个参数开始往后,每个参数被依次传入函数

当使用call或者apply的时候,如果传入的第一个参数为null,函数体内的this会指向默认的宿主对象,在浏览器中则是window

用途

1.改变this指向

call和apply最常见的用途是改变函数内部的this指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj1 = {
name: 'easy code sniper'
}

let obj2 = {
name: 'cqy'
}

window.name = 'window'

let getName = function() {
console.log(this.name)
}

getName() // window
getName.call(obj1) // easy code sniper
getName.call(obj2) // cqy

2.Function.prototype.bind

大部分高级浏览器都实现了内置的Function.prototype.bind,用来指定函数内部的this指向,即使没有原生的Function.prototype.bind实现,也可以使用call或者apply模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Function.prototype.bind = function() {
let self = this // 保存调用bind的原函数
context = [].shift.call(arguments) // 借用数组的shift方法拿到arguments的第一项,即需要指向的this
args = [].slice.call(arguments) // 将剩余参数转化为数组

return function() {
// 指定this为之前传入的context
// 合并两次传入的参数,作为新函数的参数
return self.apply(context, [].concat.call(args, [].slice.call(arguments)))
}
}


let obj = {
name: 'cqy'
}

let func = function(a,b,c,d) {
console.log(this.name)
console.log([a,b,c,d])
}.bind(obj, 1, 2)

func(3, 4) // 输出:cqy [1, 2, 3, 4]

3.借用其他对象的方法

借用方法较为常见的就是:
函数的参数列表arguments是一个类数组对象,它并非真正的数组,所以不能像数组一样,进行排序操作或者往集合里添加一个新的元素。我们常常会借用Array.prototype对象上的方法

比如想往arguments中添加一个新的元素,通常借用Array.prototype.push

1
2
3
4
5
6
let func = function() {
Array.prototype.push.call(arguments, 3)
console.log(arguments);
}

func(1,2) // [1,2,3]

在操作arguments的时候,我们经常非常频繁地找Array.prototype对象借用方法。
想把arguments转成真正的数组的时候,可以借用Array.prototype.slice方法;想截去arguments列表中的头一个元素时,又可以借用Array.prototype.shift方法。

闭包

变量的作用域

在JS中,函数可以用来创造函数作用域。函数就像一层半透明的玻璃,在函数里面可以看到外面的便利,而在函数外面则无法看到函数里面的变量。

这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。变量的搜索是从内到外的。

变量的生存周期

对于全局变量来说,生存周期是永久的,除非主动销毁这个全局变量

对于函数内的局部变量来说,当退出函数时,这些局部变量就失去了他们价值,随着函数调用的结束而销毁

退出函数后局部变量a将被销毁

1
2
3
4
5
6
7
let func = function() {
let a = 1
console.log(a) // 1
}

func()
console.log(a) // ReferenceError: a is not defined

再看看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
let func = function() {
let a = 1
return function() {
a++
console.log(a)
}
}

let f = func()

f() // 2
f() // 3

在这段代码中,退出函数之后,局部变量a并没有消失。当执行let f = func()时,f拿到一个匿名函数的引用,它可以访问到func被调用时产生的环境,而局部变量a一直处在这个环境里。

既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起来被延续了。

闭包的应用

封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成‘私有变量’,例如下面有一个计算参数乘积的简单函数,并使用全局的cache变量缓存结果来提高性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
let cache = {} // 缓存结果

let mult = function() {
let args = Array.prototype.join.call(arguments, ',') // 将参数拼接成字符串,作为cache中的属性名
if( cache[args] ) { // 如果命中缓存就返回结果
return cache[args]
}
let a = 1
for( let i = 0; i < arguments.length; i++ ) {
a *= arguments[i]
}
return cache[args] = a
}

cache变量仅在mult函数中被使用,与其让他暴露在全局作用域下,不如将它封闭在mult函数内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let mult = (function() {
let cache = {} // 将cache封装在mult里面
return function() {
let args = Array.prototype.join.call(arguments, ',') // 将参数拼接成字符串,作为cache中的属性名
if( cache[args] ) { // 如果命中缓存就返回结果
return cache[args]
}
let a = 1
for( let i = 0; i < arguments.length; i++ ) {
a *= arguments[i]
}
return cache[args] = a
}
})()

高阶函数

高阶函数是指至少满足下列条件之一的函数:

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

函数作为参数传递

将函数作为参数进行传递,一个很重要的应用场景就是回调函数。这代表我们将容易变化的业务逻辑抽离出来,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分

比如我们想在页面中创建一个div节点,然后给这个节点设置一些样式,下面是一种编写代码的方式:

1
2
3
4
5
6
let appendDiv = function() {
let div = document.createElement('div')
div.innerHTML = '我是一个div'
document.body.appendChild(div)
div.style.display = 'none' // 设置样式
}

div.style.display = 'none'的逻辑硬编码在appendDiv里显然不太合理,这使得appendDiv有点太个性化里,成为了一个难以敷用的函数。

如果把div.style.display = 'none'的逻辑抽离出来,用回调函数的形式传入appendDiv,这样appendDiv只要专注于创建节点就行了

1
2
3
4
5
6
7
8
9
10
11
12
let appendDiv = function(callback) {
let div = document.createElement('div')
div.innerHTML = '我是一个div'
document.body.appendChild(div)
if(typeof callback === 'function') {
callback(div)
}
}

appendDiv(function(node) {
node.style.display = 'none'
})

除此之外,Array.prototype.sort方法接受一个函数作为参数,这个函数里面封装了数组元素的排序规则。排序规则是可变的,把可变的封装在函数参数里。

1
2
3
4
5
[1, 5, 3].sort(function(a, b) {
return a - b
})

// 1, 3, 5

函数作为返回值输出

让函数返回一个可执行的函数,意味着运算过程是可延续的,更能体现函数式编程的巧妙

1.判断数据的类型

要判断数据类型,更好的方法是用Object.prototype.toString来计算,Object.prototype.toString.call(obj)返回一个字符串

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let isString = function(obj) {
return Object.prototype.toString.call(obj) === '[object String]'
}

let isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]'
}

let isNumber = function(obj) {
return Object.prototype.toString.call(obj) === '[object Number]'
}

let isObject = function(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}

这些函数的发部分实现都是相同的,不同的是判断部分。为了避免多余的代码,我们可以把这些代表数据类型的字符串作为参数提前传入isType函数

1
2
3
4
5
6
7
8
9
10
let isType = function(type) {
return function(obj) {
return Object.prototype.toString.call(obj) === `[object ${type}]`
}
}

let isString = isType('String')
let isArray = isType('Array')
let isNumber = isType('Number')
let isObject = isType('Object')

高阶函数的其他应用

1.函数柯里化(currying)

一个currying的函数首先会接受一些参数,接受了这些参数后,该函数不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数真正需要求值的时候,之前传入的所有参数都会被一次性用于求值

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
/**
* 用于将函数进行柯里化
* 接受一个参数:即将要被currying的函数
*/
let currying = function(fn) {
let args = []

return function() {
if(arguments.length === 0) {
return fn.apply(this, args)
} else {
[].push.apply(args, arguments)
// arguments.callee 是一个在函数内部可用的属性,它指向当前正在执行的函数。这在匿名函数中特别有用,因为它允许你引用函数本身,而不需要知道函数的名字。
return arguments.callee
}
}
}

let sum = function() {
let res = 0
for(let i = 0; i < arguments.length; i++) {
res += arguments[i]
}
return res
}

// 将sum函数进行柯里化
let curryingSum = currying(sum)

curryingSum(1)

curryingSum(2)

curryingSum(3)

console.log(curryingSum()); // 6

2.函数节流

函数有可能被非常频繁地调用,而造成大的性能问题。函数节流的原理:将即将要被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求

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
/**
* 节流函数
* fn:需要被延迟执行的函数
* interval:延迟执行的时间
*/
let throttle = function(fn, interval) {
let _self = fn // 保存被延迟执行的函数引用
let timer = null // 定时器
let firstTime = true // 是否是第一次调用

return function() {
let args = arguments
let _me = this

if(firstTime) { // 如果是第一次调用,不需要延迟执行
_self.apply(_me, args)
return firstTime = false
}

if(timer) { // 前一次延迟执行还没有完成,忽略此次请求
return false
}

timer = setTimeout(function() { // 延迟一段时间执行
clearTimeout(timer)
timer = null
_self.apply(_me, args)
}, interval || 500)
}
}

// 使用示例:监听浏览器尺寸变化
window.onresize = throttle(function() {
console.log(1)
}, 500)

3.分时函数

某些函数是用户主动调用的,但因为一些客观原因,这些函数会严重影响页面性能

例如我们要在页面上渲染1000个div节点,在短时间内往页面中大量添加DOM节点会导致浏览器卡顿甚至假死

解决方案之一就是将创建节点的工作分批进行。比如把1秒创建1000个节点,改为每隔200毫秒创建8个节点

设计一个timeChunk函数来分批创建节点,函数接受3个参数:

  • ary:创建节点时需要用到的原始数据
  • fn:封装了创建节点逻辑的函数
  • count:每一批创建的节点数量
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
let timeChunk = function(ary, fn, count) {
let obj, timer
let len = ary.length

let start = function() {
for(let i = 0; i < Math.min(count || 1, len); i++) {
let item = ary.shift()
fn(item)
}
}

return function() {
timer = setInterval(function() {
if(len === 0) { // 如果全部节点都创建好了,取消定时器
return clearInterval(timer)
}
start()
}, 200)
}
}

// 使用示例

let ary = []

for(let i = 0; i <= 1000; i++) {
ary.push(i)
}

let renderList = timeChunk(ary, function(n) {
let div = document.createElement('div')
div.innerHTML = n
document.body.appendChild(div)
}, 8)

renderList()

useMemo & useCallback

useMemo

useMemo用于缓存每次重新渲染都需要计算的结果

语法

useMemo(calculateValue,dependencies)

  • calculateValue: 缓存计算值的函数,它应该是一个没有参数的纯函数。React将在首次渲染时调用该函数,在之后的渲染中,如果依赖项(dependencies)没有发生变化,React将不会调用该函数,而是直接返回缓存的值
  • dependencies: 依赖项

返回值

  • 在初次渲染时,返回calculateValue函数的返回值
  • 在之后的渲染中,如果依赖项没有变化,则返回缓存的值;如果依赖项发生了变化,将重新调用calculateValue函数并计算出新的返回值

用法

useMemo主要的用法有两种:

  • 通过缓存数据,来跳过代价昂贵的计算
  • 跳过组件的重新渲染

跳过代价昂贵的计算

例如,我们有一个todos的大数组,需要根据tab来筛选出某些数组项。在初次渲染之后,只有当todos数组或者tab发生改变了才会重新执行过滤数组的方法。

1
2
3
4
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}

跳过组件的重新渲染

例如,ParentComp组件接受一个theme的props,用于改变组件内部样式,默认情况下,一个组件重新渲染时,React会递归重新渲染它的所有子组建。也就是,当theme重新渲染时,会导致SonComp组件的重新渲染,但其实SonComp组件并不需要重新渲染。

1
2
3
4
5
6
7
8
9
function ParentComp({ theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
return (
<div className={theme}>
<SonComp items={visibleTodos}/>
</div>
)
}

可以通过将组件用memo包裹起来,这样当它的props和上次传入的相同时,就不会触发重新渲染

1
2
3
4
import { memo } from 'react';
const SonComp = memo(function SonComp({ items }) {
// ...
})

引出usecallback

现在,SonComp组件被包裹在memo中,接收一个函数作为props。每次ParentComp组件重新渲染,都会导致handleClick函数的重新创建,即产生不同的handleClick,这就会导致SonComp组件的重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ParentComp({ theme }) {

function handleClick = () => {
// ...
}

// ...
return (
<SonComp handleClick={handleClick}/>
)
}

const SonComp = memo(function SonComp({ handleClick }) {
// ...
})

我们很容易想到使用useMemo来缓存handleClick函数,如下所示:

1
2
3
4
5
const handleClick = useMeme(() => {
return () => { // 原来handleClick的逻辑

}
},[xxx])

这样看上去就很冗余了,在React中有另外一个Hook用于缓存函数。

useCallback

useCallback允许你在多次渲染中缓存函数

语法

useCallback(fn, dependencies)

  • fn: 想要缓存函数,它可以传入任何参数并且返回任何值。React将在首次渲染时返回(不是调用)该函数,在之后的渲染中,如果依赖项(dependencies)没有发生变化,React将返回相同的函数。在任何渲染中,React都不会调用该函数,而是返回该函数
  • dependencies: 依赖项

返回值

在初次渲染时,useCallback返回你已经传入的fn函数

在之后的渲染中, 如果依赖没有改变,useCallback返回上一次渲染中缓存的fn函数;否则返回这一次渲染传入的fn

用法

接着上面的例子对handleClick进行改造,使它也被缓存起来

1
2
3
const handleClick = useCallback((args) => { //和普通函数一样使用
// 直接在此处书写逻辑代码
},[xxx])

useRef

useRef可以用于引用一个不需要渲染的值

语法

useRef(initialValue)

  • initialValue: ref对象的current属性的初始值,只在初次渲染的时候有效

返回值

useRef返回一个只有current属性的对象。current初始值为传递的initialValue,之后可以将其设置为其他值

如果将ref对象作为一个JSX节点的ref属性,React会为它设置current属性

注意

  • ref.current属性可以修改,它是可变的,这与state不同
  • 改变ref.current属性,不会重新渲染组件
  • 除了进行初始化操作,不要在渲染期间写入或读取ref.current

不要在渲染期间写入或者读取ref.current,如果不得不在渲染期间读取或写入,应该使用state

1
2
3
4
5
6
function App() {
// ...
myRef.current = 123 // ❌ 不要再渲染期间写入 ref
// ...
return <div>{myRef.current}</div> // ❌ 不要再渲染期间读取 ref
}

事件处理程序或者Effect中读取或写入ref

1
2
3
4
5
6
7
function App() {
// ...
useEffect(() => {
myRef.current = 123
})
// ...
}

使用

useRef的使用场景主要有两种:

  • 引用或存储一些不影响视图的信息。(因为改变ref不会触发重新渲染)
  • 通过ref对象赋值给DOM的ref属性来获取或操作DOM

引用或存储一个值

示例:保存定时器的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function App() {
let timerRef = useRef(null)

function handleStartClick() {
const timer = setTimeout(() => {
// 业务逻辑
}, 1000)
timerRef.current = timer // 保存定时器id
}

function handleStopClick() {
clearTimeout(timerRef.current)
}

return (
<div className="App">
// ....
</div>
)
}

通过ref操作DOM

1
2
3
4
5
6
7
8
9
10
11
function App() {
const inputRef = useRef(null) // 声明一个初始值为 null 的ref对象

//...

function handleClick() {
inputRef.current.focus() //inputRef.current获取到DOm节点,并调用input节点上的focus方法
}

return <input ref={ref} /> // 将DOM节点的ref对象赋值给ref
}

无法获取自定义组件的ref

默认情况下,自定义组件不会暴露他们内部DOM节点的ref

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
import { useState, useRef } from "react";

function MyComp() {
const [x, setX] = useState(0)

return <p>{x}</p>
}

function App() {

const myRef = useRef(null)

function handleClick() {
console.log(myRef.current);
}


return (
<>
<MyComp ref={myRef} />
<button onClick={handleClick}>获取ref</button>
</>
)
}

export default App;

控制台警告,无法获取到ref

为了解决这个问题,可以使用forwardRef包裹子组件,这样父组件就能得到它的ref

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
import { useEffect, useState, useRef, forwardRef } from "react";

const MyComp = forwardRef((props, ref) => { //forwardRef包裹组件,函数的第二个参数就是ref对象
const [x, setX] = useState(0)

return <p ref={ref}>{x}</p>
})

function App() {

const myRef = useRef(null)

function handleClick() {
console.log(myRef.current); //可以获取到子组件的ref
}


return (
<>
<MyComp ref={myRef} />
<button onClick={handleClick}>获取ref</button>
</>
)
}

export default App;

useState

作为一个React Hook,它只能在组件的顶层调用

语法

useState(initialState)

  • initialState:state的初始值。当传递函数作为initialState时,它将被视为初始化函数,它应该是一个纯函数,不应该传入任何参数,并且应该返回一个值来作为初始值。此参数只有在初始化渲染时使用,在之后将被忽略

useState返回一个数组:状态变量 和 状态设置(set)函数

按照惯例,使用解构赋值的方法来命名状态和状态设置函数,例如const [x, setX] = useState(0)

set函数

通过调用set函数来更新状态变量的值,你可以:

  • 传入一个不同的新值,例如: setName('cqy')
  • 基于待定的state来计算新的状态(更新函数),例如: setAge(a => a + 1)

当将函数作为参数传递给set函数时,它被视为更新函数。它必须是纯函数,唯一的参数是待定的state,并应该返回下一个状态

注意事项

set函数仅更新 下一次 渲染的状态变量,如果在调用set函数后立刻读取状态便了,得到的仍然是渲染在屏幕上的旧值

待定的state

下面的例子中,我们希望在点击之后将count增加3,但结果是count只会增加1。
原因就在于,前一个set函数执行完后,后一个set函数读取的仍然是旧的状态,React会批量处理状态更新,在所有事件处理函数运行并调用其set函数后更新屏幕。所以在页面重新渲染前,获取到的仍然是旧值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function Counter() {
const [count, setCount] = useState(0);

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

return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}

为了解决这个问题,可以给set函数传递一个更新函数,它可以根据**最新的state**(即待定的state,可以理解为若没有后续改变state的操作,该state将会在下一次渲染时被使用)状态来计算更新下一个state状态

1
2
3
4
5
6
function handleClick() {
setCount(c => c + 1); // setCount(0 => 1)
setCount(c => c + 1); // setCount(1 => 2)
setCount(c => c + 1); // setCount(2 => 3)
}
// c => c + 1 是更新函数,它获取 待定状态 并根据此计算下一个状态

更新状态中的对象和数组

当把对象和数组作为状态变量时,你应该替换它而不应该单纯的改变对象中的某个属性或者数组中的某项

示例代码:

1
2
3
4
5
6
7
8
9
10
11
const [obj, setObj] = useState({
name: 'easycodesniper',
age: 22
})

obj.name = 'xxx' //❌ 错误的修改方法

setObj({ // 正确的修改方法
...obj,
name: 'xxx'
})

避免重复创建初始状态

React只在初次渲染时保存初始状态,后续渲染都会忽略

1
2
3
function App() {
const [ x, setX ] = useState(createInitialX()) //不好的实践
}

尽管createInitialX()的结果仅用于初次渲染,但是每次重新渲染时都要调用此函数,如果它的执行需要昂贵的计算,会导致资源浪费
为了解决这个问题,你应该把函数本身作为初始化函数传递给useState,而不是调用该函数

1
const [ x, setX ] = useState(createInitialX) //合理的实践

源码