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

相關文章
相關標籤/搜索