Javascript

React系列-状态逻辑复用问题(中)

本文主要是介绍React系列-状态逻辑复用问题(中),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

我们在第一篇中介绍了Mixin HOC Render Props,接下来来介绍另外一种非常重要的状态逻辑复用模式,那就是react-hooks

React系列-状态逻辑复用(上)
React系列-状态逻辑复用(中)
React系列-状态逻辑复用(下)

HOC、Render Props、组件组合、Ref 传递……代码复用为什么这样复杂?,根本原因在于细粒度代码复用不应该与组件复用捆绑在一起 也就是我们前面所说的这些模式是在既有(组件机制的)游戏规则下探索出来的上层模式

❗️❗️HOC、Render Props 等基于组件组合的方案相当于先把要复用的逻辑包装成组件再利用组件复用机制实现逻辑复用。自然就受限于组件复用,因而出现扩展能力受限、Ref 隔断、Wrapper Hell……等问题

🤔直接的代码复用方式

想想在我们平时开发中,我们要复用一段逻辑是不是抽离出一个函数,比如用到的防抖函数、获取token函数但是对于react的复用逻辑不同,在没有hooks出来之前,函数是内部是无法支持state的,所以抽离成函数的模式好像是办不到,实际也可以做到的

// 设置提示语tip
export const setTip = function (context: any) {
  // vote-tip提示
  const tipVisible = JSON.parse(localStorage.getItem('tipVisible') as string)
  if (Object.is(tipVisible, null)) {
    localStorage.setItem('tipVisible', 'true')
  } else if (Object.is(tipVisible, false)) {
    context.setState({
      tipVisible: false
    })
  }
}
复制代码

比如笔者在业务开发中尝试把关联到state复用逻辑像基本工具函数一样单独抽离出来,这里的context实际就是当前组件,也就是我通过this去让函数支持了state,但是这样的代码很难维护,因为 你可能找不到它们的关联性

hooks应运而生

从Mixin、HOC 、Render Props模式解决状态逻辑复用问题,但是没有去根本的解决复用问题,函数应是代码复用的基本单位,而不是组件,所以说为甚么hook是颠覆性的,因为它从本质上解决了状态逻辑复用问题,以函数作为最小的复用单位,而不是组件

什么是 Hook?

官方介绍:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

什么是函数组件

函数组件只是一个执行函数取返回值的过程,简单理解:state变化,函数组件执行,触发render更新视图,跟Class组件还是不一样的,类组件是state变化,触发render方法更新而不是,这代表什么❓,代表类组件的属性不会被重复声明,而函数组件每次state一变化,就重新执行,会重复声明,所以这也是为什么需要useMemouseCallBack这两个hook,我们接下来会讲到

const Counter=()=>{
  const [
    number,
    setNumber
  ] = useState(0)
  console.log("我触发执行了")
  return (
    <>
      <p>{number}</p>
      <button
        onClick={
          () => setNumber(number + 1)
        }
      >
        改数字
      </button>
    </>
  )
}
复制代码

另外一个有意思的点是:开发中如果我们使用类组件那么就要跟this打交道,然而使用了Hook帮我们摆脱了this场景问题,但是又引入了一个问题,你使用了函数,那么自然而然就会跟闭包打交道,有什么你会不知不觉陷入闭包陷阱(接下来会说到),挺神奇的羁绊,但是闭包带来的好处太多了

记忆函数or缓存函数❓

react-hook的实现离不开记忆函数(也称做缓存函数)或者应该说得益于闭包,我们来实现一个记忆函数

const memorize = function(fn) {
    const cache = {}       // 存储缓存数据的对象
    return function(...args) {        // 这里用到数组的扩展运算符
      const _args = JSON.stringify(args)    // 将参数作为cache的key
      return cache[_args] || (cache[_args] = fn.apply(fn, args))   // 如果已经缓存过,直接取值。否则重新计算并且缓存
    }
  }
复制代码

测试一下:

const add = function(a) {
  return a + 1
}
const adder = memorize(add)
adder(1)    // 2    cache: { '[1]': 2 }
adder(1)    // 2    cache: { '[1]': 2 }
adder(2)    // 3    cache: { '[1]': 2, '[2]': 3 }
复制代码

