react 16.8 新特性总结(二)掌握useEffect.md

useEffect是什么

  1. useEffect 是一个专门react hook的一部分内容主要是为函数组件服务。
  2. 一般情况下我们可以看作componentDidMount, componentDidUpdate,componentWillUnmount 三个生命周期的结合体。
  3. 会在每次 render 的时候必定执行一次。
  4. 如果返回了函数,那么在下一次 render 之前或组件 unmount 之前必定会运行一次返回函数的代码。
  5. 如果指定了依赖数组,且不为空,则当数组里的每个元素发生变化时,都会重新运行一次。
  6. 如果数组为空,则只在第一次 render 时执行一次,如果有返回值,则同 3
  7. 如果在 useEffect 中更新了 state,且没有指定依赖数组,或 state 存在于依赖数组中,就会造成死循环。

在每一次渲染时都会锁住自己的props和state

我们来看一个例子

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

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

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

然后我们按照一下方式去执行

  • 点击增加counter到3
  • 点击一下 “Show alert”
  • 点击增加 counter到5并且在定时器回调触发前完成大家猜一下结果是什么

链接: 代码结果查看地址.
在这里插入图片描述
我们可以看到答案是三
为什么呢
我们发现count在每一次函数调用中都是一个常量值。值得强调的是 — 我们的组件函数每次渲染都会被调用,但是每一次调用中count值都是常量,并且它被赋予了当前渲染中的状态值。
实际上每渲染一次,每一次渲染都有一个新版本的handleAlertClick,并且每一次都有自己的count

effects的每一次渲染

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>
  );
}

有一个问题effect 是如何取到自己的count的状态值的呢?
答案是:
并不是count的值在“不变”的effect中发生了改变,而是effect 函数本身在每一次渲染中都不相同。
React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它

// During first render
function Counter() {
  // ...
  useEffect(
    // Effect function from first render
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...
}

// After a click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from second render
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...
}

// After another click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from third render
    () => {
      document.title = `You clicked ${2} times`;
    }
  );
  // ..
}

看起来是不是很高大上,其实原理很简单,大家当作js中的顺序执行就可以理解了
最后对于这个,我们思考一下下面代码的结果会是什么样子呢?

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

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

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

结果如下所示:
在这里插入图片描述
你答对了没有
如果看懂的话,你可能会说,这不就是闭包嘛,对的,就是闭包
在组件内什么时候去读取props或者state是无关紧要的。因为它们不会改变。在单次渲染的范围内,props和state始终保持不变。(解构赋值的props使得这一点更明显。)
但是class 确不是这样的哦

 componentDidUpdate() {
    setTimeout(() => {
      console.log(`You clicked ${this.state.count} times`);
    }, 3000);
  }

class组件中的结果
在这里插入图片描述

使用useEffect 模仿class版本

在这里我们会用到useRef
关于useRef与ref 有什么关系我会后续在其他文章中解释。

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

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });

防止effect

上文中我们知道,effect 每次运行都是独立的顺序调用。
那我们思考一下,如果这样是否会出现内存泄漏的问题呢,答案是肯定的,
effect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值:

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

React只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:

React 渲染{id: 20}的UI。
浏览器绘制。我们在屏幕上看到{id: 20}的UI。
React 清除{id: 10}的effect。
React 运行{id: 20}的effect。

去掉不必要的Effect的渲染

举个例子:

function Greeting({ name }) {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    document.title = 'Hello, ' + name;
  });

  return (
    <h1 className="Greeting">
      Hello, {name}
      <button onClick={() => setCounter(counter + 1)}>
        Increment
      </button>
    </h1>
  );
}

但是我们的effect并没有使用counter这个状态。我们的effect只会同步name属性给document.title,但name并没有变。在每一次counter改变后重新给document.title赋值并不是理想的做法。
react 并不能区分effect的不同
这是为什么你如果想要避免effects不必要的重复调用,你可以提供给useEffect一个依赖数组参数(deps):

 useEffect(() => {
    document.title = 'Hello, ' + name;
  }, [name]); // Our deps
```只有依赖数组改变时useEffect 才会改变

## 选择对的依赖项
我们看下面的代码
```function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
     // 让resize事件触发handleResize
     window.addEventListener('resize', handleResize)
     return () => window.removeEventListener('resize', handleResize)
  }, [])

  const handleResize = () => {
    // 把count输出
    console.log(`count is ${count}`)
  }

  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+</button>
      <h1>{count}</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

这段代码会画出我们熟悉的Counter例子。
在这里插入图片描述
现在我们如果点击那个+按钮,下面的数字0当然会增长,比如我们现在让count增长为1,这时候你去改变浏览器窗口的大小,console上会输出什么呢?
你可能预期这样输出:

count is 1
事实上,输出是这样:

count is 0

在第一次渲染中,count是0。因此,setCount(count + 1)在第一次渲染中等价于setCount(0 + 1)。既然我们设置了[]依赖,effect不会再重新运行,它后面每一秒都会调用setCount(0 + 1)
我们可以依赖一下规则做规定
链接: (https://www.csdn.net/).

Effect自给自足

为了实现这个目的,我们需要问自己一个问题:我们为什么要用count?可以看到我们只在setCount调用中用到了count。在这个场景中,我们其实并不需要在effect中使用count。当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式:

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

我们来修改上面的例子让它包含两个状态:count 和 step。我们的定时器会每次在count上增加一个step值:

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

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

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}

我们用一个dispatch依赖去替换effect的step依赖:

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]);

函数也可以为依赖

如果某些函数仅在effect中调用,你可以把它们的定义移到effect中:

function SearchResults() {
  // ...
  useEffect(() => {
    // We moved these functions inside!
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=react';
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, []); 
  // ...
}

但是函数每次渲染都会改变这个事实本身就是个问题,比如有两个effects会调用getFetchUrl

function SearchResults() {
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // 🔴 Missing dep: getFetchUrl

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // 🔴 Missing dep: getFetchUrl

  // ...
}

但是如果两个依赖都会调用getFetchUrl,而它每次的渲染都不同,所以我们的依赖数组就会变得无用

function SearchResults() {
  // 🔴 Re-triggers all effects on every render
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  // ...
}

解决办法
如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在effects中使用:

function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  // ...
}

或者写成useCallback Hook:

  // ✅ Preserves identity when its own deps are the same
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ✅ Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}

useMemo 可以让我们对复杂的对象做类似的事情

function ColorPicker() {
  // Doesn't break Child's shallow equality prop check
  // unless the color actually changes.
  const [color, setColor] = useState('pink');
  const style = useMemo(() => ({ color }), [color]);
  return <Child style={style} />;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值