React

React

官网:React (docschina.org)

创建项目

html中直接导入

1
2
3
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
  • react.min.js - React 的核心库
  • react-dom.min.js - 提供与 DOM 相关的功能
  • babel.min.js - Babel 可以将 ES6 代码转为 ES5 代码,这样我们就能在目前不支持 ES6 浏览器上执行 React 代码。Babel 内嵌了对 JSX 的支持。通过将 Babel 和 babel-sublime 包(package)一同使用可以让源码的语法渲染上升到一个全新的水平。

(过时)使用create-react-app构建项目

create-react-app 是来自于 Facebook,通过该命令我们无需配置就能快速构建 React 开发环境

官网:Create React App (create-react-app.dev)

1
2
3
npx create-react-app my-app
cd my-app
npm start

使用框架创建项目

Next.js

充分利用了 React 的架构,支持全栈 React 应用

1
npx create-next-app@latest

React Router

最流行的路由库,可以与 Vite 结合创建一个全栈 React 框架

1
2
3
4
npx create-react-router@latest

// 运行
npm run dev

JSX

HTML语法转为JSX

语法规则

  1. return 只能返回一个元素,有多个需要用个父元素框起来
  2. 标签必须闭合
  3. 可以在标签文本内、属性内用{ }来使用JavaScript语法
  4. {{ }}表示传递一个对象,在属性style中会用到

组件

根组件

程序入口,在root.tsx里

1
export default function App() { ... }

组件导入导出

将Gallery组件放到一个新的Gallery.tsx文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Profile() {
return (
<img
src="https://i.imgur.com/QIrZWGIs.jpg"
alt="Alan L. Hart"
/>
);
}

