Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 国产成人久久精品一区二区三区 ,日韩高清中文字幕,日本一区二区三区国产

          整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          React 的正確使用方法:ref 篇

          React 的正確使用方法:ref 篇

          谷 阿里云開發者

          2024年08月09日 08:30 浙江

          阿里妹導讀


          你真的用對了 useRef 嗎?在與 TypeScript 一起使用、以及撰寫組件庫的情況下,你的寫法能夠避開以下所有場景的坑嗎?

          說到 useRef,相信你一定不會陌生:你可以用它來獲取 DOM 元素,也可以多次渲染之間保持引用不變……

          然而,你真的用對了 useRef 嗎?在與 TypeScript 一起使用、以及撰寫組件庫的情況下,你的寫法能夠避開以下所有場景的坑嗎?

          場景一:獲取 DOM 元素

          以下幾種寫法,哪種是正確的?

          function MyComponent() {
            // 寫法 1
            const ref=useRef();
          
          
            // 寫法 2
            const ref=useRef(undefined);
          
          
            // 寫法 3
            const ref=useRef(null);
          
          
            // 通過 ref 計算 DOM 元素尺寸
            //  這段代碼故意留了坑,坑在哪里?請看下文。
            useLayoutEffect(()=> {
              const rect=ref.current.getBoundingClientRect();
            }, [ref.current]);
          
          
            return <div ref={ref} />;  
          }

          如果只看 JS,幾種寫法似乎并沒有差別,但如果你開啟了 TS 的類型提示,就能夠發現其中端倪:

          function MyComponent() {
            // ? 寫法 1
            // 你會得到一個 MutableRefObject<HTMLDivElement | undefined>,
            // 即 ref.current 類型是 HTMLDivElement | undefined,
            // 這導致你每次獲取 DOM 元素都需要判斷是否為 undefined,很是麻煩。
            const ref=useRef<HTMLDivElement>();
          
          
            // ? 寫法 2.1
            // 你可能想得到一個 MutableRefObject<HTMLDivElement>,但初始值傳入的
            // undefined 并不是 HTMLDivElement,所以會 TS 報錯。
            const ref=useRef<HTMLDivElement>(undefined);
          
          
            // ? 寫法 2.2
            // 等價于寫法 1,但需要多打一些字。
            const ref=useRef<HTMLDivElement | undefined>(undefined);
          
          
            // ? 寫法 3
            // 你會得到一個 RefObject<HTMLDivElement>,其中
            // ref.current 類型是 HTMLDivElement | null。
            // 這個 ref 的 current 是不可從外部修改的,更符合使用場景下的語義,
            // 也是 React 推薦的獲取 DOM 元素方式。
            // 注意:如果 tsconfig 沒開 strictNullCheck,則不會匹配到這個定義,
            // 因此請務必開啟 strictNullCheck。
            const ref=useRef<HTMLDivElement>(null);
          
          
            // 通過 ref 計算 DOM 元素尺寸
            //  這段代碼故意留了坑,坑在哪里?請看下文。
            useLayoutEffect(()=> {
              const rect=ref.current.getBoundingClientRect();
            }, [ref.current]);
          
          
            return <div ref={ref} />;  
          }

          Ref 還可以傳入一個函數,會把被 ref 的對象應用作為參數傳入,因此我們也可以這樣獲取 DOM 元素:

          function MyComponent() {
            const [divEl, setDivEl]=useState<HTMLDivElement | null>(null);
          
          
            // 計算 DOM 元素尺寸
            useEffect(()=> {
              if (divEl) {
                divEl.current.getBoundingClientRect();
              }
            }, [divEl]);
          
          
            return <div ref={setDivEl} />;
          }

          場景二:DOM 元素與 useLayoutEffect

          在場景一中,我們留了一個坑,你能看出以下代碼有什么問題嗎?

          /*  錯誤案例,請勿照抄 */
          
          
          function MyComponent({ visible }: { visible: boolean }) {
            const ref=useRef<HTMLDivElement>(null);
          
          
            useLayoutEffect(()=> {
              const rect=ref.current.getBoundingClientRect();
              // ...
            }, [ref.current]);
          
          
            return <>{visible && <div ref={ref}/>}</>;
          }

          這段代碼有兩個問題:?


          1. useLayoutEffect 中沒有判空

          按照場景一中的分析:

          useRef<HTMLDivElement>(null) 返回的類型是RefObject<HTMLDivELement>,其ref.current 類型為HTMLDivELement | null。因此單從 TS 類型出發,也應該判斷ref.current 是否為空。

          你也許會認為,我都在 useLayoutEffect 里了,此時組件 DOM 已經生成,因而理應存在 ref.current,是否可以不用判斷呢?(或用 ! 強制設為非空)

          上述使用場景中,確實可以這樣做,但如果div 是條件渲染的,則無法保證useLayoutEffect 時組件已被渲染,自然也就不一定存在ref.current。

          2. useLayoutEffect deps 配置錯誤

          這個問題涉及到useLayoutEffect 更本質的使用目的。?

          useLayoutEffect 的執行時機是:

          ?VDOM 生成后(所有render 執行完成);

          ?DOM 生成后(createElement 等 DOM 操作完成);

          ?最終提交渲染之前(同步任務返回前)。?

          由于其執行時機在 repaint 之前,此時對已生成的 DOM 進行更改,用戶不會看到「閃一下」。舉個例子,你可以計算元素的尺寸,如果太大則修改 CSS 使其自動換行,從而實現溢出檢測。?

          另一個常見場景是在useLayoutEffect 中獲取原生組件,用來添加原生 listener、獲取底層HTMLMediaElement 實例來控制播放,或添加ResizeObserverIntersectionObserver 等。

          這里,由于div 是條件渲染的,我們顯然會希望useLayoutEffect 的操作在每次渲染出來之后都執行一遍,因此我們會想把ref.current 寫進useLayoutEffectdependencies,但這是完全錯誤的。

          讓我們盤一下MyComponent 的渲染過程:

          1.visible 變化導致觸發 render。

          2.useRef 執行,ref.current 還是上一次的值。

          3.useLayoutEffect 執行,對比 dependencies 發現沒有變化,跳過執行。

          4.渲染結果包含div。

          5.由于<div ref={ref}>,React 使用新的 DOM 元素更新ref.current

          顯然,這里并沒有再次觸發useLayoutEffect,直到下一次渲染中才會發現ref.current 有變化,這背離了我們對于 useLayoutEffect 能讓用戶看不到「閃一下」的預期。

          解決方案是,使用與條件渲染相同的條件作為useLayoutEffect 的 deps:

          function MyComponent({ visible }: { visible: boolean }) {
            const ref=useRef<HTMLDivElement>(null);
          
          
            useLayoutEffect(()=> {
              // 這里不必額外判斷 if (visible),因為只要這里有 ref.current 那就必然是 visible
              if (ref.current) {
                const rect=ref.current.getBoundingClientRect();
              }
            }, [/* ? */ visible]);
            // 這樣,在 visible 變化時,就必然會在同一次渲染內觸發 useLayoutEffect
          
          
            return <>{visible && <div ref={ref}/>}</>;
          }
          
          
          // 或者也可以將 <div> 抽取成一個獨立的組件,從而避免上述問題

          最后,如果并非是要在 repaint 之前對 DOM 元素進行操作,更推薦的寫法是用函數寫法:

          function MyComponent({ visible }: { visible: boolean }) {
            // ? 無需使用 ref
            const [video, setVideo]=useState<Video | null>(null);
          
          
            const play=useCallback(()=> video?.play(), [video]);
          
          
            // ? 使用普通 useEffect 即可
            useEffect(()=> {
              console.log(video.currentTime);
            }, [video]);
          
          
            return <>{visible && <video ref={setVideo}/>}</>;
          }

          場景三:組件中同時傳遞 & 獲取 Ref

          ——你實現了一個組件,想要將傳入的 ref 傳給組件中渲染的根元素,聽起來很簡單!

          哦對了,出于某種原因,你的組件中也需要用到根組件的 ref,于是你寫出了這樣的代碼:

          /*  錯誤案例,請勿照抄 */
          
          
          const MyComponent=forwardRef(
            function (
              props: MyComponentProps, 
              // type ForwardedRef<T>=//   | ((instance: T | null)=> void)
              //   | MutableRefObject<T | null>
              //   | null
              // ? 這個工具類型覆蓋了傳 useRef 和傳 setState 的情況,是正確的寫法
              ref: ForwardedRef<HTMLDivElement>
            ) {
              useLayoutEffect(()=> {
                const rect=ref.current.getBoundingClientRect();
                // 使用 rect 進行計算
              }, []);
              
              return <div ref={ref}>{/* ... */}</div>;
            }
          });

          等等,如果調用者沒傳ref 怎么辦?想到這里,你把代碼改成了這樣:

          /*  錯誤案例,請勿照抄 */
          
          
          const MyComponent=forwardRef(
            function (
              props: MyComponentProps, 
              ref: ForwardedRef<HTMLDivElement>
          ) {
              const localRef=useRef<HTMLDivElement>(null);
              
              useLayoutEffect(()=> {
                const rect=localRef.current.getBoundingClientRect();
                // 使用 rect 進行計算
              }, []);
          
          
              return <div ref={(el: HTMLDivElement)=> {
                localRef.current=el;
                if (ref) {
                  ref.current=el;
                }
              }}>{/* ... */}</div>;
            }
          });

          這樣的代碼顯然是會 TS 報錯的,因為ref 可能是個函數,本來你只需要把它直接傳給<div> 就好了,因此你需要寫一堆代碼,處理多種可能的情況……

          更好的解決方式是使用 react-merge-refs

          import { mergeRefs } from "react-merge-refs";
          
          
          const MyComponent=forwardRef(
            function (
              props: MyComponentProps, 
              ref: ForwardedRef<HTMLDivElement>
          ) {
              const localRef=React.useRef<HTMLDivElement>(null);
          
          
              useLayoutEffect(()=> {
                const rect=localRef.current.getBoundingClientRect();
                // 使用 rect 進行計算
              }, []);
              
              return <div ref={mergeRefs([localRef, ref])} />;
            }
          );

          場景四:組件透出命令式操作

          Form 和 Table 這種復雜的組件往往會在組件內維護較多狀態,不適合受控操作,當調用者需要控制組件行為時,往往就會采取這樣的模式:

          function MyPage() {
            const ref=useRef<FormRef>(null);
          
          
            return (
              <div>
                <Button onClick={()=> { ref.current.reset(); }}>重置表單</Button>
                <Form actionRef={ref}>{/* ... */}</Form>
              </div>
            );
          }

          這種用法實際上脫胎于 class component 時代,人們使用 ref 來獲取 class 實例,通過調用實例方法來控制組件。

          現在,你的超級復雜絕絕子組件也希望通過這種方式與調用者交互,于是你寫出了以下實現:

          /*  錯誤案例,請勿照抄 */
          
          
          interface MySuperDuperComponentAction {
            reset(): void;
          }
          
          
          const MySuperDuperComponent=forwardRef(
            function (
              props: MySuperDuperComponentProps,
              ref: ForwardedRef<MySuperDuperComponentAction>
          ) {
              const action=useMemo((): MySuperDuperComponentAction=> ({
                reset() {
                  // ...
                }
              }), [/* ... */]);
              
              if (ref) {
                ref.current=action;
              }
          
          
              return <div/>;
            }  
          );   

          然而 TS 不會容許這樣的代碼通過類型檢查,因為調用者可以函數作為 ref 來接收 action,這與獲取 DOM 元素時類似。?

          正確的做法是,你應該使用 React 提供的工具函數useImperativeHandle

          const MyComponent=forwardRef(
            function (
              props: MyComponentProps, 
              ref: ForwardedRef<MyComponentRefType>
          ) {
              // useImperativeHandle 這個工具函數會自動處理函數 ref 和對象 ref 的情況,
              // 后兩個參數基本等于 useMemo
              useImperativeHandle(ref, ()=> ({
                refresh: ()=> {
                  // ...
                },
                // ...
              }), [/* deps */]);
          
          
              // 命令式 + 下傳
              // 如果你的組件內部也會用到這個命令式對象,推薦的寫法是:
              const actions=useMemo(()=> ({
                refresh: ()=> {
                  // ...
                },
              }), [/* deps */]);
              useImperativeHandle(ref, ()=> actions, [actions]);
              
              return <div/>;
            }
          );

          場景五:組件 TS 導出如何聲明 Ref 類型

          如果內部的組件類型正確,forwardRef 會自動檢測 ref 類型:

          const MyComponent=forwardRef(
            function (
              props: MyComponentProps,
              ref: ForwardedRef<MyComponentRefType>
          ) {
              return <div/>;
            }
          });
          
          
          // 其結果類型為:
          // const MyComponent: ForwardRefExoticComponent<
          //   PropsWithoutRef<MyComponentProps> & RefAttributes<MyComponentRefType>
          // >
          
          
          // 這里最后導出的 PropsWithoutRef<P> & RefAttributes<T> 就是用戶側最終可傳的類型,
          // 其中 PropsWithoutRef 會無視你 component 中 props 的 ref。

          這里有一個問題:你的組件導出的 Props 中需要包含 ref 嗎?由于forwardRef 會強行改掉你的 ref,這里有兩種方法:

          1.在MyComponentProps 中寫上 ref,類型為MyComponentRefType,直接導出它作為最終的 Props;

          2.用ComponentProps<typeof MyComponent> 取出最終的 Props。

          然而,當組件內需要必須層層透傳 ref 的時候,如果把 ref 寫進 Props 里,就需要每層組件都使用 forwardRef,否則就會出現問題:

          /*  錯誤案例,請勿照抄 */
          
          
          interface OtherComponentProps {
            ref?: Ref<OtherComponentActions>;
          }
          
          
          interface MyComponentProps extends OtherComponentProps {
            myAdditionalProp: string;
          }
          
          
          // 這是錯誤的,props 里根本拿不到 ref!
          function MyComponent({ myAdditionalProp, ...props }: MyComponentProps) {
            console.log(myAdditionalProp);
          
          
            return <OtherComponent {...props} />;
          }

          因此,更推薦的方案是不用 ref 這個名字,比如叫 actionRef 等等,這樣也可以毫無痛苦地寫進 props 并導出了。

          Bonus: 與 Ref 相關的 TS 類型

          • PropsWithoutRef<Props>:從 Props 中刪除 ref,可用于 HOC 等場景。
          • PropsWithRef<Props>:并不會添加 ref,而是保證 ref 里沒有 string。這是因為在古代,可以給 ref 傳 string 來代替傳函數,現代我們一般不這么做。
          • 如果想要添加 ref,可以仿照forwardRef 里的寫法:PropsWithoutRef<Props> & RefAttribute<RefType>。
          • RefAttribute<RefType>{ ref?: Ref<T> | undefined; }。
          • ForwardedRef<RefType>:組件內部拿外部 ref 唯一指定類型。
          • MutableRefObject<RefType>useRefcreateRef 的結果。
          • RefObject<RefType>useRef(null) 的結果。
          • Ref<RefType>:傳入的 ref 參數類型,RefTyperef.current 拿到的類型,會自動加上 null。
          • 注意:這個類型包括 RefObject 與函數。
          • 注注意:這個類型與 ForwardedRef 的區別是,它只需要 RefObject 而非 MutableRefObject,因此可以接受 useRef(null) 的結果并被用于 props。組件內部由于需要修改 ref.current,必須使用 MutableRefObject
          • ForwardRefExoticComponentforwardRef 的返回類型。

          ?這些類型類似于 React 提供的類型接口,為了保證你的組件能夠兼容盡可能多的 React 版本,請務必使用最合適的類型。

          之前學習過ref聲明響應式對象,前幾天讀代碼遇到了發懵的地方,詳細學習了一下才發現,用法還有很多,遂總結一下ref的用法備忘。


          一、組件的ref用法總結

          Vue3 中的 ref 是一種創建響應式引用的方式,它在Vue生態系統中扮演著重要角色。以下是Vue3中ref屬性及其相關API的幾個關鍵點:

          創建響應式變量:使用 ref 函數可以創建一個響應式的數據引用,返回的對象包含 .value 屬性,該屬性既可以讀取也可以寫入,并且是響應式的。例如:

          Javascript
          1import { ref } from 'vue';
          2
          3const count=ref(0); // 創建一個響應式引用,初始值為0
          4console.log(count.value); // 輸出0
          5count.value++; // 改變值,這將觸發視圖更新
          

          在模板中使用 ref:在模板中,可以使用 v-ref 或簡寫 ref 來給 DOM 元素或組件添加引用標識符。對于DOM元素:

          <div ref="myDiv">Hello World</div>
          

          然后在組件的 setup 函數內或者生命周期鉤子如 onMounted 中通過 ref 訪問到該元素:

          onMounted(()=> {
           console.log(myDiv.value); // 這將輸出對應的DOM元素
          });
          
          // 注意,在setup函數中使用需要解構
          setup() {
            const myDiv=ref<HTMLElement | null>(null);
            // ...
          

          對于子組件,ref 則指向子組件的實例:

          <MyChildComponent ref="childRef" />
          

          動態 refs:在動態渲染的組件或循環列表中,可以使用動態 ref 名稱:

          1<component v-for="(item, index) in items" :is="item.component" :key="index" :ref="`child${index}`" />
          然后通過 getCurrentInstance() 獲取這些動態 ref:
          
          Javascript
          1setup() {
          2  const instance=getCurrentInstance();
          3  const childrenRefs=computed(()=> {
          4    return instance.refs;
          5  });
          6  // ...
          7}
          

          組件間通信:通過 ref 可以方便地在組件之間傳遞并操作狀態,尤其適用于父子組件之間的通信。
          (1)創建一個子組件 ChildComponent.vue:

          
          <template>
            <div>
              <h2>{{ childMessage }}</h2>
              <button @click="handleClick">點擊我</button>
            </div>
          </template>
          
          <script>
          import { ref, defineComponent } from 'vue';
          
          export default defineComponent({
            setup(props, { emit }) {
              const childMessage=ref('Hello from Child');
              const handleClick=()=> {
                emit('child-clicked', 'Child component clicked!');
              };
          
              return {
                childMessage,
                handleClick,
              };
            },
          });
          </script>
          

          (2)創建一個父組件 ParentComponent.vue,并使用 ref 屬性訪問子組件實例:

          <!-- ParentComponent.vue --><template>
            <div>
              <h1>Parent Component</h1>
             <ChildComponent ref="childRef" />
             <button @click="callChildMethod">Call Child Method</button>
            </div>
          </template><script>
          import { ref } from 'vue';
          import ChildComponent from './ChildComponent.vue';
          
          export default {
            components: {
              ChildComponent,
            },
            setup() {
              const childRef=ref(null);
          
              function callChildMethod() {
                childRef.value.showMessage();
              }
          
              return {
                childRef,
                callChildMethod,
              };
            },
          };
          </script>
          

          在這個示例中,我們在父組件的模板中使用了 ref 屬性,并將其值設置為 “childRef”。然后,在組合式 API 的 setup 函數中,我們創建了一個名為 childRef 的響應式引用,并將其初始值設置為 null。接著,我們定義了一個名為 callChildMethod 的方法,用于調用子組件的 showMessage 方法。當用戶點擊按鈕時,callChildMethod 方法會被調用,從而觸發子組件的 showMessage 方法并在控制臺輸出一條消息。

          import { reactive, toRef } from 'vue';
          2
          3const state=reactive({ count: 0 });
          4const countRef=toRef(state, 'count'); // 提取出count屬性的響應式引用
          

          總結

          總之,Vue3 的 ref 功能增強了Vue的響應式系統,使得開發者能夠更靈活地處理組件的狀態及組件間交互,同時提供了對DOM元素的直接訪問能力。

          人總是在接近幸福時倍感幸福,在幸福進行時卻患得患失。

          用ref函數獲取組件中的標簽元素,可以操作該標簽身上的屬性,還可以通過ref來取到子組件的數據,可以修改子組件的數據、調用子組件的方法等、非常方便. 適用于vue3.0版本語法,后續會講到vue3.2版本setup語法糖有所不同。

          語法示例:

          <input標簽 type="text" ref="inputRef">

          <子組件 ref="childRef" />

          const inputRef=ref<HTMLElement|null>(null)

          const childRef=ref<HTMLElement|null>(null)

          父組件代碼:

          <template>
            <div style="font-size: 14px;">
              <h2>測試ref獲取普通標簽 讓輸入框自動獲取焦點</h2>
              <input type="text" ref="inputRef">
          
              <h2>測試ref獲取子組件</h2>
              <Child ref="childRef" />
            </div>
          </template>
          
          <script lang="ts">
          // vue3.0版本語法
          import { defineComponent, ref, onMounted } from 'vue'
          import Child from './child.vue'
          export default defineComponent({
            components: {
              Child
            },
            setup() {
              const inputRef=ref<HTMLElement|null>(null)
              const childRef=ref<HTMLElement|null>(null)
                
              onMounted(()=> {
                // ref獲取元素: 利用ref函數獲取組件中的標簽元素
                // 需求實現1: 讓輸入框自動獲取焦點
                inputRef.value && inputRef.value.focus()
                // ref獲取元素: 利用ref函數獲取組件中的標簽元素
                // 需求實現2: 查看子組件的數據,修改子組件的某個值
                console.log(childRef.value);
                setTimeout(()=> {
                  childRef.value.text='3秒后修改子組件的text值'
                }, 3000)
              })
          
              return {
                inputRef,childRef
              }
            },
          })
          </script>
          

          子組件代碼:

          <template>
            <div>
              <h3>{{ text }}</h3>
            </div>
          </template>
          
          <script lang="ts">
          // vue3.0版本語法
          import { ref, defineComponent } from "vue";
          
          export default defineComponent({
            name: "Child",
            setup() {
              const text=ref('我是子組件');
          
              return {
                text
              };
            },
          });
          </script>

          初始頁面效果-輸入框獲取到了焦點,log打印出了子組件的所有數據:

          初始3秒后頁面效果,修改了子組件的text數據:

          vue3.2版本語法:


          主站蜘蛛池模板: 国产福利电影一区二区三区,免费久久久久久久精 | 国产亚洲一区区二区在线 | 色综合视频一区二区三区44| 免费一区二区无码视频在线播放| 亚洲av无码一区二区三区不卡| 一区二区三区电影在线观看| 国产精品一区二区毛卡片| 精品无码人妻一区二区三区品| 国产精品免费一区二区三区| 一区二区三区在线|日本| AV天堂午夜精品一区二区三区| 亚洲一区二区三区电影| 国产丝袜视频一区二区三区| 久久99精品免费一区二区| 波多野结衣一区二区三区高清av| 无码一区二区波多野结衣播放搜索| 一区高清大胆人体| 亚洲电影一区二区三区| 无码毛片视频一区二区本码| 麻豆精品久久久一区二区| 国产成人无码AV一区二区 | 午夜无码视频一区二区三区| 国产精品成人免费一区二区| 亚洲色偷精品一区二区三区| 中文字幕日韩精品一区二区三区| 精品三级AV无码一区| 末成年女A∨片一区二区| 亚洲一区二区三区高清| 国产福利一区二区三区在线观看| 一本一道波多野结衣AV一区| 亚洲AV日韩AV一区二区三曲| 波多野结衣AV一区二区三区中文| 99偷拍视频精品一区二区| 狠狠综合久久av一区二区| 精品一区二区久久久久久久网精| 国产一区二区三区内射高清| 日本精品夜色视频一区二区| 国产一区二区福利| 91福利一区二区| 日本一区二区三区久久| 国产情侣一区二区三区|