响应事件
- 你可以通过将函数作为 prop 传递给元素如
<button>
来处理事件。 - 必须传递事件处理函数,而非函数调用!
onClick={handleClick}
,不是onClick={handleClick()}
。 - 你可以单独或者内联定义事件处理函数。
- 事件处理函数在组件内部定义,所以它们可以访问组件的 props。
- 你可以在父组件中定义一个事件处理函数,并将其作为 prop 传递给子组件。
- 你可以根据特定于应用程序的名称定义事件处理函数的 prop。
- 事件会向上传播。通过事件的第一个参数调用
e.stopPropagation()
来防止这种情况。 - 事件可能具有不需要的浏览器默认行为。调用
e.preventDefault()
来阻止这种情况。 - 从子组件显式调用事件处理函数 prop 是事件传播的另一种优秀替代方案。
State:组件的记忆
Hooks
在 React 中,useState
以及任何其他以“use
”开头的函数都被称为 Hook。Hook 是特殊的函数,只在 React 渲染时有效。
useState Hook
useState Hook 提供了两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
//使用方法 import { useState } from 'react'; export default function StatePractice(){ const [index, setIndex] = useState(0); function handleClick(){ setIndex(index+1); } return( <div> <button onClick={handleClick}>增加</button> <h1>{index}</h1> </div> ); }
State 是隔离且私有的
- State是隔离的,如果渲染相同的组件两次,每个副本都会有完全隔离的 state(即这两个组件不共享State)
- state 完全私有于声明它的组件,父组件无法访问和修改子组件的State
渲染和提交
请求和提供 UI 的过程总共包括三个步骤:
- 触发 一次渲染
- 渲染 组件
- 提交 到 DOM
触发渲染
有两种原因会导致组件的渲染:
- 组件的 初次渲染。通过调用目标 DOM 节点的 createRoot,然后用你的组件调用
render
函数完成的。import Image from './Image.js'; import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')) root.render(<Image />);
- 组件(或者其祖先之一)的 状态发生了改变,此时会重新渲染。React 在更新时会递归渲染组件。如果一个组件返回了另一个组件,React 会继续渲染那个组件,直到所有嵌套组件都被渲染完成,最终确定屏幕上要显示的内容。
举例:
export default function Gallery() {
return (
<section>
<h1>鼓舞人心的雕塑</h1>
<Image />
<Image />
<Image />
</section>
);
}
function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
初次渲染:React 创建 <section>
、<h1>
和三个 <img>
标签的 DOM 节点。
重新渲染:React 计算这些节点的属性自上次渲染以来的更改,但在提交阶段之前不会执行任何操作。
React 把更改提交到 DOM 上
- 对于初次渲染, React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
- 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
- React 仅在渲染之间存在差异时才会更改 DOM 节点。例如:
export default function Clock({ time }) { return ( <> <h1>{time}</h1> <input /> </> ); }
这个组件每时每刻都在重新渲染,但<input />标签内的文本保持不变。
state 如同一张快照
“快照”
“快照”的比喻与上一节所讲渲染的过程相同:
阶段名称 | 实际行为(React 内部) | 和“快照”的关系 |
---|---|---|
触发渲染 | 调用 setState() ,React 标记该组件需要更新 | 意味着准备拍一张新“照片” |
执行渲染 | 重新执行组件函数,生成新的 JSX(虚拟 DOM) | 实际“拍照”:组件函数执行一次,输出 JSX |
提交渲染 | React 对比新旧虚拟 DOM,更新真实 DOM,执行副作用(useEffect) | React 把这张“照片”展示到真实界面上 |
State是“组件的记忆”,它实际上存在于React本身,而不被函数限制;所有的“快照”(即当前的静态页面)是根据State的值来进行渲染的。
(个人理解:所谓“快照”,就如同一幅画布,首次渲染画出草稿,再画到画布上去;重新渲染则是画一份新的草稿,比对两份草稿的差异,然后用彩笔修改画布上一小部分。
-
草稿是新的 JSX(React内部的虚拟DOM),它只是页面的描述;
-
画布是实际的 DOM,它展示了当前的 UI。
-
React 每次渲染时都会画一份新的草稿,并根据差异修改画布中需要更新的部分。
)
状态更新是异步的(设置 state 不会立即生效)
设置 state
只会影响下一次渲染的状态值,而不会立即改变当前代码执行过程中读取到的 state 值。第一个例子
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>
</>
)
}
在第一次渲染时,number
的值是 0。所以即使在点击事件处理函数中调用了三次 setNumber(number + 1)
,每次number
的值仍然是 0,
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
直到下一次渲染时,state
才会被更新。
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。第二个例子
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
这里虽然等待了3s,然而number仍然是0。
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
类比上一个例子,state在同一次渲染中并没有变化。
把一系列 state 更新加入队列
为什么要“排队”进行state 更新?
React 出于性能考虑会批量处理多个 state 更新(对于state的更新被放入队列,依次进行)。这意味着:
-
React 会等待事件处理函数执行完毕后再去更新 DOM。
-
多次调用
setState
时,React 不会立刻渲染,而是将更新任务排进队列,等时机合适一起更新。
在下次渲染前多次更新同一个 state
如果想在下次渲染之前多次更新同一个 state,可以使用更新函数。这种写法被称为“函数式更新”的写法:
setCount(c => c + 1);
setCount(c => c + 1);
传入 n => n + 1
,不是立即更新,而是:
-
放进队列,等待当前函数内事件函数执行完;
-
下一次渲染前,React 统一处理队列;
-
每个更新函数用上一次的结果计算下一个,得出最终的 state。