关于 hooks 使用过程中的疑问:
为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?
为什么只能在函数的最外层调用 Hook,不能在循环、条件判断或者子函数中调用?
自定义的 Hook 是如何影响使用它的函数组件的?
最简单的 useState 用法是这样的:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<Button onClick={() => { setCount(count + 1); }}>
点击
</Button>
</div>
);
}
基于 useState 的用法, 我们尝试着自己实现一个 useState
function useState(initialValue) {
let state = initialValue;
function setState(newState) {
state = newState;
render(); // 模拟 reRender
}
return [state, setState];
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<Button
onClick={() => {
setCount(count + 1);
}}
>
点击
</Button>
</div>
);
}
这时我们发现,点击 Button 的时候,count 并不会变化。
let _state; // 把 state 存储在外面
function useState(initialValue) {
_state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
useEffect
useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:
useEffect(() => {
console.log(count);
}, [count]);
我们知道 useEffect 有几个特点:
有两个参数 callback 和 dependencies 数组
如果 dependencies 不存在,那么 callback 每次 render 都会执行
如果 dependencies 存在,只有当它发生了变化, callback 才会执行
实现一个 useEffect
let _deps; // _deps 记录 useEffect 上一次的 依赖
function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
为什么第二个参数是空数组,相当于 componentDidMount ?
因为依赖一直不变化,callback 不会二次执行。
到现在为止,我们已经实现了可以工作的 useState 和 useEffect。
但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。
const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
我们需要可以存储多个 _state 和 _deps。我们可以使用数组,来解决 Hooks 的复用问题。初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
memoizedState是用来记录当前useState应该返回的结果的
更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
到这里,我们实现了一个可以任意复用的 useState 和 useEffect。
同时,也可以解答几个问题:
Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用?
A:memoizedState 数组是按hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
//示例
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
useState('banana'); //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
useState([{ text: 'Learn Hooks' }]); //...
//我们改一下结构
let showFruit = true;
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// 这样一来
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
// useState('banana');
useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错
Q:自定义的 Hook 是如何影响使用它的函数组件的?
A: 共享同一个 memoizedState,共享同一个顺序。