主要是围绕函数式组件讲,18主要用就是函数式组件,学习前先熟悉下原生js的基本使用,主要是事件
1、UI操作
1.1、书写jsx标签语言
基本写法和原生如同一则,只是放在一个方法里面返回而已,我们称这样的写法为函数式组件,组件命名方式为:xxxx.jsx/xxx.tsx,当然也可以不要后面的x
export default function TodoList() {return (<><h1>海蒂·拉玛的待办事项</h1><img src="https://i.imgur.com/yXOvdOSs.jpg" alt="Hedy Lamarr" className="photo" /><ul><li>发明一种新式交通信号灯</li><li>排练一个电影场景</li><li>改进频谱技术</li></ul></>);
}
1.2、属性和值的插入
1.2.1、大括号和双大括号插入的使用
使用大括号在渲染的结构上面插入新的结构
export default function TodoList() {const name = 'Gregorio Y. Zara';return (<h1>{name}'s To Do List</h1>);
}
使用双大括号插入元素属性
export default function TodoList() {return (<ul style={{backgroundColor: 'black',color: 'pink'}}><li>Improve the videophone</li><li>Prepare aeronautics lectures</li><li>Work on the alcohol-fuelled engine</li></ul>);
}
1.3、props传递
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。Props 可能会让你想起 HTML 属性,但你可以通过它们传递任何 JavaScript 值,包括对象、数组和函数。
其实跟vue的使用差不多:
子组件
function Avatar({ person, size }) {return (<imgclassName="avatar"alt={person.name}width={size}height={size}/>);
}父组件:export default function Profile() {return (<div><Avatarsize={100}person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2'}}/><Avatarsize={80}person={{name: 'Aklilu Lemma', imageId: 'OKS67lh'}}/><Avatarsize={50}person={{ name: 'Lin Lanying',imageId: '1bX5QH6'}}/></div>);
}
1.4、条件渲染
这里使用跟vue的v-if完全不一样,是基于原生的js中的if来操作的,结合if判断渲染组件的,这是本质区别。,这里比较灵活,当时同样可以使用三元表达式也是可以的,根据具体开发而定。
function Item({ name, isPacked }) {let itemContent = name;if (isPacked) {itemContent = (<del>{name + " ✔"}</del>);}return (<li className="item">{itemContent}</li>);
}export default function PackingList() {return (<section><h1>Sally Ride 的行李清单</h1><ul><Item isPacked={true} name="宇航服" /><Item isPacked={true} name="带金箔的头盔" /><Item isPacked={false} name="Tam 的照片" /></ul></section>);
}
1.5、渲染列表
列表渲染会用到循环,当然啦,并不是vue中的那种v-for循环,是用的结合原生的循环方法,比如map,用来循环数据,记住,必须要赋予key,不然后台会提示错误!!
为什么需要key,这里不过多讲,学过vue的就知道不给key会出现意想不到的bug,而且key必须唯一。
key 需要满足的条件
- key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。
- key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。
渲染的列表:
import { people } from './data.js';export default function List() {const listItems = people.map(person =><li key={person.id}><imgsrc=''alt={person.name}/><p><b>{person.name}</b>{' ' + person.profession + ' '}因{person.accomplishment}而闻名世界</p></li>);return <ul>{listItems}</ul>;
}
渲染的数据:
export const people = [{id: 0, // 在 JSX 中作为 key 使用name: '凯瑟琳·约翰逊',profession: '数学家',accomplishment: '太空飞行相关数值的核算',imageId: 'MK3eW3A',},{id: 1, // 在 JSX 中作为 key 使用name: '马里奥·莫利纳',profession: '化学家',accomplishment: '北极臭氧空洞的发现',imageId: 'mynHUSa',},{id: 2, // 在 JSX 中作为 key 使用name: '穆罕默德·阿卜杜勒·萨拉姆',profession: '物理学家',accomplishment: '关于基本粒子间弱相互作用和电磁相互作用的统一理论',imageId: 'bE7W1ji',},{id: 3, // 在 JSX 中作为 key 使用name: '珀西·莱温·朱利亚',profession: '化学家',accomplishment: '开创性的可的松药物、类固醇和避孕药',imageId: 'IOjWm71',},{id: 4, // 在 JSX 中作为 key 使用name: '苏布拉马尼扬·钱德拉塞卡',profession: '天体物理学家',accomplishment: '白矮星质量计算',imageId: 'lrWQx8l',},
];
1.6、渲染树
这个本不想多讲,这里过一下吧,不管是原生还是vue都有dom树结构,react也不例外,每一个组件都可以视为一个节点(从App出发-根节点):
2、交互
2.1、响应事件
其实跟原生的dom操作有点像,不管是dom操作还是事件都可以直接使用的。
给一个简单案例:
export default function Toolbar() {return (<div className="Toolbar" onClick={() => {alert('你点击了 toolbar !');}}><button onClick={() => alert('正在播放!')}>播放电影</button><button onClick={() => alert('正在上传!')}>上传图片</button></div>);
}
2.2、hook的使用
注意:
Hooks ——以
use
开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。
2.2.1、useState的使用
剖析 useState
当你调用 useState 时,你是在告诉 React 你想让这个组件记住一些东西:
const [index, setIndex] = useState(0);
在这个例子里,你希望 React 记住 index
。
useState
的唯一参数是 state 变量的初始值。在这个例子中,index
的初始值被useState(0)
设置为 0
。
每次你的组件渲染时,useState
都会给你一个包含两个值的数组:
- state 变量 (
index
) 会保存上次渲染的值。 - state setter 函数 (
setIndex
) 可以更新 state 变量并触发 React 重新渲染组件。
以下是实际发生的情况:
const [index, setIndex] = useState(0);
- 组件进行第一次渲染。 因为你将
0
作为index
的初始值传递给useState
,它将返回[0, setIndex]
。 React 记住0
是最新的 state 值。 - 你更新了 state。当用户点击按钮时,它会调用
setIndex(index + 1)
。index
是0
,所以它是setIndex(1)
。这告诉 React 现在记住index
是1
并触发下一次渲染。 - 组件进行第二次渲染。React 仍然看到
useState(0)
,但是因为 React 记住 了你将index
设置为了1
,它将返回[1, setIndex]
。 - 以此类推!
案例1:
import { useState } from 'react';
import { sculptureList } from './data.js';export default function Gallery() {const [index, setIndex] = useState(0);const [showMore, setShowMore] = useState(false);function handleNextClick() {setIndex(index + 1);}function handleMoreClick() {setShowMore(!showMore);}let sculpture = sculptureList[index];return (<><button onClick={handleNextClick}>Next</button><h2><i>{sculpture.name} </i> by {sculpture.artist}</h2><h3> ({index + 1} of {sculptureList.length})</h3><button onClick={handleMoreClick}>{showMore ? 'Hide' : 'Show'} details</button>{showMore && <p>{sculpture.description}</p>}<img src={sculpture.url} alt={sculpture.alt}/></>);
}
案例2:
import { useState } from 'react';
export default function MovingDot() {const [position, setPosition] = useState({x: 0,y: 0});return (<divonPointerMove={e => {setPosition({x: e.clientX,y: e.clientY});}}style={{position: 'relative',width: '100vw',height: '100vh',}}><div style={{position: 'absolute',backgroundColor: 'red',borderRadius: '50%',transform: `translate(${position.x}px, ${position.y}px)`,left: -10,top: -10,width: 20,height: 20,}} /></div>);
}
3、状态管理
3.1、useReducer的使用
对比 useState
和 useReducer
Reducers 并非没有缺点!以下是比较它们的几种方法:
- 代码体积: 通常,在使用
useState
时,一开始只需要编写少量代码。而useReducer
必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer
可以减少代码量。 - 可读性: 当状态更新逻辑足够简单时,
useState
的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer
允许你将状态更新逻辑与事件处理程序分离开来。 - 可调试性: 当使用
useState
出现问题时, 你很难发现具体原因以及为什么。 而使用useReducer
时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个action
)。 如果所有action
都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用useState
相比,你必须单步执行更多的代码。 - 可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和
action
,断言 reducer 返回的特定状态会很有帮助。 - 个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在
useState
和useReducer
之间切换,它们能做的事情是一样的!
如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useState
和 useReducer
。
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.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} /><TaskListtasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);
}let nextId = 3;
const initialTasks = [{id: 0, text: '参观卡夫卡博物馆', done: true},{id: 1, text: '看木偶戏', done: false},{id: 2, text: '打卡列侬墙', done: false},
];export default 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);}}
}
3.2、Context的使用
app.jsx
import Heading from './Heading.js';
import Section from './Section.js';export default function Page() {return (<Section><Heading>主标题</Heading><Section><Heading>副标题</Heading><Heading>副标题</Heading><Heading>副标题</Heading><Section><Heading>子标题</Heading><Heading>子标题</Heading><Heading>子标题</Heading><Section><Heading>子子标题</Heading><Heading>子子标题</Heading><Heading>子子标题</Heading></Section></Section></Section></Section>);
}
sections.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';export default function Section({ children }) {const level = useContext(LevelContext);return (<section className="section"><LevelContext.Provider value={level + 1}>{children}</LevelContext.Provider></section>);
}
heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';export default function Heading({ children }) {const level = useContext(LevelContext);switch (level) {case 0:throw Error('Heading 必须在 Section 内部!');case 1:return <h1>{children}</h1>;case 2:return <h2>{children}</h2>;case 3:return <h3>{children}</h3>;case 4:return <h4>{children}</h4>;case 5:return <h5>{children}</h5>;case 6:return <h6>{children}</h6>;default:throw Error('未知的 level:' + level);}
}
LevelContext.js
import { createContext } from 'react';export const LevelContext = createContext(0);
写在你使用 context 之前
使用 Context 看起来非常诱人!然而,这也意味着它也太容易被过度使用了。如果你只想把一些 props 传递到多个层级中,这并不意味着你需要把这些信息放到 context 里。
在使用 context 之前,你可以考虑以下几种替代方案:
- 从 传递 props 开始。 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。
- 抽象组件并 将 JSX 作为 children 传递 给它们。 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。举个例子,你可能想传递一些像
posts
的数据 props 到不会直接使用这个参数的组件,类似<Layout posts={posts} />
。取而代之的是,让Layout
把children
当做一个参数,然后渲染<Layout><Posts posts={posts} /></Layout>
。这样就减少了定义数据的组件和使用数据的组件之间的层级。
如果这两种方法都不适合你,再考虑使用 context。
Context 的使用场景
- 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
- 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
- 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
- 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。
Context 不局限于静态值。如果你在下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。
一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。
3.3、使用 Reducer 和 Context 拓展你的应用
Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。
这点不做详细介绍,参考官网:
使用 Reducer 和 Context 拓展你的应用 – React 中文文档
4、拖围机制
有些组件可能需要控制和同步 React 之外的系统。例如,你可能需要使用浏览器 API 聚焦输入框,或者在没有 React 的情况下实现视频播放器,或者连接并监听远程服务器的消息。在本章中,你将学习到一些脱围机制,让你可以“走出” React 并连接到外部系统。大多数应用逻辑和数据流不应该依赖这些功能。
4.1、ref的使用
当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref:
const ref = useRef(0);
与 state 一样,ref 在重新渲染之间由 React 保留。但是,设置 state 会重新渲染组件,而更改 ref 不会!你可以通过 ref.current
属性访问该 ref 的当前值。
import { useRef } from 'react';export default function Counter() {let ref = useRef(0);function handleClick() {ref.current = ref.current + 1;alert('你点击了 ' + ref.current + ' 次!');}return (<button onClick={handleClick}>点我!</button>);
}
ref 就像组件的一个不被 React 追踪的秘密口袋。例如,可以使用 ref 来存储 timeout ID、DOM 元素 和其他不影响组件渲染输出的对象。
4.1.1、使用 ref 操作 DOM
由于 React 会自动更新 DOM 以匹配渲染输出,因此组件通常不需要操作 DOM。但是,有时可能需要访问由 React 管理的 DOM 元素——例如聚焦节点、滚动到此节点,以及测量它的尺寸和位置。React 没有内置的方法来执行此类操作,所以需要一个指向 DOM 节点的 ref 来实现。例如,点击按钮将使用 ref 聚焦输入框:
import { useRef } from 'react';export default function Form() {const inputRef = useRef(null);function handleClick() {inputRef.current.focus();}return (<><input ref={inputRef} /><button onClick={handleClick}>聚焦输入框</button></>);
}
4.2、 Effect的使用
有些组件需要与外部系统同步。例如,可能需要根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。与处理特定事件的事件处理程序不同,Effect 在渲染后运行一些代码。使用它将组件与 React 之外的系统同步。
多按几次播放/暂停,观察视频播放器如何与 isPlaying
属性值保持同步:
import { useState, useRef, useEffect } from 'react';function VideoPlayer({ src, isPlaying }) {const ref = useRef(null);useEffect(() => {if (isPlaying) {ref.current.play();} else {ref.current.pause();}}, [isPlaying]);return <video ref={ref} src={src} loop playsInline />;
}export default function App() {const [isPlaying, setIsPlaying] = useState(false);return (<><button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? '暂停' : '播放'}</button><VideoPlayerisPlaying={isPlaying}src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"/></>);
}
许多 Effect 也会自行“清理”。例如,与聊天服务器建立连接的 Effect 应该返回一个 cleanup 函数,告诉 React 如何断开组件与该服务器的连接:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';export default function ChatRoom() {useEffect(() => {const connection = createConnection();connection.connect();return () => connection.disconnect();}, []);return <h1>欢迎前来聊天!</h1>;
}export function createConnection() {// 真正的实现实际上会连接到服务器return {connect() {console.log('✅ 连接中...');},disconnect() {console.log('❌ 断开连接。');}};
}
在开发环境中,React 将立即运行并额外清理一次 Effect。这就是为什么你会看到 "✅ 连接中..."
打印了两次。这能够确保你不会忘记实现清理功能。
4.3、响应式 Effect 的生命周期
Effect 的生命周期不同于组件。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某些东西,然后停止同步它。如果 Effect 依赖于随时间变化的 props 和 state,这个循环可能会发生多次。
这个 Effect 依赖于 roomId
props 的值。props 是 响应值,这意味着它们可以在重新渲染时改变。注意,如果 roomId
更改,Effect 将会 重新同步(并重新连接到服务器):
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';const serverUrl = 'https://localhost:1234';function ChatRoom({ roomId }) {useEffect(() => {const connection = createConnection(serverUrl, roomId);connection.connect();return () => connection.disconnect();}, [roomId]);return <h1>欢迎来到 {roomId} 房间!</h1>;
}export default function App() {const [roomId, setRoomId] = useState('general');return (<><label>选择聊天室:{' '}<selectvalue={roomId}onChange={e => setRoomId(e.target.value)}><option value="general">所有</option><option value="travel">旅游</option><option value="music">音乐</option></select></label><hr /><ChatRoom roomId={roomId} /></>);
}export function createConnection(serverUrl, roomId) {// 真正的实现实际上会连接到服务器return {connect() {console.log('✅ 连接到 "' + roomId + '" 房间,在' + serverUrl + '...');},disconnect() {console.log('❌ 断开 "' + roomId + '" 房间,在' + serverUrl);}};
}
4.4、使用自定义 Hook 复用逻辑
React 有一些内置 Hook,例如 useState
,useContext
和 useEffect
。有时需要用途更特殊的 Hook:例如获取数据,记录用户是否在线或者连接聊天室。为了实现效果,可以根据应用需求创建自己的 Hook。
这个示例中,自定义 Hook usePointerPosition
追踪当前指针位置,而自定义 Hook useDelayedValue
返回一个“滞后”传递的值一定毫秒数的值。将光标移到沙盒预览区域上以查看跟随光标移动的点轨迹:
import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';export default function Canvas() {const pos1 = usePointerPosition();const pos2 = useDelayedValue(pos1, 100);const pos3 = useDelayedValue(pos2, 200);const pos4 = useDelayedValue(pos3, 100);const pos5 = useDelayedValue(pos4, 50);return (<><Dot position={pos1} opacity={1} /><Dot position={pos2} opacity={0.8} /><Dot position={pos3} opacity={0.6} /><Dot position={pos4} opacity={0.4} /><Dot position={pos5} opacity={0.2} /></>);
}function Dot({ position, opacity }) {return (<div style={{position: 'absolute',backgroundColor: 'pink',borderRadius: '50%',opacity,transform: `translate(${position.x}px, ${position.y}px)`,pointerEvents: 'none',left: -20,top: -20,width: 40,height: 40,}} />);
}import { useState, useEffect } from 'react';export function usePointerPosition() {const [position, setPosition] = useState({ x: 0, y: 0 });useEffect(() => {function handleMove(e) {setPosition({ x: e.clientX, y: e.clientY });}window.addEventListener('pointermove', handleMove);return () => window.removeEventListener('pointermove', handleMove);}, []);return position;
}import { useState, useEffect } from 'react';export function useDelayedValue(value, delay) {const [delayedValue, setDelayedValue] = useState(value);useEffect(() => {setTimeout(() => {setDelayedValue(value);}, delay);}, [value, delay]);return delayedValue;
}
你可以创建自定义 Hooks,将它们组合在一起,在它们之间传递数据,并在组件之间重用它们。随着应用不断变大,你将减少手动编写的 Effect,因为你将能够重用已经编写的自定义 Hooks。React 社区也维护了许多优秀的自定义 Hooks。