理解hooks原理 ---简单实现useState/useEffect
Hooks 是什么?
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其 他的 React 特性。也是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。
在《effect 范式下的组件状态和依赖》中,useState/useEffect 是被最多提及的。
useState / useEffect 是驱动 render 触发和运行的基础
useState 是如何实现的?
先回想下,useState 是如何调用的?
const Components = () => {
const [value, setValue] = useState(0);
return <button onClick={() => setValue(1)}>{value}</button>;
};
javascript
useState 实现了:
- 传入了一个初始状态
- 返回一个数组,一个初始值和调用 set 更新之后的值
- 调用 set 方法时,替换原来 state 状态,类似于 class 组件里 this.setState
// 定义初始值
let currenInitValue;
function _UseState(initialValue) {
// 输入值/默认初始值
const state = currenInitValue || initialValue;
const setState = (newValue) => {
// 将新的值重新覆盖 更新state
currenInitValue = newValue;
// 触发视图更新
render();
};
// 返回数组形式,解构可写成任意变量
return [state, setState];
}
javascript
当然事情没那么简单,实际上 useState 在整个 app 中,甚至时单个组件内都通常都不会值调用一次,那将如何实现呢?当然不要破坏 useState 执行顺序。
// 下标
let index = 0;
// 利用收纳盒原理。存储调用者不同的存储值
let currenInitValueBox = [];
function _UseState(initialValue) {
// 每用一次进行➕1
index++;
// 利用闭包维护函数调用位置
const currentIndex = index;
currenInitValueBox[currentIndex] =
currenInitValueBox[currentIndex] || initvalue;
const setValue = (newValue) => {
// 更新state
currenInitValueBox[currentIndex] = newValue;
// 触发视图更新
render();
};
return [currenInitValueBox[currentIndex], setValue];
}
javascript
那么为什么不能破坏 useState 的顺序呢?
从实现来看,每次 hook 的执行,都是从索引为 0 即第一个 hook 开始执行。也是依靠索引记录当前操作的 Hook,假如使用条件语句或者循环,那么 hook 执行的顺序可能与我们在数组中存放的顺序不一致,就会乱掉。因此不能在条件语句或循环中使用 Hook。
useEffect 如何实现?
useEffect 是如何调用的?
const Components = () => {
const [value, setValue] = useState(0);
useEffect(() => {...}, [value])
return <button onClick={() => setValue(1)}>{value}</button>
}
javascript
useEffect 原理是什么?
useEffect 在依赖发生变化时,执行回调函数,这个变化是本次 render 和上次 render 时的依赖比较当然我们需要:
- 参数是回调函数,依赖以数组的形式
- 存储上一次 render 时的依赖
- 兼容多次调用,同一个组件下可能会有多次使用
- 比较本次 render 和上一次 render 依赖,执行回调
- 增加副作用清除(effect 触发后会将清除函数暂存起来,等下次触发时执行)
let index = 0;
// 同一组件下可能会出现多个useEffect使用,以数组的形式存储
let lastDepsBox = [];
let lastClearFnCallback = [];
/**
*
* @param {callback} fn 回调函数
* @param {Array} deps 依赖
*/
function UseEffect(fn, deps) {
// 存储上一次的依赖 存储的是[[]、[]、[]]
const lastDeps = lastDepsBox[index];
// 记录状态变化
const flag =
!lastDeps || // 首次渲染 刚开始就会触发
!deps || // 没有依赖,次次触发
deps.some((dep, index) => dep !== lastDeps[index]); // 依赖进行比较
if (flag) {
lastDepsBox[index] = deps;
// effect触发后会将清除函数暂存起来,等下次触发时执行
if (lastClearFnCallback[index]) {
lastClearFnCallback[index]();
}
// 将清除函数暂存起来
lastClearFnCallback[index] = fn();
}
index++;
}
javascript
总结
- 更新是如何发生:
调用useState,内部通过setState修改状态后,调用scheduleUpdate方法,从根节点执行完整的 dom-diff 比较,进行组件的更新。
- 为什么不能再条件语句或循环中使用 Hook
从实现来看,每次 hook 的执行,都是从索引为 0 即第一个 hook 开始执行。也是依靠索引记录当前操作的 Hook,假如使用条件语句或者循环,那么 hook 执行的顺序可能与我们在数组中存放的顺序不一致,就会乱掉。因此不能在条件语句或循环中使用 Hook。