最近 React 方面 又学了些新东西,还是写写笔记吧, 不然会忘。
首先,先明确super不是React的知识点,而是es6的。
constructor(..args){super(...args)}
在React中super有两个参数super(props, context)
。
React Hook规则
- 以 use 开头的函数被称为 Hook
- 没有 调用 Hook 的函数不需要 变成 Hook
- 自定义 Hook 命名必须以 use 开头,后面跟一个大写字母。
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调⽤
- 只能在 React 的函数组件中 或 ⾃定义的 Hook 中 调用Hook
- 自定义 Hook 共享的只是状态逻辑,不是状态本身。
hook的使用
- 提取组件间的共享逻辑为自定义组件
- 自定义 Hook 让你可以在组件间共享逻辑。
- 每次组件重新渲染时,所有的 Hook 会重新运行。
- 自定义 Hook 的代码应该和组件代码一样保持纯粹。
- 把事件处理函数传到自定义 Hook 中
- 不要创建像 useMount 这样的自定义 Hook。保持目标具体化。
hooks
是作为一个单链表存储在fiber.memoizedState
上的,因为这些hook
没有名字,所以为了区分它们,我们必须保证这个链表节点顺序的稳定性。
如果源码中返回的是个对象,那么状态值和修改状态值的函数名字都写死了,不方便用户多处使用。
先看一下class组件的setState的回调函数
jsxexport class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
}
handle = () => {
const {count} = this.state;
this.setState({count: count + 1}, function(state, props) {
console.log("a", state, props); // 组件重新渲染后被调用
});
};
render() {
const {count} = this.state;
return (
<div className="class border">
{this.props.name}
<button onClick={this.handle}>{count}</button>
</div>
);
}
}
在FunctionComponent中可以使用useEffect, useLayoutEffect模拟
jsxexport function FunctionComponent({name}) {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("useEffect count", count); //sy-log
}, [count]);
React.useLayoutEffect(() => {
console.log("useLayoutEffect count", count); //sy-log
}, [count]);
return (
<div className="function border">
{name}
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
随着class Component的功能越来越多,组件也变得越来越臃肿,很多不相关的逻辑混合在生命周期函数中,这时组件就变得杂乱而没有条理,查找确切的逻辑使用范围就变得困难,调试和优化也变得越来越难。
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。
你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
具体将在自定义 Hook 中对此展开更多讨论。
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在
componentDidMount
和componentDidUpdate
中获取数据。但是,同一个componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
我们将在使用 Effect Hook 中对此展开更多讨论。
除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中
this
的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。
另外,React 已经发布五年了,我们希望它能在下一个五年也与时俱进。就像 Svelte,Angular,Glimmer等其它的库展示的那样,组件预编译会带来巨大的潜力。尤其是在它不局限于模板的时候。最近,我们一直在使用 Prepack 来试验 component folding,也取得了初步成效。但是我们发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。
为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
函数组件和类组件都可以取代对方。可以参考以下规则选择:
函数组件颗粒度更小,是函数式编程的优先选择。
颗粒度体现在state定义与useEffect、useLayoutEffect上,函数组件可以写多个,也可以拆成自定义hook调用。但是相比之下,类组件 的state和每个生命周期函数在组件中都最多能用一次,拆到组件外也比较繁琐。
类组件有实例,如果需要用到实例的话,类组件是首选。
函数组件和类组件都可以复用状态逻辑。
类组件可以通过hoc、render props,但是容易形成嵌套地狱,参考antd3 form的createForm和react-redux的connect一起用在一个组件里的时候,四层括号,闹心~。但是函数组件用一个自定义hook就完事了,你看antd4 form的useForm。
类组件有个this,合成事件中可能会比较难以理解,但是函数组件并没有这种设计,上手简单~
具体API
在React源码中判断如果useState和useReducer前后两次值相等,则放弃更新
如下三种使用forceUpdate的方法:useReducer、useState或者useForceUpdate,
jsimport {useCallback, useReducer, useState} from "react";
export default function FunctionComponentForceUpdate(props) {
console.log("omg"); //sy-log
// const [count, forceUpdate] = useState(0);
// ! 方法1
// const [, forceUpdate] = useReducer((x) => x + 1, 0);
// ! 方法2
const forceUpdate = useForceUpdate();
const handleClick = () => {
// forceUpdate(count + 1);
// forceUpdate((prev) => prev + 1);
forceUpdate();
};
return (
<div>
<h3>FunctionComponentForceUpdate</h3>
<button onClick={handleClick}>count</button>
</div>
);
}
function useForceUpdate() {
const [state, setState] = useState(0);
// const [, setState] = useReducer((x) => x + 1, 0);
const update = useCallback(() => {
setState((prev) => prev + 1);
// setState();
}, []);
return update;
}
React为了 实现跨平台兼容性,对于事件处理有自己的一套代码。
React中有自己的事件系统模式,即通常被称为React合成事件。之所以采用这种自己定义的合成事件,一方面是为了抹平差异性,使得React开发者不需要自己再去关注浏览器事件兼容性问题,另一方面是为了统一管理事件,提高性能,这主要体现在React内部实现事件委托,并且记录当前事件发生的状态上。
事件委托,也就是我们通常提到的事件代理机制,这种机制不会把时间处理函数直接绑定在真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听和处理函数。当组件加载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件放生时,首先被这个统一的事件监听器处理,然后在映射表里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。
记录当前事件发生的状态,即记录事件执行的上下文,这便于React来处理不同事件的优先级,达到谁优先级高先处理谁的目的,这里也就实现了React的增量渲染思想,可以预防掉帧,同时达到页面更顺滑的目的,提升用户体验。
在React@16中,JSX 语法被 @babel/parset-react
编译为React.createElement()
的语法躺,因此必须手动导入React(即使没有直接使用),否则React.createElement会报错。
React17引入了新的JSX运行时,Babel和TypeScript等工具会自动从React包中导入必要的JSX运行时函数,而不在依赖React.createElement
jsx/ 你的代码(无需导入 React)
const element = <div>Hello</div>;
// Babel 转换后
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('div', { children: 'Hello' });
webpack编译后的结果为
jsfunction jsxWithValidation () {
// <h1><h2><h3>3333</h3></h2></h1>
return (
(0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)
("h1", {
children: (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)
("h2", { children: (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)
("h3", { children: "3333" })
})
})
);
}
;;
function jsxWithValidationDynamic(type, props, key) {
return jsxWithValidation(type, props, key, false);
}
为什么这样做?
import React
。如何启用新转换?
在协调阶段,组件复用的前提是必须同时满足三个条件:同一层级下、同一类型、同一个key值。所以我们尽量保证这三者的稳定性。
key值标记了节点在当前层级下的唯一性,因此我们尽量不要取index
组件重新render会导致组件进入协调,协调的核心就是我们常说的vdom diff, 所以协调本身就是比较耗时的算法,因此如果能够减少协调,复用旧的fiber节点,那么肯定会加快渲染完成的速度。组件如果没有进入协调阶段,我们称为进入bailout阶段,意思就是这层组件退出更新。让组件进入bailout阶段,有以下方法
memo
memo 允许你的组件在props没有改变的情况下跳过重新渲染
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
这里的arePropsEqual是一个函数, 用户可以自定义,如果没有定义,默认使用浅比较。
比较组件更新前后的props是否相同,如果相同,则进入bailout阶段。
使用示例:避免不必要的字组件变更
tsxconst App: React.FC = () => {
// ...
const SearchInputRef = useRef(memo(SearchInput));
return <div>
{...}
<SearchInputRef.current />
</div>
}
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState, nextContext)
PureComponent
PureComponent
同Component
,但是前者会浅比较props和state以及减少错过必要更新的概率。
Context
Context 本身就是一旦Provider传递的value变化,所有消费这个value的后代组件都要更新,因此应该精良精简使用Context
Context使用场景: 当祖先组件想要和后代组件快速通信。
useMemo
useMemo 是一个React Hook, 它在每次重新渲染的时候能够缓存计算的结果。
const cachedValue = useMemo(calculateValue, dependcies)
useMemo可以缓存参数,可以对比useCallback使用,useCallback(fn, deps)相当于useMemo(() => fn, deps)。
注:以下内容来自deepSeek(比react官方介绍更加清晰)
useEffectEvent 是 React 正在实验中的一个新 Hook,旨在解决在 useEffect 中使用最新 props 或 state 而不触发重新执行的问题。目前它还不是 React 稳定版的一部分,但了解它的用法对未来开发很有帮助。
基本概念
useEffectEvent 允许你定义一个不会成为 useEffect 依赖项的"稳定引用"函数,但又能访问最新的 props 和 state。
为什么需要它
在传统的 useEffect 中,我们经常遇到这样的问题
jsxfunction ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createConnection() {
// 连接逻辑
}
const connection = createConnection();
connection.on('message', (receivedMessage) => {
// 这里需要最新的 message 状态
console.log(message); // 可能不是最新的
});
return () => connection.disconnect();
}, [roomId]); // 这里如果添加 message 依赖,会导致频繁重连
}
使用 useEffectEvent 解决
jsximport { experimental_useEffectEvent as useEffectEvent } from 'react';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const onMessage = useEffectEvent((receivedMessage) => {
console.log(message); // 总是能获取最新的 message
});
useEffect(() => {
function createConnection() {
// 连接逻辑
}
const connection = createConnection();
connection.on('message', onMessage);
return () => connection.disconnect();
}, [roomId]); // 不需要添加 onMessage 或 message 作为依赖
}
使用场景
注意事项
替代方案 在 useEffectEvent 正式发布前,可以使用 useRef 和 useCallback 的组合来模拟类似行为:
jsxfunction useEventCallback(fn) {
const ref = useRef(fn);
useEffect(() => {
ref.current = fn;
});
return useCallback((...args) => ref.current(...args), []);
}
useEffectEvent 提供了一种更官方、更简洁的解决方案来处理这类常见问题。
React 19 引入的实验性 API
在组件的顶层调用 useActionState 即可创建一个随 表单动作被调用 而更新的 state。在调用 useActionState 时在参数中传入现有的表单动作函数以及一个初始状态,无论 Action 是否在 pending 中,它都会返回一个新的 action 函数和一个 form state 以供在 form 中使用。这个新的 form state 也会作为参数传入提供的表单动作函数。
jsximport { useActionState } from 'react';
async function loginAction(prevState, formData) {
const username = formData.get('username');
const password = formData.get('password');
try {
// 模拟服务端验证
if (username === 'admin' && password === '123') {
return { success: true, message: '登录成功!' };
} else {
return { success: false, error: '用户名或密码错误' };
}
} catch (error) {
return { success: false, error: '服务器错误' };
}
}
export default function LoginForm() {
const [state, formAction, isPending] = useActionState(loginAction, {
success: null,
error: null,
message: null,
});
return (
<form action={formAction}>
<div>
<label>用户名:</label>
<input type="text" name="username" required />
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required />
</div>
<button type="submit" disabled={isPending}>
{isPending ? '登录中...' : '登录'}
</button>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state.success && <p style={{ color: 'green' }}>{state.message}</p>}
</form>
);
}
React Suspense 是 React 16.6 引入的一个组件,主要用于处理异步操作(如数据获取、代码分割等)时的加载状态管理。以下是 Suspense 组件主要做的事情:
核心功能
底层原理
Suspense 是 React 异步渲染模型的核心组件之一,为未来的并发特性提供了基础支持。
先来看一下suspend的示例
jsxlet cache = new Map();
export function fetchData(url: string) {
if(cache.has(url)) {
cache.set(url, getData(url));
}
return cache.get(url)
}
async function getData(url) {
await new Promise(resolve => {
setTimeout(resolve, 1000)
});
return ['a', 'b', 'c'].slice(0, Math.floor(Math.random() * 3))
}
const SearchResults = ({query}) => {
const arr = use(fetchData(`/search?q=${query}`));
if(query === '') return null;
if(arr.length === 0) {
return <p>No matches for <i>"${query}"<i><p>
}
return (<ul>
{arr.map(it => (
<li key={it}>{it}</li>
))}
</ul>)
}
export default function App() {
const [query, setQuery] = useState('');
return <>
<label>
Search results:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={query}>
</Suspense>
</>
}
Suspend 与 React.lazy() 实现代码分割示例
jsximport React, { Suspense, lazy } from 'react';
function App() {
return (
<div>
<h1>我的应用</h1>
{/* 使用 Suspense 包裹懒加载组件 */}
<Suspense fallback={<div>加载中...</div>}>
{lazy(() => import('./LazyComponent'))}
</Suspense>
</div>
);
}
useDeferedValue 是配合Suspend 进行延迟展示(请求不会延时)。
可以让你延迟更新 UI 的某些部分
todo
本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!