useState

为什么使用

开发中我们会经常遇到,当我们一个函数组件想要有自己维护的state的时候,不得已只能转换成class

useState 的出现是 :useState 是允许你在 React 函数组件中添加 state 的 Hook 简单的讲就是:可以让你在在函数组件里面使用 class的setState

如何使用

useState接受一个参数,返回了一个数组

  // 使用es6解构赋值,useState(0)的意思是给count赋予初始值0
  // count是一个状态值,setCount是给这个状态值进行更新的函数
  const [count, setCount] = useState(0);
复制代码

举个例子🌰:

import React, { useState } from 'react'

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码

知识点合集

初始化值

useState的初始值,只在第一次有效

场景;点击按钮更新子组件的count

const Child = ({data}) =>{
    const [count, setCount] = useState(data)
    return (
        <div>
            <div>child</div>
            <div>count:{count} --- data:{data}</div>
        </div>
    );
}

const Parent =()=>{
    const [data, setData] = useState(0)

    return(
        <div>
            <div>
                {data}
            </div>
            <button onClick={()=>setCount(data+1)}>更新data</button>
            <Child data={data}/>
        </div>
    )
}
复制代码

测试一下:

// 点击按钮
 <div>count:0 --- data:1</div>
复制代码

更新是直接替换

useState返回更新state的函数与class 组件的 this.setState不同,它不会把新的 state 和旧的 state 进行合并,而是直接替换相当于直接返回一个新的对象,所以这也是闭包陷阱产生的原因之一

let testUser={name:"vnues"} // 定义全局变量
const Parent =()=>{
    const [user, setUser] = useState(testUser)
    if(user!==testUser){
        testUser=user
        console.log(testUser)
    }
    return(
        <div>
            <button onClick={()=>setCount({age:18})}>更新data</button>
            <Child data={data}/>
        </div>
    )
}
复制代码

测试一下:

// 点击按钮
testUser:{age:18}
复制代码

可以看到,函数运行是进入if条件里的,这说明什么,说明user和testUser的指向不同了,证明是直接替换

useState原理

一般而言,函数重新执行,代表着重新初始化状态以及声明,那么我就很好奇,函数组件的hook是如何保存上一次的状态,来看看它的原理吧

let memoizedStates = [] // 存储state 
let index = 0 
function useState (initialState) {
  // 判断memoizedStates有没有缓存值,没有则还是个初始化的useState 
  memoizedStates[index] = memoizedStates[index] || initialState
  let currentIndex = index
  function setState (newState) {
    memoizedStates[currentIndex] = newState // 直接替换
    render() // 进行视图更新
  }
  return [memoizedStates[index++], setState]
}

function render() {
  index = 0 // 重新执行函数组件,重新清零
  ReactDOM.render(<Counter />, document.getElementById('root'));
}
复制代码

❗️注意上面的代码,有个 index=0 的操作,因为添加的时候按照顺序添加的,渲染的时候也要按照顺序渲染的。,所以我们不能把hook写在循环或者判断里

举个例子🌰

const Test=()=>{
   const [count, setCount] = useState(0)
   // 只执行一次
   useEffect(()=>{
       setCount(100) 
   },[])
   return (<div>{count}</div>)
}
复制代码

函数组件Test运行如下:

所以了解useState原理有助于我们日常开发中解决bug

useEffect

Effect Hook 可以让你在函数组件中执行副作用操作,

什么是副作用操作(side effect)

副作用是函数式编程里的概念,在函数式编程的教材中,如下的行为是称之为副作用的

  • 修改一个变量
  • 修改一个对象的字段值
  • 抛出异常
  • 在控制台显示信息、从控制台接收输入
  • 在屏幕上显示(GUI)
  • 读写文件、网络、数据库。

为什么使用

Effect Hook的出现: 一点是让你可以在函数组件里面使用 class的生命周期函数,你可以认为是componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合(官方后续还会实现其它生命周期函数,敬请期待),另外一点是可以让你集中的处理副作用操作(比如网络请求,DOM操作等)

如何使用

useEffect(fn, []) // 接收两个参数 一个是回调函数 另外一个是数组类型的参数(表示依赖)
复制代码