export default function Gallery() {
return (
<section>
<h1>了不起的科学家们</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}

在其他组件中导入

1
2
3
4
5
6
7
import Gallery from "./Gallery"

export default function About() {
return (
<Gallery/>
)
}

具名导出:当要导入一个没有default的组件时,要用 import { 组件名, … } from ‘文件名’;

默认导出:而导入有default的组件时,可用 import 别名 from ‘文件名’

默认导出只能有一个

传递参数给组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Avatar({ imgInfo, size = 100 }) {
return (
<img
src = {imgInfo.imgUrl}
alt = {imgInfo.imgName}
width = {size}
height = {size}
/>
);
}

export default function Home() {
return (
<Avatar
imgInfo={{
imgUrl: 'https://i.imgur.com/1bX5QH6.jpg',
imgName: '111'
}}
size={200}
/>
)
}
  • 组件的唯一参数就是props,在接收参数时通过{ }结构出来
  • {{ }}传递的是对象

传递子组件给组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Card({ children }) {
return (
<div>
{children}
</div>
)
}

export default function Home() {
return (
<Card>
<Avatar
imgInfo={{
imgUrl: 'https://i.imgur.com/1bX5QH6.jpg',
imgName: '111'
}}
/>
</Card>
)
}
  • 通过props中的children参数来接收子组件
  • 在父组件中通过{}渲染子组件

渲染列表

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
function List() {
let persons = [
{
id: 0,
name: 'a'
},
{
id: 1,
name: 'b'
},
{
id: 2,
name: 'c'
},
]
let content = persons.filter(p => p.id > 0)
.map(p => <li key={p.id}>{p.name}</li>)
return (
<ul>{content}</ul>
)
}

export default function Home() {
return (
<List />
)
}
  • 使用数组.map()来遍历
  • 使用数组.filter()来过滤
  • map()里如果用 => {},那么{}里要用return
  • 直接放在 map() 方法里的 JSX 元素一般都需要指定 key
  • 尽量不修改传进来的参数,要修改也是拷贝一份再修改,保证组件的纯粹

组件渲染

渲染做了什么

  1. 重新执行函数组件函数
  2. 根据返回的JSX,生成新的虚拟DOM树
  3. 将新虚拟DOM树和旧的比较
  4. 只更新真实DOM的变化的部分

渲染时机

  • 应用启动时初次渲染
  • 组件或祖先组件状态发生改变时重渲染(修改state会被标记要需要重新渲染)

组件间共享状态

提升参数位置

将要共享的state从子组件提到最近的父组件

缺点:可能会导致要往很深的组件传递参数

Context

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

步骤:

  1. 创建Context

    1
    2
    3
    4
    // 在一个文件中创建context并导出
    import { createContext } from "react";

    export const LevelContext = createContext(1);
  2. 使用和提供Context

    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
    // 在要使用的地方引入context
    import { LevelContext } from "./LevelContext";

    // 给子组件提供这个context时,子组件会找最近的
    export function Section({ children, level }) {
    return (
    <section>
    <LevelContext value={level + 1}>
    {children}
    </LevelContext>
    </section>
    );
    }

    // 子组件请求这个context
    function Heading({children}){
    const level = useContext(LevelContext)

    switch (level) {
    case 1:
    return <h1>{children}</h1>;
    case 2:
    return <h2>{children}</h2>;
    default:
    throw Error('未知的 level: ' + level);
    }
    }

    // 下层的子组件可以一起拿到state,不用一个个传递
    export default function Home() {

    return (
    <Section level={1}>
    <Heading>子标题</Heading>
    <Section level={2}>
    <Heading>子标题</Heading>
    <Heading>子标题</Heading>
    </Section>
    </Section>
    )
    }

交互

添加事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function Home() {
// return <Welcome />;

function handleClick() {
alert("你点了我")
}

return (
<button onClick={handleClick}>
点我
</button>
)
}
  • 事件处理函数通常命名为handleXXX

  • 事件处理函数参数通常命名为onXXX

  • 事件传播:如果父组件也有事件,那么会先触发子组件的,然后按向上顺序触发,除了onScroll

  • 阻止事件传播:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function Button({onClick, children}) {
    return (
    <button onClick={(e) => {
    e.stopPropagation() // 阻止了事件想上传播
    onClick() // 执行的是父组件传来的函数
    }}>
    {children}
    </button>
    )
    }

    export default function Home() {
    // return <Welcome />;

    return (
    <div onClick={() => alert("点击了div")}>
    <Button onClick={() => alert('点击了按钮')}>按钮</Button>
    </div>
    )
    }

注意:

正确 错误
<button onClick={handleClick}> <button onClick={handleClick()}>
<button onClick={() => alert('...')}> <button onClick={alert('...')}>

state

和普通变量的区别

  • 更改state会触发组件重新渲染,普通变量不会

  • 普通变量在组件重新渲染时不会保存当前值,state会保留渲染之前的值

  • 应将state视为只读

  • 组件不被渲染时,对应的state也会被销毁

  • state是通过组件在渲染树的位置来保存的,比如相同位置相同的组件,切换时会保留state

    同一个位置指的是像 ? <Button/> : <Button/>if else这样的,而<> {<Button/>} {<Button/>} </>这种的就不是同一个位置

    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 Button(){
    const [a, setA] = useState(0)
    return (
    <button onClick={() => setA(a + 1)}>
    { a }
    </button>
    )
    }

    function Button1(){
    const [a, setA] = useState(0)
    return (
    <button onClick={() => setA(a + 1)}>
    { a }
    </button>
    )
    }

    export default function Home() {
    const [b, setB] = useState(true)

    // 如果两个都是Button, 那么会保留state,
    // 如果一个Button一个Button1,切换时不会保留state
    // 如果key值不同,也不会保留state
    return (
    <>
    <button onClick={() => setB(! b)}>change</button>
    {b ? <Button/> : <Button1/>}
    </>
    )
    }

使用

1
2
3
4
5
6
7
8
9
10
11
import { useState } from "react";

export default function Home() {
// 组件记住了index的值,当第一次触发重新渲染时,会返回[1, setIndex]
const [index, setIndex] = useState(0);
return (
<button onClick={() => setIndex(index + 1)}>
{index}
</button>
)
}

注意

  • use开头的函数称为Hook
  • 不要在循环、条件语句里用useState()是通过给每个组件维护一个数组来保存state,每次渲染都是按顺序把值重新赋给变量
  • 如果是普通变量改变,不会触发重新渲染,新的值不会显示在组件上
  • 修改state会生成新的快照,在下次渲染生效
  • 可以用更新函数index => index + 1,这样会把更新后的值传给下一个函数

更新state

state中可以存放任意JavaScript对象

更新对象:应当创建新的,而不是直接修改源对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const [position, setPosition] = useState({x: 0, y: 0});
// 正确
setPosition({
x: 1,
y: 1
});
// 错误
position.x = 1;

// 对象展开写法, 是浅拷贝
setPosition({
...position,
x: 1
});

当要更新多层嵌套的对象时,使用Immer更方便

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
// 1.对象对层嵌套时,使用Immer库
// 2.导入npm install use-immer
// 3.导包
import { useImmer } from 'use-immer';

export default function Home() {
const [position, setPosition] = useImmer({
x: 0,
y: 0,
site: {
nation: 'China'
}
})

function handleClick() {
setPosition(draft => {
draft.site.nation = 'American'
})
}

return (
<button onClick={handleClick}>
{position.site.nation}
</button>
)
}

更新数组

  • 添加元素: 使用...展开
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
export default function Home() {
const [arr, setArr] = useState([])

function add() {
setArr([
...arr,
1
])
}

return (
<>
<button onClick={add}>
add
</button>
<ul>
{
arr.map(a => (
<li>{a}</li>
))
}
</ul>
</>
)
}
  • 删除元素: 使用filter, filter会创建一个新数组
  • 修改/替换元素:使用map来遍历并找到要修改的
  • 特定位置插入元素:使用...数组.slice(起始坐标, 终点坐标)展开前面和后面,然后往中间插入

Reducer管理state

将state迁移至Reducer方便管理

优点:

  1. 比较好读懂
  2. 好调试
  3. 后期代码量比state低

步骤

  1. 定义操作:将设置状态的逻辑修改成dispatch的一个action
  2. 实现操作的具体步骤:编写reducer函数
  3. 使用useReducer代替useState
  4. 还可以用useImmerReducer进一步简化
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
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}

