React的hooks理解和使用

一个大基调,函数组件的每一次更新,就是把函数组件重新执行,这样每次会产生新的闭包,而闭包中所有创建函数的操作,都会重新创建新的堆内存。

useState

setState 是直接赋值,而不是局部替换

const [obj, setObj] = useState({ name: '颜酱', age: 18 });
setObj({ name: '换一个名字' });

上面的语句,这样 set 之后,obj 的值会变成{name:'换一个名字'},而没有 age 字段。

所以对象赋值,需要拿到之前的,然后覆盖

const [obj, setObj] = useState({ name: '颜酱', age: 18 });
setObj({ ...obj, name: '换一个名字' });

组件需要多个属性的时候,【推荐尽量分开写 state】,而不是用一个 obj 涵盖所有。

setState 是异步的

const [name, setName] = useState('颜酱');
setName('花花');
// 是 颜酱,因为更新是异步的
console.log(name);

上面的输出是颜酱,setName 修改值不是同步操作,所以 name 仍然是旧值。、

useState 的 set 操作会有一个更新队列

setXX 之后,并不会立马更新视图,而是先进去队列,然后批次修改,更新视图

import { useState } from 'react';

export default function Counter() {
  console.log('渲染');
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(1);

  function handleClick() {
    setCount(count + 1);
    setCount1(count1 + 1);
  }

  return <button onClick={handleClick}>You pressed me {count} times</button>;
}

以上代码,初始进来,打印一次渲染,点击按钮之后,虽然有两次 setXXX,但其是在一个队列里,只更新视图一次,所以,也只打印一次渲染

如果需要同步更新,使用flushSync

function handleClick() {
  flushSync(() => {
    setCount(count + 1);
  });
  setCount1(count1 + 1);
}

flushSync,会直接截断当前队列,然后更新。
所以点击按钮之后,会打印 2 次渲染

useState 的闭包逻辑

import { useState } from 'react';

export default function Counter() {
  console.log('渲染');
  const [count, setCount] = useState(0);

  function handleClick() {
    for (let i = 0; i < 10; i++) {
      setCount(count + 1);
    }
  }

  return <button onClick={handleClick}>数字 {count}</button>;
}

点击一次按钮之后,打印了几次渲染,count 是多少?

count 初始是 0,点击按钮之后,handleClick 执行,注意,这里形成上下文,count 是 0,在 handleClick 的上级作用域

  • 循环第一次,count 往上寻,所以是 0,setCount(0+1)
  • 循环第二次,count 往上寻,所以是 0,setCount(0+1)

  • 其实循环多少次多一样,最后一次循环结束,队列开始执行,只执行一次 setCount(1)
    视图更新,重新执行 Counter,所以只打印一次渲染,count 是 1

如果循环体里加上flushSync呢?

function handleClick() {
  for (let i = 0; i < 10; i++) {
    flushSync(() => {
      setCount(count + 1);
    });
  }
}

点击一次按钮之后,打印了几次渲染,count 是多少?

count 初始是 0,点击按钮之后,handleClick 执行,注意,这里形成上下文,count 是 0,在 handleClick 的上级作用域

  • 循环第一次,count 往上寻,所以是 0,setCount(0+1),因为是 flushSync,所以截断队列,视图更新,重新执行 Counter,所以此时打印一次渲染,count 是 1
  • 循环第二次,count 往上寻,所以是 0,setCount(0+1),因为是 flushSync,所以截断队列,但是因为值是 1 同 上一次,所以视图并不更新

所以只打印一次渲染,count 是 1。也有可能会打印 2 次渲染,因为视图还没有完全渲染,所以有了第二轮。

换成setTimeout

function handleClick() {
  for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      setCount(count + 1);
    }, i * 1000);
  }
}

其实也一样。

那怎么修改循环体内的代码,让其变成 10 呢

setState使用函数参数

上面的答案就是

function handleClick() {
  for (let i = 0; i < 10; i++) {
    setCount((prev) => prev + 1);
  }
}

点击按钮之后,handleClick 执行

  • 循环第一次,prev 是 0,setCount(0+1),进入队列
  • 循环第二次,prev 是 1,setCount(1+1),进入队列
  • 循环第十次,prev 是 9,setCount(9+1),进入队列