❗️❗️注意:useEffect的执行时机是:React 保证了每次运行 effect 的同时,DOM 都已经更新完毕,默认情况下,useEffect 会在每次渲染后都执行, ,它在第一次渲染之后和每次更新之后都会执行,我们可以根据依赖项进行控制

知识点合集

useEffect只接受函数
 // ❌因为async返回的是个promise对象
 useEffect(async() => {
     const data = await getAjax()
 })
 // 建议😄
  useEffect(() => {
     // 利用匿名函数
     (async()=>{
         const data = await getAjax()
     })()

 })
复制代码

模拟React的生命周期

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。

  • componentDidMount:通过 useEffect 传入第二个参数为[]实现。

  • componentDidUpdate:通过 useEffect 传入第二个参数为空或者为值变动的数组。

  • componentWillUnmount:主要用来清除副作用。通过 useEffect 函数 return 一个函数来模拟。

  • shouldComponentUpdate:你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较。来模拟是否更新组件。

  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

// 模拟 componentDidMount
useEffect(()=>{
    // 逻辑
},[])
复制代码
 // 模拟componentDidUpdate
 useEffect(fn)
复制代码

模拟Vue的$watch方法

useEffect(fn,[user]) // 对user做监控
复制代码

使用多个 Effect 实现关注点分离

就像你可以使用多个 state 的 Hook 一样,你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
}
复制代码

如何清除副作用

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的

无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码
需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!

如何清除:在useEffect的回调函数return一个取消订阅的函数

 useEffect(() => {
    // 订阅
    ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    return () => {
      // 取消订阅
      ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    };
  });
复制代码

useEffect与闭包陷阱

闭包陷阱:就是我们在React Hooks进行开发时,通过useState定义的值拿到的都不是最新的现象。

来看一个场景:我们想要count一直加1下去

const App = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(count)
      setCount(count + 1)
    }, 1000)
    return () => { clearInterval(timeId) }
  }, [])
  return (
    <span style={{ fontSize: 30, color: "red" }}>{count}</span>
  )
}
复制代码

如上图,useEffect的回调函数访问App函数的变量count形成了闭包Closure(App)

来看看结果:

count并不会和想象中那样每过一秒就自身+1并更新dom,而是从0变成1后。count一直都是为1,并不会一直加下去,这就是常见的闭包陷阱

原因是useEffect(fn,[])只执行一次(后面不再执行),setInterval里的回调函数与APP函数组件形成了闭包,count为0,此时执行setCount操作,state变化,函数组件App重新执行,执行const [count, setCount] = useState(0) ,你可以理解成重新声明count变量也就是说setInterval里访问的count变量跟这次重新声明的count变量无关(❗️理解这句话很重要),我们可以稍微改变了,useEffect(fn,[])只执行一次,也就是拿到第一次count变量就不再拿了,我们把依赖性去掉,让它更新后就重新拿到count

const App = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(count)
      setCount(count + 1)
    }, 1000)
    return () => { clearInterval(timeId) }
  })
  return (
    <span style={{ fontSize: 30, color: "red" }}>{count}</span>
  )
}
复制代码

👆可以看到效果是实现的了

如果你没看明白上述所讲的,我们换个例子🌰看看就清晰了:

const App = () => {
  const [user, setUser] = useState({ name: "vnues", age: 18 })
  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(user)
      setUser({ name: "落落落洛克", age: user.age + 1 })
    }, 1000)
    return () => { clearInterval(timeId) }
  }, [])
  return (
    <span style={{ fontSize: 30, color: "red" }}>{user.name}{user.age}</span>
  )
}
复制代码

⚠️上述需要注意的点:setUser操作是直接替换,另外,解决闭包陷阱的几种方式我们放到下面再具体介绍

useRef

useRef 返回一个可变的 ref 对象,其 .current属性被初始化为传入的参数(initialValue),另外ref对象的引用在整个生命周期保持不变

为什么使用

useRef可以用于访问DOM节点或React组件实例和作为容器保存可变变量

如何使用

const refContainer = useRef(initialValue)
复制代码

知识点合集

