为您推荐:
精华内容
最热下载
问答
  • 3KB weixin_42169971 2021-02-15 09:46:18
  • 829KB weixin_42134097 2021-05-01 19:13:50
  • 190KB weixin_42131261 2021-02-11 23:41:54
  • React性能优化之useCallback与useMemo useMemo 当我们在写一个函数式组件时,经常会遇到非依赖的变量改变导致某些方法重新执行,造成性能浪费,这个时候,我们可以考虑使用useMemo对我们的值进行缓存,只有当这个值...

    React性能优化之useCallback与useMemo

    useMemo

    当我们在写一个函数式组件时,经常会遇到非依赖的变量改变导致某些方法重新执行,造成性能浪费,这个时候,我们可以考虑使用useMemo对我们的值进行缓存,只有当这个值的依赖项改变时,我们采取重新执行方法获取新值,否则直接从缓存中获取上一次的值直接返回。

    场景与示例

    我们来看下面这段伪代码:

    function Demo(){
        const [count, setCount] = useState<number>(0);
        const [name, setName] = useState<string>("");
        
        const showCount = () => {
            console.log("执行了showCount");
            let sum = 0;
            for(let i=0;i<count;i++){
                sum+=i;
            }
            return sum;
        };
        
        return (
        	<div>
            	<h2>这是useMemo测试实例(未优化)</h2>
                <p>累加结果: {showCount()}</p>
                <p>计数器: {count}</p>
                <button onClick={()=>setCount(count + 1)}>增加</button>
                <input value={name} onChange={e => setName(e.target.value)} />
            </div>
        );
    }
    

    上面的伪代码是没有使用useMemo优化过的代码,当我们点击增加按钮时,确实能够达到我们预期的效果,无论是累加结果还是计数器都改变了。但当我们设置文本框的值得时候,此时只是改变了name,并没有改变count,我们的预期是不会重新出发showCount方法执行重新计算的,但是上述代码依然会反复的触发。我们可以想象一下,如果在showCount方法中执行的是一个极其复杂且耗费时间和性能的计算,那么这段看起来没几行的代码就有可能导致整个网站的卡顿甚至崩溃。

    那么,我们来使用useMemo改造一下上述代码:

    function Demo(){
        const [count, setCount] = useState<number>(0);
        const [name, setName] = useState<string>("");
        
        // 使用useMemo处理计算方法,只有当依赖变量count改变时才会触发重新计算获得新的结果,否则将会直接获取上一次计算的结果的缓存直接返回,避免了无异议的重复计算
        const showCount = useMemo(() => {
            console.log("执行了showCount");
            let sum = 0;
            for(let i=0;i<count;i++){
                sum+=i;
            }
            return sum;
        }, [count]);
        
        return (
        	<div>
            	<h2>这是useMemo测试实例(未优化)</h2>
                <p>累加结果: {showCount}</p>
                <p>计数器: {count}</p>
                <button onClick={()=>setCount(count + 1)}>增加</button>
                <input value={name} onChange={e => setName(e.target.value)} />
            </div>
        );
    }
    

    使用详解

    useMemo(nextCreateFn, deps)

    官方解释:

    返回一个 memoized 值。

    把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

    记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

    如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

    **你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。**将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

    其中nextCreateFn需要我们传入一个函数,用于计算目标结果的函数,这个函数需要一个返回值,函数的返回值就是我们最终的计算结果

    deps是一个依赖数组,我们需要将在函数中所使用的外部状态,也就是依赖变量添加进去,这样一来,当依赖没有改变时,我们就可以直接获取上一次缓存的结果直接返回,无需重复执行nextCreateFn计算结果。当deps为null是,将每次渲染都会重新计算,这样其实就失去了这个hooks的意义。因此,及时不传这个依赖参数程序也不会报错,我们在开发时也需要明确函数依赖项并传入依赖数组,否则就无须使用此hooks。

    源码分析

    React源码中useMemo的实现

    // 这个函数用于对比两个依赖数组的依赖是否相同
    function areHookInputsEqual(
      nextDeps: Array<mixed>,
      prevDeps: Array<mixed> | null,
    ) {
      // 此处删除一些与逻辑无关的开发环境调试代码
    
      // 上一次的依赖为null,当前依赖不为null的话,那肯定不相同,依赖发生改变,因此返回false
      if (prevDeps === null) {
       // 此处删除一些与逻辑无关的开发环境调试代码
        return false;
      }
    
      // 此处删除一些与逻辑无关的开发环境调试代码
      // 循环遍历每一个依赖项,并对比上一次的依赖于当前依赖是否相同,只要有一个不相同,则直接返回false
      for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (is(nextDeps[i], prevDeps[i])) {
          continue;
        }
        return false;
      }
      // 循环对比结束后,则说明所有的依赖项都没有发生改变,都是相同的,返回true
      return true;
    }
    // 以下为React中实现useMemo逻辑的主要代码
    // 从下面代码我们可以看出React在执行useMemo的时候其实是分为两个阶段的,一个是挂载,一个是更新
    // 挂载时会先执行一次我们传入的计算方法,即: nextCreate,得到首次计算结果nextValue,然后将计算结果和依赖数组都保存在memoizedState当中缓存起来,方便更新时用于对比与获取缓存结果。最后返回首次计算结果作为初次渲染的结果
    function mountMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      const hook = mountWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    // 当组件因为某些操作触发重新渲染时,会将上一次的依赖数组拿出来,与当前的依赖数组对比,如果发现依赖的状态并没有发生改变,则直接从memoizedState中缓存的上一次的计算结果返回,无须重新执行nextCreate进行重新计算。否则进行重新计算,并将最新的计算结果和新的依赖数组缓存,并将新的计算结果返回作为本次渲染的结果
    function updateMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const prevState = hook.memoizedState;
      if (prevState !== null) {
        // Assume these are defined. If they're not, areHookInputsEqual will warn.
        if (nextDeps !== null) {
          const prevDeps: Array<mixed> | null = prevState[1];
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            return prevState[0];
          }
        }
      }
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    

    useCallback

    场景与示例

    我们经常会给一些元素绑定事件,或者是将一些函数通过属性的形式传递给子组件,如果在函数式组件中,如果不经任何处理的函数通过属性的形式传递给子组件,那么,一旦父组件的任意状态发生变化进行重新渲染时,也会因为每次的函数都是一个新的引用而导致子组件因为属性的改变而重新渲染。我们来看一下下面的例子就比较清晰了:

    function Demo(){
        const [count, setCount] = useState<number>(0);
        const [name, setName] = useState<string>("");
        
        const showCount = () => {
            console.log("执行了showCount");
            let sum = 0;
            for(let i=0;i<count;i++){
                sum+=i;
            }
            return sum;
        };
        
        return (
        	<div>
            	<h2>这是useMemo测试实例(未优化)</h2>
                <p>计数器: {count}</p>
                <button onClick={()=>setCount(count + 1)}>增加</button>
                <input value={name} onChange={e => setName(e.target.value)} />
                <Child onClick={showCount} />
            </div>
        );
    }
    function Child(props) {
        console.log("child rerender!!");
        return <div onClick={props.onClick}>这是子节点</div>
    }
    

    上面的实例代码中,我们将showCount函数作为属性传递给子组件Child,当我们父组件Demo的任意状态如:countname发生改变时,都会重新创建showCount函数,导致函数引用不一致而触发Child组件的重新渲染。但是,我们的showCount方法很明显是跟我们的name这个状态没有关系的,因此,我们希望只有当count状态改变时才触发Child组件的重新渲染。那么,这个时候我们就可以用到useCallback了。

    function Demo(){
        const [count, setCount] = useState<number>(0);
        const [name, setName] = useState<string>("");
        
        // 使用useCallback优化函数,当且仅当count改变时,我们才改变我们的回调,否则直接获取缓存的函数,保持引用一致
        const showCount = useCallback(() => {
            console.log("执行了showCount");
            let sum = 0;
            for(let i=0;i<count;i++){
                sum+=i;
            }
            return sum;
        }, [count]);
        
        return (
        	<div>
            	<h2>这是useMemo测试实例(未优化)</h2>
                <p>计数器: {count}</p>
                <button onClick={()=>setCount(count + 1)}>增加</button>
                <input value={name} onChange={e => setName(e.target.value)} />
                <Child onClick={showCount} />
            </div>
        );
    }
    function Child(props) {
        console.log("child rerender!!");
        return <div onClick={props.onClick}>这是子节点</div>
    }
    

    使用详解

    useCallback(callback, deps)

    官方解释:

    返回一个 memoized 回调函数。

    把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

    useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

    useCallbackuseMemo的用户基本相同,都是传入两个参数,第一个是函数,第二个是依赖数组,不同的是,useMemo会去执行我们传递过去的函数用来计算目标结果,而useCallback则仅仅只是将我们传入的函数缓存并返回,不会去执行它。

    源码分析

    React源码中useCallback的实现

    // 与useMemo不同,useCallback不会去执行callback获得结果,而是直接缓存并返回callback
    function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
      const hook = mountWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      hook.memoizedState = [callback, nextDeps];
      return callback;
    }
    // 更新也是一样,除了不执行callback获取结果之外,其他的都跟useMemo一样的
    function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const prevState = hook.memoizedState;
      if (prevState !== null) {
        if (nextDeps !== null) {
          const prevDeps: Array<mixed> | null = prevState[1];
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            return prevState[0];
          }
        }
      }
      hook.memoizedState = [callback, nextDeps];
      return callback;
    }
    

    结语

    这次我们讨论的东西其实很简单,估计大家不用几分钟就能够融会贯通了,之所以把这个点单独拎出来作为一个话题的讨论,是因为随着我们的项目中对于函数式组件和hooks的使用越来越频繁,我们的业务功能也拆分的越来越细,组件越来越小,我们需要从每一个小点做好性能的优化处理,不然在页面上大量引入某些未经优化的组件时,可能就会把一些很小的性能问题无限放大,最终导致页面的卡顿甚至崩溃,在项目中善用useMemouseCallback,可以让我们尽可能得避免这些情况的发生,让项目运行得更加平稳高效。

    展开全文
    u010651383 2021-05-03 10:34:31
  • useCallback问题缘由 先回顾下hook之前组件的写法 class组件 export class ClassProfilePage extends React.Component<any,any>{ showMessage = ()=>{ alert('Fllowed'+ this.props.user) } ...
    useCallback问题缘由

    先回顾下hook之前组件的写法

    class组件

    export class ClassProfilePage extends React.Component<any,any>{
        showMessage = ()=>{
            alert('Fllowed'+ this.props.user)
        }
        handleClick = ()=>{
            setTimeout(this.showMessage, 3000);
        }
        render(){
            return <button onClick={this,handleClick}>Fllow</button>
        }
    }
    
    

    functional组件

    export function FunctionProfilePage(props){
        const showMessage = () => {
            alert('Followed' + props.user)
        }
        const handleClick = () =>{
            setTimeout(showMessage, 3000);
        }
        return (
            <button onClick={handleClick}>Follow</button>
        )
    }
    

    点击按钮,同时将user由A切换到B时,class组件显示的是B而function组件显示的是A,这两个行为难以说谁更加合理

    import React,{useState} from 'react'
    import ReactDom from 'react-dom'
    import {FunctionProfilePage,ClassProfilePage} from './profile'
    import "./styles.css"
    
    function App(){
        const [state,setState] = useState(1)
        return (
            <div className="App">
              <button onClick={()=>{
                  setState(x=>x+x)
              }}>double</button>
              <div>state:{state}</div>
              <FunctionProfilePage user={state}/> //点击始终显示的是快照值
              <ClassProfilePage user={state}/>//点击始终显示的是最新值
            </div>
        )
    }
    const rootElement = document.getElementById("root")
    ReactDom.render(<App />,rootElement)
    
    

    当你的应用里同时存在Functional组件和class组件时,你就面临着UI的不一致性,虽然react官方说function组件是为了保障UI的一致性,但这是建立在所有组件都是functional组件,事实上这假设几乎不成立,如果你都采用class组件也可能保证UI的一致性(都显示最新值),一旦你页面里混用了class组件和functional 组件(使用useref暂存状态也视为class组件),就存在的UI不一致性的可能

    快照 or 最新值

    所以function和class最大区别只在于默认情况不同,两者可以相互转换,快照合理还是最新值合理,这完全取决于你的业务场景,不能一概而论

    事实上在class里也可以拿到快照值,在function里也可以拿到最新值

    class里通过触发异步之前保存快照即可

    export class ClassProfilePage extends React.Component<any,any>{
        showMessage = (message)=>{
            alert('Followed'+message)
        }
        handleClick = () => {
            const message = this.props.user //在触发异步函数之前保存快照
            setTimeout(()=> showMessage(message),3000)
        }
        render(){
            return <button onClick={this.handleClick}>Follow</button>
        }
    }
    

    function里通过ref 容器存取最新值

     export function FunctionProfilePage(props){
         const ref = useRef("")
         useEffect(()=>{
             ref.current = props.user
         })
         const showMessage = () => {
             console.log('ref:',ref);
             alert('Followed' + props.user + ','+ ref.current)
         }
         const handleClick = () => {
             setTimeout(showMessage,3000)
         }
         return <button onClick={handleClick}>function Follow</button>
     }
    

    其实就是个经典的函数闭包问题

    在异步函数执行前可以对闭包访问的自由变量进行快照捕获:实现快照功能
    在异步函数执行中可以通过ref读取最新的值

    for(var i = 0;i<10;i++){
        setTimeout(() => {
            console.log('val:',i); 
        }); //拿到的是最新值
    }
    
    for(var i = 0;i<10;i++){
       setTimeout(((val)=>console.log('val',val)).bind(null,i)) //拿到的是快照
    }
    
    const ref = {current:null}
    for(var i =0;i<10,i++){
        ref.current = i
        setTimeout(((val)=>console.log('val:',ref.current)).bind(null,ref)) //拿到的是最新的值
    }
    for(var i =0; i<10;i++){ //拿到的是快照
        let t = i;
        setTimeout(()=>{
            console.log('t:',t);
        })
    }
    
    重渲染机制

    虽然functional和class组件在快照处理方式不一致,但是两者的重渲染机制,并没有大的区别

    class重渲染触发条件,此处暂时不考虑采用shouldComponentUpdate和pureComponent优化

    this.setState : 无条件重渲染,不进行新旧比较
    this.forceUpdate: 无条件重渲染,不进行新旧比较
    父组件render带动子组件render: 无条件,和props是否更新无关
    祖先组件context变动: 不做props变动假设
    我们发现react默认的重渲染机制压根没有对props做任何假设,性能优化完全交给框架去做,react-redux 基于shouldComponent, mobx-react 基于this.forceUpdatehooks 来做一些性能优化

    带来的问题

    我们发现即使不用hooks本身functional组件和class组件表现就存在较大差异,由于hook目前只能在function组件里使用,这导致了一些本来是functional组件编程思维的问题反映到了hooks上。

    hooks的使用引入了两条强假设,导致了编程思维的巨大变动

    只能在functional组件里使用: 导致我们需要处理最新值的问题
    副作用(包括rerender和effect)基于新旧值的reference equality : 强制我们使用immutable进行编程
    上述两条带来了很大的心智负担

    Stale closure 与 infinite loop

    这两个问题是硬币的两面,通常为了解决一个问题,可能导致另外一个问题

    一个最简单的case就是一个组件依赖了父组件的callback,同时内部useffect依赖了这个callback

    如下是一个典型的搜索场景

    export function Parent(){
        const [query,setQuery] = useState('react')
        const fetchData=()=>{
            const url = 'https://hn.algolia.com/api/v1/search?query='+query
            return fetch(url).thne(x=>x.text())
        }
        return (
            <div>
              <input onChange={e=>setQuery(e.target.value)} value={query}/>
              <Child fetchData={fetchData} query={query}/>
            </div>
        )
    }
    

    上述代码存在的一个问题就是,每次Parent重渲染都会生成一个新的fetchData,因为fetchData是Child的useEffect的dep,每次fetchData变动都会导致子组件重新触发effect,一方面这会导致性能问题,假如effect不是幂等的这也会导致业务问题(如果在effect里上报埋点怎么办)

    解决思路1:
    不再useEffect里监听fetchData: 导致stale closure 问题 和页面UI不一致

      useEffect(() => {
        fetchData().then(result => {
          setResult(result);
        })
      },[]) // 去掉fetchData依赖
    

    此时一方面父组件query更新,但是子组件的搜索并未更新但是子组件的query显示却更新了,这导致了子组件的UI不一致

    解决思路2:
    在思路1的基础上加强刷token

    //child
     useEffect(()=>{
         fetchData().then(result=>{
             setResult(result)
         })
     },[refreshToken]) 
    
     //parent
     <Child fetchData={fetchData} query={query} refreshToken={query}/>
    

    问题:

    如果子组件的effect较多,需要建立refreshToken和effect的映射关系
    触发eslint-hook的warning,进一步的可能触发eslint-hook的auto fix功能,导致bug
    fetchData仍然可能获取的是旧的闭包?
    为了更好的语义化和避免eslint的报错,可以自定义封装useDep来解决

    useDepChange(()=>{
        fetchData().then(result=>{
            setResult(result)
        },[fetchData])
    },[queryToken]) //只在dep变动的时候触发 约等于componentWillReceiveProps了
    

    实际上是放弃了eslint-hook的 exhaustive检查,可能会导致忘记添加某些依赖,需要写代码时非常仔细了

    解决思路3:

    useCallback包裹fetchData, 这实际上是把effect强刷的控制逻辑从callee转移到了caller

    const fetchData = useCallback(()=>{
        const url = 'https://hn.algolia.com/api/v1/search?query=' + query
        return fetch(url).then(x=>x.text())
    },[query])
    //child
    useEffect(()=>{
        fetchData().then(result=>{
            setResult(result)
        })
    },[fetchData])
    

    问题:

    如果child的useEffect里依赖了较多的callback,需要所有的callback都需要进行useCallback包装,一旦有一个没用useCallback包装,就前功尽弃
    props的不可控制,Parent的fetchData很可能是从其他组件里获取的,自己并没有控制fetchData不可变的权限,这导致千里之外的一个祖先组件改变了fetchData,导致Child最近疯狂刷新effect,这就需要将callback做层层useCallback处理才能避免该问题
    官方说useCallback不能做语义保障,而且存在cache busting的风险
    组件API的设计:我们发现此时设计组件时需要关心传进来的组件是否是可变的了,但是在接口上并不会反馈这种依赖

    <Button onClick={clickHandler} />  // onClick改变会触发Button的effect吗? 
    
    解决思路4:

    使用useEventCallback作为逃生舱,这也是官方文档给出的一种用法useEventCallback

    //child
    useEventCallback(()=>{
        fetchData().then(result=>{
            setResult(result)
        })
    },[fetchData])
    function useEventCallback(fn,dependencies){
        const ref = useRef(()=>{
            throw new Error('Cannot call an event handler while rendering')
        })
        useEffect(()=>{
            ref.current = fn
        },[fn,...dependencies])
    
        return useCallback(()=>{
            const fn = ref.current
            return fn()
        },[ref])
    }
    
    

    这仍然存在问题,

    在这里插入图片描述

    解决思路5:

    拥抱mutable,实际上这种做法就是放弃react的快照功能(变相放弃了concurrent mode ),达到类似vue3的编码风格

    实际上我们发现hook + mobx === vue3, vue3后期的api实际上能用mobx + hook进行模拟

    问题就是: 可能放弃了concurrent mode (concurrent mode更加关注的是UX,对于一般业务开发效率和可维护性可能更加重要)

    import {ref,onMounted,onUnmounted} from 'vue'
    export function useMousePosition(){
        const x = ref(0)
        const y = ref(0)
        function update(e){
            x.value= e.pageX
            y.value = e.pageY
        }
        onMounted(()=>{
            window.addEventListener('mousemove',update)
        })
    
        onUnmounted(()=>{
            window.removeEventListener('mousemove',update)
        })
        return {x,y}
    }
    
    <template>
      <div>
        x:{{pos.x}}
        y:{{pos.y}}
      </div>
    </template>
    export default{
        setup(){
            const {x,y} = useMousePosition()
            return pos
        }
    }
    

    调用者约定:

    父组件传递给子组件的callback: 永远获取到的是父组件的最新state (通过useObservable|useRef)
    被调用者约定
    被调用者约定

    不要把callback作为useEffect的依赖:因为我们已经限定了callback永远是最新的,实际上避免了陈旧闭包问题,所以不需要把callback作为depdency

    代码里禁止直接使用useEffect:只能使用自定义封装的hook,(因为useEffect会触发eslint-hook的warning,每次都禁止不好,且useEffect没有那么语义化)如可以使用如下hook
    useMount: 只在mount触发(更新不触发)
    useUpdateEffect: 只在更新时触发(mount不触发)
    useDepChange: dep改变时触发,功能和useEffect类似,不会触发wanring

    //parent.js
    export observer(function VueParent(){
        const [state] = useState(observable({
            query:'reqct'
        }))
        const fetchData = () =>{
            const url = 'https://hn.algolia.com/api/v1/search?query=' + state.query
            return fetch(url).then(x=>x.text())
        }
        return (
            <div>
              <input onChange={e=>state.query = e.target.value} value={state.query}/>
              <Child fetchData={fetchData} query={state.query}/>
            </div>
        )
    })
    
    //child.js
    export function observable(VueChild(props){
        const [result,setResult] = useState('')
        useMount(()=>{
            props.fetchData().then(result=>{
                setResult(result)
            })
        })
        useUpdateEffect(()=>{
            props.fetchData().then(result=>{
                setResult(result)
            })
        },[props.query])
    
        return (
            <div>
              <div>query:{props.query}</div>
              <div>result:{result}</div>
            </div>
        )
    })
    
    解决思路6

    useReducer 这也是官方推荐的较为正统的做法

    我们仔细看看我们的代码,parent里的fetchData为什么每次都改变,因为我们父组件每次render都会生成新的函数,为什每次都会生成新的函数,我们依赖了query导致没法提取到组件外,除了使用useCallback我们还可以将fetchData的逻辑移动至useReducer里。因为useReducer返回的dispatch永远是不变的,我们只需要将dispatch传递给子组件即可,然而react的useReducer并没有内置对异步的处理,所以需要我们自行封装处理,幸好有一些社区封装可以直接拿来使用,比如zustand, 这也是我目前觉得较好的方案,尤其是callback依赖了多个状态的时候。https://codesandbox.io/s/github/hardfist/hooks-problem/tree/master/

    function Child(props){
        const [result,setResult] = useState("")
        const {fetchData} = props
        useEffect(()=>{
            console.log('trigger effect');
            fetchData().then(result=>{
                setResult(result)
            })
        },[props.query,fetchData])
        return(
            <>
             <div>query:{props.query}</div>
             <div>result:{result}</div>
            </>
        )
    }
    
    const [useStore] = create((set,get)=>({
        query:'react',
        setQuery(query){
            set(state=>({
                ...state,
                query
            }))
        },
        fetchData:async() => {
            const url = "https://hn.algolia.com/api/v1/search?query=" + get().query;
            const x = await (await fetch(url)).text()
            return x
        }
    }))
    export function Parent(){
        const store = useStore()
        const forceUpdata = useForceUpdata()
        console.log('parent render');
        useEffect(()=>{
            setInterval(() => {
                forceUpdata({})
            }, 1000);
        },[forceUpdata])
        return(
            <div>
              <input onChange={e=>store.setQuery(e.target.value)} value={store.query}/>
              <Child fetchData={store.fetchData} query={store.query}/>
            </div>
        )
    }
    
    解决思路7:

    这也是我觉得可能的最佳解法了,核心问题还是在于js语言对于并发|immutable|函数式编程的羸弱支持如(thread local object | mutable, immutable 标记| algebraic effects 支持),导致react官方强行在框架层面对语言设施进行各种hack,引起了各种违反直觉的东西,换一门语言做react可能是更好的方案(如reasonml)。

    展开全文
    weixin_43852916 2021-01-26 16:46:50
  • 190KB weixin_42160425 2021-05-17 08:38:42
  • 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 ...

    关注 程序员成长指北,回复“1”

    加入我们一起学习,天天进步

    返回一个 memoized 回调函数。把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

    使用 useCallback 的场景?

    1. 函数被 useEffect 内部所使用,但为了避免频繁 useEffect 的频繁调用,所以我包一下;

    2. 我只是为了解决 hooks lint 的提示报警,所以我包一下;

    3. 因为有一个使用了 useCallback 的函数引用了我这个函数,所以我不得不包一下;

    4. 当这个函数会被传递给子组件,为了避免子组件频繁渲染,使用 useCallback 包裹,保持引用不变;

    5. 需要保存一个函数闭包结果,如配合 debounce、throttle 使用;

    6. 我希望这个useCallback包裹但函数,但某个依赖项变化时,引用了我这个函数的所有 useEffect 都得重新执行一下;

    我们做了投票,发现场景 4 是使用的最多的

    案例

    假如这里有个文章组件,我想观察当「文章内容」明确后, 用户对「文章标题」的修改频率如何。这个 具体实现:当「文章内容」的长度大于 0 时编辑「文章标题」就上报埋点,同时带上「文章标题」和「文章内容」的 字符长度。

    点击链接,打开 console 看Demo:https://codepen.io/huyao/pen/LYbdomv?editors=0010

    小胡写出了下面这一段代码,大家可以细看一下,有哪些地方需要优化?它有哪些地方不规范?

    // 新建文章组件
    function EditArticle() {
      const [title, setTitle] = useState("");
      const [content, setContent] = useState("");
      const [other, setOther] = useState("");
    
      // 获取当前「标题」和「内容」的长度
      const getTextLen = () => {
        return [title.length, content.length];
      };
    
      // 上报当前「标题」和「内容」的长度
      const report = () => {
        const [titleLen, contentLen] = getTextLen();
        if (contentLen > 0) {
          console.log(`埋点 >>> 标题长度 ${titleLen}, 内容长度${contentLen}`);
        }
      };
    
      /**
       * 副作用
       * 当「标题」长度变化时,上报
       */
      useEffect(() => {
        report();
      }, [title]);
    
      return (
        <div className="App">
           文章标题   <input value={title} onChange={(e) => setTitle(e.target.value)} />
           
           文章内容  <input value={content} onChange={(e) => setContent(e.target.value)} />
          
           其他不相关状态: <input value={other} onChange={(e) => setOther(e.target.value)} />
    
          <MemoArticleTypeSetting getTextLen={getTextLen} />
        </div>
      );
    }
    enum ArticleType {
      WEB = "前端",
      SERVER = "后端",
    }
    
    // 子组件,修改文章类型(无需关注,它只是接受了父组件的一个参数而已)
    const ArticleTypeSetting: FC<{ getTextLen: () => number[] }> = ({  getTextLen }) => {
      console.log(" --- ArticleTypeSetting 组件重新渲染 --- ");
    
      const [articleType, setArticleType] = useState<ArticleType>(ArticleType.WEB);
    
      const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setArticleType(e.target.value as ArticleType);
    
        console.log(  "埋点 >>> 切换类型,当前「标题」和「内容」长度:", getTextLen()  );
      };
    
      return (
        <div>
          <div>文章类型组件,当选择类型时上报「标题」和「内容」长度</div>
          <div>
            {[ArticleType.WEB, ArticleType.SERVER].map((type) => (
              <div>  
                <input  type="radio" value={type} checked={articleType === type} onChange={handleChange}  /> 
               {type} 
              </div>
            ))}
          </div>
        </div>
      );
    };
    
    const MemoArticleTypeSetting = memo(ArticleTypeSetting);
    

    注:这段代码是我为了模拟 useEffect 和 useCallback 同时使用而构思出来的,大家不用深究。当然有其他写法可以避免问题,我们这里只是借这个 Demo 描述一下这种类型的场景。

    CodeReview 与修改

    哪些地方需要优化?

    子组件 ArticleTypeSetting 是使用 memo 包裹的,这个组件是希望尽可能的减少渲染次数的(假装这个组件有性能问题,一般不用包)。但是,现在每当修改任意一个值(如 other),子组件都会重新渲染,这显然是没有达到优化的预期的。

    哪些地方不规范?

    image

    这里不规范, useEffect 中使用了 report 函数,但是没有将它放到依赖数组中。我认为这是一件比较危险的事情,在 Hooks 中经常有过期状态的问题。插件已经帮你提示了,虽然现在你自测感觉没问题,但你很难保证在经过几轮轮修改之后,虽然你的代码一堆 warning 或 error,但跑起来没问题。

    修改代码

    小胡于是对代码进行了一些修改:

    1. 将 getTextLen 和 report 使用 useCallback 包裹

      // 获取当前「标题」和「内容」的长度
      const getTextLen = useCallback(() => {
        return [title.length, content.length];
      }, [title, content]);
    
      // 上报当前「标题」和「内容」的长度
      const report = useCallback(() => {
        const [titleLen, contentLen] = getTextLen();
        if (contentLen > 0) {
          console.log(`埋点 >>> 内容长度 ${titleLen}, 内容长度${contentLen}`);
        }
      }, [getTextLen]);
    
      /**
       * 副作用
       * 当「标题」长度变化时,上报
       */
      useEffect(() => {
        report();
      }, [title, report]);
    

    还有问题吗?

    有,当 「文章内容」修改了之后,会触发 useEffect 继续上报,这个问题比较隐晦,不再回归测试的话难以发现;并且编辑文章内容时子组件也在重新渲染。

    为什么出了问题?

    我的初衷只是使用 useCallback 避免频繁调用,但当一个 useCallback 的依赖项变化后,这个 useEffect 会被执行,就像上面修改过后的代码一样,「文章内容」修改了之后,也会触发 useEffect 的,这就是「useCallback 带来的隐式依赖问题」。

    如何解决?

    方式 1(不推荐):将所有状态都挂到 Ref 上,然后每次修改状态之后主动触发渲染

    这种做法确实可以解决这个问题,但是代码不宜维护,理由如下:

    1. 一旦一个组件这样写了之后,之后要有什么新的状态也只好放到这里面。而在新写组件的时候,你不知道什么时候会碰到这个问题,因为一旦碰到了你只有使用 forceUpdate 来解决,要改相关状态的定义,每次使用的时候还要把 someSate 改为 ref.someState。

    2. 每次想更新视图时都需要 forceUpdate,官网是不推荐这种方式的,链接点我

    方式 2:将 函数绑定到 useRef 上来解决

      const getTextLenRef = useRef<() => [number, number]>(() => [0, 0]);
    
      // 获取当前「标题」和「内容」的长度
      getTextLenRef.current = () => {
        return [title.length, content.length];
      };
    
      // 上报当前「标题」和「内容」的长度
      const report = () => {
        const [titleLen, contentLen] = getTextLenRef.current();
        if (contentLen > 0) {
          console.log(`埋点 >>> 标题长度 ${titleLen}, 内容长度${contentLen}`);
        }
      };
    
      /**
       * 副作用
       * 当「标题」长度变化时,上报
       */
      useEffect(() => {
        report();
      }, [title]);
    

    将函数绑定到 Ref上,ref 引用不论怎样都保持不变,而且函数每次 render ref 上又会绑定最新的函数,不会有闭包问题。我在开发一个复杂项目中,大量的使用了这种方式,这让我的开发效率提升。它让我专注于写业务,而不是专注于解决闭包问题。

    这种处理方式的灵感来源于 Dan 的博客:使用 React Hooks 声明 setInterval

    优化使用 ref 的体验

    虽然把函数挂到 ref 上可以很好到解决这个问题,但是我在开发的时候我并不知道一个函数之后会不会碰到这个闭包问题,但我又不想所以函数全部都这样干。

    我对这种方式抽象封装了一下,得到这样一个工具函数,它通过将函数挂到 ref 上,保证永远都是拿到最新状态的函数,往外暴露时使用 useCallback 包裹,保证函数引用不更新。

    export function useRefCallback<T extends (...args: any[]) => any>(callback: T) {
      const callbackRef = useRef(callback);
      callbackRef.current = callback;
      return useCallback((...args: any[]) => callbackRef.current(...args), []) as T;
    }
    

    使用时,简单的把原来有闭包问题的函数包裹一下,不需要传递依赖性,方便简单又好用。

    点击链接,打开 console 看Demo:https://codepen.io/huyao/pen/XWNELYr?editors=0010

      // 获取当前「标题」和「内容」的长度
      const getTextLen = useRefCallback(() => {
        return [title.length, content.length];
      });
    
      // 上报当前「标题」和「内容」的长度
      const report = useRefCallback(() => {
        const [titleLen, contentLen] = getTextLen();
        if (contentLen > 0) {
          console.log(`埋点 >>> 内容长度 ${titleLen}, 内容长度${contentLen}`);
        }
      });
    
      /**
       * 副作用
       * 当「标题」长度变化时,上报
       */
      useEffect(() => {
        report();
      }, [title, report]);
    

    思考我们最开始使用 useCallback 的理由

    我认为其实最开始使用 useCallback 的理由中,只有「需要保存一个函数闭包结果,如配合 debounce、throttle 使用」这个是真正需要使用 useCallback 的,其他的都可能带来风险,比如:

    当 useCallback 和 useEffect 组合使用时,由于 useCallback 的依赖项变化也会导致 useEffect 执行,这种隐式依赖会带来BUG或隐患。因为在编程中,函数只是一个工具,但现在一旦某个函数使用了 useCallback ,当这个函数的依赖项变化时所有直接或间接调用这个 useCallback 的都需要回归。所以我说这是成本高、有风险的事情。

    而「为了避免子组件频繁渲染,使用 useCallback 包裹,保持引用不变」这种情况下,当 useCallback 的依赖项变化时,函数的引用也在更新,没有完全的避免子组件频繁渲染问题。

    而「希望这个useCallback函数的某个依赖项变化时,引用了我这个函数的所有 useEffect 都得重新执行一下」这个理由,我认为它是有风险的,虽有有时候你确实希望这么做,但我认为这样在设计上就不对,副作用怎么调用应该由副作用来决定,不应该由依赖的函数来影响,当你真正碰上这个场景,你应该将所有应该主动的把触发 useEffect 执行的状态都放入依赖数组中。

    结论

    在绝大多数情况下,开发者想要的仅仅只是避免函数的引用变化而已,而 useCallback 带来的隐式依赖问题会给你带来很大的麻烦,所以推荐使用 useRefCallback,把函数挂到 ref 上,这样代码更不容易留下隐患或带来问题,还可以省去维护 useCallback 依赖项的精力。

    而 useRefCallback 本质上就是帮你把函数挂在 ref 上,并方便你使用 ref.current。

    export function useRefCallback<T extends (...args: any[]) => any>(callback: T) {
      const callbackRef = useRef(callback);
      callbackRef.current = callback;
    
      return useCallback((...args: any[]) => callbackRef.current(...args), []) as T;
    }
    

    作者:胡耀  原文:https://github.com/huyaocode/webKnowledge/issues/12

    ❤️爱心三连击

    1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
    2.关注公众号【程序员成长指北】,回复「1」加入高级前端交流群!「在这里有好多 前端 开发者,会讨论 前端 Node 知识,互相学习」!
    3.也可添加微信【ikoala520】,一起成长。

     

    展开全文
    xgangzai 2021-04-03 00:18:52
  • 一、useCallback的作用 usecallback不是用来解决组件中有过多内部函数导致的性能问题: 1.我们要知道,js创建一个函数的成本是非常小的,这点计算对于计算机来说是小case 2.其实使用useCallback会产成额外的性能:对...

    一、useCallback的作用

    usecallback不是用来解决组件中有过多内部函数导致的性能问题:
    1.我们要知道,js创建一个函数的成本是非常小的,这点计算对于计算机来说是小case
    2.其实使用useCallback会产成额外的性能:对deps的判断
    3.其实每次组件重新渲染时,都无所谓避免重新创建内部函数,因为即使useCallback的deps没有变,它也会重新创建内部函数作为useCallback的实参

    那么,它的作用到底是什么?useCallback的作用其实是用来避免子组件不必要的reRender:

    首先,假如我们不使用useCallback,在父组件中创建了一个名为handleClick的事件处理函数,根据需求我们需要把这个handleClick传给子组件,当父组件中的一些state变化后(这些state跟子组件没有关系),父组件会reRender,然后会重新创建名为handleClick函数实例,并传给子组件,这时即使用React.memo把子组件包裹起来,子组件也会重新渲染,因为props已经变化了,但这个渲染是无意义的

    如何优化呢?这时候就可以用useCallback了,我们用useCallback把函数包起来之后,在父组件中只有当deps变化的时候,才会创建新的handleClick实例,子组件才会跟着reRender(注意,必须要用React.memo把子组件包起来才有用,否则子组件还是会reRender。React.memo是类似于class组件中的Pure.Component的作用)

    对于这种deps不是经常变化的情况,我们用useCallback和React.memo的方式可以很好地避免子组件无效的reRender。但其实社区中对这个useCallback的使用也有争议,比如子组件中只是渲染了几个div,没有其他的大量计算,而浏览器去重新渲染几个dom的性能损耗其实也是非常小的,我们花了这么大的劲,使用了useCallback和React.memo,换来的收益很小,所以一些人认为就不用useCallback,就让浏览器去重新渲染好了。至于到底用不用,此处不深入讨论,我的建议是当子组件中的dom数量很多,或者有一些大量的计算操作,是可以进行这样的优化的。

    以上都是讨论的deps不会经常改变的情况的优化,而很多时候useCallback中的deps数组中的变量是会经常改变的,这个时候我们用useCallback已经没啥意义了,反而会造成性能损耗(deps判断)。有没有什么办法可以让子组件不重新渲染,也能拿到父组件中handleClick函数中的最新state值呢?下面我们讨论useRef,useReducer,usePersistFn这三种解决方法

    二、useRef解决方案

    为了让子组件不进行reRender,我们必须保证
    1.父组件不会重新创建handleClick函数实例
    2.handleClick函数能拿到最新的state
    使用useRef

    const [text, setText] = useState('Initial value');
    const textRef = useRef(text);
    const handleClick= useCallback(() => {
         console.log(textRef.current);
     }, []); 
    
     useEffect(() => {
         console.log('update text')
         textRef.current = text;
     }, [text])
    

    textRef在每次reRender时不会改变,这样我们把handleClick传给子组件,handleClick函数中每次都能拿到父组件中最新的state

    三、useReducer解决方案

    使用useReducer

    function reducer(state, action) {
        switch(action.type) {
            case 'update':
                return action.preload;
            case 'childComponent':
                // 要执行的函数  
                return state;     
        }
    }
    export default function Index() { // 父组件
        const [state, dispatch] = useReducer(reducer, 'Initial value');
    
        return (
            <>
                <input value={state} onChange={(e) => dispatch({
                    type: 'update', 
                    preload: e.target.value
                })} />
                <ChildComponent dispatch={dispatch} />
            </>
        )
    }
    
    //在 ChildComponent中,拿到dispatch,通过dispatch({type: 'childComponent' })的方式调用
    

    dispatch自带memoize,所以子组件不会进行 re-render

    四、usePersistFn解决方案

    usePersistFn是aooks库中的一个钩子函数,它接收一个函数,返回一个永远不变的函数引用,在这个函数中每次都能拿到最新的state值,看看usePersistFn的源码:

    function usePersistFn(fn) {
      const fnRef = useRef(fn);
      fnRef.current = fn;
    
      const persistFn = useRef();
      if (!persistFn.current) {
        persistFn.current = function (...args) {
          return fnRef.current.apply(this, args);
        };
      }
    
      return persistFn.crrent;
    }
    

    这里它用了两个useRef,保证返回的函数引用不变,并且每次函数内部能拿到最新的state。

    这里可以用useRef和useCallback到达同样的效果:

    function usePersistFn(fn) {
      const fnRef = useRef();
      fnRef.current = fn; 
    	
      const persist= useCallback((...rest) => {
         return fnRef.current(...rest);
      }, []);
      
      return persist
    }
    

    这种方法和上面的useRef解决方案差不多,只是封装了起来而已


    另外,本文介绍一下useContext使用的问题和优化:
    问题:当context中的值改变时,只要使用useContext订阅了context的组件,不管该组件用到的state改不改变,该组件都会reRender,此时用React.memo是没有办法优化的。
    优化:
    1.拆分context,把经常改变的数据和不经常改变的数据拆分开,在只使用稳定数据的组件中,我们只使用stableContext
    2.使用useMemo

    const {state}= useContext(AppContext);
    return useMemo(() => <span>data:{state.depData}</span>, [state.depData]);
    

    3.如何有效减少使用useContext导致的不必要渲染

    展开全文
    Kobe_G 2021-12-06 17:59:13
  • weixin_44691608 2021-07-24 10:41:37
  • zz_jesse 2021-03-03 14:36:10
  • m0_37557930 2021-06-11 11:41:56
  • Zong_0915 2021-06-21 18:36:44
  • s1879046 2021-09-09 08:42:15
  • qq_36538012 2021-11-27 01:08:26
  • ddwddw4 2021-02-19 16:59:06
  • qq_41635167 2021-08-23 22:01:15
  • baidu_39067385 2020-12-19 23:40:28
  • glorydx 2020-11-23 09:49:42
  • XHSRookies 2021-07-30 13:06:33
  • qq_29438877 2020-03-11 12:50:00
  • James_xyf 2021-09-22 00:42:43
  • qq_50646256 2021-11-23 11:40:17
  • u011705725 2021-05-19 19:35:04
  • Joshmo 2021-02-14 17:12:27
  • zhongzk69 2021-07-04 02:43:30
  • zw686668 2021-06-30 23:29:20
  • sinat_17775997 2019-07-02 16:29:20
  • weixin_43905830 2020-10-11 13:31:00
  • weixin_44552249 2021-04-29 17:22:28
  • weixin_43844392 2020-06-06 19:10:26

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,864
精华内容 1,545
关键字:

usecallback