WHY
1.React 没有提供组件的可复用性行为
class组件解决此类问题的方案: 高阶组件
- 高阶组件(HOC) React 中用于复用组件逻辑的一种高级技巧。 不是 React API 的一部分, 是一种基于 React 的组合特性而形成的设计模式。
- 具体而言, 高阶组件是参数为组件,返回值为新组件的函数。
- 组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。 HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer
- 需要重新组织 组件的结构。
- 在React DevTools 中观察React 应用,会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。
- 尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。React DevTools在线模拟使用
class组件解决此类问题的方案:使用 Hook 从组件中提取状态逻辑\
- 使得这些逻辑可以单独测试并复用
- 无需修改组件结构的情况下复用状态逻辑
- 在组件间或社区内共享 Hook 很便捷
2.组件预编译会带来巨大的潜力 使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案,而且class 也给目前的工具带来了一些问题。e.g:class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,推荐函数式
3.使用 Hook 其中一个目的就是要聚合功能模块,不被生命周期函数分割
- 比如useState实现值与setState的映射关系
- 比如effect解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
函数式组件
函数式组件没有实例的概念,函数通过执行去渲染
//函数组件1.0
const Example = (props) => {
// 你可以在这使用 Hook
return <div />;
}
//函数组件2.0
function MyFunctionalComponent() {
// 你可以在这使用 Hook
return <input />;
}
Hook API
- 是 React 16.8 的新增特性
- Hook API 提供了一系列可嵌入函数式组件的复用逻辑代码,满足开发在函数组件中使用 state 以及其他的 React class组件的特性。hooks名字通常都以 use 开始
- React Hooks 是函数,在函数组件中使用,当React渲染函数组件时,组件里的每一行代码就会依次执行,Hooks 也就依次调用执行。
规定:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。可以在自定义的 Hook 中调用
一、基本的生命周期相关
useState
const [state, setState] = useState(initialState);
如果 initialState 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
- setState 函数用于更新 state,它接收一个新的 state 值并将组件的一次重新渲染加入队列。
- React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。
setState 同步or异步
React 中 setState 什么时候是同步的,什么时候是异步的?
- React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事 件,setTimeout/setInterval 等。
- 由 React 控制的事件处理程序,以及生命周期函数调用 setState异步更新 state 。
- 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接同步更新this.state还是放到队列中异步更新
- isBatchingUpdates默认是false,表示同步更新this.state,但是,有一个函数batchedUpdates会把isBatchingUpdates修改为true。
- 而当React在调用事件处理函数之前就会调用这个batchedUpdates,所以由React控制的事件处理过程setState不会同步更新this.state。
setState函数中传入函数还是数值
- 数值:3秒内在同一个异步队列内被优化为一次执行
- 函数:保有每一次handleClick被触发的执行
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
useEffect
- 在函数式组件的重新渲染是一次函数的执行过程,使用这个 Hook,react会保存传递的函数(即effect),定期调用effect。
- useEffect Hook 的功能等同于 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让应用看起来响应更快。
- 默认情况下,useEffect 在第一次渲染之后和每次更新之后都会执行(等同于同时使用componentDidMount 和componentDidUpdate ),此外也可以通过第二个参数控制它的触发条件。
- React 保证了每次运行 effect 的同时,DOM 都已经更新完毕,effect主要负责处理副作用
- 大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
- useEffect 放在组件内部可以在 effect 中直接访问 count state 变量(或其他 props),不需要特殊的 API 来读取。已经保存在函数作用域中。
对比JavaScript的类中方法没有绑定this,Hook 使用了 JavaScript 的闭包机制
副作用
副作用:数据获取,设置订阅以及手动更改 React 组件中的 DOM
- 例如:在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志
- 副作用操作的分类:需要清除的和不需要清除的。
无需清除的effect
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
需要清除的effect
- effect 可选的清除机制:如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它,如此可以将添加和移除订阅的逻辑放在一起。
- React 会在执行当前 effect 之前对上一个 effect 进行清除。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
why频繁的取消上一次effect的订阅
- 为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。
- 此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。 忘记正确地处理 componentDidUpdate 是 React 应用中常见的 bug 来源 例如:假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
跳过 Effect
- 传递数组作为 useEffect 的第二个可选参数
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
二、自定义hooks
render props 和高阶组件
想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式
- 当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样用这种方式可以将组件逻辑提取到可重用的函数中。
- 自定义 Hook,自定义 Hook 的名字应该始终以 use 开头。
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
三、数据管理相关
共享数据useContext
- 全局:ThemeContext = React.createContext(语义化的对象)
- 全局:ThemeContext.Provider的 value注入值
- 底层使用:useContext(ThemeContext)
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> );
}
全局数据管理方案:React.useReducer(处理数据方案)+React.useContext(跨层级通信)
处理数据useReducer(useState的代替)
相比useState,更好支持了 设置数据值的 方法隐射,适用于更复杂的数据处理场景,
- useState:对数据的处理是使用方自己掌控
- useReducer:useReducer自己定义 对数据的处理(Reducer),使用方通过传参一个type值来调用对应的方法
state:只读(所有修改都要通过action)
DomainData服务器响应数据、网络请求 UI APP级别
action:一个具有type属性和其他属性的js对象
reducer:接收初始化state和action的函数,响应不同类型的action,修改并返回state 发送给store
Object.assign({},newstate)
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};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
四、缓存优化相关 useCallback和useMemo
- 函数式组件中的state的变化都会导致整个组件被重新执行useCallback、useMemo和useEffect可以设置只有在依赖数据发生变化后,才会重新计算结果,起到缓存的作用
- 不使用memo和useMemo不应该会导致你的业务逻辑发生变化(memo和useMemo只是用来做性能优化),类似于类组件中的 shouldComponentUpdate
- 如果该函数或变量作为 props 传给子组件,请一定要用,避免子组件的非必要渲染,类似PureComponent的功能
- 第二个参数是用于触发-检测上下文中对应值是否变化,如果有变化则会重新声明回调函数,获取静态作用域的值。
- 如果第二个参数为空数组,则只会在component挂载即componentDidMount运行。如果不存在这个参数,则会在每次渲染时运行。
- React.memo 和 React.useCallback 需要配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也是要消耗那么一点点点的性能。
useMemo 缓存值
useMemo(计算函数,依赖项数组)
- useMemo和useEffect的执行时机有区别:
useMemo缓存计算后的状态。返回值直接参与渲染的,所以useMemo是在渲染期间完成的
useEffect执行的是副作用,所以在渲染之后执行的。
- 计算函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback 缓存函数(缓存函数作用域,减少函数创建的性能消耗)
inline 函数
- 因为 onClick 使用了 inline 函数,所以 PureComponent 默认的浅比较也同样失去了意义。
- 该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。
- useCallback 缓存了每次渲染时 inline callback 的实例
<button onClick={() => this.handleClick()}>
Click me
</button>
const handleClick = useCallback(
(value: any) => {
const targetOption: any = options.find(o => o.value === value);
if (targetOption) {
setInstantEditing(targetOption.data);
}
},
[options],
);
五、其他
useRef
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
- 当 ref 对象内容发生变化时,
useRef并不会通知你 - 给ref赋值一个回调函数,可以实现ref 对象内容发生变化时通知。例如测量DOM的需求
测量DOM
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}