您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“React ref的原理和應(yīng)用”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
提到 ref或者 refs 如果你用過React 16以前的版本 第一印象都是用來訪問DOM或者修改組件實例的,
正如官網(wǎng)所介紹的這樣:
然后到了React 16.3出現(xiàn)的 createRef 以及16.8 hooks中的 useRef出現(xiàn)時,發(fā)現(xiàn)這里的ref好像不僅僅只有之前的綁定到DOM/組件實例的 作用?本文將帶你逐一梳理這些知識點,并嘗試分析相關(guān)源碼。
這部分知識點不是本文重點,每個點展開都非常龐大,了方便本文理解先在這里簡單提及。
Fiber是React更新時的最小單元,是一種包含指針的數(shù)據(jù)結(jié)構(gòu),從數(shù)據(jù)結(jié)構(gòu)上看Fiber架構(gòu) ≈ 樹 + 鏈表。
Fiber單元是從 jsx createElement之后根據(jù)ReactElement生成的,相比 ReactElement,F(xiàn)iber單元具備動態(tài)工作能力。
使用chrome perfomance錄制一個react應(yīng)用渲染看函數(shù)調(diào)用棧會看到下面這張圖
這三塊內(nèi)容分別代表: 1.生成react root節(jié)點 2.reconciler 協(xié)調(diào)生成需要更新的子節(jié)點 3.將節(jié)點更新commit 到視圖
在函數(shù)組件中每執(zhí)行一次use開頭的hook函數(shù)都會生成一個hook對象。
type Hook = { memoizedState: any, // 上次更新之后的最終狀態(tài)值 queue: UpdateQueue, //更新隊列 next, // 下一個 hook 對象 };
其中memoizedState會保存該hook上次更新之后的最終狀態(tài),比如當(dāng)我們使用一次useState之后就會在memoizedState中保存初始值。
React 中大部分 hook 分為兩個階段:第一次初始化時`mount`階段和更新`update`時階段
hooks函數(shù)的執(zhí)行分兩個階段 mount和 update,比如 useState只會在初始化時執(zhí)行一次,下文中將提到的
useImperativeHandle 和 useRef也包括在內(nèi)。
本文已梳理摘取了源碼相關(guān)的函數(shù),但你如果配合源碼調(diào)試一起食用效果會更加。
本文基于React v17.0.2。
拉取React代碼并安裝依賴
將react,scheduler以及react-dom打包為commonjs
yarn build react/index,react-dom/index,scheduler --type NODE
3.進入build/node_modules/react/cjs 執(zhí)行yarn link 同理 react-dom
4.在 build/node_modules/react/cjs/react.development.js中加入link標(biāo)記console以確保檢查link狀態(tài)
5.使用create-react-app創(chuàng)建一個測試應(yīng)用 并link react,react-dom
組件上的ref屬性是一個保留屬性,你不能把ref當(dāng)成一個普通的prop屬性在一個組件中獲取,比如:
const Parent = () => { return <Child ref={{test:1}}> } const Child = (props) => { console.log(props); // 這里獲取不到ref屬性 return <div></div> }
這個ref去哪里了呢, React本身又對它做了什么呢?
我們知道React的解析是從createElement開始的,找到了下面創(chuàng)建ReactElement的地方,確實有對ref保留屬性的處理。
export function createElement(type, config, children) { let propName; // Reserved names are extracted const props = {}; let ref = null; if (config != null) { if (hasValidRef(config)) { ref = config.ref; } for (propName in config) { if ( hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; } } } return ReactElement( type, key, ref, props, ... ); }
從createElement開始就已經(jīng)創(chuàng)建了對ref屬性的引用。
createElement之后我們需要構(gòu)建Fiber工作樹,接下來主要講對ref相關(guān)的處理。
React對于不同的組件有不通的處理
先主要關(guān)注 FunctionComponent/ClassComponent/HostComponent(原生html標(biāo)簽)
FunctionComponent
function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) { try { nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes); } finally { reenableLogs(); } reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } functin renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes){ children = Component(props, secondArg); // 這里的Component就是指我們的函數(shù)組件 return children; }
我們可以看到函數(shù)組件在渲染的時候就是直接執(zhí)行。
ClassComponent
function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) { ... { ... constructClassInstance(workInProgress, Component, nextProps); .... } var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes); ... return nextUnitOfWork; } function constructClassInstance(workInProgress, ctor, props) { .... var instance = new ctor(props, context); // 把instance實例掛載到workInProgress stateNode屬性上 adoptClassInstance(workInProgress, instance); ..... return instance; } function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) { // 標(biāo)記是否有ref更新 markRef(current, workInProgress); } function markRef(current, workInProgress) { var ref = workInProgress.ref; if (current === null && ref !== null || current !== null && current.ref !== ref) { // Schedule a Ref effect workInProgress.flags |= Ref; } }
ClassComponent則是通過構(gòu)造函數(shù)生成實例并標(biāo)記了ref屬性。
回顧一下之前提到的React工作流程,既然是要將組件實例或者真實DOM賦值給ref那肯定不能在一開始就處理這個ref,而是根據(jù)標(biāo)記到commit階段再給ref賦值。
function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) { .... { if (finishedWork.flags & Ref) { commitAttachRef(finishedWork); } } .... } function commitAttachRef(finishedWork) { var ref = finishedWork.ref; if (ref !== null) { var instance = finishedWork.stateNode; var instanceToUse; switch (finishedWork.tag) { case HostComponent: // getPublicInstance 這里調(diào)用了DOM API 返回了DOM對象 instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } // 對函數(shù)回調(diào)形式設(shè)置ref的處理 if (typeof ref === 'function') { { ref(instanceToUse); } } else { ref.current = instanceToUse; } } }
在commit階段,如果是原生標(biāo)簽則將真實DOM賦值給ref對象的current屬性, 如果是class componnet 則是組件instance。
如果你對function組件未做處理直接加上ref,react會直接忽略并在開發(fā)環(huán)境給出警告
函數(shù)組件沒有實例可以賦值給ref對象,而且組件上的ref prop會被當(dāng)作保留屬性無法在組件中獲取,那該怎么辦呢?
React提供了一個forwardRef函數(shù) 來處理函數(shù)組件的 ref prop,用起來就像下面這個示例:
const Parent = () => { const childRef = useRef(null) return <Child ref={childRef}/> } const Child = forWardRef((props,ref) => { return <div>Child</div> }}
這個方法的源碼主體也非常簡單,返回了一個新的elementType對象,這個對象的render屬性包含了原本的這個函數(shù)組件,而$$typeof則標(biāo)記了這個特殊組件類型。
function forwardRef(render) { .... var elementType = { $$typeof: REACT_FORWARD_REF_TYPE, render: render } .... return elementType; }
那么React對forwardRef這個特殊的組件是怎么處理的呢
function beginWork(current, workInProgress, renderLanes) { ... switch (workInProgress.tag) { case FunctionComponent: { ... return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes); } case ClassComponent: { .... return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes); } case HostComponent: return updateHostComponent(current, workInProgress, renderLanes); case ForwardRef: { .... // 第三個參數(shù)type就是forwardRef創(chuàng)建的elementType return updateForwardRef(current, workInProgress, type, _resolvedProps2, renderLanes); } } function updateForwardRef(current, workInProgress, Component, nextProps, renderLanes) { .... var render = Component.render; var ref = workInProgress.ref; // The rest is a fork of updateFunctionComponent var nextChildren; { ... // 將ref引用傳入renderWithHooks nextChildren = renderWithHooks(current, workInProgress, render, nextProps, ref, renderLanes); ... } workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; }
可以看到和上面 FunctionComponent的主要區(qū)別僅僅是把ref保留屬性當(dāng)成普通屬性傳入 renderWithHooks方法!
那么又有一個問題出現(xiàn)了,如果只是傳了一個ref引用,而沒有像Class組件那樣可以attach的實例,豈不是沒有辦法操作子函數(shù)組件的行為?
用上面的例子驗證一下
const Parent = () => { const childRef = useRef(null) useEffect(()=>{ console.log(childref) // { current:null } }) return <Child ref={childRef}/> } const Child = forwardRef((props,ref) => { return <div>Child</div> }} const Parent = () => { const childRef = useRef(null) useEffect(()=>{ console.log(childref) // { current: div } }) return <Child ref={childRef}/> } const Child = forwardRef((props,ref) => { return <div ref={ref}>Child</div> }}
結(jié)合輸出可以看出如果單獨使用forwardRef僅僅只能轉(zhuǎn)發(fā)ref屬性。如果ref最終沒有綁定到一個ClassCompnent或者原生DOM上那么這個ref將不會改變。
假設(shè)一個業(yè)務(wù)場景,你封裝了一個表單組件,想對外暴露一些接口比如說提交的action以及校驗等操作,這樣應(yīng)該如何處理呢?
react為我們提供了這個hook來幫助函數(shù)組件向外部暴露屬性
先看下效果
const Parent = () => { const childRef = useRef(null) useEffect(()=>{ chilRef.current.sayName();// child }) return <Child ref={childRef}/> } const Child = forwardRef((props,ref) => { useImperativeHandle(ref,()=>({ sayName:()=>{ console.log('child') } })) return <div>Child</div> }}
看一下該hook的源碼部分(以hook mount階段為例):
useImperativeHandle: function (ref, create, deps) { currentHookNameInDev = 'useImperativeHandle'; mountHookTypesDev(); checkDepsAreArrayDev(deps); return mountImperativeHandle(ref, create, deps); } function mountImperativeHandle(ref, create, deps) { { if (typeof create !== 'function') { error('Expected useImperativeHandle() second argument to be a function ' + 'that creates a handle. Instead received: %s.', create !== null ? typeof create : 'null'); } } // TODO: If deps are provided, should we skip comparing the ref itself? var effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null; var fiberFlags = Update; return mountEffectImpl(fiberFlags, Layout, imperativeHandleEffect.bind(null, create, ref), effectDeps); } function imperativeHandleEffect(create, ref) { if (typeof ref === 'function') { var refCallback = ref; var _inst = create(); refCallback(_inst); return function () { refCallback(null); }; } else if (ref !== null && ref !== undefined) { var refObject = ref; { if (!refObject.hasOwnProperty('current')) { error('Expected useImperativeHandle() first argument to either be a ' + 'ref callback or React.createRef() object. Instead received: %s.', 'an object with keys {' + Object.keys(refObject).join(', ') + '}'); } } // 這里執(zhí)行了傳給hook的第二個參數(shù) var _inst2 = create(); refObject.current = _inst2; return function () { refObject.current = null; }; } }
其實就是將我們需要暴露的對象及傳給useImperativeHandle的第二個函數(shù)參數(shù)執(zhí)行結(jié)果賦值給了ref的current對象。
到此為止我們大致梳理了組件上ref prop 的工作流程,以及如何在函數(shù)組件中使用ref prop,貌似比想象中簡單。
上面的過程我們注意到從createElement再到構(gòu)建WorkInProgess Fiber樹到最后commit的過程,ref似乎是一直在被傳遞。
中間過程的代碼過于龐大復(fù)雜,但是我們可以通過一個簡單的測試來驗證一下。
const isEqualRefDemo = () => { const isEqualRef = useRef(1) return <input key="test" ref={isEqualRef}> }
對于 class component 和 原生標(biāo)簽來說 就是 createElement 到 commitAttachRef之前:
在createElement里將ref掛載給window對象,然后在commitAttachRef里判斷一下這兩次的ref是否全等。
對于函數(shù)組件來說就是 createElement 到 hook執(zhí)行 imperativeHandleEffect 之前:
const Parent = () => { const childRef = useRef(1) useEffect(()=>{ chilRef.current.sayName();// child }) return <Child ref={childRef}/> } const Child = forwardRef((props,ref) => { useImperativeHandle(ref,()=>({ sayName:()=>{ console.log('child') } })) return <div>Child</div> }}
從createElement添加ref到React整個渲染過程的末尾(commit階段)被賦值前,這個ref都是同一份引用。
這也正如 ref單詞的本意 reference引用一樣。
1.ref出現(xiàn)在組件上時是一個保留屬性
2.ref在組件存在的生命周期內(nèi)維護了同一個引用(可變對象 MutableObject)
3.當(dāng)ref掛載的對象是原生html標(biāo)簽時會ref對象的current屬性會被賦值為真實DOM 而如果是React組件會被賦值為React"組件實例"
4.ref掛載都在commit階段處理
ref prop相當(dāng)于在組件上挖了一個“坑” 來承接 ref對象,但是這樣還不夠我們還需要先創(chuàng)建ref對象
這兩種創(chuàng)建ref的方式不再贅述,官網(wǎng)以及社區(qū)優(yōu)秀文章可供參考。
https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
https://blog.logrocket.com/how-to-use-react-createref-ea014ad09dba/
createRef
16.3引入了createRef這個api
createRef的源碼就是一個閉包,對外暴露了 一個具有 current屬性的對象。
我們一般會這樣在class component中使用createRef
class CreateRefComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef() } componentDidMount() { this.myRef.current.focus() console.log(this.myRef.current) // dom input } render() { return <input ref={this.myRef} /> } }
結(jié)合第一節(jié)的內(nèi)容以及 createRef的源碼,我們發(fā)現(xiàn),這不過就是在類組件內(nèi)部掛載了一個可變對象。因為類組件構(gòu)造函數(shù)不會被反復(fù)執(zhí)行,因此這個createRef自然保持同一份引用。但是到了函數(shù)組件就不一樣了,每一次組件更新, 因為沒有特殊處理createRef會被反復(fù)重新創(chuàng)建執(zhí)行,因此在函數(shù)組件中使用createRef將不能達到只有同一份引用的效果。
const CreateRefInFC = () => { const valRef = React.createRef(); // 如果在函數(shù)組件中使用createRef 在這個例子中點擊后ref就會被重新創(chuàng)建因此將始終顯示為null const [, update] = React.useState(); return <div> value: {valRef.current} <button onClick={() => { valRef.current = 80; update({}); }}>+ </button> </div> }
React 16.8中出現(xiàn)了hooks,使得我們可以在函數(shù)組件中定義狀態(tài),同時也帶來了 useRef
再來看moutRef和updateRef所做的事:
function mountRef(initialValue) { var hook = mountWorkInProgressHook(); { var _ref2 = { current: initialValue }; hook.memoizedState = _ref2; return _ref2; } } function updateRef(initialValue) { var hook = updateWorkInProgressHook(); return hook.memoizedState; }
借助hook數(shù)據(jù)結(jié)構(gòu),第一次useRef時將創(chuàng)建的值保存在memoizedState中,之后每次更新階段則直接返回。
這樣在函數(shù)組件更新時重復(fù)執(zhí)行useRef仍返回同一份引用。
因此實際上和 createRef一樣本質(zhì)上只是創(chuàng)建了一個 Mutable Object,只是因為渲染方式的不同,在函數(shù)組件中做了一些處理。而掛載和卸載的行為全部交由組件本身來維護。
從 createRef開始我們可以看到,ref對象的消費不再和DOM以及組件屬性所綁定了,這意味著你可以在任何地方消費他們,這也回答了本文一開始的那個問題。
由于函數(shù)組件每次執(zhí)行形成的閉包,下面這段代碼會始終打印1
export const ClosureDemo = () => { const [ count,setCount ] = useState(0); useEffect(()=> { const interval = setInterval(()=>{ setCount(count+1) }, 1000) return () => clearInterval(interval) }, []) // count顯示始終是1 return <div>{ count }</div> }
將 count 作為依賴傳入useEffect可以解決上面這個問題
export const ClosureDemo = () => { const [ count,setCount ] = useState(0); useEffect(()=> { const interval = setInterval(()=>{ setCount(count+1) }, 1000) return () => clearInterval(interval) }, [count]) return <div>{ count }</div> }
但是這樣定時器也會隨著count值的更新而被不斷創(chuàng)建,一方面會帶來性能問題(這個例子中沒有那么明顯),更重要的一個方面是它不符合我們的開發(fā)語義,因為很明顯我們希望定時器本身是不變的。
另外一個方式也可以處理這個問題
export const ClosureDemo = () => { const [ count,setCount ] = useState(0); useEffect(()=> { const interval = setInterval(()=>{ setCount(count=> count + 1) // 使用setSate函數(shù)式更新可以確保每次都取到新的值 }, 1000) return () => clearInterval(interval) }, []) return <div>{ count }</div> }
這樣做確實可以處理閉包帶來的影響,但是僅限于需要使用setState的場景,對數(shù)據(jù)的修改和觸發(fā)setState是需要綁定的,這可能會造成不必要的刷新。
使用useRef創(chuàng)建引用
export const ClosureDemo = () => { const [ count,setCount ] = useState(0); const countRef = useRef(0); countRef.current = count useEffect(()=> { const interval = setInterval(()=>{ // 這里將更新count的邏輯和觸發(fā)更新的邏輯解耦了 if(countRef.current < 5){ countRef.current++ } else { setCount(countRef.current) } }, 1000) return () => clearInterval(interval) }, []) return <div>{ count }</div> }
通過factory函數(shù)來避免類似于 useRef(new Construcotr)中構(gòu)造函數(shù)的重復(fù)執(zhí)行
import { useRef } from 'react'; export default function useCreation<T>(factory: () => T, deps: any[]) { const { current } = useRef({ deps, obj: undefined as undefined | T, initialized: false, }); if (current.initialized === false || !depsAreSame(current.deps, deps)) { current.deps = deps; current.obj = factory(); current.initialized = true; } return current.obj as T; } function depsAreSame(oldDeps: any[], deps: any[]): boolean { if (oldDeps === deps) return true; for (const i in oldDeps) { if (oldDeps[i] !== deps[i]) return false; } return true; }
通過創(chuàng)建兩個ref來保存前一次的state
import { useRef } from 'react'; export type compareFunction<T> = (prev: T | undefined, next: T) => boolean; function usePrevious<T>(state: T, compare?: compareFunction<T>): T | undefined { const prevRef = useRef<T>(); const curRef = useRef<T>(); const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true; if (needUpdate) { prevRef.current = curRef.current; curRef.current = state; } return prevRef.current; } export default usePrevious;
自定義的元素失焦響應(yīng)hook
import { useEffect, useRef } from 'react'; export type BasicTarget<T = HTMLElement> = | (() => T | null) | T | null | MutableRefObject<T | null | undefined>; export function getTargetElement( target?: BasicTarget<TargetElement>, defaultElement?: TargetElement, ): TargetElement | undefined | null { if (!target) { return defaultElement; } let targetElement: TargetElement | undefined | null; if (typeof target === 'function') { targetElement = target(); } else if ('current' in target) { targetElement = target.current; } else { targetElement = target; } return targetElement; } // 鼠標(biāo)點擊事件,click 不會監(jiān)聽右鍵 const defaultEvent = 'click'; type EventType = MouseEvent | TouchEvent; export default function useClickAway( onClickAway: (event: EventType) => void, target: BasicTarget | BasicTarget[], eventName: string = defaultEvent, ) { // 使用useRef保存回調(diào)函數(shù) const onClickAwayRef = useRef(onClickAway); onClickAwayRef.current = onClickAway; useEffect(() => { const handler = (event: any) => { const targets = Array.isArray(target) ? target : [target]; if ( targets.some((targetItem) => { const targetElement = getTargetElement(targetItem) as HTMLElement; return !targetElement || targetElement?.contains(event.target); }) ) { return; } onClickAwayRef.current(event); }; document.addEventListener(eventName, handler); return () => { document.removeEventListener(eventName, handler); }; }, [target, eventName]); }
以上自定義hooks均出自ahooks
還有許多好用的自定義hook以及倉庫比如react-use都基于useRef自定義了很多好用的hook。
“React ref的原理和應(yīng)用”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。