function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}

function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}

return (
<>
<h1>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}

function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action: ' + action.type);
}
}
}

let nextId = 3;
const initialTasks = [
{id: 0, text: '参观卡夫卡博物馆', done: true},
{id: 1, text: '看木偶戏', done: false},
{id: 2, text: '打卡列侬墙', done: false}
];

注意:

  • reducer应该只用来计算下一个状态,而不应该做其他事

ref

和state的区别:更新ref时不会触发重新渲染;更改会立即表现出来,而不是等到下次渲染

和普通变量的区别:每次重新渲染时,raect会记住ref的值

何时使用:很少用到,要存储一些值,但不影响渲染逻辑

用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
28
29
30
31
32
33
34
35
36
37
38
39
// 计时器
export default function Home() {
// 用于渲染的数据,用state保存
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
// interval的ID,用于停止循环,不需要渲染出来,用ref保存
const intervalRef = useRef(null);

function handleStart() {
setStartTime(Date.now());
setNow(Date.now());

clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}

function handleStop() {
clearInterval(intervalRef.current);
}

let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}

return (
<>
<h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
开始
</button>
<button onClick={handleStop}>
停止
</button>
</>
);
}

用ref操作DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default function Home() {
const inputRef = useRef(null);

function handleClick() {
// 让输入框被选中
inputRef.current.focus();
}

return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}

当DOM是一个数组,而数组大小未知时:

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
// useRef同样不能在循环中使用
// ref回调:通过将函数传递给ref的方法来解决
// 会给每个dom调用回调函数,在销毁dom时执行return
export default function Home() {
const itemsRef = useRef(null);
const [items, setItems] = useState(initialData);

function getMap() {
if(! itemsRef.current) {
itemsRef.current = new Map();
}
return itemsRef.current;
}

return (
<>
<ul>
{
items.map((item) => (
<li
key={item}
ref={(node) => {
const map = getMap();
map.set(item, node);

return () => {
map.delete(item);
};
}}
>
{ item.name }
</li>
))
}
</ul>
</>
);
}

function initialData() {
const l = []
for(let i = 0; i < 5; ++i) {
l.push(
{
name: "name" + i
}
)
}
return l
}

effect

在渲染后触发的效果,用于和外部系统交互

使用方法

  • 基本方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 导入
    import { useEffect } from 'react';

    export function VideoPlayer({src, isPlaying}) {
    const ref = useRef(null);

    // 渲染应该是纯粹的计算,应当将修改dom的操作放到effect中
    useEffect(() = {
    if (isPlaying) {
    ref.current.play();
    } else {
    ref.current.pause();
    }
    });

    return <video ref={ref} src={src} loop playsInline />;
    }
  • 可以指定参数,当有参数与上次渲染时一样时不触发Effect

    如果为空[],那么只会在挂载时触发一次

    1
    2
    3
    4
    5
    6
    7
    8
    // 如果effect里面用到这个参数来决策运行哪段代码,则指定参数时必须要加进去
    useEffect(() = {
    if (isPlaying) {
    ref.current.play();
    } else {
    ref.current.pause();
    }
    }, [isPlaying]);
  • 可以return清除函数,会在每次 Effect 重新运行之前调用清理函数,并在组件卸载(被移除)时最后一次调用清理函数

    1
    2
    3
    4
    5
    6
    7
    8
    useEffect(() = {
    if (isPlaying) {
    ref.current.play();
    } else {
    ref.current.pause();
    }
    }, [isPlaying]);
    return () => { doSomething... }

注意

  • 在effect中使用setState会形成死循环

useMemo

用来缓存数据

1
2
3
4
5
6
7
8
import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ 除非 todos 或 filter 发生变化,否则不会重新执行 getFilteredTodos()
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}

React
http://xwww12.github.io/2023/04/26/前端/react/React/
作者
xw
发布于
2023年4月26日
许可协议