获取DOM元素的节点

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue),通过current属性可以访问到DOM节点

const App=()=>{
  const inputRef = useRef(null);
  console.log(inputRef) // 没有访问到 此时dom还未挂载
  useEffect(() => {
    console.log(inputRef) // dom挂载完毕
  }, [])
  return <div>
    <input type="text" ref={inputRef} />
  </div>
}
复制代码

结果如下:

注意:createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。具体关于(ref React.createRef useRef、React.forwardRef这些形式我会单独抽一个章节来讲到)

获取子组件的实例

// 实际就是利用了一个回调
const Child2 = React.forwardRef((props, ref) => {
  return <button ref={ref}>Child2</button>
})


const App = () => {
  const childRef = useRef();
  const child2Ref = useRef()
  useEffect(() => {
    console.log('useRef')
    console.log(childRef.current)
    childRef.current.handleLog();
    console.log("child2Ref", child2Ref)

  }, [])
  return (
    <div>
      <h1>Hello World!</h1>
      <Child ref={childRef} count="1" />
      <Child2 ref={child2Ref} />
    </div>
  )
}

// 因为函数组件没有实例,如果想用ref获取子组件的实例,子组件组要写成类组件
class Child extends Component {
  handleLog = () => {
    console.log('Child Component');
  }
  render() {
    const { count } = this.props;
    return <h2>count: { count }</h2>
  }
}
复制代码

结果:

注意一点:组件实例是对于类组件来说的 函数组件没有实例,使用React.forwardRefAPI是转发ref拿到子组件的DOM中想要获取的节点,并不是获取实例因为函数组件没有实例这一概念,

存储可变变量的容器

记住useRef不单单用于获取DOM节点和组件实例,还有一个巧妙的用法就是作为容器保留可变变量,可以这样说:无法自如地使用useRef会让你失去hook将近一半的能力

官方的说法:useRef 不仅适用于 DOM 引用。 “ref” 对象是一个通用容器, 其 current 属性是可变的,可以保存任何值(可以是元素、对象、基本类型、甚至函数)

我们来看看👇的分析:

在类组件和函数组件中,我们都有两种方法在re-render(重新渲染)之间保持数据:

在类组件中
  • 在组件状态中:每次状态更改时,都会重新渲染组件。

  • 在实例变量中:该变量的引用将在组件的整个生命周期内保持不变。
    实例变量的更改不会产生重新渲染。

在函数组件中

在函数组件中使用Hooks可以达到与类组件等效的效果:

  • 在state中:使用useState或useReducer。state的更新将导致组件的重新渲染。

  • 在ref(使用useRef返回的ref)中:等效于类组件中的实例变量,更改.current属性不会导致重新渲染。(相当于 ❗️❗️ref对象充当this,其current属性充当实例变量

const App = () => {
  const counter = useRef(0);
  const handleBtnClick = () => {
    counter.current = counter.current + 1;
    console.log(counter)
  }
  console.log("我更新啦")
  return (
    <>
      <h1>{`The component has been re-rendered ${counter.current} times`}</h1>
      <button onClick={handleBtnClick}>点击</button>
    </>
  );
}
复制代码
替代函数组件的局部变量
//  const name="vnues"
const App = () => {
  const [count, setCount] = useState(0)
  const name="vnues" // 声明局部变量
   const handleBtnClick = () => {
    setCount(count+1)
  }
  return (
    <>
      <span>{count}</span>
      <button onClick={handleBtnClick}>点击</button>
    </>
  )
}
复制代码

有时候我们存在这种情况,需要声明一个变量去保存值,但是如果函数组件state变化,函数重新执行,会造成重新声明name,显然没有必要,有同学说可以放在全局下,避免没必要的重复声明,实际也是一个解决方法,但是如果没有及时回收,容易造成内存泄漏,我们可以利用Ref容器的特点,使用current去保存可变的变量

const App = () => {
  const [count, setCount] = useState(0)
  const ref=useRef("vnues") // 利用容器的特点
   const handleBtnClick = () => {
    setCount(count+1)
  }
  return (
    <span>{count}</span>
     <button onClick={handleBtnClick}>点击</button>
  )
}
复制代码

ref引用保持不变

由于useRef返回ref对象的引用在FunctionComponent 生命周期保持不变,本身又是作为容器的情况保存可变的变量,所以我们可以利用这些特性可以做很多操作,这一点与useState不同

解决闭包陷阱

你可以这样理解:此处的countRef就是相当于全局变量,一处被修改,其他地方全更新…

const App = () => {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)
  useEffect(() => {
    const timeId = setInterval(() => {
      countRef.current = countRef.current + 1
      setCount(countRef.current)
    }, 1000)
    return () => clearInterval(timeId)
  }, [])
  return (
    <span>{countRef.current}</span>
  )
}
复制代码

