一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
前言
- 为什么 useEffect 第二个参数是空数组,就相当于
ComponentDidMount,只会执行一次? - 自定义的 Hook 是如何影响使用它的函数组件的?
Capture Value特性是如何产生的?
继实现useState钩子函数后,这是第二篇探讨hooks钩子函数的内容实现useEffect钩子函数
useEffect是干什么的?是处理副作用的,它相当于classComponent的三个life-cycle
componentDidMount、
componentDidUpdate、
componentWillUnmount
动手实现
useEffect初始化
useEffect接受两个参数,第一个参数是一个函数,第二个参数是一个数组,第二个参数非必传
function useEffect(callback:()=>void,arr?:any[]):void{
if(typeof callback !== 'function'){
throw new Error('the first parameter must be a function');
}
if(arr && !Array.isArray(arr)){
throw new Error('the second parameter must be a array');
}
}
首先我们实现没有参数的时候
useEffect(()=>{
console.log('good');
});
这个很好实现
此时我们只要判断一下arr是否为undefined就可以了
if(typeof arr==='undefined'){
callback();
}
然后我们实现监听一个state的时候
useEffect(()=>{
console.log('good');
},[count]);
其实这个的思路也很简单,就是判断一下数组中的count的值是否和上一次的值是否相等,如果相等,那么就不执行callback,如果不相等就执行callback
首先我们要做的就是存储[count],这样才能每次都进行对比
因为我们可能有多个useEffects,所以和实现useState的时候一样,先定义一个数组。
这是一个二维数组,因为useEffect的第二个参数是一个数组,我们要把这第二个参数整个存储到我们定义的二维数组里面去
let preArray:any[] = [];
let effectIndex:number = 0;
接着我们就要判断preArray[effectIndex]是否有值?
如果没有值得话,证明是第一次存储,此时要执行callback();
如果有值的话,进行对比,是否有一个值和以前不相等,只要有一个不相等,就执行callback()
// 如果当前index没有值,那么是第一次执行
if(!preArray[effectIndex]){
callback();
} else {
// 判断是否改变
let changed = arr.some((item,index)=>item !== preArray[effectIndex][index]);
if(changed){
callback();
}
}
然后更新preArray中的值,并把effectIndex++;
// 更新preArray中的值
preArray[effectIndex] = arr;
// effectIndex进行++
effectIndex++;
此时还存在一个问题
就是每次渲染组件effectIndex都会不停++;
所以在render中设置 effectIndex =0;
function render() {
// 更新组件
index = 0;
effectIndex =0;
ReactDOM.render(<App />, document.getElementById('root'));
}
此时我们在浏览器测试
useEffect(()=>{
console.log('good');
},[count]);
useEffect(()=>{
console.log('love');
},[name]);
并没有问题
接下来我们在看一下如果为空数组的时候是否有问题
也没有问题,因为我们已经实现了这个内容。
完整代码
import ReactDOM from 'react-dom';
let target: any[]=[];
let index = 0;
// preArray是一个二维数组,因为有多个effects,所以要存储多个useEffects的第二个参数
let preArray:any[] = [];
let effectIndex:number = 0;
function setState(currentIndex:number) {
return function (state: any) {
// update阶段
target[currentIndex] = state;
render();
}
}
function useState(initialState) {
let value = target[index] ? target[index] : initialState;
target[index] = value;
let set = setState(index);
index++;
return [value, set]
}
function render() {
// 更新组件
index = 0;
effectIndex =0;
ReactDOM.render(<App />, document.getElementById('root'));
}
function useEffect(callback:()=>void, arr?:any[]):void{
if(typeof callback !== 'function'){
throw new Error('the first parameter must be a function');
}
if(typeof arr==='undefined'){
callback();
} else {
if(arr && !Array.isArray(arr)){
throw new Error('the second parameter must be a array');
}
// 如果当前index没有值,那么是第一次执行
if(!preArray[effectIndex]){
callback();
} else {
// 判断是否改变
let changed = arr.some((item,index)=>item !== preArray[effectIndex][index]);
if(changed){
callback();
}
}
// 更新preArray中的值
preArray[effectIndex] = arr;
// effectIndex进行++
effectIndex++;
}
}
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('张三');
useEffect(()=>{
console.log('good');
},[]);
return (
<div>
<div>
count:{count}
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
<div>
name:{name}
<button onClick={() => setName('李四')}>点击</button>
</div>
</div>
);
}
export default App;
闲谈
我们在useEffect中写setInterval的时候会遇到一个bug,就是在setInterval中更新state,一直不会变
useEffect(()=>{
setInterval(()=>{
setCount(count+1);
},1000)
},[]);
比如如上代码,count初始值为0的话,那么无论如何执行setInterval中的函数,count都为1
这是为啥呢?
传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。
那么闭包的本质是什么?
当前环境存在指向父级作用域的引用;
如果子函数引用了父函数的作用域,那么父函数不会消失,他会以一个闭包的形式存储到堆中

我们可以看到App()被存到了堆中
那么当你访问count的时候,你会访问当前作用域的count,也就是你父级的count,他为0,所以这就是为啥在setInterval中更新state,一直不会变的原因