一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情
让我们从实现两个简单的hook入手来探究hooks原理
useState
useState用于在函数式组件中声明并保存一个变量,useState的使用是这样的:
const [count, setCount] = useState(0);
console.log(count);
setCount(1);
setCount((pre) + pre + 1);
有几个特点:
- 接受一个函数或值作为变量的初始值
- 返回一个数组(元组),第一个参数是变量值,第二个参数是一个函数,可用来更新变量值
- 返回的更新函数支持传入一个函数,改函数的参数是当前的变量值
据此,可以实现一版简单的useState
function useState(initialState) {
// 没有考虑传入一个函数的情况
let state = initialState;
const setState = (newState) => {
state = newState;
}
return [state, setState];
}
但在使用的时候会发现,当调用setCount的时候,count 并不会变化,这是因为我们没有存储state,导致每次渲染组件的时候,state都会重新设置
为解决这个问题,会自然而然地想到,把state提取出来,存在useState外面:
let _state;
function useState(initialState) {
_state = _state || initialState;
const setState = (newState) => {
_state = typeof newState === 'function' ? newState(_state) : newState;
};
return [
_state,
setState,
];
}
至此,实现了一个简单的useState,后边会进一步完善
useEffect
useEffect的使用是这样的
useEffect(() => {
// do something
});
useEffect(() => {
// do something
}, [])
useEffect(() => {
// do something
}, [deps])
useEffect的使用有几个特点:
- 有两个参数
callback和deps数组 - 如果
deps不存在,那么callback在每次render时都会执行 - 如果
deps存在,只有当它发生了变化,callback才会执行
根据使用方法和特点,可以做一个简单地实现:
let _deps;
function useEffect(callback, deps) {
const hasNoDeps = !deps;
const hasChangeDeps = _deps
? !deps?.every((dep, index) => _deps[index] === dep)
: true;
if (hasNoDeps || hasChangeDeps) {
callback();
_deps = deps;
}
}
到这里,我们又实现了一个可以工作的丐版useEffect,hook貌似没有那么难
优化
我们上边实现的两个简单的hook存在一个致命缺点,在一个组件内只能使用一次,对此,我们可以将_state和_deps保存至一个全局数组memoizedState 中,并用一个变量存储当前memoizedState下标
let memoizedStates = []; // hooks存放在这个数组
let cursor = 0; // 当前memoizedState下标
function useState(initialState) {
memoizedStates[cursor] = memoizedStates[cursor] || initialState;
const currentCursor = cursor;
const setState = (newState) => {
memoizedStates[currentCursor] = typeof newState === 'function'
? newState(memoizedStates[currentCursor])
: newState;
};
const res = [memoizedStates[cursor], setState];
cursor += 1;
return res;
}
function useEffect(callback, deps) {
const hasNoDeps = deps === undefined;
const preDeps = memoizedStates[cursor];
const hasChangedDeps = preDeps
? !deps.every((dep, index) => dep === preDeps[index])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedStates[cursor] = deps;
}
cursor += 1;
}
function resetCursor() {
cursor = 0;
}
function resetMemoizedStates() {
memoizedStates = [];
}
Not Magic, just Arrays
真正的React实现
虽然我们用数组基本实现了一个可用的Hooks,了解了Hooks的原理,但在React中,实现方式却有一些差异的。
- React中是通过类似单链表的形式来代替数组的。通过
next按顺序串联所有的hook memoizedState,cursor是存在哪里的?如何和每个函数组件一一对应的?
我们知道,React会生成一棵组件树(或Fiber单链表),树中每个节点对应了一个组件,hooks的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
type Hooks = {
// others
memoizedState: any, // useState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 deps | useRef 中保存的是 ref 对象
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一个hooks
}
解惑
Q. 为什么只能在函数最外层调用Hook,不要在循环、条件判断或者子函数中调用?
A. memoizedState是按hook定义的顺序来放置数据的,如果hook顺序变化,memoizedState并不会感知到
Q. 为什么useEffect第二个参数是空数组,在组件更新时回调只会执行一次?
A. 因为依赖一直不变化,callback不会二次执行
Q. 自定义的Hook是如何影响使用它的函数组件的?
A. 共享同一个memoizedState,共享同一个顺序
Q. Capture Value 特性是如何产生的?
A. 每一次rerender的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。即每次渲染(执行),都有它自己的xxx