结果:

获取上一轮的 props 或 state

Ref 不仅可以拿到组件引用、创建一个 Mutable 副作用对象,还可以配合 useEffect 存储一个较老的值,最常用来拿到 previousProps,React 官方利用 Ref 封装了一个简单的 Hooks 拿到上一次的值:

const usePrevious=(value)=>{
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  });
  return ref.current
}
复制代码

由于 useEffect 在 Render 完毕后才执行,因此 ref 的值在当前 Render 中永远是上一次 Render 时候的,我们可以利用它拿到上一次 props或者state

更新Ref是副作用操作

官方文档说到:Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects

简单点说就是更新Ref是副作用操作,我们不应该在render-parse(渲染阶段)更新,而是在useEffect或者useLayoutEffect去完成副作用操作

我们先来看看FunctionComponent 生命周期图:

从图中可以发现,在Render phase 阶段是不允许做 “side effects” 的,也就是写副作用代码,这是因为这个阶段可能会被 React 引擎随时取消或重做。

修改 Ref 属于副作用操作,因此不适合在这个阶段进行。我们可以看到,在 Commit phase 阶段可以做这件事

// ❌的写法
const RenderCounter = () => {
  const counter = useRef(0);

  // Since the ref value is updated in the render phase,
  // the value can be incremented more than once
  counter.current = counter.current + 1;

  return (
    <h1>{`The component has been re-rendered ${counter} times`}</h1>
  );
};
复制代码
// 正确✅的写法
const RenderCounter = () => {
  const counter = useRef(0);

  useEffect(() => {
    // 每次组件重新渲染,
    // counter就加1
    counter.current = counter.current + 1;
  }); 

  return (
    <h1>{`The component has been re-rendered ${counter} times`}</h1>
  );
};
复制代码

useCallback

该hooks返回一个 memoized 回调函数,❗️根据依赖项来决定是否更新函数

为什么使用

react中Class的性能优化。在hooks诞生之前,如果组件包含内部state,我们都是基于class的形式来创建组件。react中,性能的优化点在于:

  • 调用setState,就会触发组件的重新渲染,无论前后的state是否不同
  • 父组件更新,子组件也会自动的更新

基于上面的两点,我们通常的解决方案是:

  • 使用immutable进行比较,在不相等的时候调用setState

  • shouldComponentUpdate中判断前后的props和state,如果没有变化,则返回false来阻止更新

在hooks出来之后,我们能够使用function的形式来创建包含内部state的组件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此useMemo 和useCallback就是用来优化性能问题

举个例子🌰:

const set = new Set() // 借助ES6新增的数据结构Set来判断
export default function Callback() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
    const callback =() => {
        console.log(count);
    }
    set.add(callback)
    return <div>
        <h4>{count}</h4>
        <h4>{set.size}</h4>
        <button onClick={() => setCount(count + 1)}>+</button>
    </div>;
}
复制代码

如何使用

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

⚠️不是根据前后传入的回调函数fn来比较是否相等,而是根据依赖项决定是否更新回调函数fn,笔者一开始想错了

const memoizedCallback = useCallback(fn, deps)
复制代码

知识点合集

useCallback的依赖参数

该回调函数fn仅在某个依赖项改变时才会更新如果没有任何依赖项,则deps为空

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
)
复制代码

useCallback与React.memo完美搭配

场景:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

React.memo 为高阶组件。它与 React.PureComponent 非常相似但只适用于函数组件,而不适用 class 组件能对props做浅比较,防止组件无效的重复渲染

