正确使用React组件缓存

简介

正常来讲的话当我们点击组件的时候,该组件以及该组件的子组件都会重新渲染,但是如何避免子组件重新渲染呢,我们经常用memo来解决

React.memo配合useCallback缓存组件

  • 父组件没有传props
const Index = ()=> {
  console.log('子组件刷新了');
  return (
    <div>
      这是子组件
    </div>
  )
}
//这里我们用react.memo对组件进行包裹,包裹一次之后react在render的过程中不会给该fiber打上更新的tag
//从而跳过更新,这个原理其实就是react.memo的第二个参数上,如果react.memo第二个参数不传递,react回默
//认给我们补充上第二个参数的逻辑,其中逻辑就是浅比较Index组件的props参数,如果相等的话默认第二个参数返
//回true,组件就会缓存了,如果不相等的话就会返回false组件就会重新打上更新的tag然后重新渲染。
const MemoIndex = React.memo(Index);

const App = ()=>{
  const [state, setState] = useState(0);
  return (
    <div className="App">
      <button onClick={()=>setState(state+1)}>点我看看子组件刷新了吗</button>
      <MemoIndex/>
    </div>
  );
}

  • 父组件传了state
const Index = ()=> {
  console.log('子组件刷新了');
  return (
    <div>
      这是子组件
    </div>
  )
}
//其实这Index组件不会更新的,react检测到我们不传递第二个参数的话,会把之前的props拆出来,和现在的
//props做比较 发现pre.next === cur.next 然后回返回true组件就会缓存了
const MemoIndex = React.memo(Index);
//这行代码就相当这样的代码
const MemoIndex = React.memo(Index, (pre, cur)=> {
    //这样写的就比较简单了,因为这是是针对于当前的demo来说的。react比较的代码逻辑比较复杂,因为react
    //需要考虑到多种情况,props中参数可能多一个少一个的情况,所以react默认提供的代码比较复杂
    if(pre.name === cur.name) {
        return true;
    }
    return false;
});

const App = ()=>{
  const [state, setState] = useState(0);
  return (
    <div className="App">
      <button onClick={()=>setState(state+1)}>点我看看子组件刷新了吗</button>
      <MemoIndex name={0}/>
    </div>
  );
}

  • 父组件传了函数
const Index = ()=> {
  console.log('子组件刷新了');
  return (
    <div>
      这是子组件
    </div>
  )
}
const MemoIndex = React.memo(Index);

const App = ()=>{
  const [state, setState] = useState(0);
  const func = ()=> {

  };
  return (
    <div className="App">
        //这时候子组件会不会刷新呢,有的同学可能说不会因为浅比较发现pre.func === cur.func 返回
        //true所以不会刷新,但是其实是会刷新的,因为APP组件中触发了setState之后App组件重新渲染,也
        //就是相当于执行了App()这个方法,所以里面func的指向地址发生了变化,所以pre.func !== 
        //cur.func  子组件会重新渲染,
      <button onClick={()=>setState(state+1)}>点我看看子组件刷新了吗</button>
      <MemoIndex func={func}/>
    </div>
  );
}

使用useCallback缓存函数

const Index = ()=> {
  console.log('子组件刷新了');
  return (
    <div>
      这是子组件
    </div>
  )
}

const MemoIndex = React.memo(Index);

const App = ()=>{
  const [state, setState] = useState(0);
  //这里我们用了useCallback,useCallback主要是缓存我们当前的函数,如果我们第二个参数传递空数组的话
  //他的地址不会改变,如果我们第二个参数传递的是一个变量,这个变量发生变化他的地址就会发生变化。所以这
  //和useEffect的第二个参数是一样的,但是请注意不要滥用useCallback的第二个参数。如果第二个参数滥用
  //会拿到我们之前的值。我们看下一个示例就知道了
  const func = useCallback(()=> {

  }, [])
  return (
    <div className="App">
      <button onClick={()=>setState(state+1)}>点我看看子组件刷新了吗</button>
      <MemoIndex func={func}/>
    </div>
  );
}

使用useMemo缓存组件

useMemo不仅可以缓存变量,函数还可以缓存组件

const Index = (props)=> {
  console.log('子组件刷新了');
  return (
    <div>
      这是子组件
    </div>
  )
}

