-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5ec1129
commit 0c7c0d9
Showing
24 changed files
with
15,798 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# React 进阶教程 | ||
|
||
适用对象:刚出新手村,渴望提升前端开发效率的学生。 | ||
使用环境:yarn + React(使用`vite`创建)。 | ||
|
||
启动教学项目: | ||
```shell | ||
yarn run dev | ||
``` | ||
|
||
请配合[文档](./docs/README.md)使用。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# 文档目录 | ||
1. [代码简化秘籍——自己动手写hooks](代码简化秘籍——自己动手写hooks.md) | ||
2. [性能调优秘籍](性能调优秘籍.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# 代码简化秘籍——自己动手写hooks | ||
|
||
假设你已经理解了函数式组件常用的钩子函数,如 `useEffect`,`useState`。现在有一个使用场景,我希望使用一个 `radio`,实时获取它当前的值,并且能够通过函数来控制是否选中它,你会怎么做? | ||
|
||
我猜你估计会使用下面的方法: | ||
|
||
```typescript | ||
export default function SomeComponent() { | ||
const [value, setValue] = useState<boolean>(false); | ||
const toggleRadio = () => { | ||
setValue(value => !value); | ||
} | ||
const selectRadio = () => { | ||
setValue(true); | ||
} | ||
const cancelRadio = () => { | ||
setValue(false); | ||
} | ||
return <Space> | ||
<Radio checked={value} onClick={toggleRadio}>写法1</Radio> | ||
<Button onClick={toggleRadio}>翻转状态</Button> | ||
<Button onClick={selectRadio}>选中</Button> | ||
<Button onClick={cancelRadio}>取消</Button> | ||
</Space> | ||
} | ||
``` | ||
|
||
这么写当然是可以的,组件能够正常运行,同时也满足我们控制的需求。然而,为了实现这一功能,你定义了一个状态和一组回调函数,总共 **10** 行代码。试想一下,如果这个组件里面需要创建多个这样的 radio,你要怎么写?复制多份吗,状态冲突了怎么办?即便你只需要一个 radio,是否能够通过更好的方式进行封装,屏蔽上述的实现细节呢?当然可以,请大胆地使用 hooks(钩子函数),不仅仅局限于各种官方/第三方库中提供的 hooks,你也可以自己动手,丰衣足食😄。 | ||
|
||
你可以通过如下的钩子函数来简化和屏蔽实现细节: | ||
|
||
```typescript | ||
function useRadio() { | ||
const [value, setValue] = useState<boolean>(false); | ||
const MyRadio = (props: RadioProps) => <Radio checked={value} onClick={toggle} {...props}></Radio> | ||
const toggle = () => { | ||
setValue(value => !value); | ||
} | ||
const select = () => { | ||
setValue(true); | ||
} | ||
const cancel = () => { | ||
setValue(false); | ||
} | ||
|
||
return { | ||
MyRadio, toggle, select, cancel | ||
} | ||
} | ||
``` | ||
注意,我们一般使用 `useXXX` 来命名一个钩子函数,其中 `XXX` 通常为你希望获取的主体。例如我们这里需要一个可控的 radio,所以命名为 `useRadio`。 | ||
通过上述的钩子函数,我们可以将之前的代码简化为: | ||
```typescript | ||
const { | ||
MyRadio, toggle, select, cancel | ||
} = useRadio(); | ||
|
||
<Space> | ||
<MyRadio>写法2</MyRadio> | ||
<Button onClick={toggle}>翻转状态</Button> | ||
<Button onClick={select}>选中</Button> | ||
<Button onClick={cancel}>取消</Button> | ||
</Space> | ||
``` | ||
是不是简化了特别多?你可以运行我们提供的教程项目(案例1),然后感受两种写法的不同。 | ||
现在,你遇到了另一个需求:你需要在前端请求一个耗时较长的接口,在请求开始后,你希望前端组件呈现加载中的状态,你会怎么做?回想一下前端如何请求后端?哦,你想起来了,可以用原生的 fetch 函数写出这样的钩子函数: | ||
```typescript | ||
function useData<T>(url: string) { | ||
const [loading, setLoading] = useState<boolean>(false); | ||
const [data, setData] = useState<T | undefined>(); | ||
const [error, setError] = useState<Error | undefined>(); | ||
const mutate = async () => { | ||
setLoading(true); | ||
try { | ||
const response = await fetch(url, { method: "GET" }); | ||
if (!response.ok) { | ||
const error = Error(`HTTP error! status: ${response.status}`); | ||
setError(error); | ||
return; | ||
} | ||
const data = await response.json() as T; | ||
setData(data); | ||
} catch (error) { | ||
setError(error as Error); | ||
} finally { | ||
setLoading(false); | ||
} | ||
} | ||
useEffect(() => { | ||
mutate(); | ||
}, [url]); | ||
return { data, loading, error, mutate } | ||
} | ||
``` | ||
在使用的时候,我们可以这样: | ||
```typescript | ||
const { | ||
data, loading, error, mutate | ||
} = useData<DataType>("https://dogapi.dog/api/v2/breeds"); | ||
return <Card title='案例1' extra={<Button onClick={mutate}>刷新</ Button>}> | ||
<Space style={{ width: "100%" }} direction='vertical'> | ||
<Table dataSource={data?.data} loading={loading} columns={columns} /> | ||
</Space> | ||
</Card> | ||
``` | ||
你还可以在此基础上添加一个 `mutate` 函数,当你进行一些数据修改,希望重新刷新数据的时候,可以调用这个暴露出来的 mutate 函数重新获取数据: | ||
你可以在“案例2”中体验两种写法的区别。这里我们使用的是一个公开的 api(https://dogapi.dog/docs/api-v2)。 | ||
当然,随着业务的发展,你可能还会有更多的需求,在这个 useData 上面新增更多的状态和配置。既然如此,**何必自己造轮子呢**?你完全可以用 `useSWR + axios` 这套方案,详见:useSWR 的官方文档:https://swr.vercel.app/zh-CN/docs/data-fetching,此处不再赘述😁。 | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# 性能调优秘籍 | ||
|
||
## 使用 useMemoized 和 useMemo 减少不必要的渲染 | ||
|
||
我们很容易会写出如下的函数组件: | ||
|
||
```typescript | ||
export default function SomeComponent() { | ||
|
||
} | ||
``` | ||
|
||
所谓函数式组件,就是将函数当作组件来用。每次渲染组件的时候,其实都会执行一遍对应的函数。所以在本案例的代码中,形如: | ||
```typescript | ||
const onChange = () => {}; | ||
const cnt = doSomethingSlowly(); | ||
``` | ||
|
||
实际上每次组件重新渲染都会调用一次。你可以通过案例3的“按下按钮次数”和 “props 变更次数”佐证这一点。因为每次按下按钮,都会调用父组件传入的 onChange 函数,而这个函数会修改父组件的 `cnt` 状态,从而导致再次渲染。再次渲染会重新调用父组件的函数,从而定义一个新的 onChange 函数,而 onChange 函数凑巧又会作为 props 传入子组件。我们知道,React 里面组件刷新有两种情况,要么状态改变,要么传入的 props 改变。所以这里子组件因为传入的 onChange 地址发生了改变,也会触发一次重新渲染,运行一次子组件的函数。而这个函数很不巧又在通过一个很慢的运算得到变量 `cnt`,这实际上并不是我们想要的结果。 | ||
|
||
所以,大家要明确一个误区:在 javascript 中,创建一个函数的开销是很小的,但是如果新建的函数被作为 props 传入子组件,可能会产生影响性能的副作用。解决这一问题的关键就是让函数的地址固定,这时候 ahooks 库中的 `useMemoizedFn` 就可以发挥作用了。它可以固定一个函数的地址,无论何时调用,它的地址都是固定的。 | ||
|
||
此外,我们其实不需要每次都重新计算 `cnt`,我们只是希望它在一开始的时候初始化一下,所以你可以使用原生的 `useMemo`,通过类似于 `useEffect` 的方式,给定一个依赖数组,当且仅当依赖发生变更的时候,才会去计算新的值。 | ||
|
||
你可以通过我们给定的案例3中优化后的结果来更好地理解这一点。 | ||
|
||
## 使用 useImmer 提升更新效率 | ||
|
||
安装: | ||
```shell | ||
yarn add immer use-immer | ||
``` | ||
|
||
假设你有这样的一个类型的数据: | ||
|
||
```typescript | ||
interface TagType { | ||
color: string; | ||
text: string; | ||
} | ||
``` | ||
|
||
你希望渲染出一堆 tags,并且按下按钮修改其中一个 tag 的 text,如果用原生的 `useState` 会这样写: | ||
|
||
```typescript | ||
const [tags, setTags] = useState<TagType[]>(generateTestTags()); | ||
const modify = () => { | ||
tags[randIndex].text = 'changed'; | ||
setTags([...tags]); | ||
}; | ||
``` | ||
|
||
由于 `useState` 监听的是一个数组(引用类型),所以浅拷贝是无法产生页面重渲染的,此处必须传递深拷贝,修改数组的地址,引发页面重渲染。然而,显而易见,这种方式一旦数组长度达到一定程度,性能开销将无法忽视。例如,在本案例中,我们渲染了 10000个标签。每次修改的时候,都会对这个大小为 10000 的数据进行深拷贝。通过性能工具,我们可以发现这一修改过程将耗费几百毫秒。 | ||
|
||
而 `useImmer` 则允许我们直接修改数组/对象的字段,无需进行深拷贝,极大地节省性能并提升代码可读性: | ||
|
||
```typescript | ||
setOptTags(tags => { tags[randIndex].text = 'changed' }); | ||
``` | ||
|
||
你可以通过以下视频对比优化前后的性能差距: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import js from '@eslint/js' | ||
import globals from 'globals' | ||
import reactHooks from 'eslint-plugin-react-hooks' | ||
import reactRefresh from 'eslint-plugin-react-refresh' | ||
import tseslint from 'typescript-eslint' | ||
|
||
export default tseslint.config( | ||
{ ignores: ['dist'] }, | ||
{ | ||
extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
files: ['**/*.{ts,tsx}'], | ||
languageOptions: { | ||
ecmaVersion: 2020, | ||
globals: globals.browser, | ||
}, | ||
plugins: { | ||
'react-hooks': reactHooks, | ||
'react-refresh': reactRefresh, | ||
}, | ||
rules: { | ||
...reactHooks.configs.recommended.rules, | ||
'react-refresh/only-export-components': [ | ||
'warn', | ||
{ allowConstantExport: true }, | ||
], | ||
}, | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Vite + React + TS</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "tutorial", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "tsc -b && vite build", | ||
"lint": "eslint .", | ||
"preview": "vite preview" | ||
}, | ||
"dependencies": { | ||
"antd": "^5.23.2", | ||
"immer": "^10.1.1", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1", | ||
"use-immer": "^0.11.0" | ||
}, | ||
"devDependencies": { | ||
"@eslint/js": "^9.17.0", | ||
"@types/react": "^18.3.18", | ||
"@types/react-dom": "^18.3.5", | ||
"@vitejs/plugin-react": "^4.3.4", | ||
"eslint": "^9.17.0", | ||
"eslint-plugin-react-hooks": "^5.0.0", | ||
"eslint-plugin-react-refresh": "^0.4.16", | ||
"globals": "^15.14.0", | ||
"typescript": "~5.6.2", | ||
"typescript-eslint": "^8.18.2", | ||
"vite": "^6.0.5" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Space } from 'antd' | ||
import Case1 from './cases/case1' | ||
import Case2 from './cases/case2' | ||
import Case3 from './cases/case3' | ||
import Case4 from './cases/case4' | ||
|
||
function App() { | ||
return <Space style={{ width: "100%" }} direction='vertical'> | ||
<Case1 /> | ||
<Case2 /> | ||
<Case3 /> | ||
<Case4 /> | ||
</Space> | ||
} | ||
|
||
export default App |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Button, Card, Radio, RadioProps, Space } from 'antd'; | ||
import { useState } from 'react'; | ||
|
||
function useRadio() { | ||
const [value, setValue] = useState<boolean>(false); | ||
const MyRadio = (props: RadioProps) => <Radio checked={value} onClick={toggle} {...props}></Radio> | ||
const toggle = () => { | ||
setValue(value => !value); | ||
} | ||
const select = () => { | ||
setValue(true); | ||
} | ||
const cancel = () => { | ||
setValue(false); | ||
} | ||
|
||
return { | ||
MyRadio, toggle, select, cancel | ||
} | ||
} | ||
|
||
export default function Case1() { | ||
const [value, setValue] = useState<boolean>(false); | ||
const toggleRadio = () => { | ||
setValue(value => !value); | ||
} | ||
const selectRadio = () => { | ||
setValue(true); | ||
} | ||
const cancelRadio = () => { | ||
setValue(false); | ||
} | ||
|
||
const { | ||
MyRadio, toggle, select, cancel | ||
} = useRadio(); | ||
return <Card title='案例1'> | ||
<Space style={{ width: "100%" }} direction='vertical'> | ||
<Space> | ||
<Radio checked={value} onClick={toggleRadio}>写法1</Radio> | ||
<Button onClick={toggleRadio}>翻转状态</Button> | ||
<Button onClick={selectRadio}>选中</Button> | ||
<Button onClick={cancelRadio}>取消</Button> | ||
</Space> | ||
<Space> | ||
<MyRadio>写法2</MyRadio> | ||
<Button onClick={toggle}>翻转状态</Button> | ||
<Button onClick={select}>选中</Button> | ||
<Button onClick={cancel}>取消</Button> | ||
</Space> | ||
</Space> | ||
</Card> | ||
} |
Oops, something went wrong.