前言:
本人使用vue已有三年,现来记录学习一下React。参考React官网学习
!!!官方useMemo及其它Hook地址: useMemo
二者区别:
1、React是用JSX语法编写,主要是用JS语言,而vue则是用自己特有的模板语法;
2、Vue和React设计理念上有所区别,Vue使用的是可变数据,而React强调数据的不可变,两者没有好坏之分,Vue更加简单,而React构建大型应用的时候更好。Vue2.0 通过Object.defineproperty()方法的getter/setter属性, 实现数据劫持,vue3是用Proxy实现数据响应式。React默认是通过shouldComponentUpdata生命周期来决定是否需要渲染更新, 再触发它的diff算法(比较引用)如果不优化可能导致大量不必要的VDOM的重新渲染;
3、Vue是双向数据绑定,React则是单向数据流。React中通过将state(Model层)与View层数据进行双向绑定达数据的实时更新变化,
具体来说就是在View层直接写JS代码Model层中的数据拿过来渲染,一旦像表单操作、触发事件、ajax请求等触发数据变化,则进行双同步。
推崇结合immutable来实现数据不可变。
React在setState之后会重新走渲染的流程,如果shouldComponentUpdate返回的是true,就继续渲染, 如果返回了false,就不会重新渲染,PureComponent就是重写了shouldComponentUpdate,然后在里面作了props和state的浅层对比;
4、组件通信:Vue用Prors/emit实现父子之间的通信。React父组件通过Props向子组件传递数据;父组件向子组件传递函数,然后子组件中调用这些函数,利用回调函数实现子传父的数据传递。
5、Vuex和Redux的区别:从表面上来说, store 注入和使用方式有一些区别 。在 Vuex 中, $store 被直接注入到了组件实例中 ,因此可以比较灵活的使用:
- 使用dispatch和commit提交更新
- 通过mapState或者直接通过this.$store来读取数据
在 Redux 中, 我们每一个组件都需要显示的用 connect 把需要的 props 和 dispatch 连接起来。 另外 Vuex 更加灵活一些, 组件中既可以 dispatch action 也可以 commit updates ,而 Redux 中只能进行 dispatch,并不能直接调用 reducer 进行修改。
从实现原理上来说,最大的区别是两点:
- Redux 使用的是不可变数据,而Vuex的数据是可变的。 Redux每次都是用新的state替换旧的state,而Vuex是直接修改
- Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的(如果看Vuex源码会知道,其实他内部直接创建一个Vue实例用来跟踪数据变化)
总结而言:vue简单、快速、强大、对模块友好,但不支持IE8。React速度快、跨浏览器兼容、模块化;但学习曲线陡峭,需要深入的知识来构建应用程序,React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
正文:
注意:JSX 虽然看起来很像 HTML,但在底层其实被转化为了JavaScript 对象。不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。如下return中必须有一个div将其包裹起来,也可以是空标签<></>
/**
*下方代码就是一段简单的React
**/
export default function App() {
return (
<div>
<h2>React</h2>
<p>开始学习</p>
</div>
);
}
//或
export default function App() {
return (
<>
<h2>React</h2>
<p>开始学习</p>
</>
);
}
使用组件时禁止嵌套的写法,如下不能将Profile放到Gallery中,如需使用组件的数据,请使用Props传递。
//正确的
function Profile() {
return (
<p>开始学习</p>
);
}
export default function Gallery() {
return (
<div>
<h1>React</h1>
<Profile />
</div>
);
}
//错误的
export default function Gallery() {
function Profile() {
return (
<p>开始学习</p>
);
}
return (
<div>
<h1>React</h1>
<Profile />
</div>
);
}
在JSX中通过大括号使用JavaScript
const name = "张三"
export default function Avatar() {
return (
<h1>{name}</h1>
);
}
通过双大括号写CSS和使用对象
const name = "张三"
export default function Avatar() {
return (
<h1 style={{color:red,width:50px}}>{name}</h1>
);
}
通过Props传递数据
function Profile({title,age}) { //注意接收数据时需要用大括号
return (
<p>{title.name + age}</p>
);
}
export default function Gallery() {
return (
<div>
<h1>React</h1>
<Profile title={{name:"张三",text:"文本"}} age={24}/>
</div>
);
}
还可以用map,if等方法快速并符合要求的写JSX
提示:部分 JavaScript 函数是 纯粹 的,这类函数通常被称为纯函数。纯函数仅执行计算操作,不做其他操作。可以通过将组件按纯函数严格编写,以避免一些随着代码库的增长而出现的、令人困扰的 bug 以及不可预测的行为。
添加交互
简单的点击事件
export default function App() {
return (
<Toolbar
onPlayMovie={() => alert('Playing!')}
/>
);
}
function Toolbar({ onPlayMovie, onUploadImage }) {
return (
<div>
<button onClick={onPlayMovie}>
Play Movie
</button>
</div>
);
}
传递给事件处理函数的函数应直接传递,而非调用。例如:
| 传递一个函数(正确) | 调用一个函数(错误) |
|---|---|
<button onClick={handleClick}> | <button onClick={handleClick()}> |
区别很微妙。在第一个示例中,handleClick 函数作为 onClick 事件处理函数传递。这会让 React 记住它,并且只在用户点击按钮时调用你的函数。
在第二个示例中,handleClick() 中最后的 () 会在 渲染 过程中 立即 触发函数,即使没有任何点击。这是因为在 JSX { 和 } 之间的 JavaScript 会立即执行。
当你编写内联代码时,同样的陷阱可能会以不同的方式出现:
| 传递一个函数(正确) | 调用一个函数(错误) |
|---|---|
<button onClick={() => alert('...')}> | <button onClick={alert('...')}> |
useState Hook
useState Hook 为组件添加状态。Hook 是能让组件使用 React 功能的特殊函数(状态是这些功能之一)。useState Hook 声明一个状态变量。接收初始状态并返回一对值:当前状态,以及一个让你更新状态的设置函数。用法如下:
import { useState } from 'react';
export default function Gallery() {
//下方定义了一个showMore变量,初始值为false
//定义一个setShowMore函数,可以更新showMore变量
const [showMore, setShowMore] = useState(false);
function handleMoreClick() {
//当触发点击事件时,就用setShowMore函数更新showMore的值
setShowMore(!showMore);
}
return (
<>
<button onClick={handleMoreClick}>
{showMore ? '隐藏' : '显示'}
</button>
{showMore && <p>你好,React</p>}
</>
);
}
useState内可以是任意类型的 JavaScript 值,包括对象。
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value //使用中括号实现属性的动态命名
});
}
return (
<>
<label>
First name:
<input
name="firstName"
value={person.firstName}
onChange={handleChange}
/>
</label>
<label>
Last name:
<input
name="lastName"
value={person.lastName}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
name="email"
value={person.email}
onChange={handleChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}
更新一个嵌套对象
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
//修改上方city
setPerson({
...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 将 city 的值替换为 New Delhi!
}
});
React中,随时间变化的数据被称为状态(state)。可以向任何组件添加状态,并按需进行更新。useState 以及任何其他以“use”开头的函数都被称为 Hook。
注意:禁止使用修改原数组的方法:
数组只是另一种对象。同对象一样,需要将 React state 中的数组视为只读的。这意味着不应该使用类似于 arr[0] = 'bird' 这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法。
禁止使用的方法(会改变原数组):增加元素:push,unshift;删除元素:pop,shift,splice;替换元素:splice,arr[i] = ...;排序:reverse,sort。
推荐使用的方法(会返回一个新数组):增加元素:concat,[...arr];删除元素:filter,slice;替换元素:map;排序:先复制一份,再排序。
推荐:可以使用 Immer ,这样便可以使用所有方法了。原理:Immer遇到直接修改原值的语法,会自动生成拷贝值,在重新赋值,相当于一个语法糖。用法如下:
import { useState } from 'react';
import { useImmer } from 'use-immer';
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, updateMyList] = useImmer(
initialList
);
//此时可对myList使用各种方法
return (
<></>
);
}
注意:Hooks 以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。
不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
渲染和提交
一、当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它是通过调用目标 DOM 节点的 createRoot,然后用你的组件调用 render 函数完成。
一旦组件被初次渲染,就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。
二、触发渲染后,React 会调用你的组件来确定要在屏幕上显示的内容。 “渲染中” 即 React 在调用你的组件。
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
三、在渲染(调用)你的组件之后,React 将会修改 DOM。
- 对于初次渲染, React 会使用
appendChild()DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
React 仅在渲染之间存在差异时才会更改 DOM 节点。 例如,有一个组件,它每秒使用从父组件传递下来的不同属性重新渲染一次。注意,你可以添加一些文本到 <input> 标签,更新它的 value,但是文本不会在组件重渲染时消失:
State响应输入
提示:preventDefault()取消事件默认行为,如表单自动提交,链接跳转。stoppropagation()阻止事件冒泡,取消事件传播。
import { useState } from 'react';
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');
if (status === 'success') {
return <h1>That's right!</h1>
}
async function handleSubmit(e) {
e.preventDefault(); //取消事件的默认行为,即取消表单自动提交
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
Submit
</button>
{error !== null &&
<p className="Error">
{error.message}
</p>
}
</form>
</>
);
}
function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== 'lima'
if (shouldError) {
reject(new Error('Good guess but a wrong answer. Try again!'));
} else {
resolve();
}
}, 1500);
});
}
状态提升
如下,调用了两次Panel组件,看似共用了同一个isActive,但修改isActive时并不会影响另外一个组件,因为组件是独立的。
import { useState } from 'react';
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
显示
</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<Panel title="关于">
1
</Panel>
<Panel title="词源">
2
</Panel>
</>
);
}
如果想修改某一组件时,同步修改其余组件值,那就需要状态提升。实现步骤:
- 从子组件中 移除 state 。
- 从父组件 传递 硬编码数据。
- 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递。
修改上方代码:
import { useState } from 'react';
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
显示
</button>
)}
</section>
);
}
export default function Accordion() {
const [isActive, setIsActive] = useState(false);
return (
<>
<Panel title="关于" isActive={isActive} onShow={() => setIsActive(true)}>
1
</Panel>
<Panel title="词源" isActive={isActive} onShow={() => setIsActive(true)}>
2
</Panel>
</>
);
}
Reducer
比如增删改查,需要写四个方法,随着业务增加,代码可读性会很差。reducer 函数就是放置状态逻辑的地方。它接受两个参数,可以将逻辑统一存放,提高代码可读性.
reducer 可以 “减少” 组件内的代码量,但它实际上是以数组上的 reduce() 方法命名的。
提示:reducer中一般使用switch语法
用法示例:
import { useReducer } from 'react';
let nextId = 3;
const initialTasks = [
{id: 0, text: '参观卡夫卡博物馆', done: true},
{id: 1, text: '看木偶戏', done: false},
{id: 2, text: '打卡列侬墙', done: false}
];
export default function TaskApp() {
//跟useState很像,只不过多了个方法(tasksReducer)
//tasksReducer: 使用dispatch修改initialTasks数据时,会先调用tasksReducer方法
//initialTasks: 和useState一样,自定义的数据
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 (
<>
<button onClick={handleAddTask}>增加</button>
<button onClick={handleChangeTask}>修改</button>
<button onClick={handleDeleteTask}>删除</button>
</>
);
}
//用dispatch时都会调用该方法,根据该方法的return回的数据修改initialTasks
//tasks: 当前状态,即当前initialTasks
//action: dispatch方法传来的参数
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);
}
}
}
总结:调用dispatch会经过自定义tasksReducer方法,根据自定义tasksReducer方法返回值修改initialTasks
对比 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。
编写一个好的 reducers
编写 reducers 时最好牢记以下两点:
- reducers 必须是纯粹的。 这一点和 状态更新函数 是相似的,
reducers在是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着reducers必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。 - 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由
reducer管理的表单(包含五个表单项)中点击了重置按钮,那么 dispatch 一个reset_form的 action 比 dispatch 五个单独的set_field的 action 更加合理。如果你在一个reducer中打印了所有的action日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。
使用ref
使用方法如下:
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
console.log(ref.current)
}
return (
<button onClick={handleClick}>
点击
</button>
);
}
//输出:1,2,3.....
与useState不同的是更改数据时ref不会重置,但useState会重新渲染,即重置。
state 和 ref 的对比:
| ref | state |
|---|---|
useRef(initialValue)返回 { current: initialValue } | useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue]) |
| 更改时不会触发重新渲染 | 更改时触发重新渲染。 |
可变 —— 可以在渲染过程之外修改和更新 current 的值。 | “不可变” —— 必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。 |
不应在渲染期间读取(或写入) current 值。 | 可以随时读取 state。但是,每次渲染都有自己不变的 state 快照。 |
useRef内部大概是这样实现:
// React 内部
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
第一次渲染期间,useRef 返回 { current: initialValue }。 该对象由 React 存储,因此在下一次渲染期间将返回相同的对象。 请注意,在这个示例中,state 设置函数没有被用到。它是不必要的,因为 useRef 总是需要返回相同的对象。
使用 ref 操作 DOM
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>
</>
);
}
操作子/其它组件DOM——forwardRef
ref无法访问其它组件的DOM,想要 暴露其 DOM 节点的组件必须使用forwardRef(应尽量避免操作其它组件DOM)。使用如下:
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
实现过程:
<MyInput ref={inputRef} />告诉 React 将对应的 DOM 节点放入inputRef.current中。但是,这取决于MyInput组件是否允许这种行为, 默认情况下是不允许的。MyInput组件是使用forwardRef声明的。 这让从上面接收的inputRef作为第二个参数ref传入组件,第一个参数是props。MyInput组件将自己接收到的ref传递给它内部的<input>。
限制暴露属性
MyInput 中的 realInputRef 保存了实际的 input DOM 节点。 但是,useImperativeHandle 指示 React 将你自己指定的对象作为父组件的 ref 值。 所以 Form 组件内的 inputRef.current 将只有 focus 方法。在这种情况下,ref “句柄”不是 DOM 节点,而是你在 useImperativeHandle 调用中创建的自定义对象。
import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
Effect同步
在 React 中,JSX 的渲染必须是纯粹操作。如果在渲染期间设置或使用一些函数方法,会导致组件还未渲染,就对其进行操作,从而运行异常报错,要解决这种问题,就需要用到Effect同步。
如下,VideoPlayer组件在渲染期间就对isPlaying进行判断,进而操作play()和pause()方法,此时就需将调用DOM 方法的操作封装在 Effect 中,可以让 React 先更新屏幕,确定相关 DOM 创建好了以后然后再运行 Effect。
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.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"
/>
</>
);
}
当 VideoPlayer 组件渲染时(无论是否为首次渲染),都会发生以下事情。首先,React 会刷新屏幕,确保 <video> 元素已经正确地出现在 DOM 中;然后,React 将运行 Effect;最后,Effect 将根据 isPlaying 的值调用 play() 或 pause()。
!!!注意:
一般来说,Effect 会在 每次 渲染后执行,而以下代码会陷入死循环中:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
每次渲染结束都会执行 Effect;而更新 state 会触发重新渲染。但是新一轮渲染时又会再次执行 Effect,然后 Effect 再次更新 state……如此周而复始,从而陷入死循环。
Effect 通常应该使组件与 外部 系统保持同步。如果没有外部系统,只想根据其他状态调整一些状态,那么 你也许不需要 Effect。
指定 Effect 依赖
Effect 会在 每次 渲染时执行。但更多时候,并不需要每次渲染的时候都执行 Effect。示例:
const refVid = useRef(null);
useEffect(() => {
if (isPlaying) { //在此处使用了isPlaying
refVid.current.play(); //refVid可以被省略
} else {
refVid.current.pause();
}
}, [isPlaying]);//所以必须在此处声明
现在所有的依赖都已经声明。指定 [isPlaying] 会告诉 React,如果 isPlaying 在上一次渲染时与当前相同,它应该跳过重新运行 Effect。依赖数组可以包含多个依赖项。
为什么ref可以被省略:因为 ref 具有 稳定 的标识:React 保证 每轮渲染中调用 useRef 所产生的引用对象时,获取到的对象引用总是相同的,即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。因此,依赖数组中是否包含它并不重要。也可以包括它,示例:
const refVid = useRef(null);
useEffect(() => {
if (isPlaying) { //在此处使用了isPlaying
refVid.current.play();
} else {
refVid.current.pause();
}
}, [isPlaying,refVid]);//所以必须在此处声明
注意:
useEffect(() => {
// 这里的代码会在每次渲染后执行
});
useEffect(() => {
// 这里的代码只会在组件挂载后执行
}, []);
useEffect(() => {
//这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);
按需添加清理函数
Effect内的方法函数等可能会执行两次。原因:当用户在应用程序中不断切换界面再返回时,与服务器的连接会不断堆积,在下面代码中,组件被卸载时没有关闭连接:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
}, []);
Effect可以使用return返回一个清理函数,解决服务器堆积,多次执行的问题。例如:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close(); //showModal方法在连续调用两次时会抛出异常。使用清理函数,关闭对话框
}, []);
提示:Effect仅在开发模式下会执行两次,主要是由于严格模式引起。
控制非React组件
有时需要添加不是使用 React 编写的 UI 小部件。例如,假设你要向页面添加地图组件,并且它有一个 setZoomLevel() 方法,你希望调整缩放级别(zoom level)并与 React 代码中的 zoomLevel state 变量保持同步。Effect 看起来应该与下面类似:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
在这种情况下不需要清理。开发环境中,React 会调用 Effect 两次,但这两次挂载时依赖项 zoomLevel 都是相同的,所以会跳过执行第二次挂载时的 Effect。开发环境中它可能会稍微慢一些,但这问题不大,因为它在生产中不会进行不必要的重复挂载。
某些 API 可能不允许连续调用两次。例如,内置的 <dialog> 元素的 showModal 方法在连续调用两次时会抛出异常,此时实现清理函数并使其关闭对话框:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
在开发环境中,Effect 将调用 showModal(),然后立即调用 close(),然后再次调用 showModal()。这与调用只一次 showModal() 的效果相同。也正如在生产环境中看到的那样。
订阅事件
如果 Effect 订阅了某些事件,清理函数应该退订这些事件:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
在开发环境中,Effect 会调用 addEventListener(),然后立即调用 removeEventListener(),然后再调用相同的 addEventListener(),这与只订阅一次事件的 Effect 等效;这也与用户在生产环境中只调用一次 addEventListener() 具有相同的感知效果。
触发动画
如果 Effect 对某些内容加入了动画,清理函数应将动画重置:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // 触发动画
return () => {
node.style.opacity = 0; // 重置为初始值
};
获取数据
如果 Effect 将会获取数据,清理函数应该要么 中止该数据获取操作,要么忽略其结果:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
我们无法撤消已经发生的网络请求,但是清理函数应当确保获取数据的过程以及获取到的结果不会继续影响程序运行。如果 userId 从 'Alice' 变为 'Bob',那么请确保 'Alice' 响应数据被忽略,即使它在 'Bob' 之后到达。
在开发环境中,浏览器调试工具的“网络”选项卡中会出现两个 fetch 请求。这是正常的。使用上述方法,第一个 Effect 将立即被清理,而 ignore 将被设置为 true。因此,即使有额外的请求,由于有 if (!ignore) 判断检查,也不会影响程序状态。
初始化应用时不需要使用 Effect 的情形
某些逻辑应该只在应用程序启动时运行一次。比如,验证登陆状态和加载本地程序数据。你可以将其放在组件之外:
if (typeof window !== 'undefined') { // 检查是否在浏览器中运行
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ……
}
这保证了这种逻辑在浏览器加载页面后只运行一次。
不要在 Effect 中执行购买商品一类的操作
有时,即使编写了一个清理函数,也不能避免执行两次 Effect。例如,Effect 包含会发送 POST 请求以执行购买操作:
useEffect(() => {
// 🔴 错误:此处的 Effect 会在开发环境中执行两次,这在代码中是有问题的。
fetch('/api/buy', { method: 'POST' });
}, []);
移除多余的Effect
有两种不必使用 Effect 的常见情况:
- 不必使用 Effect 来转换渲染所需的数据。例如,你想在展示一个列表前先做筛选。你的直觉可能是写一个当列表变化时更新 state 变量的 Effect。然而,这是低效的。当你更新这个 state 时,React 首先会调用你的组件函数来计算应该显示在屏幕上的内容。然后 React 会把这些变化“提交”到 DOM 中来更新屏幕。然后 React 会执行你的 Effect。如果你的 Effect 也立即更新了这个 state,就会重新执行整个流程。为了避免不必要的渲染流程,应在你的组件顶层转换数据。这些代码会在你的 props 或 state 变化时自动重新执行。
- 不必使用 Effect 来处理用户事件。例如,你想在用户购买一个产品时发送一个
/api/buy的 POST 请求并展示一个提示。在这个购买按钮的点击事件处理函数中,你确切地知道会发生什么。但是当一个 Effect 运行时,你却不知道用户做了什么(例如,点击了哪个按钮)。这就是为什么你通常应该在相应的事件处理函数中处理用户事件。
例如:
假设有一个包含了两个 state 变量的组件:firstName 和 lastName。你想通过把它们联结起来计算出 fullName。此外,每当 firstName 和 lastName 变化时,你希望 fullName 都能更新。你的第一直觉可能是添加一个 state 变量:fullName,并在一个 Effect 中更新它:
错误写法(效率低,代码复杂):
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 避免:多余的 state 和不必要的 Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
正确写法:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ 非常好:在渲染期间进行计算
const fullName = firstName + ' ' + lastName;
// ...
}
当 props 变化时重置所有 state
需求:userId变化时,重置comment。
错误写法:
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 避免:当 prop 变化时,在 Effect 中重置 state
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
正确写法:
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
const [comment, setComment] = useState('');
// ...
}
更多省略Effect写法:你可能不需要 Effect – React 中文文档 (docschina.org)
响应式 Effect 的生命周期
Effect 与组件有不同的生命周期。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某些东西,然后停止同步它。如果 Effect 依赖于随时间变化的 props 和 state,这个循环可能会发生多次。React 提供了代码检查规则来检查是否正确地指定了 Effect 的依赖项,这能够使 Effect 与最新的 props 和 state 保持同步。
Effect 的生命周期
每个 React 组件都经历相同的生命周期:
- 当组件被添加到屏幕上时,它会进行组件的 挂载。
- 当组件接收到新的 props 或 state 时,通常是作为对交互的响应,它会进行组件的 更新。
- 当组件从屏幕上移除时,它会进行组件的 卸载。
这种组件方式,并不适用于 Effect。Effect 描述了如何将外部系统与当前的 props 和 state 同步。随着代码的变化,同步的频率可能会增加或减少。
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
//Effect 的主体部分指定了如何 **开始同步**:
const connection = createConnection(serverUrl, roomId);
connection.connect();
//Effect 返回的清理函数指定了如何 **停止同步**:
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
在组件保持挂载状态的同时,可能还需要 多次开始和停止同步。
注意:有些 Effect 根本不返回清理函数。在大多数情况下,应该返回一个清理函数,但如果没有返回,React将返回一个空的清理函数。
总结
React与Vue差别挺大,React偏向于使用 JS (JSX) 操作整个页面。个人而言React灵活度更高,耦合性强,适合大型项目开发,Vue适合中小型项目开发。除了熟悉React外,还要熟悉其各种Hook说明和用法。