首先,本文只是本人在学习react-hooks
的知识汇总,个人描述偏少,借鉴的文章较多。所有参考的文章都在文末列出,如果看过本篇意犹未尽,可以具体阅读其他的参考文章。
1.使用hooks的目的
1.1 类组件的特点
- 大型组件很难拆分和重构,也很难测试。
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
- 组件类引入了复杂的编程模式,比如
render props
和高阶组件。 - 类组件需要创建类组件的实例。
1.2 函数组件的特点
- 不能包含状态
- 不支持生命周期方法
- 函数式组件不需要创建实例
1.3 函数式组件捕获了渲染所使用的值
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
结论:React Hooks
的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件。
2.从渲染开始
函数式组件只是返回一个 React Element
。我们渲染一个函数式组件时,只是执行了一次函数,得到了一个React Element
,并没有产生实例,这使得函数式组件的props
和state
等都是独立存在的,重新渲染函数式组件时只是重新执行了一次函数。
3.常用hooks
useState、useEffect、useLayoutEffect、useRef、useMemo、useCallback...
3.1 useState
使用方法:
const [state, setState] = useState(initState);
与类组件相同,更改组件状态时会导致组件重新渲染,这里调用useState返回的setState也会导致函数组件重新执行。
function Acount() {
console.log('---app run again----')
const [num, setNum] = useState(0)
console.log('---render----')
console.log(`num:${num}`)
return (
<div className='App'>
<p>{num}</p>
<p>
<button
onClick={() => {
setNum(num + 1)
}}
>
+1
</button>
</p>
</div>
)
}
点击两次按钮结果
可以在单个组件中使用多个 State Hook
使用多次useState
function Acount() {
console.log('---app run again----')
const [num1, setNum1] = useState(0)
const [num2, setNum2] = useState(0)
console.log('---render----')
console.log(`num:${num1}`)
console.log(`num:${num2}`)
return (
<div className='App'>
<p>{num1}</p>
<p>{num2}</p>
<p>
<button
onClick={() => {
setNum1(num1 + 1)
}}
>
num1 +1
</button>
<button
onClick={() => {
setNum2(num2 + 1)
}}
>
num1 +2
</button>
</p>
</div>
)
}
结果互不影响。
useState
的模拟实现(链表实现):
每次调用useState
时,会生成一个链表节点,会把初始化的状态存起来,并将每一次调用生成的节点链接成链表,由于闭包的作用,返回的setState
所能获取的节点都是那一次生产的链表节点,且每次重新渲染时会依照链表顺序进行遍历。
let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;
function useState(initState) {
let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};
function setState(newState) {
currentHook.memoizedState = newState;
render();
}
// 这就是为什么 useState 书写顺序很重要的原因
// 假如某个 useState 没有执行,会导致指针移动出错,数据存取出错
if (workInProgressHook.next) {
// 这里只有组件刷新的时候,才会进入
// 根据书写顺序来取对应的值
workInProgressHook=workInProgressHook.next;
} else {
// 只有在组件初始化加载时,才会进入
// 根据书写顺序,存储对应的数据
// 将 firstWorkInProgressHook 变成一个链表结构
workInProgressHook.next = currentHook;
// 将 workInProgressHook 指向 {memoizedState: initState, next: null}
workInProgressHook = currentHook;
}
return [currentHook.memoizedState, setState];
}
function Counter() {
// 每次组件重新渲染的时候,这里的 useState 都会重新执行
const [name, setName] = useState('计数器');
const [number, setNumber] = useState(0);
return (
<>
<p>{name}:{number}</p>
<button onClick={() => setName('新计数器' + Date.now())}>新计数器</button>
<button onClick={() => setNumber(number + 1)}>+</button>
</>
)
}
function render() {
// 每次重新渲染的时候,都将 workInProgressHook 指向 firstWorkInProgressHook
workInProgressHook = firstWorkInProgressHook;
ReactDOM.render(<Counter/>, document.getElementById('root'));
}
render();
由模拟实现看使用原则:
不要在循环,条件或嵌套函数中调用 Hook。
3.2 useReducer
使用方法:
const [state, dispatch] = useReducer(reducer, initState);
使用时dispatch(action)
来使用。具体使用方法同redux
。
适用情况:
主要时为了弥补useState
的不足
-
1.复杂的
state
操作 -
2.嵌套的
state
对象
示例:
改造前:全部使用useState
模拟了一次登录
function LoginPage() {
const [name, setName] = useState(''); // 用户名
const [pwd, setPwd] = useState(''); // 密码
const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中
const [error, setError] = useState(''); // 错误信息
const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录
const login = (event) => {
event.preventDefault();
setError('');
setIsLoading(true);
login({ name, pwd }) // 请求登录接口
.then(() => {
setIsLoggedIn(true);
setIsLoading(false);
})
.catch((error) => {
// 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
setError(error.message);
setName('');
setPwd('');
setIsLoading(false);
});
}
return (
// 返回页面JSX Element
)
}
使用useReducer
进行改造
将繁琐的setState
变成了action-reducer
,使得更改状态的逻辑更加统一,便于管理。
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
return (
// 返回页面JSX Element
)
}
相比之下代码有更好的可读性,我们也能更清晰的了解state
的变化逻辑。
在useEffect
中使用dispatch
时,不需要在依赖数组中引入dispatch
的依赖,因为React会保证dispatch
在组件的声明周期内保持不变,可以有效减少依赖数组的大小。
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);// 修改step会重启定时器
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
假如我们不想在step
改变后重启定时器,我们该如何从effect
中移除对step
的依赖呢?
依靠distpatch
在组件的生命周期内保持不变的特性,可以使用useReducer
来处理那些依赖于另一个状态的状态更新。
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
3.3 useEffect和useLayoutEffect
使用
1.useEffect(() => {
......
return cleanUpfunc
}, [deps]);
2.useLayoutEffect(() => {
......
return cleanUpfunc
}, [deps]);
依赖数组
同useState
的原理一样,useEffect
也应当拥有一个链式或其他形式的存储区用于存放本次渲染的状态,即依赖数组。通过依赖数组可以控制effects
在state
更新时的执行时机,避免effects
不必要的重复调用。
注:
-
1.依赖数组为空时,导致只有第一次渲染能够执行useEffect中的方法。
-
2.第二个参数的比较是一个浅比较。
-
3.
effect hooks
的返回值为清除函数,相当于componentWillUnmount
的作用
例子: 有一个输入框和一个提交按钮,点击提交之后将输入框的内容同步到Count
状态
function NewCounter() {
const [input, setInput] = useState(0)
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => {console.log(`count is ${count}`);}, 3000);
}, [count])
const handleChange = (e) => {
setInput(e.target.value)
}
return (
<div className="App">
<input type="text" value={input} onChange={handleChange}/>
<button onClick={() => setCount(input)}>提交</button>
</div>
);
}
这个例子在输入与上一次相同的内容时不会执行useEffect
的中方法。
执行时机
为了方便理解,一下对两种hooks的执行时机以及class的生命周期函数进行一次对比。
export default function LifeTime () {
const [item, setItem] = useState(0)
useEffect(() => {
console.log(item);
console.log('useEffect');
return () => {
console.log('effectDestory')
}
})
useLayoutEffect(() => {
console.log(item);
console.log('useLayoutEffect');
return () => {
console.log('LayoutEffectDestory')
}
})
function handleBtn() {
console.log('changeItem');
setItem(6)
}
return (
<div>
<button onClick={handleBtn}>change</button>
</div>
);
}
初次渲染的时候
点击按钮,更改item
,执行了组件销毁,并再次触发渲染
在父组件上引入一个类组件后进行对比,这里看似componentDidMount
的执行处于useLayoutEffect
和useEffect
之间。
实际上在更改了类组件与函数组件的顺序后,说明类组件的生命周期方法和useLayoutEffect
执行时机没有区别。
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
useLayoutEffect 在渲染时是同步执行,其执行时机componentDidMount
,componentDidUpdate
一致。
所以建议将修改 DOM
的操作放到 useLayoutEffect
里,以减少浏览器进行回流、重绘。
具体的执行过程: www.cnblogs.com/iheyunfei/p…
不过,正如DAN
在《useEffect
完整指南》中所说“effects的心智模型和componentDidMount以及其他生命周期是不同的,试图找到它们之间完全一致的表达反而更容易使你混淆。”
从根本上来讲,react-hooks
的作用是一种同步的作用,同步hooks函数内部的内容与外部的props以及state,所以才会在每次render
之后执行useEffect
里面的函数,这时可以获取到当前render
结束后的props
和state
,来保持一种同步。
3.4 useMemo和useCallback
useMemo和useCallback
主要用来解决使用React hooks产生的无用渲染的性能问题。由于使用function的形式来声明组件,失去了shouldCompnentUpdate
(在组件更新之前)这个生命周期,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。而且在函数组件中,也不再区分mount
和update
两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗
使用
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);
useMemo
和useCallback
都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks
都返回缓存的值,useMemo
缓存函数的返回值,useCallback
缓存的函数。
3.5 useMemo
看一个例子:
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
function expensive() {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return( <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>);
}
这里创建了两个state
,然后通过expensive
函数,执行一次计算,拿到count
对应的某个值。我们可以看到:无论是修改count
还是val,由于组件的重新渲染,都会触发expensive
的执行(能够在控制台看到,即使修改val
,也会打印);
但是这里的昂贵计算只依赖于count
的值,在val
修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo
,只在count
的值修改时,执行expensive
计算。
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return (<div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>);
}
使用useMemo来执行计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。
3.6 useCallback
由于函数式组件内的函数在每次渲染都不同,而useCallback本质上是添加了一层依赖检查。它解决了每次渲染都不同的问题,我们可以使函数本身只在需要的时候才改变。
使用场景
- 1.有一个父组件,其中包含子组件,子组件接收一个函数作为props
- 2.dom的事件处理函数
示例
组件中的getData方法通过props的形式传给子组件
// 用于记录 getData 调用次数
let count = 0;
function App() {
const [val, setVal] = useState("");
function getData() {
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}
return <Child val={val} getData={getData} />;
}
function Child({val, getData}) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
分析下代码的执行过程:
-
App
渲染Child
,将val
和getData
传进去
-
Child
使用useEffect
获取数据。因为对getData
有依赖,于是将其加入依赖列表
-
getData
执行时,调用setVal
,导致App
重新渲染
-
App
重新渲染时生成新的getData
方法,传给Child
-
Child
发现getData
的引用变了,又会执行getData
3 -> 5 是一个死循环
useEffect(() => {
getData();
}, []);
// 由于使用了外部传递的props,依据规则需要将getData放入依赖数组中。
// 如果装了 hook 的lint 插件,会提示:React Hook useEffect has a missing dependency
// 改变策略,对getData本身进行缓存
const getData = useCallback(() => {
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}, []);
有助于性能改善的,有 2 种场景:
- 函数
定义
时需要进行大量运算 - 需要比较引用的场景,如上文提到的
useEffect
,又或者是配合React.Memo
使用
const Child = React.memo(function({val, onChange}) {
console.log('render...');
return <input value={val} onChange={onChange} />;
});
function App() {
const [val1, setVal1] = useState('');
const [val2, setVal2] = useState('');
const onChange1 = useCallback( evt => {
setVal1(evt.target.value);
}, []);
const onChange2 = useCallback( evt => {
setVal2(evt.target.value);
}, []);
return (
<>
<Child val={val1} onChange={onChange1}/>
<Child val={val2} onChange={onChange2}/>
</>
);
}
上面的例子中,如果不用useCallback
, 任何一个输入框的变化都会导致另一个输入框重新渲染。
执行时机
**官方的提示:**传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
3.7 useRef
因为函数组件能够捕获props
和state
的特点,就需要有打破这个特点的方法。
使用方式
const FocusInput = function () {
const refInput = useRef()
const handleFocus = () => {
refInput.current.focus();
}
return (
<div>
<input type="" ref={refInput}/>
<button onClick={handleFocus}>Focus</button>
</div>
);
}
createRef 和 useRef 的使用方式完全一样
但其本质却不同。
function Ref () {
const [ renderIndex, setRenderIndex ] = useState(1);
const refFromUseRef = useRef();
const refFromCreateRef = React.createRef();
if(!refFromUseRef.current) {
refFromUseRef.current = renderIndex;
}
if(!refFromCreateRef.current) {
refFromCreateRef.current = renderIndex;
}
const handleClick = () => {
setRenderIndex(pre => pre + 1);
}
return (
<div>
<p>current render index {renderIndex}</p>
<p>
refFromUseRef<span>value: {refFromUseRef.current}</span>
</p>
<p>
refFromCreateRef<span>value: {refFromCreateRef.current}</span>
</p>
<button onClick={handleClick}>
re-render
</button>
</div>
);
}
useRef
在 react hook
中的作用, 它像一个变量, 类似于 this
, 它就像一个盒子, 你可以存放任何东西. createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
4.总结
-
1.由于闭包,几乎所有的
hook
都能捕获当前渲染的状态,state
和props
,每次渲染都有自己的事件处理函数 -
2.除了
state hook
,其他hook
都可以通过配置依赖项来优化执行次数。 -
3.可以通过
useRef
获取来消除由于闭包造成的每次渲染状态都不同的影响。