本文编写自26届同学:狗儿要听狗儿歌
初识React
什么是React
React是一个用于构建用户界面的JavaScript库,由Facebook开发和维护。它允许开发者通过组合简单的组件来构建复杂的用户界面。
为什么选择React
- 组件化:React的设计思想是将UI分解成独立的、可复用的组件。
- 高效:React使用虚拟DOM来减少直接操作DOM的次数,从而提高性能。
- 社区支持:React有一个庞大的开发者社区和丰富的生态系统。
创建一个React 项目
我们可以利用vite 来创建一个react项目(Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。)
npm create vite@latest
经过漫长的等待和一系列选择后,一个react项目就创建好了,不过最好在桌面开启终端(右键桌面,点击打开终端),这样项目的文件会直接出现在桌面上,免得找不到
用你的编译器打开项目,并且在终端执行命令安装依赖,再用npm run dev 来启动项目
npm intall
npm run dev
启动成功可以看到一个链接,用浏览器打开就可以看到这个页面就表示这个项目没有问题
(我这深色模式看起来有点诡异)
那么就可以开始正式学习react了
React的核心概念
创建和嵌套组件
React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。
React 组件是返回标签的 JavaScript 函数:
function MyButton() {
return (
<button>I'm a button</button>
);
}
新建一个文件夹page,再新建button.tsx,同时我们需要删除掉一些没用的文件,将app.tsx return 里面删下面这样,将一些没用的css 文件也删掉,如下图,因为还有一些import 引用这些css , 所以连带着把所有引用css的地方也删干净,比如main.ts 中, 和app.tsx 中的一些import
至此,你已经声明了 MyButton,现在把它嵌套到另一个组件中:
你可能已经注意到 <MyButton /> 是以大写字母开头的。你可以据此识别 React 组件。React 组件必须以大写字母开头,而 HTML 标签则必须是小写字母。
如果你的项目还没有停止dev运行,再返回http://localhost:5173/ 就可以看到页面已经发生了变化,如果产生了报错,打开控制台,寻找报错原因
上面所使用的标签语法被称为 JSX。它是可选的,但大多数 React 项目会使用 JSX,主要是它很方便。所有 我们推荐的本地开发工具 都开箱即用地支持 JSX。
JSX 比 HTML 更加严格。你必须闭合标签,如 <br />。你的组件也不能返回多个 JSX 标签。你必须将它们包裹到一个共享的父级中,比如 <div>...</div> 或使用空的 <>...</> 包裹:
function AboutPage() {
return (
<>
<h1>About</h1>
<p>Hello there.<br />How do you do?</p>
</>
);
}
不过因为我们选择的是typescript,所以相应的是tsx,其实只是语言变了,其他区别不大
export default function AboutPage() {
const name = "张三"
return (
<>
<h1 className="name" style={{color: "red"}}>{name}</h1>
<p>Hello there.<br />How do you do?</p>
</>
);
}
在 React 中,你可以使用 className 来指定一个 CSS 的 class。它与 HTML 的 class 属性的工作方式相同:
然后,你可以在一个单独的 CSS 文件中为它编写 CSS 规则:
/* In your CSS */
.name {
border-radius: 50%;
}
不过要想这段css 起作用,我们还需要将css文件用import 导入到对应的tsx 文件中
我们想要在jsx 中显示一个变量数据 如上面代码中的name 变量表示为 {name} 同时对于标签上的各种属性也可以使用{},再使用变量进行操作,{} 可以写表达式,但是不能写语句
(style={{}} 并不是一个特殊的语法,而是 style={ } JSX 大括号内的一个普通 {} 对象。当你的样式依赖于 JavaScript 变量时,你可以使用 style 属性。)
return (
<img
className="avatar"
src={user.imageUrl}
/>
);
条件渲染
当在遇到不同的条件下要渲染不同的组件时
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return (
<div>
{content}
</div>
);
或者写为
<div>
{isLoggedIn ? (
<AdminPanel />
) : (
<LoginForm />
)}
</div>
这是三元表达式的写法,看不懂的话搜索三元表达式
当我们只需要一种情况时
<div>
{isLoggedIn && <AdminPanel />}
</div>
渲染列表
依赖 JavaScript 的特性,例如 for 循环 和 array 的 map() 函数 来渲染组件列表。
const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
return (
<>
{products.map(({title,id},index) => {
return <div key={index}>title: {title},id: {id}</div>
})}
</>
)
我们利用了map这个方法(不懂请查阅),map会遍历原数组并且返回一个新的数组,react 会将这个数组里的结构渲染,效果如下
事件响应
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
和原生并不相同,在react 中我们应该尽量避免自己去调用 document.querySelector() 之类的api 去获取dom元素
更新界面
在原生的技术当中,如果我们要更改一个div中的文字,我们需要先获取这个div ,然后通过修改一些属性值的方式来修改这些文字,或者修改这个div的样式(颜色或字体大小.....),但在react中这件事变得非常简单
import {useState} from "react";
export default function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
我们引入一个hook (以 use 开头的函数被称为 Hook。useState 是 React 提供的一个内置 Hook。),通过这个hook,即可达到改变视图的作用,用法如上
这个hook 将返回一个长度为2 的数组,我们用两个变量来接住他们,其中 count 中存储的是一个number,而setCount 中存储的是一个函数,在调用这个函数的时候,组件会重新渲染一份新的,并且新的组件中count 的值 ,就是setCount 传入的值。也就是说,当我们调用setCount(2) 时,会产生一个新的快照,在那个快照下,count 的值为2。但是在这个快照下,count的值依旧为1,你可以理解为出现了两个MyButton组件.
通过控制台我们可以发现这么一个现象
通过上面的思路我们即可理解这件事,当前快照下的count 并未发生改变
不过这个setCount() 不能直接裸写在 组件里像下面这样
如果你再去查看控制台会发现一堆报错,因为代码陷入了一种死循环当中,加载组件时,执行setCount(),再去加载新的快照,然后又遇到setCount() 这样就出现了死循环
组件间共享数据
对于两个兄弟组件,他们内部的变量是隔离开的,要想共享一份数据,就得在父组件中定义变量
function App() {
const [count, setCount] = useState(0);
return (
<>
<MyButton count={count} setCount={setCount}></MyButton>
<MyButton count={count} setCount={setCount}></MyButton>
</>
)
}
interface Props {
count: number,
setCount: (count: number) => void
} // 相信你们能够看懂ts
export default function MyButton(props: Props) {
const {count,setCount} = props;
function handleClick() {
setCount(count + 1);
console.log(count);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
这里的props 就是接收父组件传过来的东西,你需要事先在子组件内定义propS的类型(ts)
这样就实现了兄弟组件之间的数据共享
# 更新 state 中的对象和对象
const [position, setPosition] = useState({
x: 0,
y: 0
});
我们都知道,对于对象和数组来说,变量中存储的是这个对象或者数组的引用,当我们进行更改时需要特别注意,像下面这种写法就不能起到更新视图的效果
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<>
<div style={{cursor: "pointer"}} onClick={() => {
position.x = 100
position.y = 100
setPosition(position)
console.log(position)
}}>x:{position.x},y: {position.y}</div>
</>
)
可以发现除了最初加载此组件时触发的123之外setPosition()的调用显然没有起到重新加载组件的作用,因为react认为这个引用值没有发生改变,所以不需要重新加载,因此在这种情况下我们要换为下面这种写法
setPosition({
...position,
x: 100
}) //将x 变为100 ,y值不变
这种写法在只改变对象中的一个属性值时特别有用
同理在数组中相信你也能够理解
使用 ref 引用值
当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref 。
观察下面这些代码
import {useRef, useState} from 'react';
export default function Counter() {
const ref = useRef(0);
let times = 0;
const [f5,setF5] = useState(0);
function handleClick() {
ref.current = ref.current + 1;
times ++ ;
console.log('你点击了 ' + ref.current + ' 次!');
console.log(times);
setF5(f5 + 1);
}
return (
<button onClick={handleClick}>
点击我!
</button>
);
}
此处的f5 只是起到刷新此组件的作用
你会发现通过useRef()保管的值是不会因为刷新而重新变回初始值的,而普通的变量会因为组件的刷新,多次赋值为0,useRef 和 useState 的区别仅仅是,useRef 不会返回状态更新函数。
# 使用 ref 操作 DOM
由于 React 会自动处理更新 DOM 以匹配你的渲染输出,因此你在组件中通常不需要操作 DOM。但是,有时你可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个指向 DOM 节点的 ref 来实现。
const myRef = useRef(null);
return (
<>
<div ref={myRef}>
</>
)
// 你可以使用任意浏览器 API,例如:
myRef.current.scrollIntoView();
不过在使用之前你最好先判断myRef.current 是否为null或者undefinde,你可以理解为获取dom 需要一定的时间(
useRef Hook 返回一个对象,该对象有一个名为 current 的属性。最初,myRef.current 是 null。当 React 为这个 <div> 创建一个 DOM 节点时,React 会把对该节点的引用放入 myRef.current。然后,你可以从 事件处理器 访问此 DOM 节点,并使用在其上定义的内置浏览器 API。)
# 使用 Effect 同步
有些组件需要与外部系统同步。例如,你可能希望根据 React state 控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。Effects 会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。
不要随意在你的组件中使用 Effect。记住,Effect 通常用于暂时“跳出” React 代码并与一些 外部 系统进行同步。这包括浏览器 API、第三方小部件,以及网络等等。如果你想用 Effect 仅根据其他状态调整某些状态,那么 你可能不需要 Effect。
编写 Effect 需要遵循以下三个规则:
- 声明 Effect。默认情况下,Effect 会在每次渲染后都会执行。
- 指定 Effect 依赖。大多数 Effect 应该按需执行,而不是在每次渲染后都执行。例如,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。文章将介绍如何通过指定依赖来控制如何按需执行。
- 必要时添加清理(cleanup)函数。有时 Effect 需要指定如何停止、撤销,或者清除它的效果。例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”。你将学习如何使用 清理函数 来做到这一切。
function MyComponent() {
useEffect(() => {
// 每次渲染后都会执行此处的代码
});
return <div />;
}
每当你的组件渲染时,React 将更新屏幕,然后运行 useEffect 中的代码。换句话说,useEffect 会把这段代码放到屏幕更新渲染之后执行。
useEffect(() => {
// 这里的代码会在每次渲染后执行
});
useEffect(() => {
// 这里的代码只会在组件挂载后执行(且只执行一次(或者说两次))
}, []);
useEffect(() => {
//这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);
后面的这个数组是一个可选项
没有依赖数组作为第二个参数,与依赖数组位空数组 [] 的行为是不一致的:
- 与事件不同,Effect 是由渲染本身,而非特定交互引起的。
- Effect 允许你将组件与某些外部系统(第三方 API、网络等)同步。
- 默认情况下,Effect 在每次渲染(包括初始渲染)后运行。
- 如果 React 的所有依赖项都与上次渲染时的值相同,则将跳过本次 Effect。
- 不能随意选择依赖项,它们是由 Effect 内部的代码决定的。
- 空的依赖数组(
[])对应于组件“挂载”,即添加到屏幕上。 - 仅在严格模式下的开发环境中,React 会挂载两次组件,以对 Effect 进行压力测试。
- 如果 Effect 因为重新挂载而中断,那么需要实现一个清理函数。
- React 将在下次 Effect 运行之前以及卸载期间这两个时候调用清理函数。
useEffect 的内容很多也很关键,建议大家去翻翻官网再仔细看看,我写不完了
作业:没有作业,react这部分要学的很多,这课件实在是写不完,大家加油