描述UI
组件导入与导出
语法 | 导出语句 | 导入语句 |
---|---|---|
默认 | export default function Button() {} | import Button from ‘./Button.js’; |
具名 | export function Button() {} | import { Button } from ‘./Button.js’; |
props传递组件
-
在 React 组件中,children 是一个特殊的 prop(属性),用来表示那些被传递到组件的子元素。当一个组件在使用时包裹了其他的内容(无论是文本、HTML、或其他组件),这些被包裹的内容就会作为 children prop 传递给该组件。
-
例如,对于下面的 AlertButton 组件,使用 children 允许你自定义按钮内显示的内容。这里的 children 使得 AlertButton 组件非常灵活,因为你可以在其中嵌入任何 React 元素或组件,而这些嵌入的内容将显示在按钮内部。看一个例子:
function AlertButton({ message, children }) { return ( <button onClick={() => alert(message)}> {children} </button> ); } <AlertButton message="Hello World!"> Click me! </AlertButton>
渲染列表的小技巧
- 使用map()函数
- 使用filter()函数
保证组件的纯粹性
- 一个组件必须是纯粹的,就意味着:
- 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
- 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX。
- 渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
- 你不应该改变任何用于组件渲染的输入。这包括 props、state 和 context。通过 “设置” state 来更新界面,而不要改变预先存在的对象。
- 努力在你返回的 JSX 中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect。
添加交互
state与渲染
- Hook 是特殊的函数,只在 React 渲染时有效。它们能让你 “hook” 到不同的 React 特性中去。
Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。 Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块
- 理解react的触发、渲染、提交流程
渲染提交流程
- 触发第一次渲染:
- 组件的第一次渲染
- 组件状态发生改变时
- React渲染你的组件:在你触发渲染后,React 会调用你的组件来确定要在屏幕上显示的内容。“渲染中” 即 React 在调用你的组件。
- React 把更改提交到 DOM 上
对于State的理解
- 一张快照,注意下面这个例子更能说明一切,下面点击按钮之后,虽然看起来调用了三次set,但是实际上,只会加1
setNumber(number + 1):number 是 0 所以 setNumber(0 + 1)。尽管你调用了三次 setNumber(number + 1),但在 这次渲染的 事件处理函数中 number 会一直是 0,所以你会三次将 state 设置成 1。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number 等于 1 而不是 3。export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}>+3</button> </> ) }
- **一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。**它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。
如何对state进行多次操作
-
这里的比喻是这样的:餐厅里帮你点菜的服务员。服务员不会在你说第一道菜的时候就跑到厨房!相反,他们会让你把菜点完,让你修改菜品,甚至会帮桌上的其他人点菜。
-
当然也可以同时更新同一个State:
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> ) }
在这里
n => n + 1
被称为 更新函数。- React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理
- 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
下面是 React 在执行事件处理函数时处理这几行代码的过程:
setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。 这一操作完成了三次
当你在下次渲染期间调用 useState 时,React 会遍历队列。之前的
number
的 state 的值是 0,所以这就是 React 作为参数 n 传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为 n 传递给下一个更新函数,以此类推:更新队列 n
返回值 n => n + 1 0 0+1=1 n => n + 1 1 1+1=2 n => n + 1 2 2+1=3 因此最终返回值为
n=3
-
如果是下面的处理逻辑呢?
<button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}>
会增加到6,因为
setNumber(number + 5)
:number
为 0,所以setNumber(0 + 5)
。React 将 “替换为 5” 添加到其队列中。setNumber(n => n + 1)
:n => n + 1
是一个更新函数。 React 将 该函数 添加到其队列中。
或者你可以把setState(x)
理解为setState(n => x)
一样运行,只是没有使用 n!
更新State中的对象
- 对于所有state的值应该都被视为是只读的
- 比如更新鼠标位置
当然使用export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); ... <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} > }
...
对象展开语法可以只改变对象中的一个值 - 更新嵌套对象:
如果使用mutation的方式直接,const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } });
person.artwork.city = 'New Delhi';
即可,但是在react中为了修改 city 的值,你首先需要创建一个新的 artwork 对象(其中预先填充了上一个 artwork 对象中的数据),然后创建一个新的 person 对象,并使得其中的 artwork 属性指向新创建的 artwork 对象:const nextArtwork = { ...person.artwork, city: 'New Delhi' }; const nextPerson = { ...person, artwork: nextArtwork }; setPerson(nextPerson);
更新State中的数组
- 当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
- 这意味着你不应该使用类似于
arr[0] = 'bird'
这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如push()
和pop()
。
避免使用 (会改变原始数组) 推荐使用 (会返回一个新数组)避免使用 推荐使用 添加元素 push,unshift concat,[…arr] 展开语法 删除元素 pop,shift,splice filter,slice 替换元素 splice,arr[i] = … 赋值 map 排序 reverse,sort 先将数组复制一份
状态管理
用State响应输入
- 命令式控制UI与声明式控制UI,学会用状态机的方式控制
在组件之间共享状态
- 主要是通过Props进行传递
对State进行保留与重置
相同位置的相同组件的state会被保留
- 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置! 这个组件在 if 内外有两个return 语句,它们带有不同的 JSX 标签:
你可能以为当你勾选复选框的时候 state 会被重置,但它并没有!这是因为 两个 标签被渲染在了相同的位置。 React 不知道你的函数里是如何进行条件判断的,它只会“看到”你返回的树。在这两种情况下,App 组件都会返回一个包裹着 作为第一个子组件的 div。这就是 React 认为它们是 同一个 的原因。import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); if (isFancy) { return ( <div> <Counter isFancy={true} /> <label> <input type="checkbox" checked={isFancy} onChange={e => {setIsFancy(e.target.checked) }}/> 使用好看的样式 </label> </div> ); } return ( <div> <Counter isFancy={false} /> <label> <input type="checkbox" checked={isFancy} onChange={e => {setIsFancy(e.target.checked)}}/> 使用好看的样式 </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) {className += ' hover'; } if (isFancy) { className += ' fancy';} return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> 加一 </button> </div> ); }
同样这里如果换成,也可以成立
但是如果换成,下述两种就没办法保留state<div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> 使用好看的样式 </label> </div>
{isPaused ? ( <p>待会见!</p> ) : ( <Counter /> )} ---------------------- {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )}
如何重置相同位置的State
- 将组件渲染在不同的位置 ,呃
- 使用 key 来重置 state ,key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序(“第一个计数器”、“第二个计数器”)来区分组件。
脱围机制
使用ref引用值
-
ref本质上就是个对象,当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref 。
ref state useRef(initialValue)
返回{ current: initialValue }
useState(initialValue)
返回 state 变量的当前值和一个 state 设置函数 ([value, setValue]
)更改时不会触发重新渲染 更改时触发重新渲染。 可变 —— 你可以在渲染过程之外修改和更新 current 的值。 “不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。 你不应在渲染期间读取(或写入) current 值。 你可以随时读取 state。但是,每次渲染都有自己不变的 state。 -
最简单的例子就是计数器,注意不要在渲染过程中读取或者写入ref.current,就像下面所示那样
// 会引起更新 const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> 你点击了 {count} 次 </button> ); -------------------------------------- // 不会引起更新 let countRef = useRef(0); function handleClick() { // 这样并未重新渲染组件! countRef.current = countRef.current + 1; console.log(countRef.current); //这个值会增加 } return ( <button onClick={handleClick}> 你点击了 {countRef.current} 次 </button> );
-
何时需要使用Ref:通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:
- 存储 timeout ID(就是SetInterval)
- 存储和操作 DOM 元素
- 存储不需要被用来计算 JSX 的其他对象。
Ref操作DOM
- 注意ref只能操作自己组件的DOM,不允许操作子组件,如果想要操作子组件,则需要用props去传递,比如
const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; });
使用Effect进行同步
在谈到 Effect 之前,你需要熟悉 React 组件中的两种逻辑类型:
-
渲染逻辑代码(在 描述 UI 中有介绍)位于组件的顶层。你将在这里接收 props 和 state,并对它们进行转换,最终返回你想在屏幕上看到的 JSX。渲染的代码必须是纯粹的——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。
-
事件处理程序(在 添加交互性 中介绍)是嵌套在组件内部的函数,而不仅仅是计算函数。事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品,或者将用户导航到另一个屏幕。事件处理程序包含由特定用户操作(例如按钮点击或键入)引起的“副作用”(它们改变了程序的状态)。
而上述这些可能在功能实现上不能满足所有的需求,这里举了个例子,就是比如有一个chatroom的app,当他显示在屏幕上时,就应该连接上了服务器。那么这一个动作(连接上服务器)不是一个主动事件(点击按钮),他是一个被动过程。
于是乎,Effect 允许你指定由渲染本身(当chatroom被渲染到屏幕上时产生的连接服务器这一副作用),而不是特定事件引起的副作用。
side effect是一个程序设计中的属于,一般翻译为副作用,指的是如果一个函数修改了自己范围之外的资源,那就叫做有副作用,反之,就是没有副作用。
在聊天中发送消息是一个“事件”,因为它直接由用户点击特定按钮引起。然而,建立服务器连接是 Side Effect。因为它应该在组件出现时始终发生,无论是由哪种交互引起的。(because it should happen no matter which interaction caused the component to appear) 这是一个很好的时机,可以将 React 组件与某个外部系统(如网络或第三方库)同步。(原文档什么奇怪的翻译
如何编写Effect
- 声明 Effect。默认情况下,Effect 会在每次渲染后都会执行。
- 指定 Effect 依赖。大多数 Effect 应该按需执行,而不是在每次渲染后都执行。例如,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。
- 必要时添加清理(cleanup)函数。有时 Effect 需要指定如何停止、撤销,或者清除它的效果。例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”。你将学习如何使用 清理函数 来做到这一切。
一个例子
step1:声明Effect
- 下述代码是有问题的,问题出在,这段代码中的
ref.current.play()
和ref.current.pause()
是在函数组件的主体内(即组件函数的顶层)直接调用的。在 React 中,函数组件的主体部分在组件渲染期间被执行,因此这些代码也就是在渲染期间被执行。因此,每次重新渲染时,都会调用 ·ref.current.play()· 或· ref.current.pause()·,这样就会在渲染阶段尝试执行 DOM 操作,从而导致错误。import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); // 渲染期间不能调用 `play()`。 } else { ref.current.pause(); // 同样,调用 `pause()` 也不行。 } return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
- 解决方法就是:把调用 DOM 方法的操作封装在 Effect 中,你可以让 React 先更新屏幕,确定相关 DOM 创建好了以后然后再运行 Effect。
当 VideoPlayer 组件渲染时(无论是否为首次渲染),都会发生以下事情。首先,React 会刷新屏幕,确保<video>
元素已经正确地出现在 DOM 中;然后,React 将运行 Effect;最后,Effect 将根据isPlaying
的值调用play()
或pause()
。useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } });
- 当然上述问题还有一个解法,就是把对ref的操作放到一个回调函数中,在触发阶段去更改是否播放,首先第一次渲染时,不会调用
handleClick
,之后改变播放暂停状态也是在触发阶段进行的import { useState, useRef } from 'react'; export default function VideoPlayer() { const [isPlaying, setIsPlaying] = useState(false); const ref = useRef(null); function handleClick() { const nextIsPlaying = !isPlaying; setIsPlaying(nextIsPlaying); if (nextIsPlaying) { ref.current.play(); } else { ref.current.pause(); } } return ( <> <button onClick={handleClick}> {isPlaying ? '暂停' : '播放'} </button> <video width="250" ref={ref} onPlay={() => setIsPlaying(true)} onPause={() => setIsPlaying(false)}> <source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4" /> </video> </> ) }
step2:指定Effect的依赖
- 一般来说,Effect 会在 每次 渲染时执行。但更多时候,并不需要每次渲染的时候都执行 Effect。再回到上面的代码,我们增加了一个input,由于
const [text, setText] = useState('');
的存在,如果不指定依赖,每次输入一个文字就会执行一次useEffect中的逻辑,显然时不行的,因此需要放入一个[依赖项],这里依赖的时isPlayingimport { 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); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
- 依赖项参数的区别
useEffect(() => { // 这里的代码会在每次渲染后执行 }); useEffect(() => { // 这里的代码只会在组件挂载后执行 }, []); useEffect(() => { //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行 }, [a, b]);
- 为什么依赖项省略了ref,这是因为 ref 具有 稳定 的标识:React 保证 每轮渲染中调用 useRef 所产生的引用对象时,获取到的对象引用总是相同的,即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。当然实际上你也可以添加ref进去,但是因为他是稳定的,所以没什么影响
step3:按需添加清理函数
- 注意在开发环境下,react会挂在两次组件,以帮你找到一些错误,比如示例中控制台实际打印 “✅ 连接中……” 了两次。
- 我们可以在return中进行cleanup