const App = ()=>{
  const [state, setState] = useState(0);
  //使用useMemo也要和useCallback一样特别注意第二个参数,因为他有可能导致我们拿不到最新的数据解决
  //解决方案就和useCallback的一样,简单来说套用react官方的话就是请确保数组中包含了所有外部作用域
  //中会随时间变化并且在useMemo中使用的变量都要放到第二个参数中。
  const Component = useMemo(()=><Index/>, []);
  return (
    <div className="App">
      <button onClick={()=>setState(state+1)}>点我看看子组件刷新了吗</button>
      {
        Component
      }
    </div>
  );
}

利用props.children缓存组件

这样在Index组件re-render的时候,由于App(父组件)中的组件没有变化,所以拿到的children依然是上一次的(没有发生变化的)所以children部分不会re-render。

const Index = (props)=> {
  const [state, setState] = useState(0);
  return (
    <div>
       //当这个按钮点击之后我们发现Children组件并不重新刷新了,其实原理我理解的是react帮我们做了一层
       //处理当渲染前与渲染后两个组件的引用地址一样他就会放弃render,当然这是我的猜测,这个的话之后
       //我看到源码的时候会和大家讲一下在补充一下。
      <button onClick={()=>setState(state+1)}>点我我看看子组件刷新不刷新</button>
      {props.children}
    </div>
  )
};
const Children = ()=> {
  return (
    <div>
      {console.log('子组件刷新了')}
      这是children组件
    </div>
  )
}

const App = ()=>{
  return (
    <div className="App">
      <Index>
        <Children/>
      </Index>
    </div>
  );
}

注意事项

  • useCallBack不是每个函数都需要使用!不要滥用useCallback
const Index = (props)=> {
  console.log('子组件刷新了');
  return (
    <div>
        //点击这个按钮之前先点击App组件下面的按钮,让state变大,然后在点击这个按钮看看state是啥
        //我们发现state一直是一个0。这是为什么呢,因为很简单我们之前讲了useCallback第二个和
        //useEffect的第二个参数是一样的,因为我们传递的是空数组说以useCallback一直拿到的是最原始的
        //值,所以会造成这个问题,我们写代码的时候千万要注意第二个参数,只要useCallback需要什
        //值我们就在第二个参数传递什么值,这样才可以确保我们拿到的是最新的值。同样的里面如果不需要一些
        //参数的话我们也不要把这些参数加到第二个参数上面否则会出现func的地址多次改变。
      <button onClick={props.func}>点我看看state是啥</button>
      这是子组件
    </div>
  )
}
const MemoIndex = React.memo(Index);

const App = ()=>{
  const [state, setState] = useState(0);
  const func = useCallback(()=> {
    console.log(state);
  }, [])
  return (
    <div className="App">
      <button onClick={()=>setState(state+1)}>点我看看子组件刷新了吗</button>
      <MemoIndex func={func}/>
    </div>
  );
}

  • useCallBack是一个缓存工具没错。但实际上他并不能阻止函数都重现构建

示例:
大家看上方这种结构的组件,Com组件中包含了fun1和fun2两个函数。

是不是认为当Com组件重新渲染的时候,只有fun2(没有使用useCallBack的函数)函数会被重新构建,而fun1(使用了useCallBack的函数)函数不会被重新构建。

实际上,被useCallBack包裹了的函数也会被重新构建并当成useCallBack函数的实参传入。
useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建!!

每一个被useCallBack的函数都将被加入useCallBack内部的管理队列。而当我们大量使用useCallBack的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack的组件重新渲染的时候都需要去遍历useCallBack内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。

在以上这个过程中,寻找指定函数需要性能,校验也需要性能。所以,滥用useCallBack不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。

//Com组件
const Com =  () => {

    //示例1包裹了useCallBack的函数
    const fun1 = useCallBack(() => {
        console.log('示例一函数');
        ...
    },[])
    
     //示例2没有包裹useCallBack的函数
    const fun2 = () => {
        console.log('示例二函数');
        ...
    }
    return <div></div>
}

不要过度缓存组件

其实不必过度优化代码 react官方没有帮你做 其实也证明了 如果你的代码没有明显的卡顿 你自己去做优化 可能造成负优化

优化的手段一般都是针对组件本身比较复杂且数据量大每次re-render都会造成卡顿的情况下才去做的,没有太明显的卡顿出现时没必要做这些优化。而且在使用memo前其实也有手段去规避这些无效的re-render,比如将组件粒度划分的更细一些

参考文章

useCallBack你真的知道怎么用吗。

你可能感兴趣的:(react.js,缓存,前端)