react hooks 函数组件渲染优化

107 阅读5分钟

背景

在日常开发中, 总会有不想更新的组件,却频繁更新,这都是 react 渲染机制所导致的,我们要做的便是如何在不该更新的时候避免其刷新,造成性能的消耗。下面是我对 react hooks 中性能优化的一些见解,可能会有很多理解不足的地方。

原因

React的Dom结构可以理解为一个组件树,从父组件不断往下伸展渲染子孙组件,由于函数组件本身没有识别prop值的能力,每次父组件更新的时候都相当于是给子组件一个新的prop值,因此只要组件树下的某个组件发生了state状态的变更,都可能导致整个组件树的渲染更新

实例

1. 父组件更新state,子组件并无引用父组件变更的state,却也发生了无意义的更新.

如下面的代码: 可以看到子组件随着父组件的状态更新从而进行无意义的重复渲染了

const Father = () => {
    console.log('父组件');
    const [count, setCount] = useState({
        number: 0,
        text: '无意义的一段文字',
    });
    return (
        <div className="m-10">
            <div>这里是父组件</div>
            <div>count: {count.number}</div>
            <button  onClick={() =>
                    setCount(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +1
            </button>
            <Son />

        </div>
    );
};

const Son = () => {
    console.log('子组件');

    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是子组件</div>
        </div>
    );
};

1.jpg

1. 第一种解决方法就是memo

memo是一种缓存技术,这个函数可以检测从父组件接收的props,并且在父组件改变state的时候对比这个state是否是本组件在使用,如果不是,则拒绝重新渲染。

const _Son = memo(Son);

<_Son />

2.jpg

2. 插槽引入组件

这种方式则利用了插槽的原理,隔离了两个组件

// 父组件
const Father = ({children}) => {
    const [count, setCount] = useState({
        number: 0,
        text: '无意义的一段文字',
    });
    return (
        <div className="m-10">
            <div>这里是父组件</div>
            <div>count: {count.number}</div>
             <button  onClick={() =>
                    setCount(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +1
            </button>

            {/* 子组件 */}
            { children }     // 通过父组件传入子组件
        </div>
    );
};

// 子组件
const Son = () => {
    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是子组件</div>
        </div>
    );
};

// 上层结构
const Comp = () =>{
  return <Father>
      <Son />
  </Father>
}

3.jpg


3. useMemo()

通常我们认为useMemo用来缓存变量props,useCallback用来缓存函数props,但在实际项目中,我们也可以通过这种方式达到子组件性能优化的目的。

const Father = () =>{

  return {
    <div>我是父组件</div>

    // 子组件
    {
      // 第二个参数数组中也可以监听父组件state状态实现更新
      useMemo(() => <Son />, [])
    }
  
  }
}

const memo(Son = () =>{
  return <div>我是子组件</div>
})

2. 上面的解决方式是基于父组件是没有副作用的纯函数,但实际开发场景中,父组件需要在useEffect中执行副作用(查询服务Ajax或者操作DOM)查询数据传给子组件。

如下面这段代码,才是我们日常开发最常遇到的,父组件通过服务查询得到的值传给子组件渲染。

const Father = () => {
    console.log('父组件');
    const [count, setCount] = useState({
        number: 0,
        text: '无意义的一段文字',
    });
    
    useEffect(()=> {
        // 模拟服务查询
        setTimeout(()=> {
            setCount((val) =>{
                ...val,
                number:100
            })
        },500)
    },[])
    
    return (
        <div className="m-10">
            <div>这里是父组件</div>
            <button  onClick={() =>
                    setCount(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +1
            </button>
            <Son count={count}/>

        </div>
    );
};

const Son = ({count}) => {
    console.log('子组件');
    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是子组件</div>
             <div>count: {count.number}</div>
        </div>
    );
};

当父组件下有多个子组件时,各个组件的更新应该是独立的,彼此间的更新是不影响的.

如下面这段代码:

const Father = () => {
    console.log('父组件');
    const [count, setCount] = useState({
        number: 0,
        text: '无意义的一段文字',
    });
    return (
        <div className="m-10">
            <div>这里是父组件</div>
            <button  onClick={() =>
                    setCount(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +1
            </button>
            <OtherSon count={count} />
            <Son count={count}/>
        </div>
    );
};

const Son = ({count}) => {
    console.log('子组件A');

    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是子组件A</div>
             <div>文本: {count.text}</div>
        </div>
    );
};
const OtherSon = ({count}) => {
    console.log('子组件B');

    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是另一个子组件B</div>
            <div>number: {count.number}</div>
        </div>
    );
};

4.jpg

解决:

  1. 基于上面第一种方式我们调整一下,由于 memo 是浅比较,监听不了复杂对象,需要利用 memo 的第二个参数
const _Son = memo(Son,(oldVal,newVal) => {
  if(oldVal.count.text === newVal.count.text){
    return true
  }
  return false
})

5.jpg

  1. 利用 react 的钩子 useMemo,利用此钩子可以对数据进行缓存和 memo 并用,根据具体的值更新执行
const Father = () => {
    console.log('父组件');
    const [count, setCount] = useState({
        number: 0,
        text: '无意义的一段文字',
    });

    const cacheCount = useMemo(()=>{
      return {
        ...count
      }
    },[count.text])

    return (
        <div className="m-10">
            <div>这里是父组件</div>
            <button  onClick={() =>
                    setCount(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +1
            </button>
            <OtherSon count={count} />

            <Son count={cacheCount}/> // 传useMemo缓存后的值

            // 也可以改写成
            {
              useMemo(() =>  <Son count={cacheCount}/>, [cacheCount])
            }

        </div>
    );
};

const Son = memo(({count}) => {
    ......
});

const OtherSon = memo(({count}) => {
    ......
});

6.jpg

补充

若当父子组件传的参数不再是数据而是一个回调函数时, 父组件每一次state的状态更新渲染都相当于重新生成一个函数,那自然里面的回调函数也会重新生成,也就是说这个回调函数是新生成的,引用地址是新的,那子组件便会认为props中传递的这个函数也是新的,自然子组件也会重新渲染更新。
简单来说,就是父组件的更新导致回调函数的重新生成,那即props改变,因而触发子组件的渲染。下面我们来改造下,如下代码:

const Father = () => {
    console.log('父组件');
    const [count, setCount] = useState({
        number: 0,
        text: '无意义的一段文字',
    });

    const cacheCount = useMemo(()=>{
      return {
        ...count
      }
    },[count.text])

    // 要想更新缓存可以通过第二个参数传参,useCallback会监听其变化从而更新缓存值
    const countAdd2 = useCallback(()=>{
        setCount((count)=>{
            return {
                ...count,
                number:count.number + 2
            }
        })
    },[])

    return (
        <div className="m-10">
            <div>这里是父组件</div>
            <button  onClick={() =>
                    setCount(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +1
            </button>
            /* 传给B组件触发父组件的回调方法 */
            <Son count={cacheCount} countAdd2={countAdd2}/>
            <OtherSon count={count}  />

        </div>
    );
};

const Son = memo(({count,countAdd2}) => {
    console.log('子组件A');

    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是A组件</div>
             <div>文本: {count.text}</div>
                <button  onClick={() =>
                    countAdd2(count => {
                        return {
                            ...count,
                            number: count.number + 1,
                        };
                    })
                }
            >
                +2
            </button>
        </div>
    );
});

const OtherSon = (({count}) => {
    console.log('子组件B');

    return (
        <div className="border">
            <div style={{ marginTop: '50px' }}>这里是B组件</div>
            <div>number: {count.number}</div>
        </div>
    );
});

7.jpg


注意

useMemo,useCallback通过缓存避免重复更新,但这其实只是用内存换性能的一种处理方式,实际并不高效,因此这两个方法是不应该滥用的,应根据具体的情况去使用, 当父子组件层级结构深,数据复杂时,我们应该优先考虑利用useContext,useReducer钩子管理数据或改变组件结构的方法去处理。

总结

React的组件每一次挂载,都如同瀑布般从根组件由上至下层层遍历渲染,每个组件间的状态变更都可能导致整个组件树的更新渲染,因此我们的优化方向其实大致可分为两个方向:

  1. 抽离状态变更频繁的组件,减少父组件的渲染负担,父组件应尽可能的简单,只做挂载组件的操作,没有副作用状态的更新。
  2. 父子组件间的state变更,应该有效的监听,避免无效渲染。