// 父组件
const Parent=()=>{
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const callback = useCallback(() => {
        return count;
    }, [count]);
    return <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}

// 子组件
const Child=({ callback })=>{
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}
export default React.memo(Child) // 用React.memo包裹
复制代码

如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

useMemo

useCallback类似,区别就是useCallback返回一个 memoized函数,而useMemo返回一个memoized 值

❗️你可以这样认为:

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
复制代码

为什么使用

和为什么使用useCallback类似,另外一点就是缓存昂贵的计算(避免在每次渲染时都进行高开销的计算)

export default function WithMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    // 缓存了昂贵的计算
    const expensive = useMemo(() => {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count]);

    return <div>
        <h4>{count}-{expensive}</h4>
        {val}
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
复制代码

上面我们可以看到,使用useMemo来执行昂贵的计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。

如何使用

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
 // computeExpensiveValue得有返回值
复制代码

知识点合集

useMemo与React.memo完美搭配

const Couter = React.memo(function Couter(props) {
  console.log('Couter render')
  return (
    <> 
       <span>{count}</span>
       <div onClick={props.clickHandle}>{props.count}</div>
    </>

  )
})

const App=()=> {
  const [count, setCount] = useState(0);

  const db = useMemo(() => {
    return count * 2;

  }, [count])

  return (
    <div>
      <Couter count={count}></Couter>
      <div>{db}</div>
      <button onClick={() => { setCount(count + 1) }}>点击</button>
    </div>
  )
}
复制代码

用对性能优化

有同学会想到,竟然useCallbackuseMemo这么好用,我可不可像👇下面例子🌰中的写法一样,对我以后的项目这样优化

const App = () => {
  const [count, setCount] = useState(0);
  /**
   * 对所有的方法我都采用useCallback做缓存优化
   * const handleBtn=()=>{
   *  setCount(count + 1)
   * }
   */
  const handleBtn = useCallback(() => {
    setCount(count + 1)
  }, [count])


  /**
   * 对所有的局部变量我都采用useMemo做缓存优化
   * const db=count*2
   */

  const db = useMemo(() => {
    return count * 2;
  }, [count])

  return (
    <div>
      <Couter count={count}></Couter>
      <div>{db}</div>
      <button onClick={() => { handleBtn }}>点击</button>
    </div>
  )
}
复制代码

❗️笔者一开始也有这样想到,如果你真按照这种形式去开发我们的项目,那么恭喜你,你会死的很惨

为什么useCallback和useMemo更加糟糕

性能优化不是免费的,它们总是带来成本,但这并不总是带来好处来抵消成本,所以我们采用useCallback和useMemo做性能优化,应该是做到花费的成本大于收入的成本

首先,我们需要知道useCallback,useMemo本身也有开销。useCallback,useMemo 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useCallback,useMemo 可能会影响程序的性能,并且也加大了维护成本,毕竟代码更加复杂化了

什么时候使用 useMemo 和 useCallback?

使用useMemo 和 useCallback出于这两个目的

  • 保持引用相等

  • 对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用 useMemo 和 useCallback

  • 自定义 Hook 中暴露出来的 object、array、函数等,都应该使用useMemo 和 useCallback,以确保当值相同时,引用不发生变化(你可以理解成是第一种说法的衍生,即自定义hooks比作组件,因为一个函数组件state一变化就会重新执行函数)

  • 昂贵的计算

  • 比如👆例子🌰的expensive函数

无需使用useMemo 和 useCallback 的场景

  • 如果返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括动态声明的 Symbol),一般不需要使用useMemo 和 useCallback

  • 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件)且没有用到其他 Hook 的依赖数组中,一般不需要使用useMemo 和 useCallback

实际场景

场景:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

// 父组件
const Parent=()=>{
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const callback = useCallback(() => {
        return count;
    }, [count]);
    return <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}

// 子组件
const Child=({ callback })=>{
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}
export default React.memo(Child) // 用React.memo包裹
复制代码

这个场景是复用上述例子🌰的场景,这就是要保持引用不变的场景,显然这次收益的成本大于优化付出的成本,子组件可以避免不必要的渲染

这篇关于React系列-状态逻辑复用问题(中)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!