README
目录
钩子
react 给人特别明显的区别就是 react钩子
function CounterButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
);
}
以及可能为了监听某些请求需要使用useEffect
或者 useCallback
钩子的起源是渲染机制
造成这些问题的原因是 React 渲染机制
如果你使用过 React.component 编写页面,你的代码应该长这样
class Counter extends Component {
state = {
age: 42,
};
handleAgeChange = () => {
this.setState({
age: this.state.age + 1,
});
};
render() {
return (
<>
<h1>Hello, {this.props.name}!</h1>;
<button onClick={this.handleAgeChange}>Increment age</button>
<p>You are {this.state.age}.</p>
</>
);
}
}
react 的渲染机制是:当status 或者 props的内容发生改变时,React会重新渲染组件(调用组件的render方法)。
如果改变了其他变量,如render或者全局的变量,都不会触发重新渲染
渲染之后的render里的内容就会作为一个静态的内容被返回,并渲染到页面上
所以如果在render中编写一些变量,那么这个变量的值就会在每次render的时候被重新计算,并返回给React。
如果需要能全局保存的值,那就需要定义在 status 中(使用setStatus),或者由 props 传入
render 中不能使用 useState 这一类 Hooks 原因后面再解释
再来看现在比较新的语法(函数式)
export default function Counter(props) {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
函数和 render的功能类似,都是渲染一次,但不同的是,因为不是类,没有 status,所以使用 hooks 来实现状态管理
如果需要实现类似 status 的功能,需要使用 useState 来定义一个 state 变量
这变量会交给react管理(类似放到this.status中)
当使用 setXxx 的时候,相当与修改了state变量,就会触发渲染一次
如果一个变量依赖于某个 status 变量,并且计算方式唯一,那么就不需要将这个变量定义为状态变量(所以React中没有 Vue中的 computed 函数)
渲染和对比
:::Info 参考资料
- GitHub - acdlite/react-fiber-architecture: A description of React's new core algorithm, React Fiber | Github
- 理解 React 中 key 的作用这个问题涉及 react 渲染机制和 diff 算法,在 state 或 props 变化的情况下,会造成渲 | 掘金
:::
如果每次更新状态变量,都会重新渲染,那么就会产生性能问题
所以有如下2个机制,来优化算法:
- 合并渲染:如果短期内有多个状态变量更新,那么就合并成一个更新,然后进行渲染
- 使用了Fiber算法,将渲染任务拆分为 比较 和 更新这两个步骤
::: Info 为什么使用 Fiber 浏览器运行的时候有一个主线程和 worker线程,主线程可以修改页面DOM而worker线程无法修改DOM
如果将计算放在主线程上会拖慢页面渲染速度
Fiber算法将渲染任务拆分为渲染和计算两个步骤,将计算任务(对比节点,判断是否需要更新)放在worker线程上,将渲染任务(单纯负责渲染DOM)放在主线程上,这样主线程就不会被阻塞,从而提升页面渲染速度和性能 :::
Fiber 进行比较时候
如果某个组件的 status 和 Props 都没有更新,那个这个组件也不会更新,直接复用
setXxxx 更新 State -> 触发ststue涉及的 re-render -> 计算更新/比较 -> 渲染更新 DOM
hooks管理
React 内部靠 调用顺序 来识别每个 hooks
function App() {
const [count, setCount] = useState(0); // hook1
const [name, setName] = useState("Tom"); // hook2
useEffect(() => {
console.log(count);
}, [count]); // hook3
return (
<div>
{count} {name}
</div>
);
}
App Fiber
-> Hook1: { memoizedState: 0, queue: setCount }
-> Hook2: { memoizedState: "Tom", queue: setName }
-> Hook3: { effect callback, deps: [count] }
当调用某个hook的时候,获取找到对应的hook数据,并修改值,然后触发重新渲染
当发现某个值有了更新,就会返回新的值
这也就是React.component的render函数中为什么不能使用setState,因为status和 hooks的分开了,不能确定谁先调用,也就无法确定hooks的顺序
key 在对比中的作用
如果你使用 Array.Map
这个方法来生成元素,那么 React 会根据元素的 key
属性来识别它们,以确定它们是添加、删除还是重新排列。
如果你不定义 key 浏览器将会有一行报错
Warning: Each child in a list should have a unique “key” prop.
官方不建议你把 index作为key,并建议使用和列表项绑定的数据作为 key
这个例子可以作为参考
const [initMap, setInitMap] = useState([1, 2, 3, 4]);
const handleClick = () => {
setInitMap([3, 2, 1, 4]);
};
return (
<div className="App" id="app">
{initMap.map((it, index) => (
<div key={index}>
<input type="radio" />
{it}
</div>
))}
<button onClick={() => handleClick()}>点击</button>
</div>
);
这个代码展示4个单选框,当你选择一个单选框之后点击按钮,会发现单选框的值会被改变,而选中的位置不会改变
1 2 3x 4 -> 3 2 1x 4
如果你在一个函数内修改了一个状态变量2次,并且开始和结束的状态变量值相同,则React可能会复用之前的节点。因为React 会将临近的渲染合并为一次渲染以提升性能。那么在渲染的时候会发现,state 变量值没有改变,那么 React 就不会生成新的节点,而是直接复用之前的节点。
为什么要其他各种钩子
useCallback
如果不使用 useCallback ,一个函数在 组件渲染时,会重新创建,造成额外的性能开销
使用 useCallback 可以避免函数在每次渲染时重新创建,只在函数的依赖项改变时重新创建。
usEffect
function Comp() {
const [data, setData] = useState(null);
fetch("/api/data")
.then((res) => res.json())
.then(setData); // ❌ 渲染阶段发请求,setData 会触发再次渲染
return <div>{data}</div>;
}
function Comp() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data")
.then((res) => res.json())
.then(setData);
}, []); // 空依赖数组 → 只在首次渲染后执行一次
return <div>{data}</div>;
}
这个fetch写在“render”(也就是外层)函数中,这样每次计算的时候都会触发一次渲染,任何上游的 stste 改变都会触发请求
- 如果在外层设定一个比较慢的功能,那就会导致渲染变慢
- 如果在外层设定一个 setInterval 那么就会重复创建导致泄漏
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, [count]);
function Counter() {
const [count, setCount] = useState(0);
const id = setInterval(() => console.log(count), 1000); // ❌
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
useContext
可以将父组件的内容跨层传入子组件,适用于全局数据这种
import React, { useState, useContext } from 'react';
import ReactDOM from 'react-dom/client';
// 1️⃣ 创建 Context
interface User {
name: string;
loggedIn: boolean;
setUser: React.Dispatch<React.SetStateAction<User>>;
}
const defaultUser = {
name: 'Guest',
loggedIn: false,
setUser: () => {},
};
const UserContext = React.createContext<User>(defaultUser);
// 2️⃣ Context Provider
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState({ name: 'Guest', loggedIn: false });
return (
<UserContext.Provider value={{ ...user, setUser }}>
{children}
</UserContext.Provider>
);
}
// 3️⃣ 子组件读取 Context
function UserProfile() {
const { name, loggedIn } = useContext(UserContext);
return (
<div>
<h2>User Profile</h2>
<p>Name: {name}</p>
<p>Status: {loggedIn ? 'Logged in' : 'Guest'}</p>
</div>
);
}
// 4️⃣ 子组件修改 Context
function LoginButton() {
const { setUser } = useContext(UserContext);
const login = () => {
setUser({ name: 'Alice', loggedIn: true });
};
const logout = () => {
setUser({ name: 'Guest', loggedIn: false });
};
return (
<div>
<button onClick={login}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
}
// 5️⃣ App 组件
function App() {
return (
<UserProvider>
<UserProfile />
<LoginButton />
</UserProvider>
);
}
// 渲染
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);
React 哲学
将UI渲染和逻辑分开,UI放在子组件中, 逻辑放在父组件中
其哲学来源于:构建一个静态版本需要写大量的代码,并不需要什么思考; 但添加交互需要大量的思考,却不需要大量的代码。
大概这样:
逻辑组件
| \
UI组件 逻辑子组件
| \ \
UI子组件 UI子组件 UI子组件
分开之后, UI组件就可以复用了
UI组件需要做的,仅仅是将props中的数据展示在页面上,而逻辑组件需要做的
所有需要变化的东西都尽量放在逻辑组件中,如果有一个status父子组件都需要用到,那么就放到父组件里面去(React官方称之为:Lifting state up(状态提升))
数据通过 props 和 state 在父子组件之间传递
props可以将数据从父组件单向传递到子组件中,适合比如 xx列表、xx信息 这样的数据
status可以利用props将组件的交互信息反向传递给父组件,适合比如 xx表单、输入框等等
评价:由类转为函数
看上去可以实现的功能变多了,结合React哲学整个项目也得到了整理,但是钩子的概念对于初学者来说不容易理解,容易引发混乱
为了结局问题导入了多种 hooks 很难说到底是更方便了还是更麻烦了