唠叨几句
换了工作的新环境后,感触还是蛮深的,能感受到的是很多人对待工作的极致,相比之前而言,会更加的适合对工作充满激情的人 🤔;一个最大的挑战就是技术栈了,从自己熟练的Vue转换到React,快速的学习和上手,坑自然也是踩了不少;
useCallback踩坑的之路
在实际项目中,我们会将多一个完整的功能拆分成多个模块,用来处理逻辑,中台开发中,最常见的莫过于“搜索列表”+“添加数据”等操作;
介绍背景需求
简单描述下交互
- 列表搜索中点击【添加】按钮,弹出【添加页面】抽屉框
- 添加成功后,重新加载列表数据内容
- 重新加载需要带着当前页面的搜索条件
需求、交互都很普通,但是正是如此,我使用
useCallback掉入了大坑
- 重新加载需要带着当前页面的搜索条件
需求、交互都很普通,但是正是如此,我使用
实现
- 父组件 列表+搜索页面
const OperationList: React.FC = () => {
const [showCreateGood, setShowCreateGood] = useState(false);
const [searchQuery, setSearchQuery] = useState<ListQueryParams>(initOpQuery);
const initFetchData = async (query?: ListQueryParams) => {
const searchParams = query ?? searchQuery;
// 执行请求操作 省略
};
useEffect(() => {
initFetchData(searchQuery);
}, []);
// 搜索内容
const onSearch = (value?: ListQueryParams) =>
new Promise((resolve, reject) => {
setSearchQuery(value);
initFetchData(value)
.then(res => resolve(res))
.catch(error => reject(new Error(error)));
});
return (
<Card>
//......
<Button onClick={onSearch}></Button>
{<CreateGoodsSold visible={showCreateGood} setVisible={setShowCreateGood} createSuccess={initFetchData} />}
</Card>
);
};
- 子组件
// eslint-disable-next-line max-lines-per-function
const CreateGoodsSoldOut: React.FC<CreateGoodsSoldOutProps> = ({ visible, setVisible, createSuccess }) => {
const [form] = Form.useForm();
// 提交站内信内容
const onSubmit = async (extraParams = { flag: 0 }) => {
let postParams: postCreateNotice = form.getFieldsValue();
try {
await createSoldOut({ ...postParams, ...extraParams }); ;
//添加成功 调用父组件的方法
createSuccess();
} catch (error) {
}
};
// 表单校验完成 + 弹框提示
const onfinish =useCallback( () => {
Modal.confirm({
title: '',
content: 'ccc?',
icon: null,
okText: '确定提交',
cancelText: '取消',
centered: true,
onOk: () => {
onSubmit();
},
},[])
return (
<Drawer
destroyOnClose
forceRender
width={700}
visible={visible}
onCancel={clearForm}
onOk={() => {
form.submit();
}}
okButtonProps={{ htmlType: 'submit' }}
>
// ......表单收集项目
</Drawer>
);
};
内容嵌套有点乱?? 来张图
这看似普通的代码 在一次次的进行校验后竟然出现了问题
操作描述
- 在列表页面 进行搜索,此时列表页面
searchQuery是保存当前搜索数据的 - 添加内容成功后,重新加载列表数据,
searchQuery在请求函数中始终不是当前最新的数据 - 检查父组件的所有useCallback的使用,都没有限制searchQuery的更新;
内部的函数的searchQuery和外部的searchQuery是不同步的,也就是函数内部没有取到最新的值
问题在哪里呢?
我们梳理逻辑,父组件定义了一个函数initFetchData传给子组件,子组件在onSubmit 中调用了这个函数,但是 在onfinish中我们使用了useCallback,此时useCallback传递的第二个数组是空,也就是不依赖的,只在初始渲染时候进行定义,后面任何值变化时候都不会引起这个函数变化,为此产生了疑问? useCallback定义的无依赖的函数,对于内部所调用的函数的值是否有所影响,也就是initFetchData 中调用的是初始保留的值? 为此 进行了一番探究
探究useCallback的奥秘
根据上述问题的疑问,进行demo的测试,我们疑问点在于:
- 子组件中使用
useCallback包装的函数调用父组件函数时候,父组件函数内部的数值获取,即被useCallback包裹的函数,内部函数调用的作用域;
定义父组件 两个子组件
- 父组件
// 父组件的定义
const UseCallBackDemo = () =>{
const [query,setQuery] = useState(null)
const parentFun = (value)=>{
console.log(value,query)
}
const changeQuery = () =>{
setQuery(222)
}
return(<>
测试useCallBack
<button onClick={changeQuery}>更改query的值{query}</button>
<Children1 parentsMethod={parentFun}></Children1>
<Children2 parentsMethod={parentFun}></Children2>
</>)
}
- 定义子组件
- Childre1 的
clickParent未被useCallback包裹 - Childre2 的
clickParent被useCallback包裹
- Childre1 的
// 两个子组件
const Children1 =memo( ({ parentsMethod })=>{
const clickParent = ()=>{
parentsMethod("子组件1调用了");
}
return(<div>
我是子组件1
<button onClick={clickParent}>我是子组件1 调用parentsMethod</button>
</div>)
})
const Children2 =memo( ({ parentsMethod })=>{
//
const clickParent = useCallback(()=>{
parentsMethod("子组件2调用了 useCallback");
},[])
return(<div>
我是子组件2
<button onClick={clickParent}>我是子组件2 调用parentsMethod</button>
</div>)
})
点击按钮后
子组件2中使用了useCallback的无依赖函数,调用父组件时候,query还是初始的值,并未得到更新;
将依赖变量query传入子组件Children2中
<Children2 parentsMethod={parentFun} query={queru}></Children2>
const Children2 =memo( ({ parentsMethod,query })=>{
const clickParent = useCallback(()=>{
parentsMethod("子组件2调用了 useCallback");
},[query])
return(<div>
我是子组件2
<button onClick={clickParent}>我是子组件2 调用parentsMethod</button>
</div>)
})
此时存在一个猜想 useCallback包裹的函数,会影响内部的所有函数作用域
使用了useCallback进行包装的函数,会影响到其内部的所有调用函数
带着这个猜想进行debugger查看函数上下文和执行栈
Children2
当执行到Children2的函数时候,此时parentsMethod的上下文和作用域如下;此时parentsMethods的作用域上是产生了一个闭包,也就是定义的query的初始值;
执行到父组件函数内部
Children1
也会存在闭包,作用域使用的是范围是最新的
执行到parents的时候
useCallback 影响
````useCallback```优化性能,但是使用可能导致出现错误,影响内部调用的函数作用域,因此谨慎使用,如果存在依赖函数,一定要进行相关依赖函数的监听;
useCallback实现原理
useCallback 的作用在于利用 memoize 减少无效的 re-render,来达到性能优化的作用,callback 内部对 state 的访问依赖于 JavaScript 函数的闭包。如果希望 callback 不变,那么访问的之前那个 callback 函数闭包中的 state 会永远是当时的值。
内部实现
useCallback的实现有中,分为mountHook 和updateHook;
mountHook时候
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 利用memoizedState 缓存mount阶段时候的变量
hook.memoizedState = [callback, nextDeps];
return callback;
}
updateHook
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
// 获取下一个nextDeps
const nextDeps = deps === undefined ? null : deps;
// 获取前一个存储的内部数值
const prevState = hook.memoizedState;
// 如果前一个不是空的 则进行浅比较
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 存储当前的这个内容
hook.memoizedState = [callback, nextDeps];
return callback;
}
- 当我们传递第二个参数后,更新时候会进行浅比较数值是否变化,如果变化则更新新的值
- 如果第二个参数为空,则调用的时候会保持第一次传入时候的数值
- 父组件函数在子组件被调用的时候,此时内部的query是初始传入的,因此不会取父组件值