接上篇:深度梳理 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])
}
这里再展开说几点:
- 最好在 effect 内部 去声明它所需要的函数。 便于看出 effect 依赖了组件作用域中的哪些值:
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp);
}
doSomething();
}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)
}
这样如果我们没用到组件作用域中的任何值,就可以安全地把它指定为 []
:
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ 在这个例子中是安全的,因为我们没有用到组件作用域中的 *任何* 值
- 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,因为它们不会被数据流影响。它不可能突然意外地依赖于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
// ...
}
-
将useEffect的依赖如实告知React作为一条硬性规则,即但凡在effect中使用了,就要在依赖项数组中列出这些依赖。同时也可以选择第二种策略:尽量移除不必要的依赖,举个🌰:
把effect中的setCount(count + 1)
改成setCount(c => c + 1)
能用别的方式获取到,就不要直接使用state/props -
深比较依赖项: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);
};
});
网友评论