react-走近useCallback的“坑”

唠叨几句

     换了工做的新环境后,感触仍是蛮深的,能感觉到的是不少人对待工做的极致,相比以前而言,会更加的适合对工做充满激情的人 🤔;一个最大的挑战就是技术栈了,从本身熟练的Vue转换到React,快速的学习和上手,坑天然也是踩了很多;html

useCallback踩坑的之路

     在实际项目中,咱们会将多一个完整的功能拆分红多个模块,用来处理逻辑,中台开发中,最多见的莫过于“搜索列表”+“添加数据”等操做;react

介绍背景需求

简单描述下交互git

  • 列表搜索中点击【添加】按钮,弹出【添加页面】抽屉框
  • 添加成功后,从新加载列表数据内容
    • 从新加载须要带着当前页面的搜索条件

需求、交互都很普通,可是正是如此,我使用useCallback掉入了大坑github

实现

  • 父组件 列表+搜索页面
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>
  );
};

复制代码

内容嵌套有点乱?? 来张图数组

image.png

图1 父子组件关系图

这看似普通的代码 在一次次的进行校验后居然出现了问题缓存

操做描述

  • 在列表页面 进行搜索,此时列表页面searchQuery是保存当前搜索数据的
  • 添加内容成功后,从新加载列表数据,searchQuery在请求函数中始终不是当前最新的数据
  • 检查父组件的全部useCallback的使用,都没有限制searchQuery的更新;

image.png

图2 打印父组件内外的searchQuery

image.png

图3 添加成功后子组件调用组件的内部函数更新数据

内部的函数的searchQuery和外部的searchQuery是不一样步的,也就是函数内部没有取到最新的值性能优化

问题在哪里呢?

咱们梳理逻辑,父组件定义了一个函数initFetchData传给子组件,子组件在onSubmit 中调用了这个函数,可是 在onfinish中咱们使用了useCallback,此时useCallback传递的第二个数组是空,也就是不依赖的,只在初始渲染时候进行定义,后面任何值变化时候都不会引发这个函数变化,为此产生了疑问? useCallback定义的无依赖的函数,对于内部所调用的函数的值是否有所影响,也就是initFetchData 中调用的是初始保留的值? 为此 进行了一番探究markdown

探究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 的clickParentuseCallback包裹
// 两个子组件
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>)
})
复制代码

image.png

图4 渲染效果展现图

点击按钮后async

image.png

图5 点击按钮 打印输出值

子组件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>)
})
复制代码

image.png

图6 依赖变量进行监听

此时存在一个猜测 useCallback包裹的函数,会影响内部的全部函数做用域

使用了useCallback进行包装的函数,会影响到其内部的全部调用函数 带着这个猜测进行debugger查看函数上下文和执行栈

Children2

当执行到Children2的函数时候,此时parentsMethod的上下文和做用域以下;此时parentsMethods的做用域上是产生了一个闭包,也就是定义的query的初始值;

image.png

图7 Children2 中 clickParent内部的做用域

执行到父组件函数内部

image.png

图8 Children2 中 执行到父组件函数中做用域

Children1

也会存在闭包,做用域使用的是范围是最新的

image.png

图9 Children1中 clickParent内部的做用域

执行到parents的时候

image.png

图10 Children1中 执行到父组件函数中做用域

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是初始传入的,所以不会取父组件值

参考文档

React Hooks 第一期:聊聊 useCallback github useCallback issues

相关文章
相关标签/搜索