队列执行,合并成最后一次,setCount(10),视图更新,所以此时打印一次渲染,count 是 10

初始 state 的计算逻辑复杂的话,用函数赋值

import { useState } from 'react';

export default function Counter() {
  /** 这一段都是count初始值的逻辑 */
  let init = 0;
  for (let i = 0; i < 10; i++) {
    init++;
  }
  /** 这一段都是count初始值的逻辑 */
  const [count, setCount] = useState(init);

  function handleClick() {
    setCount(count + 1);
  }

  return <button onClick={handleClick}>数字 {count}</button>;
}

上面这样有什么不好呢?

点击按钮,更新 count,视图更新,Counter 重新执行,此时,这段逻辑又重新走了一遍,但是因为初始值只有第一次生效,所以这里执行没有任何意义。怎么让其只执行一次呢。

把函数放进参数里,返回值是初始值就可以啦,这样值更新的时候,并不会走这里的逻辑

const [count, setCount] = useState(() => {
  let init = 0;
  for (let i = 0; i < 10; i++) {
    init++;
  }
  return init;
});

useState 的简单源码理解

简单写下 useState 的实现,帮助理解使用逻辑:

let state;

function useState(value) {
  const isFirst = state === undefined;
  const isFunc = typeof value === 'function';
  // 第一次执行useState的时候,state肯定没有赋值过,如果参数是函数,那就用函数返回值赋值,否则直接赋值
  if (isFirst) {
    state = isFunc ? value() : value;
  }

  // setState是个函数
  const setState = (newValue) => {
    const isFunc = typeof newValue === 'function';
    const prev = state;
    // 如果参数是个函数,将上一次的值传过去,执行函数,否则直接赋值
    state = isFunc ? newValue(prev) : newValue;
    // 如果新值和旧值是一样的,就不需要视图更新了,否则通知视图更新
    if (newValue === prev) return;
    // 通知视图更新(视图更新的逻辑里,组件函数会重新执行)
  };
  return [state, setState];
}

useEffect

useEffect 常用的四种情况:

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(1);

  useEffect(() => {
    console.log('@1 初始渲染和每次重新渲染都会执行', count);
  });

  useEffect(() => {
    console.log('@2 只有初始渲染执行,注意后面的空依赖', count);
  }, []);

  useEffect(() => {
    console.log('@3 初始渲染和每次依赖项count变更重新渲染后执行,注意后面的依赖', count);
  }, [count]);

  useEffect(() => {
    return () => {
      console.log(
        '@4 每次依赖项count变更重新渲染后,React 将首先使用旧值运行 此cleanup 函数',
        count,
      );
    };
  }, [count]);

  function handleClick() {
    setCount(count + 1);
  }
  function handleClick1() {
    setCount(count1 + 1);
  }

  return (
    <>
      <button onClick={handleClick}>数字 {count}</button>
      <button onClick={handleClick1}>数字 {count1}</button>
    </>
  );
}
  • useEffect(fn),初始渲染和每次重新渲染,fn 都会执行,雷同于mountedupdated
  • useEffect(fn,[]),只有初始渲染,fn 会执行,雷同于mounted
  • useEffect(fn,[count]),初始渲染和依赖项 count 变更重新渲染后,fn 会执行
  • useEffect(()=>fn,[count]),依赖项 count 变更重新渲染后,React 将首先使用旧值运行 fn。在组件从 DOM 中移除后,React 将最后一次运行 fn 函数,这个类似destroyed

请求数据

如果你这么请求数据的话

useEffect(async () => {
  let data = await apiData();
  console.log(data);
});

那就会报错,因为这个 async 函数会返回Promise实例,而 useEffect 如果有返回值的话必须返回一个函数。

请求数据,简单点就是 async 用新函数包下执行,或者 then.

useEffect(() => {
  const apiData = async () => {
    let data = await apiData();
    console.log(data);
  };
  apiData();
});

useLayoutEffect

先理解下视图更新的步骤:

  1. 基于 babel-preset-react-app 把 JSX 编译为 createElement 格式
  2. 把 createElement 执行,创建出 virtualDoM
  3. 基于 root.render 方法把 virtuaLDOM 变为真实 DOM 对象 「DOM-DIFFJ
    【useLayoutEffect 阻塞第四步操作,先去执行 Effect 链表中的方法「同步操作」
    seEffect 第四步操作和 Effect 链表中的方法执行,是同时进行的「异步操作」】
  4. 浏览器渲染和绘制真实 DOM 对象

