# React
# 容器组件与展示组件分离
拆分出通用的 isLoading、data、error 作为 hooks, 与展示组件结偶 还可以传入fetchUrl 和一系列配置, 抽象一个更通用的hooks
import { useEffect, useState } from 'react';
import { ISinglePost } from '../Definitions';
export default function usePosts() {
const [posts, setPosts] = useState<ISinglePost[] | null>(null);
const [isLoading, setIsLoading] = useState<Boolean>(false);
const [error, setError] = useState<unknown>();
useEffect(() => {
(async () => {
try {
setIsLoading(true);
const resp = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await resp.json();
setPosts(data.filter((post: ISinglePost) => post.userId === 1));
setIsLoading(false);
} catch (err) {
setError(err);
setIsLoading(false);
}
})();
}, []);
return {
isLoading,
posts,
error
};
}
/**
* 展示组件
*/
import { ISinglePost } from '../Definitions';
import usePosts from '../hooks/usePosts';
import SinglePost from './SinglePost';
export default function Posts(props: { posts: ISinglePost[] }) {
const { isLoading, posts, error } = usePosts();
return (
<ul
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
{isLoading ? (
<span>Loading...</span>
) : posts ? (
posts.map((post: ISinglePost) => <SinglePost {...post} />)
) : (
<span>{JSON.stringify(error)}</span>
)}
</ul>
);
}
# hooks基本原则
- 只在最顶层使用 Hook
- 不在循环,条件或嵌套函数中调用 Hook
- 只在 React 函数中调用 Hook
# 组件设计原则
确定组件的功能、状态管理、副作用、组件结构、以及事件处理。
首先,确定组件的功能和用途。明确组件应该接受哪些props,以及应该渲染什么样的内容。
确定组件状态: 判断组件是否需要内部状态来管理数据。如果需要状态管理,可以使用useState Hook来定义状态。
设计组件结构: 根据组件的功能,设计组件的结构和UI。将组件拆分成更小的子组件,使代码更加模块化和可维护。
使用React Hooks: 在函数组件中使用React Hooks来管理状态和生命周期。常用的Hooks有useState、useEffect、useContext等。
处理副作用: 如果组件需要在挂载、更新或卸载时执行一些副作用操作,可以使用useEffect Hook来处理。
处理事件: 设计并实现组件需要响应的事件处理函数,并将其绑定到相应的UI元素上。
- 单一职责原则, 尽可能少
- 划分边界, 确定入参出参
- 高内聚/低耦合 把组件的逻辑/样式/结构组合在在一起, 降低不同组件之间的依赖关系, 给复杂的业务解耦
# 最佳实践
- 推荐 eslint-plugin-react-hooks 的 ESLint 插件
- 不需要引起页面渲染的状态用 useRef
- 使用 useContext 避免 prop-drilling
- useState 初始值使用惰性初始化函数提升性能
- 自定义hooks 抽离复用逻辑
# PureComponent & Components + shouldComponentUpdate & React.memo()
- PureComponent 会自动对子组件state 和props 进行浅比较
- shouldComponentUpdate(nextProps, nextState) 会根据state、props 的返回值判断是否需要更新, 如果是引用类型的数据使用 immutable.js 库来进行深层对比
import { is } from 'immutable'
shouldComponentUpdate (nextProps = {}, nextState = {}) => {
return !is(this.props, nextProps) || !is(this.state, nextState)
}
- React.memo
- React.memo 包裹的组件仅检查 props 变更, 默认会对 props 进行浅比较, 且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染, 需要深层比较请自己定义areEqual
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
React.memo(MyComponent, areEqual)
- React hooks的写法,在hooks中useState修改引用类型数据的时候,每一次修改都是生成一个新的对象,也就避免了引用类型数据传递的时候,子组件不更新的情况。
# useEffect & useLayoutEffect
useEffect 是在渲染函数执行完成,并绘制到屏幕之后,再异步执行
useLayoutEffect是在渲染函数执行之后,屏幕重绘前同步执行
因为 useLayoutEffect 是同步执行的,因此会发生阻塞,直到该 effect 执行完成才会进行页面重绘,如果 effect 内部有执行很慢的代码,可能会引起性能问题。因此,React 官方指出,尽可能使用标准的 useEffect 以避免阻塞视觉更新。
如果状态更新,导致组件渲染闪烁,这个时候,就应该用useLayoutEffect, useLayoutEffect会在页面绘制前阻塞执行, 避免页面重复绘制.
# setState
setState() 是同步还是异步? useState 是同步方法, 只是react的流程调度把useState变成了异步方法的样子。react的流程调度里,会把多个事件合成一个事件,一次性处理,所以,变成了异步.
setState() 同步的方法
- useEffect() 监听状态
- setTimeout() 内获取state 状态
- 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给setState。该函数将接收先前的 state,并返回一个更新后的值。
setCountState((preCount) => preCount + 1)
# immer
- 由于React 数据不可变, immer 简化update 操作, 复杂情况下省去拷贝对象这一步
import { useImmer } from 'use-immer';
const [person, updatePerson] = useImmer(initialPerson);
updatePerson(draft => {
draft.name = e.target.value;
});
# useReducer && useImmerReducer
- 当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量
- useImmerReducer 用immer 的方式修改状态
import { useReducer } from 'react';
import { useImmerReducer } from 'react';
/**
* dispatch 派发的事件
* tasksReducer 事件处理中心
*/
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
// tasksReducer.js
export default function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
}
}
import { useImmerReducer } from 'use-immer';
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
# useContent
- 向后代组件传值
import { createContext, useContent } from 'react';
import { LevelContext } from './LevelContext.js';
// createContext 创建一个 content 组件
const LevelContext = createContext(0);
export default function Section({ children }) {
// 使用这个 content, 配合Provider 声明提供的组件
const level = useContext(LevelContext);
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
)
}
export default function Heading({ children }) {
// 接收父级组件注入的属性
const level = useContext(LevelContext);
const tasks = useContext(TasksContext);
return (
{
level ? <div>{children}</div> : <div>123</div>
}
)
}
# useRef
- 处理长列表方案
import { useRef } from 'react';
export default function CatFriends() {
const itemsRef = useRef(null);
function scrollToId(itemId) {
const map = getMap();
const node = map.get(itemId);
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function getMap() {
if (!itemsRef.current) {
// 首次运行时初始化 Map。
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToId(0)}>
Tom
</button>
<button onClick={() => scrollToId(5)}>
Maru
</button>
<button onClick={() => scrollToId(9)}>
Jellylorum
</button>
</nav>
<div>
<ul>
{catList.map(cat => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
if (node) {
map.set(cat.id, node);
} else {
map.delete(cat.id);
}
}}
>
<img
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({
id: i,
imageUrl: 'https://placekitten.com/250/200?image=' + i
});
}
- 访问组件内的节点方案
- 给需要暴露内部组件的包装一层forwardRef, 把ref 传入内部
- 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>
</>
);
}
# useEffect
- 一般用于获取数据、事件监听或订阅、修改DOM 改变 DOM(changing the DOM)
- 依赖项为[], 相当于 mount 钩子, 只在dom 渲染好后执行一次
- 没有依赖项, 相当于 updated 钩子, 每次页面渲染都会执行一次
- 指定依赖项, 相当于watch , 监听指定依赖触发更新
- return 一个函数, 相当于 destroy 钩子, 常用于清除监听器计时器这些
# useMemo
- 缓存不需要更新的组件, 可以指定依赖项更新, 类似可以指定依赖的计算属性
useMemo(calculateValue, dependencies)
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
}
# useCallback
- useCallback 返回一个函数
const cachedFn = useCallback(fn, dependencies)
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
# css 样式隔离与样式穿透(Vue scoped & ::v-deep)
- 在Vue 中是利用postcss 对样式做转换
- 可以使用css module, 规则是中间带有 module (style.module.scss)
- 文件使用 :global(.className) 可以把这个类声明一个全局规则, 实现样式穿透
# router-view 是 outlet
# React 状态管理 zustand
import { create } from 'zustand'
import { User } from '@/types/api'
export const useStore = create<{
token: string
userInfo: User.UserItem
updateToken: (token: string) => void
updateUserInfo: (userInfo: User.UserItem) => void
}>(set => ({
token: '',
userInfo: {
_id: '',
userId: 0,
userName: '',
},
collapsed: false,
updateToken: token => set({ token }),
updateUserInfo: (userInfo: User.UserItem) => set({ userInfo }),
}))
# useImperativeHandle & forwardRef
- 使用useImperativeHandle,减少暴露给父组件的属性,避免使用 ref 这样的命令式代码
# 组件暴露open方法
文档地址:https://react.dev/reference/react/useImperativeHandle (opens new window)
useImperativeHandle(ref, createHandle, dependencies?)
# 方法一:ref + forwardRef + useImperativeHandle
// 父组件 OrderList
import React, { useEffect, useRef, useState } from 'react'
export default () => {
const userRef = useRef()
const handleOpen = () => {
userRef.current?.open()
}
return <CreateUser ref={userRef} />
}
// 子组件 CreateUser
const CreateUser = forwardRef((props: IProp, ref: any) => {
// 组件内部完成显隐
const [visible, setVisible] = useState(false)
// 暴露 open 方法给父组件调用
useImperativeHandle(ref, () => ({
open: () => {
setVisible(true)
}
}))
return (
<Modal
title="新增用户"
width={800}
open={visible}
okText="确定"
cancelText="取消"
onOk={handleOk}
onCancel={handleCancel}
>...此处省略...</Modal>
})
forwardRef官方解释:https://zh-hans.reactjs.org/docs/react-api.html#reactforwardref (opens new window)
# 方法二:自定义属性 + useImperativeHandle
// 父组件 OrderList
import React, { useEffect, useRef, useState } from 'react'
export default () => {
const userRef = useRef()
const handleOpen = () => {
userRef.current?.open()
}
return <CreateOrder userRef={userRef} />
}
// 子组件 CreateOrder
interface IProp {
userRef: MutableRefObject<{ open: () => void } | undefined>
}
const CreateUser = (props: IProp) => {
const [visible, setVisible] = useState(false)
useImperativeHandle(props.userRef, () => ({
open: () => {
setVisible(true)
}
}))
return (
<Modal
title="新增用户"
width={800}
open={visible}
okText="确定"
cancelText="取消"
onOk={handleOk}
onCancel={handleCancel}
>...此处省略...</Modal>
)
}
这种方式注意,
组件上面的属性不可以定义ref,需要自定义其它属性。
# 路由拦截
Loader功能介绍:
调用权限列表接口
递归生成页面路径(后续页面权限判断使用)
返回菜单列表、按钮列表和页面路径
获取Loader返回值
useRouteLoaderData('layout')
加载页面前,先执行Loader,获取权限列表,再根据权限列表动态生成左侧菜单。
// 在layout 路由里注册loader, 保证所有需要鉴权的页面里会走校验流程
{
id: 'layout',
element: <Layout />,
loader: AuthLoader,
children: []
}
// AuthLoader
export default async function AuthLoader() {
const data = await api.getPermissionList()
const menuPathList = getMenuPath(data.menuList)
return {
buttonList: data.buttonList,
menuList: data.menuList,
menuPathList
}
}
// useRouteLoaderData 会优先加载, 优先处理, 没有权限直接重定向
// 权限判断
const data = useRouteLoaderData('layout')
const route = searchRoute(pathname, router)
if (route && route.meta?.auth === false) {
// 正常向下加载页面
} else {
// 没有权限且不是白名单内重定向403
const staticPath = ['/welcome', '/403', '/404']
if (!data.menuPathList.includes(pathname) && !staticPath.includes(pathname)) {
return <Navigate to='/403' />
}
}
# 常见问题
# React16 生命周期钩子
construtor() //组件构建 getDerivedStateFromProps() //将props派生为state shouldComponentUpdate() //组件是否更新 render() //组件渲染 getSnapshotBeforeUpdate() //可返回一个参数供componentDidUpdate使用,可以操作真是dom componentDidMount() //组件渲染完成 componentDidUpdate() //组件更新完成 componentWillUnmount() //组件将要卸载
# Virtual DOM
- https://www.zhihu.com/question/31809713/answer/53544875 (opens new window)
- 虚拟DOM 分为 tag、props、children 三个属性
- React 相对于直接操作原生 DOM 最大的优势在于 batching(批处理)和 diff
- batching 就是将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。无论是 batching 还是 diff,都是为了尽量减少对 DOM 的调用
- 提供了更好的跨平台能力,因为 Virtual DOM 是以 JavaScript 对象为基础而不依赖具体的平台环境,因此可以适用于其他的平台,如 native、VR、小程序
<ul id="list">
<li class="item">Item1</li>
<li class="item">Item2</li>
</ul>
{
"tag": "ul",
"attrs": {
"id": "list"
},
"children": [
{
"tag": "li",
"attrs": { "className": "item" },
"children": ["Item1"]
},
{
"tag": "li",
"attrs": { "className": "item" },
"children": ["Item2"]
}
]
}
# fiber 架构
- Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染。 React 在 V16 之前会面临的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于——更新流程是 【同步、不可中断的】
为了解决这个问题,React 提出Fiber 架构怎么做的?
让 React 渲染的过程可以被中断,可以将控制权交回浏览器,让浏览器及时地相应用户的交互——异步可中断 通过将工作任务拆分成一个个雄安的工作单元分别来执行——Fiber Fiber 即是一种数据结构,又是一个工作单位
Fiber 作为数据结构
React Fiber 机制的实现,就是依赖于下面的这种数据结构-链表实现的。其中每个节点都是一个 Fiber,一个 Fiber 包含了 child(第一个子节点)、sibling(兄弟节点)、parent(父节点)等属性。Fiber 节点中其实还会保存节点的类型、节点的信息(比如 state、props)、节点对应的值等
Fiber 作为工作单位
将它视作一个执行单元,每次执行完一个“执行单元”,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出来
组件交互的流程,用 jsx 写 react 组件,render() 输出虚拟 dom(通过 babel 插件),虚拟 dom 转为 DOM,再在 DOM 上注册事件,事件触发 setState()修改数据,在每次调用 setState 方法时,React 会自动执行 render 方法来更新虚拟 dom,如果组件已经被渲染,那么还会更新到 DOM 中去
异步渲染中的 Fiber 的做法是:分片
把一个很耗时的任务分成很多小片,
Fiber 之前的架构是同步更新,遍历,从根组件开始到子节点,
假如更新一个组件需要 1 毫秒,如果有 200 个组件要更新,那就需要 200 毫秒,在这 200 毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这 200 毫秒内,用户往一个 input 元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被 React 占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等 React 更新过程结束之后,咔咔咔那些按键一下子出现在 input 元素里了。
这就是所谓的界面卡顿,很不好的用户体验。
现有的 React 版本,当组件树很大的时候就会出现这种问题,因为更新过程是同步地一层组件套一层组件,逐渐深入的过程,在更新完所有组件之前不停止,函数的调用栈就像下图这样,调用得很深,而且很长时间不会返回。
因为 JavaScript 单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React 的更新过程就是犯了这个禁忌,而 React Fiber 就是要改变现状。
React Fiber 的方式:
破解 JavaScript 中同步操作时间过长的方法其实很简单——分片。
把一个耗时长的任务分成很多小任务,每一个小任务完成了,就把控制权交还给 React 负责任务协调的模块,看看有没有其他其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务
如果一个任务还没完成(时间到了),就会被另一个更高优先级的更新过程打算,这个时候,优先级高的更新任务会优先处理,而低优先级更新任务所作的工作则会完全作废,然后等待机会重头再来
React Fiber 更新过程被分为两个阶段(Phase):第一个阶段 Reconciliation Phase 和第二阶段 Commit Phase
第一阶段,Fiber 会找到需要更新哪些 DOM,这个阶段可以被打算;但到了第二阶段,就会一鼓作气把 DOM 更新完,绝不会被打断
因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。
比如说,一个低优先级的任务 A 正在执行,已经调用了某个组件的 componentWillUpdate 函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务 B,接下来 React Fiber 就会去执行这个紧急任务 B,任务 A 虽然进行了一半,但是没办法,只能完全放弃,等到任务 B 全搞定之后,任务 A 重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate 函数会被再调用一次。
虚拟 DOM 是由 JSX 转译过来的,JSX 的入口函数是 React.createElement, 可操作空间不大, 第三大的底层 API 也非常稳定,因此我们只能改变第二层。
React16 将内部组件层改成 Fiber 这种数据结构,因此它的架构名也改叫 Fiber 架构。Fiber 节点拥有 return, child, sibling 三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟, 有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。