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>
}
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];
}
当然事情没那么简单,实际上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];
}
那么为什么不能破坏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>
}
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++;
}
总结
- 更新是如何发生:
调用useState,内部通过setState修改状态后,调用scheduleUpdate方法,从根节点执行完整的dom-diff比较,进行组件的更新。
- 为什么不能再条件语句或循环中使用Hook
从实现来看,每次hook的执行,都是从索引为0即第一个hook开始执行。也是依靠索引记录当前操作的Hook,假如使用条件语句或者循环,那么hook执行的顺序可能与我们在数组中存放的顺序不一致,就会乱掉。因此不能在条件语句或循环中使用Hook。