实现useEffect钩子函数(小白级教程,简单易懂)

152 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

前言

  1. 为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?
  2. 自定义的 Hook 是如何影响使用它的函数组件的?
  3. Capture Value特性是如何产生的?

实现useState钩子函数后,这是第二篇探讨hooks钩子函数的内容实现useEffect钩子函数

useEffect是干什么的?是处理副作用的,它相当于classComponent的三个life-cycle

componentDidMountcomponentDidUpdatecomponentWillUnmount

动手实现

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

那么闭包的本质是什么?

当前环境存在指向父级作用域的引用;

如果子函数引用了父函数的作用域,那么父函数不会消失,他会以一个闭包的形式存储到堆中

image.png

我们可以看到App()被存到了堆中

那么当你访问count的时候,你会访问当前作用域的count,也就是你父级的count,他为0,所以这就是为啥在setInterval中更新state,一直不会变的原因

参考