useEffect的解读

每天对自己多问几个为什么,总是有着想象不到的收获。 一个菜鸟小白的成长之路(copyer)

前面: 这篇完全是根据阅读大佬博客写下来的笔记(摘抄笔记)。

博客原文:精读《useEffect 完全指南》 (qq.com)

理解useEffect,就必须先深入理解 Function Component 的渲染机制。

Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。

理解render函数

每次 Render 都有自己的 Props 与 State

每个render函数都是自己的state和props,根据状态的不同的变化,而造成不停的render。那么可以这样理解, 每次的render的内容都会形成一个快照并保留下来,因此状态变化N次,就形成了N个render的状态,每个render的状态都有着自己固定的不变的props和state

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

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

在每次点击时,count 只是一个不会变的常量,而且也不存在利用 Proxy 的双向绑定,只是一个常量存在于每次 Render 中。

初始状态下 count 值为 0,而随着按钮被点击,在每次 Render 过程中,count 的值都会被固化为 123

// During first render
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>;
  // ...
}

// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>;
  // ...
}

// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>;
  // ...
}

其实不仅是对象,函数在每次渲染时也是独立的。这就是 Capture Value 特性,后面遇到这种情况就不会一一展开,只描述为 “此处拥有 Capture Value 特性”。

每次render函数都有着自己的逻辑处理

解释了为什么下面的代码会输出 5 而不是 10

const Main: React.FC = () => {
    const [temp, setTemp] = React.useState(5);
    const log = () => {
      setTimeout(() => {
        console.log("3 秒前 temp = 5,现在 temp =", temp);
      }, 3000);
    };
    const btn = () => {
        setTemp(10)
        console.log(temp)    // 5
        log()                // 3 秒前 temp = 5,现在 temp = 5
    }
  
    return (
      <button onClick={btn}>
        点击
      </button>
    );
}

export default Main

当触发btn点击是事件后,首先就会执行setTemp这个函数(改变temp的值后,就会生成一个新的render函数),但是接下来的代码 console.log(temp)和log()函数依旧还是本次的render函数, temp的状态为5,所以就是 打印出来 temp 为5

原因就是 templog 都拥有 Capture Value 特性。(捕获本次的render)

每次 Render 都有自己的 Effects

useEffect 也一样具有 Capture Value 的特性。

useEffect 在实际 DOM 渲染完毕后执行,那 useEffect 拿到的值也遵循 Capture Value 的特性:

function Counter() {
  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>
  );
}

上面的 useEffect 在每次 Render 过程中,拿到的 count 都是固化下来的常量。

如何绕过 Capture Value

利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。

const Main: React.FC = () => {
    const [count, setCount] = React.useState(0);
    const latestCount = useRef(count);

    React.useEffect(() => {
        // Set the mutable latest value
        latestCount.current = count;
        setTimeout(() => {
        // Read the mutable latest value
        console.log(`You clicked ${latestCount.current} times`);
        }, 3000);
    });
    return (
        <button onClick={() => setCount(count+1)}>点击</button>
    )
}

export default Main

也可以简洁的认为,ref 是 Mutable 的,而 state 是 Immutable 的。

回收机制

在组件被销毁时,通过 useEffect 注册的监听需要被销毁,这一点可以通过 useEffect 的返回值做到:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});

在组件被销毁时,会执行返回值函数内回调函数。同样,由于 Capture Value 特性,每次 “注册” “回收” 拿到的都是成对的固定值。

用同步取代 “生命周期”

Function Component 不存在生命周期,所以不要把 Class Component 的生命周期概念搬过来试图对号入座。Function Component 仅描述 UI 状态,React 会将其同步到 DOM,仅此而已。

既然是状态同步,那么每次渲染的状态都会固化下来,这包括 state props useEffect 以及写在 Function Component 中的所有函数。

然而舍弃了生命周期的同步会带来一些性能问题,所以我们需要告诉 React 如何比对 Effect。

告诉 React 如何对比 Effects

虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量:

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // Our deps

直到 name 改变时的 Rerender,useEffect 才会再次执行。

不要对 Dependencies 撒谎

如果你明明使用了某个变量,却没有申明在依赖中,你等于向 React 撒了谎,后果就是,当依赖的变量改变时,useEffect 也不会再次执行:

useEffect(() => {
  document.title = "Hello, " + name;
}, []); // Wrong: name is missing in dep
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

setInterval 我们只想执行一次,所以我们自以为聪明的向 React 撒了谎,将依赖写成 []

“组件初始化执行一次 setInterval,销毁时执行一次 clearInterval,这样的代码符合预期。” 你心里可能这么想。

但是你错了,由于 useEffect 符合 Capture Value 的特性,拿到的 count 值永远是初始化的 0相当于 setInterval 永远在 count0 的 Scope 中执行,你后续的 setCount 操作并不会产生任何作用。

诚实的代价

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

你老实告诉 React “嘿,等 count 变化后再执行吧”,那么你会得到一个好消息和两个坏消息。

好消息是,代码可以正常运行了,拿到了最新的 count

坏消息有:

  1. 计时器不准了,因为每次 count 变化时都会销毁并重新计时。
  2. 频繁 生成/销毁 定时器带来了一定性能负担。

怎么既诚实又高效呢?

上述例子使用了 count,然而这样的代码很别扭,因为你在一个只想执行一次的 Effect 里依赖了外部变量。

既然要诚实,那只好 想办法不依赖外部变量

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

setCount 还有一种函数回调模式,你不需要关心当前值是什么,只要对 “旧的值” 进行修改即可。这样虽然代码永远运行在第一次 Render 中,但总是可以访问到最新的 state

将更新与动作解耦

你可能发现了,上面投机取巧的方式并没有彻底解决所有场景的问题,比如同时依赖了两个 state 的情况:

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [step]);

你会发现不得不依赖 step 这个变量,我们又回到了 “诚实的代价” 那一章。当然 Dan 一定会给我们解法的。

利用 useEffect 的兄弟 useReducer 函数,将更新与动作解耦就可以了:

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [dispatch])

这就是一个局部 “Redux”,由于更新变成了 dispatch({ type: "tick" }) 所以不管更新时需要依赖多少变量,在调用更新的动作里都不需要依赖任何变量。 具体更新操作在 reducer 函数里写就可以了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值