美文网首页前端开发那些事儿
深度梳理 React Hook 对副作用操作的处理(二)

深度梳理 React Hook 对副作用操作的处理(二)

作者: AizawaSayo | 来源:发表于2021-02-18 04:25 被阅读0次

接上篇:深度梳理 React Hook 对副作用操作的处理(一)
在 React 组件中,副作用分需要清除的和不需要清除的。

比如手动变更 DOM,记录日志,这些通常都是无需清除的操作。这些effect只是我们在组件渲染完成(上篇详细掰扯过useEffect中副作用执行的时机)之后进行一些额外的同步操作。

// 无需清除的副作用演示
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 变化时执行

而订阅外部数据源、添加定时器等容易引起内存泄漏的操作,则很有必要清除。试想一下,比如组件卸载了(组件从应用中移除了,具体比如路由改变:navigate到已有屏幕/goBack操作、条件渲染为false等),但这个组件里的定时器还没执行,绑定的事件监听还没销毁,订阅的 observable 还没有取消订阅。也就是说在做异步操作的时候,突然被打断了。这会影响后面语句的执行,这时候就需要把异步操作中止掉。

我们按不同场景来一一演示:

异步获取数据后的赋值。

解决方式有二。

一是直接通过请求工具提供的强制中断请求方法

先列一下请求工具强制中断方法帮助文档:

axios https://github.com/axios/axios#cancellation

fetch https://developer.mozilla.org/en-US/docs/Web/API/AbortController

umi-request https://github.com/umijs/umi-request#use-cancel-token

axios CancelToken API 演示:

// axios 中断请求方法
useEffect(() => {
  const source = axios.CancelToken.source();
  const fetchData = async () => {
    try {
      const response = await Axios.get("/companies", {
        cancelToken: source.token
      });
      // ...
    } catch (error) {
      if (Axios.isCancel(error)) {
        //cancelled
      } else {
        throw error;
      }
    }
  };
  fetchData()
  return () => {
    source.cancel();
  };
}, [companies]);

用 fecth 的 AbortController API 封装一个 useFetch 的 hook:

export function useFetch = (config, deps) => {
  const abortController = new AbortController()
  const [loading, setLoading] = useState(false)
  const [result, setResult] = useState()

  useEffect(() => {
    setLoading(true)
    fetch({
      ...config,
      signal: abortController.signal // 请求时传入 signal 进行关联
    })
      .then((res) => setResult(res))
      .finally(() => setLoading(false))
  }, deps)

  useEffect(() => {
    return () => abortController.abort() // 组件卸载时调用 abort 即可取消请求
  }, [])

  return { result, loading }
}

另一种是让请求继续,在请求结果返回时判断组件是否已卸载。(组件卸载后返回一个判断标识符),根据标识符判断组件是否已卸载,若卸载则不进行后面的赋值。

useEffect(() => {
  let ignore = false;
  async function fetchData() {
    const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
    if (!ignore) setData(result.data);
  }
  fetchData();
  return () => { ignore = true; }
}, [query]);

上面这种方法和我们平常进行竞态处理非常类似:

import React , { useEffect,  useRef } from 'react'

function GuideList() {
  const [isloading, setIsloading] = useState(false) // 列表否正在加载
  const [page, setPage] = useState(1) // 列表请求参数
  const [data, setData] = useState({list: [], total: 0}) // 列表数据
  const pageSize= props.pageSize || 6
  const count = useRef(0) // 竞态处理/时序控制,在组件卸载之前都会保留我们操作后的count.current的值

  useEffect(() => {
    const currentCount = count.current;

    setIsloading(true)
    const result = await getGuideList({page, pageSize, props.query}) // 一个封装好的axios请求
    if(count.current !== currentCount) return 
    // 
    // 比如请求参数连续变化5次,那就发送了5个网络请求,我们要保证总是最后一次网络请求有效。也就是经常说的的“竞态处理”或者“时序控制”。
    setIsloading(false)
    setData({
      list: result.data.records,
      total: result.data.total
    })

    return () => { count.current += 1 }   // 清除阶段在每次渲染后都会执行,后给count.current + 1,下次再执行这个effect的时候读取到的就是+1后的结果。
  },[page, props.query])
}