useLayoutEffect 其实不是很常用,根据上面的描述,其useEffect的差别在于:

  • useLayoutEffect会阻塞浏览器渲染真实 DOM,优先执行 Effect 链表中的 callback;
  • useEffect不会阻塞浏览器渲染真实 DOM,在渲染真实 DOM 的同时,去执行 Effect 链表中的 cal1back;
  • uselayoutEffect设置的callback要优先于useEffect中的去执行!
  • 在两者设置的 callback 中,依然可以获取 DOM 元素「原因:真实 DOM 对象已经创建了,区别只是浏览器是否渲染」

如果在 callback 函数中又修改了状态值「视图又要更新」

  • useEffect:浏览器肯定是把第一次的真实已经绘制了,再去渲染第二次真实 DOM
  • uselayoutEffect:浏览器是把两次真实 DOM 的渲染,合并在一起渲染的
useEffect(() => {
  console.log('useEffect '); //第二个输出
}, [num]);
useLayoutEffect(() => {
  console.log('useLayoutEffect'); //第一个输出
}, [num]);

useRef 和 useImperativeHandle

useRef主要获取真实 DOM、子组件实例。

举一个子组件的例子,子组件用forwardRef useImperativeHandle改装下

// 子组件
import { forwardRef } from 'react';

const Child = forwardRef(({ value, onChange }, ref) => {
  return <input value={value} onChange={onChange} ref={ref} />;
});

// 父组件
const Parent = () => {
  const inputRef = useRef(null);

  return <MyInput ref={inputRef} />;
};

const Child = React.forwardRef(function Child(props, ref) {
  let [text, setText] = useState('475');
  const submit = () => {};
  useImperativeHandle(ref, () => {
    //在这里返回的内容,都可以被父组件的REF对象获取到
    return {
      text,
      submit,
    };
  });
  return (
    <div className="child-box">
      <span>哈哈哈</span>
    </div>
  );
});
const Demo = function Demo() {
  let x = useRef(null);
  useEffect(() => {
    console.log(x.current.text);
  });
  return (
    <div className="demo">
      <Child ref={x} />
    </div>
  );
};

useMemo

useMemo 和vue的computed非常相似。

import { useState, useMemo } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(1);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  });
  const handleClick1 = useCallback(() => {
    setCount(count1 + 1);
  });

  const sum = useMemo(()=>{
    return count + count1
  },[count,count1])

  return (
    <>
      <button onClick={handleClick}>数字 {count}</button>
      <button onClick={handleClick1}>数字 {count}</button>
      <div>{sum}</div>
    </>
  );
}

total = useMemo(fn,[count]),初始渲染执行 fn,后期依赖项 count 发生变化才会执行 fn。fn 的结果返回给total
useMemo 是优化操作,如果某个值需要复杂的逻辑计算,建议这样操作,减少不必要的运算,提高组件更新速度。

useCallback

每次组件更新的时候,组件函数内,所有的函数都会重新创建。如果不希望内部函数每次都重新创建,就用useCallback包起来。

import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

上面的只有依赖项发生变化的时候,handleSubmit 函数才会被重新创建。
但是不建议每个内部函数都用useCallback包起来,因为useCallback处理的逻辑和缓存机制本身也消耗时间,其代价可能不比创建新的函数小。

适合场景:父组件将内部函数传递给子组件的时候

const Child = React.memo(() => {
  console.log('子组件渲染');
  return <div>子组件</div>;
});

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  });

  return (
    <>
      <button onClick={handleClick}>数字 {count}</button>
      <Child handleClick={handleClick} />
    </>
  );
}

注意两点:

  • handleClick 用 useCallback 包起来,然后传给子组件
  • 子组件函数用React.memo包起来

只有这样,点击按钮的时候,子组件才不会每次都重新渲染。useCallback保证函数的引用地址一致。React.memo判断如果新老属性一致的话,则不更新子组件。

原文链接:https://juejin.cn/post/7321967886934294528 作者:颜酱

(0)
上一篇 2024年1月10日 上午11:03
下一篇 2024年1月10日 上午11:13

相关推荐

发表回复

登录后才能评论