react
数据绑定
react 没有数据绑定,而是通过对比更新前后的数据状态来进行更新,参考下文的 diffing 算法
事件绑定与原生的区别
react 中的事件为合成事件,通过初始化使它们在不同的浏览器中拥有一致的属性,使用 naviteEvent 属性可获取对应的原生事件(e.naviteEvent)
- 无法通过 return false 阻止默认行为,必须显示使用 preventDefault
- react 的事件处理函数都在冒泡阶段被触发,如需注册捕获阶段的函数,需为事件名添加 Capture,如 onClickCapture
- react 没有将事件真正的绑定到 DOM 上,而是在 document 处监听所有事件,当事件触发时,使用统一的分发函数执行指定事件函数
props 与 state 的区别
一个重要的不同点:props 是传递给组件的,类似于函数的形参,而 state 是在组件内部,被组件自己管理的,类似于函数内部声明的变量
| props | state | |
|---|---|---|
| 从父组件获取值 | 是 | 是 |
| 由父组件更改 | 是 | 否 |
| 在组件内部设置默认值 | 是 | 是 |
| 在组件内部更改 | 否 | 是 |
| 为子组件设置初始值 | 是 | 是 |
| 更改子组件 | 是 | 否 |
生命周期
Mounting、Updating、Unmounting
Mounting(挂载)
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下
constructor
避免在构造函数中引入任何副作用或订阅,如果不初始化 state 或不进行方法绑定,则不需要为组件实现构造函数,构造函数仅用于以下两种情况
- 通过给 this.state 赋值对象来初始化内部 state
- 为事件处理函数绑定实例
static getDerivedStateFromProps()(不常用)
每次渲染都会被调用,该函数返回一个对象来更新 state,如果返回 null 则不更新任何内容
此方法适用于罕见用例,即 state 的值在任何时候都取决于 props。如实现 <Transition> 组件
由于此方法会产生派生状态,从而导致代码冗余,且组件难以维护,因此尽量使用如下方法替代
- 如果需要执行副作用,以响应 props 中的更改,请改用 componentDidUpdate
- 如果只想在 prop 更改时重新计算某些数据,请使用 memoization helper 代替
- 如果你想在 prop 更改时重置某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控代替
render()
class 组件中唯一必须实现的方法,应该为纯函数,当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素:通常通过 JSX 创建
- 数组或 fragments:使得 render 方法可以返回多个元素。fragments,即
<Fragment /> - Portals:可以渲染子节点到不同的 DOM 子树中
- 字符串或数值类型:会被渲染为文本节点
- 布尔类型或 null:什么都不渲染
componentDidMount()
在组件挂载后立即调用,依赖于 DOM 节点的初始化放在此处
Updating(更新)
当组件的 props 或 state 发生变化时会出发更新,调用顺序如下
static getDerivedStateFromProps()
shouldComponentUpdate()(不常用)
此方法仅作为性能优化的方法而存在
根据该函数的返回值,默认为 true,判断组件的输出是否受当前 state 或 props 更改的影响,默认行为是 state 每次变化都会重新渲染
当该函数返回 false,则不会调用 render(),所以也不会调用 componentDidUpdate()
render()
getSnapshotBeforeUpdate() (不常用)
在最近一次更新渲染输出之前调用,它使得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置)
componentDidUpdate()
当组件更新后立即调用,首次渲染不会执行此方法,如果组件实现了 getSnapshotBeforeUpdate(),则它的返回值将作为 componentDidUpdate() 的第三个参数 snapshot 参数传递
Unmounting(卸载)
当组件从 DOM 中移除时会调用
componentWillUnmount()
在组件卸载及销毁之前调用
错误处理
Error boundaries 是 react 组件,它会在其子组件树中的任何位置捕获 js 错误,并记录这些错误,展示降级 UI 而不是崩溃的组件树
当渲染过程、生命周期、子组件的构造函数中抛出错误时,会调用,当组件定义了以下两个方法时(任何一个或者两个),它就成为了 Error boundaries
仅将该组件用于处理从意外异常中恢复的情况,不要将他们用于流程控制,仅捕获组件中的错误,自身的错误无法捕获
static getDerivedStateFromError()
该方法在渲染阶段调用,因此不允许出现副作用
componentDidCatch()
该方法在提交阶段被调用,因此可以出现副作用
该方法在组件抛出错误后被调用,接收两个参数:
- error:抛出的错误
- info:带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息
其他两个 API
这两个方法是在组件中手动调用的
setState()
forceUpdate()
调用该方法将强制更新组件,会跳过 shouldComponentUpdate(),尽量避免使用该方法,应该按照正常流程
react hook 对应的生命周期
| class 组件 | hooks 组件 |
|---|---|
| constructor | useState |
| getDerivedStateFromProps | useState 里面的 update 函数 |
| shouldComponentUpdate | useMemo |
| render | 函数本身 |
| componentDidMount | useEffect |
| componentDidUpdate | useEffect |
| componentWillUnmount | useEffect 里面的返回函数 |
| componentDidCatch | 暂无 |
| getDerivedStateFromError | 暂无 |
常用 hook 介绍
useEffect
第一个参数为一个函数,第二个参数为一个变化参数数组
当第二个参数传入空数组 [] 时,将只执行一次,此时对应 componentDidMount
当第二个参数不传时,则对应 componentDidUpdate,将会在每次更新之后都调用,当第二个参数传入一个带值的数组,则只在该值变化时才更新
第一个参数的返回函数对应 componentWillUnMount,该清除机制实际上是清除上一个 effect 的结果,在清除之后才执行更新
useEffect 执行两次
react 18 默认情况下,useEffect 会执行两次
- 开发模式下会出现
strict mode下会出现
使用自定义 hook 解决
function useEffectOnce(effect: () => void | (() => void)) {
const destroyFunc = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const renderAfterCalled = useRef(false);
const [val, setVal] = useState<number>(0);
if (effectCalled.current) {
renderAfterCalled.current = true;
}
useEffect(() => {
// only execute the effect first time around
if (!effectCalled.current) {
destroyFunc.current = effect();
effectCalled.current = true;
}
// this forces one render after the effect is run
setVal(val + 1);
return () => {
// if the comp didn't render since the useEffect was called,
// we know it's the dummy React cycle
if (!renderAfterCalled.current) {
return;
}
if (destroyFunc.current) {
destroyFunc.current();
}
};
}, []);
}
或者简单更改
const firstRenderRef = useRef(true);
useEffect(() => {
// React 18 开发环境下默认会执行两次,使用该方法限制第二次执行
if (firstRenderRef.current) {
firstRenderRef.current = false;
return;
}
const getData = async () => {
const res = await API.getBingPictures({ n: 8, mkt: 'zh-CN' });
setPictures(res.images);
};
getData();
}, []);
useState
该钩子主要用来初始化 state,对应了 constructor,函数式组件不需要构造函数,此处只是简单的对应
同时也可以实现 getDerivedStateFromProps,记住 getDerivedStateFromProps 的作用:在每次渲染的时候都会调用,但仅在返回 true 时才更新,所以可以用以下代码模拟实现
function scrollview({ row }) {
const [isScrollDown, setIsScrollDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
setIsScrollDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `scrolling down: ${isScrollDown}`;
}
useContext
用来获取 React.createContext 的值,此时可以不通过 Context.Consumer 来获取值
// 跨文件组件使用时,需要使用公共 context 文件
// 公共文件 context.js
import { createContext } from 'react';
const Context = React.createContext(null);
export default Context;
// 父组件
import Context from './context.js';
<Context.Provider value={value}>
<Child />
</Context.Provider>;
// 子组件
import Context from './context.js';
const count = useContext(Context);
console.log(count);
// 若未使用 useContext 则需要通过以下写法来获取值
<Context.Consumer>{(value) => value}</Context.Consumer>;
useReducer
语法:const [state, dispatch] = useReducer(reducer, initialState, init)
- useState 的替代方案,接收一个形如
(state, action) => newState的 reducer,并返回当前的 state 及配套的 dispatch(类似 redux)
dispatch 函数是稳定的,不会在组件重新渲染时改变,所以可以安全的从 useEffect 和 useCallback 依赖列表中省略 disptach
普通初始化
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
<button onClick={() => dispatch({ type: 'increment' })}>增加count</button>;
惰性初始化
传入第三个参数,此时初始值会被设置为 init(initialState)
function init(initialState = 0) {
return { count: initialState };
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState, init);
<button onClick={() => dispatch({ type: 'reset', payload: initialState })}>重置count</button>
<button onClick={() => dispatch({ type: 'increment' })}>增加count</button>
- 可以用来模拟 forceUpdate,但尽量避免使用
const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);
function handleClick() {
forceUpdate();
}
useCallback
返回一个记忆化的回调函数
语法
// 仅当第二个参数的依赖项(a,b)不改变时才返回同一个函数
const memoizedCallback = useCallback(() => {
doSomeTime(a, b);
}, [a, b]);
// 如果要记住一个变化的值,需要 useRef 配合
const [text, setText] = useState('');
const ref = useRef();
useEffect(() => {
// 写入 ref
ref.current = text;
});
const memoizedCallback = useCallback(() => {
// 从 ref 读取
const currentText = ref.current;
}, [ref]);
useCallback(fn, inputs),全等于 useMemo(() => fn, inputs)
useCallback 返回函数,useMemo 返回值
useMemo
返回一个记忆化的值,相当于 vue 的计算属性(computed),仅当某个属性变化时才重新计算
语法 const func = useMemo(()=>fn, inputs)
useRef
返回一个可变的 ref 对象,它的 current 属性被初始化为传入的参数,ref 对象在组件的整个生命周期内保持不变
语法 const ref = useRef(initialValue)
useImperativeHandle
让你在使用 ref 时自定义暴露给父组件的实例值,在大多数情况下,应当避免使用 ref 这样的命令式代码,该函数应当与 forwardRef 一起使用,因为 ref 无法直接挂载到函数式组件上,被挂载的组件需要使用 forwardRef 包裹
语法 useImperativeHandle(ref, createHandle, [inputs])
说明:
- ref:一个 ref 对象,通常由 useRef() 生成
- createHandle:回调函数,必须有返回值,返回值通常为对象
- [inputs]:依赖列表
useLayoutEffect
与 useEffect 相同,但仅在所有 DOM 变更之后才同步调用 effect,当使用 useEffect 出现问题后,才考虑使用 useLayoutEffect
执行时机
- useLayoutEffect 执行在类组件生命周期前
- useEffect 执行在类组件生命周期后
渲染
- useLayoutEffect 同步渲染,会阻塞 DOM
- useEffect 异步渲染,不会阻塞 DOM
实现 shouldComponentUpdate
const Button = React.memo((props) => {
// 组件
});
虚拟 DOM 及内核
一种编程概念,UI 以一种虚拟的形式被保存在内存中。通俗的讲:通过某种算法,将真实的 DOM,转换成某个变量,然后将其存放在内存中,当需要时,再转换成真实的 DOM
react 中,react 元素与 fibers 内部对象被认为是虚拟 DOM 实现的一部分
- react 元素:最小单元,是个普通对象,如
const element = <h1>hello, world</h1>; - fibers 对象:react 内部对象,用于存放组件树的附加信息
Shadow DOM:一种浏览器技术,在 web 组件中封装变量和 css Virtual DOM:由 js 类库基于浏览器 API 实现的概念
diffing 算法
两个假设的基础:
- 两个不同类型的元素会产生出不同的树
- 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定
当对比两颗树时,首先比较两棵树的根节点,不同类型的根节点元素会有不同的形态
- 当根节点为不同类型的元素时,拆卸原有的树,并建立新树,其中的 DOM 节点也将被销毁
- 当根节点为相同类型的元素时,保留 DOM 节点,仅对比有改变的属性,然后递归子节点
对比同类型的组件元素
当一个组件更新时,组件实例保持不变,react 将更新该组件实例的 props 以跟最新的元素保持一致
优化
- 保持 DOM 结构的稳定,尽可能的减少动态操作 DOM 结构
- 尽量选择 css 隐藏或显示,而不是真的移除或添加节点
- 酌情使用 shouldComponentUpdate() 来减少不必要的更新
- 结构类似的尽量封装成组件
- 对于列表结构,尽量减少节点移动操作
通信
父向子通信
父组件通过 props 向子组件传递
子向父通信
父组件向子组件传递函数,子组件调用该函数并返回值,父组件接收该返回值
兄弟通信
通过相同的父组件来实现
跨级通信
使用 Context,具体见下文
示例
// 创建 context
const PriceContext = React.createContext('price');
// A 组件
class ClassA extends Component {
constructor() {
super();
this.state = { price: 0 };
}
// 点击事件
clickGoods(e) {
this.setState({
price: e,
});
}
render() {
const { price } = this.state;
return (
// 传递 price
<PriceContext.Provider value={price}>
<button onClick={this.clickGoods.bind(this, 100)}>
goods1
</button>
<button onClick={this.clickGoods.bind(this, 1000)}>
goods2
</button>
</PriceContext.Provider>
);
}
}
// B 组件
class ClassB extends Component {
render() {
return (
<div>
<span>price:</span>
<span>
<ClassC />
</span>
</div>
);
}
}
// C 组件
class ClassC extends Component {
render() {
return (
// Consumer 需要包含一个函数
<PriceContext.Consumer>
{(price) => <span>{price}</span>}
</PriceContext.Consumer>
);
}
}
Context
Context 提供了一种无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法
何时使用
对于某个组件树而言,属于全局的数据,它的所有子组件都要访问同一批数据,并且能访问到后续的更新
// 创建一个 context,默认值 light
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用 Provider 将当前的 theme 传递给以下的组件树,无论多深,都可以读取这个值
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间组件无需再次传递 theme
function Toolbar() {
return (
<div>
<ThemeButton />
</div>
);
}
class ThemeButton extends React.Component {
// 指定 contextType 读取当前的 theme context
// react 会往上找到最近的 theme Provider,然后使用他们的值
static contextType = ThemeContext;
render() {
return <Button theme={this.contextType} />;
}
}
主要 API
React.createContext
创建 context:
const MyContext = React.createContext('value')Context.Provider
每个 context 都会包含一个 Provider 组件,通过该组件传递值:
<MyContext.Provider value='value'> // 组件 </MyContext.Provider>子组件通过 MyContext 获取值,react 会自动寻找最近的 Provider 作为值
Context.Consumer
订阅 context 的改变,需要一个函数式组件作为子元素,该函数接收 context 的当前值,并返回一个 react 节点