在上一篇文章中,咱们谈到 Hooks 给 React 带来的一些在开发体验上的改变,若是你已经开始尝试 React Hooks,也许你会跟我同样碰到一个使人疑惑的地方,若是没有的话,那就再好不过啦,我就权当作个记录,以便他人之需。javascript
咱们先以官方的例子开始:java
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
复制代码
看到onClick
绑定的那个匿名函数了吗?这样写的话,每次 render 的时候都会从新生成一个新的函数。这在以前可能不须要太在乎,由于咱们通常只是拿 Function Component 来实现一些展现型组件,在其之下不会有太多的子组件。可是若是咱们拥抱 Hooks 以后,那么就不可控了。react
虽说在通常状况下,这并不会形成太大的性能问题,并且 Function Component 自己的性能就要比 Class Component 更好一点,可是不免会碰到须要优化的时候,比方说在重构原来的 Class Component 的时候,其中有个子组件是个PureComponent
,便会使子组件的这个优化失效 ,那么怎么解决呢?git
使用useCallback
或useMemo
来保存函数的引用,避免重复生成新的函数github
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count => count + 1)
}, []);
// 或者用useMemo
// const handleClick = useMemo(() => () => {setCount(count => count + 1)}, []);
return (
<div> <p>count: {count}</p> {/* Child为PureComponent */} <Child callback={handleClick} /> </div> ) } 复制代码
可见useCallback(fn, inputs)
等同于useMemo(() => fn, inputs)
,那么这两个 Hook 具体是怎么作到的呢?咱们能够从源码中一窥究竟,咱们以useCallback
为例(useMemo
大致上都是同样的,就返回值不一样,后面会提到)。ide
首先,在第一次执行useCallback
时,React内部会调用ReactFiberHooks
中的mountCallback
,以后再次执行时调用的都是updateCallback
,具体代码能够看这里:github.com/facebook/re…函数
咱们一点点来看,先看下mountCallback
:post
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
复制代码
发现核心在于mountWorkInProgressHook
这个方法性能
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
复制代码
代码比较简单,就不一一解释了,从上面的代码咱们能够得知 Hooks 的本体:优化
const hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
}
复制代码
咱们主要关注memoizedState
和 next
, memoizedState
在不一样的 Hook 中存放的值会有所不一样,在useCallback
中存的就是入参的值[callback, deps]
,next
的值就是下一个 hook,也就是说 Hooks 其实就是一个单向链表,这也就解释了为何 Hooks 须要在顶层调用,不能在循环、条件语句、嵌套函数中使用,由于须要保证每次调用的顺序一致。
再来看以后的updateCallback
:
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 这个hook就是第一次mount的hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 因此这里的memoizedState就是mount时候存着的[callback, deps]
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 比较两次的deps,相同的话就直接返回以前存的callback,而不是新传进来的callback
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
复制代码
useMemo
的实现与useCallback
相似,大概看一下:
function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 与useCallback不一样的地方就是memoizedState中存的是nextCreate执行以后的结果
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
// 返回执行结果
return nextValue;
}
function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 这里也同样,存的是nextCreate执行以后的结果
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
// 返回执行结果
return nextValue;
}
复制代码
由以上代码即可以看出useCallback
和useMemo
在用法上的区别了。
除了这两个方法之外,还能够经过context
来传递由useReducer
生成的dispatch
方法,来避免直接传递callback
,由于dispatch
是不变的。这个方法跟前面两种有本质上的区别,它从源头上就阻止了callback的传递,因此也就不会有前面提到的性能方面的顾虑,这也是官方推荐的方法,特别是组件树很大的状况下。因此上面的代码若是经过这种方式来写的话,就会是下面这样,有点像Redux
:
import React, { useReducer, useContext } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
default:
throw new Error();
}
}
const TodosDispatch = React.createContext(null);
function Counter() {
const [state, dispatch] = useReducer(reducer, {count: 0});
return (
<div> <p>count: {state.count}</p> <TodosDispatch.Provider value={dispatch}> <Child /> </TodosDispatch.Provider> </div> ) } function Child() { const dispatch = useContext(TodosDispatch); return ( <button onClick={() => dispatch({type: 'increment'})}> click </button> ) } 复制代码
useCallback
或useMemo
来优化。callback
的层级可能会很深,能够经过useReducer
配合context
来处理。以上只是我我的的一些想法,若有不对之处,欢迎指正~~
原文连接:浅谈 React Hooks(二)