这里再展开说几点:

  1. 最好在 effect 内部 去声明它所需要的函数。 便于看出 effect 依赖了组件作用域中的哪些值:
function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }
    doSomething();
  }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)
}

这样如果我们没用到组件作用域中的任何值,就可以安全地把它指定为 []

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }
  doSomething();
}, []); // ✅ 在这个例子中是安全的,因为我们没有用到组件作用域中的 *任何* 值
  1. 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,因为它们不会被数据流影响。它不可能突然意外地依赖于props或state。
    如下🌰:getFetchUrl每次渲染都不同,如果把它当作我们的依赖数组会变得无用。
// ✅ Not affected by the data flow
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
  // ...
}
  1. 将useEffect的依赖如实告知React作为一条硬性规则,即但凡在effect中使用了,就要在依赖项数组中列出这些依赖。同时也可以选择第二种策略:尽量移除不必要的依赖,举个🌰:
    把effect中的 setCount(count + 1) 改成 setCount(c => c + 1) 能用别的方式获取到,就不要直接使用state/props

  2. 深比较依赖项:React会对useEffect的第二个参数(依赖项数组)中的每个值用Object.is进行比较(浅对比),来确定是否调用effect。
    但当依赖项数组中的某项为对象(包括数组、实例)时,因为对象在每次渲染中都是新的(重新开辟一个引用地址),那么即使这对象的实际内容没有更改,这个effect依然会调用。即组件每次渲染都会执行这个effect,失去了依赖项变更才执行的意义。
    因此依赖项尽量指定些简单类型数据,如果非要用引用数据,则推荐以下两个方法:

useDeepCompareEffect:基于lodash的 isEqual 方法进行比较,会牺牲些性能;
fast-deep-equal:比 lodash 的效率高好多倍,示例如下:

const { useState, useEffect, useRef } = React
const isEqual = require('fast-deep-equal')

function useDeepEffect(fn, deps) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps)

  useEffect(() => {
    const isSame = prevDeps.current.every((obj, index) =>
      isEqual(obj, deps[index])
    )
    if (isFirst.current || !isSame) {
      fn()
    }
    isFirst.current = false
    prevDeps.current = deps
  }, deps)
}

export default function App() {
  const [state, setState] = useState({ foo: "foo" })

  useEffect(() => {
    setTimeout(() => setState({ foo: "foo" }), 1000)
    setTimeout(() => setState({ foo: "bar" }), 2000)
  }, []);
  useDeepEffect(() => {
    console.log("state changed!")
  }, [state])

  return <div>{JSON.stringify(state)}</div>
}

-------------------------------- 有用的叨逼叨告一段多的分界线 ----------------------------------

下面三种副作用处理都是异曲同工的,就是在useEffect的返回函数中做相应的清除处理即可,就只贴代码了。

添加setInterval或者setTimeout

useEffect(() => {
  let timer =  setInterval(() => {
      // do something,比如setData
    }, 1000)
  }
  return () => { // 组件销毁时,清除定时器
    clearInterval(timer)
  }
})

添加监听事件addEventListener

我们用 React Navigation 举例:

function Profile({ navigation }) {
  React.useEffect(() => {
    const unsubscribe = navigation.addListener('tabPress', () => {
      // 阻止默认事件
      e.preventDefault();
    });

    return unsubscribe;
  }, [navigation]);

  return <ProfileContent />;
}

顺便对比下class组件:

class Profile extends React.Component {
  componentDidMount() {
    this._unsubscribe = navigation.addListener('focus', () => {
      // do something
    });
  }

  componentWillUnmount() {
    this._unsubscribe();
  }

  render() {
    // Content of the component
  }
}

添加和清除订阅(React官方示例)

useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
});

相关文章

网友评论

    本文标题:深度梳理 React Hook 对副作用操作的处理(二)

    本文链接:https://www.haomeiwen.com/subject/vmvvxltx.html