React的Hooks API为我们提供了一种新的处理副作用的方式 ——useEffect
。useEffect
函数接受两个参数:一个是_副作用函数_和一个_依赖数组_。副作用函数是在组件render之后运行,而依赖数组告诉React何时应该执行或跳过该副作用。如果没有提供依赖数组,`useEffect`将在每次渲染后运行。如果提供了空的依赖数组,副作用将只在第一次渲染后及在卸载时运行。
jsx
useEffect(() => {
console.log('component did mount');
return () => {
console.log('component will unmount');
}
}, []);
实现原理
useEffect
是如何完成这些工作的呢?核心在于React如何决定何时调用副作用函数。这涉及到React对组件生命周期的管理。React使用一个队列存储所有的state和side-effect,以保证他们的正确执行顺序。在每一个render周期,React都会迭代这个队列,并比较当前的依赖数组和上一次的依赖数组。如果有任何更改,React就会运行新的副作用函数。在运行新副作用函数前,React会首先调用先前副作用的cleanup函数。
假设我们有一个计数器应用。每当计数器的数值改变时,我们希望能够显示更新的数值。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
在这个例子中,useEffect
接收了一个副作用函数,该函数会更新文档的title为当前的计数器的数值。另一个参数[count]
是一个依赖数组,告诉React仅当count
变化时才应运行该效应。为实现这个功能,React内部维护了队列来跟踪state
和side-effect
。首次渲染(或count
更新时),React会调用副作用函数,创建新的副作用并将其添加到队列中。由于这个副作用依赖于count
,React还会将当前的count
值与副作用一同存储。在下一次渲染时,如果count
的值已经发生变化,React会比较新旧count
值。由于这两者不同,React首先会清理旧的副作用,然后再调用新的副作用函数。这样就确保了每次count
值改变时,useEffect
都能正确地工作。这个例子应该能帮助您理解useEffect
工作原理的主要部分:如何跟踪副作用,如何比较新旧依赖值,如何根据依赖值的变化决定是否执行副作用函数,以及如何清理旧的副作用。
手写简易版useEffect
那么如果要自己实现一个简易版的`useEffect`,应该怎么做呢?这需要保持`deps`的跟踪,再根据`deps`是否改变来决定是否调用`effect`。我们可以采用以下方式实现:
let _deps; // 全局存储依赖
function useEffect(callback, deps) {
const hasNoDeps = !deps;
const depsChanged = _deps ?
!deps.every((el, i) => el === _deps[i])
: true;
if (hasNoDeps || depsChanged) {
callback();
_deps = deps;
}
}
这样,我们就实现了一个简易版的`useEffect`。这个版本的`useEffect`仅适用于理解`useEffect`的基本工作原理,它并没有实现如何处理cleanup函数,以及如何管理多个`useEffect`等更复杂的情况。