關(guān)于ref的一切
作為React開發(fā)者,你能回答如下幾個(gè)問題么?
為什么
string類型的ref prop將會(huì)被廢棄?function類型的ref prop會(huì)在什么時(shí)機(jī)被調(diào)用?React.createRef與useRef的返回值有什么不同?
其實(shí),這三個(gè)問題中的ref包含兩個(gè)不同概念:
不管是
string、function類型或是React.createRef、useRef創(chuàng)建的ref,都是作為數(shù)據(jù)結(jié)構(gòu)看待問題2探討的時(shí)機(jī)是將
ref作為生命周期看待
接下來本文會(huì)分別從數(shù)據(jù)結(jié)構(gòu)、生命周期兩個(gè)角度探討ref。
這,就是關(guān)于ref的一切。
為什么string類型的ref prop將會(huì)被廢棄?
string類型的ref使用方式如下:
點(diǎn)擊input標(biāo)簽會(huì)打印input的value。
class?Foo?extends?Component?{
??render()?{
????return?(
??????<input
????????onClick={()?=>?this.action()}?
????????ref='input'?
??????/>
????);
??}
??action()?{
????console.log(this.refs.input.value);
??}
}
string類型ref prop最主要的兩個(gè)問題是:
- 由于是
string的寫法,無法直接獲得this的指向。
所以,React需要持續(xù)追蹤當(dāng)前render的組件。這會(huì)讓React在性能上變慢。
- 當(dāng)使用
render回調(diào)函數(shù)的開發(fā)模式,獲得ref的組件實(shí)例可能與預(yù)期不同。
比如:
class?App?extends?React.Component?{
??renderRow?=?(index)?=>?{
????//?ref會(huì)綁定到DataTable組件實(shí)例,而不是App組件實(shí)例上
????return?;
????//?如果使用function類型ref,則不會(huì)有這個(gè)問題
????//?return??this['input-'?+?index]?=?input}?/>;
??}
?
??render()?{
????return?
??}
}
還有其他原因使React團(tuán)隊(duì)決定在未來放棄string Ref,詳見#1373[1]與#8333[2]。
React.createRef
我們直接看React.createRef的源碼:
function?createRef():?RefObject?{
??const?refObject?=?{
????current:?null,
??};
??return?refObject;
}
可見,ref對(duì)象就是僅僅是包含current屬性的普通對(duì)象。
useRef
為了驗(yàn)證這個(gè)觀點(diǎn),我們?cè)倏?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">useRef的源碼。
對(duì)于mount與update,useRef分別對(duì)應(yīng)兩個(gè)函數(shù)。
對(duì)于
hook如何保存數(shù)據(jù)如果不了解,可以看本系列第一篇文章關(guān)于useState的一切
function?mountRef<T>(initialValue:?T):?{|current:?T|}?{
??//?獲取當(dāng)前useRef?hook
??const?hook?=?mountWorkInProgressHook();
??//?創(chuàng)建ref
??const?ref?=?{current:?initialValue};
??hook.memoizedState?=?ref;
??return?ref;
}
function?updateRef<T>(initialValue:?T):?{|current:?T|}?{
??//?獲取當(dāng)前useRef?hook
??const?hook?=?updateWorkInProgressHook();
??//?返回保存的數(shù)據(jù)
??return?hook.memoizedState;
}
可以看到,ref對(duì)象確實(shí)僅僅是包含current屬性的對(duì)象。
function ref
除了{current: any}類型外,ref還能作為function。
作為function時(shí),僅僅是在不同生命周期階段被調(diào)用的回調(diào)函數(shù)。
在我們接下來的討論中,只涉及function | {current: any}這兩種ref的數(shù)據(jù)結(jié)構(gòu)。
在React中,HostComponent、ClassComponent、ForwardRef可以賦值ref屬性。
這個(gè)屬性在ref生命周期的不同階段會(huì)被執(zhí)行(對(duì)于function)或賦值(對(duì)于{current: any})。
//?HostComponent
div>
//?ClassComponent?/?ForwardRef
<App?ref={cpnRef}?/>
其中,ForwardRef只是將ref作為第二個(gè)參數(shù)傳遞下去,沒有別的特殊處理。
//?對(duì)于ForwardRef,secondArg為傳遞下去的ref
const?children?=?forwardRef(
??(props,?secondArg)?=>?{
????//render邏輯...
??}
);
所以接下來討論ref的生命周期時(shí)不會(huì)單獨(dú)討論ForwardRef。
在本系列文章中我們講過,React的渲染包含兩個(gè)階段:
render階段:為需要更新的組件對(duì)應(yīng)fiber打上標(biāo)簽(effectTag)
commit階段:執(zhí)行effectTag對(duì)應(yīng)更新操作
//?部分effectTag定義
//?插入DOM
export?const?Placement?=?/*?*/?0b0000000000000010;
//?更新DOM的屬性
export?const?Update?=?/*????*/?0b0000000000000100;
//?刪除DOM
export?const?Deletion?=?/*??*/?0b0000000000001000;
//?有ref操作
export?const?Ref?=?/*???????*/?0b0000000010000000;
//?...
對(duì)于HostComponent、ClassComponent如果包含ref操作,那么也會(huì)賦值相應(yīng)的effectTag。
同其他effectTag對(duì)應(yīng)操作的執(zhí)行一樣,ref的更新也是發(fā)生在commit階段。
所以,ref的生命周期可以分為兩個(gè)大階段:
render階段為含有ref屬性的Component對(duì)應(yīng)fiber添加Ref effectTag
commit階段為包含Ref effectTag的fiber執(zhí)行對(duì)應(yīng)操作
render階段
在render階段,組件對(duì)應(yīng)fiber被賦值Ref effectTag需要滿足的條件:
fiber類型為HostComponent、ClassComponent、ScopeComponent
ScopeComponent是一種用于管理focus的測(cè)試特性,這種情況我們不討論。詳見PR[3]
對(duì)于mount,workInProgress.ref !== null,即組件首次render時(shí)存在ref屬性
對(duì)于update,current.ref !== workInProgress.ref,即組件更新時(shí)ref屬性改變
commit階段
在commit階段,ref的生命周期分為兩個(gè)子階段:
移除之前的ref
更新ref
移除之前的ref
- 對(duì)于
ref屬性改變的情況,需要先移除之前的ref。
調(diào)用的是commitDetachRef:
function?commitDetachRef(current:?Fiber)?{
??const?currentRef?=?current.ref;
??if?(currentRef?!==?null)?{
????if?(typeof?currentRef?===?'function')?{
??????//?function類型ref,調(diào)用他,傳參為null
??????currentRef(null);
????}?else?{
??????//?對(duì)象類型ref,current賦值為null
??????currentRef.current?=?null;
????}
??}
}
可以看到,function與{current: any}類型的ref的生命周期并沒有什么不同,只是一種會(huì)被調(diào)用,一種會(huì)被賦值。
- 對(duì)于
Deletion effectTag的fiber(對(duì)應(yīng)需要?jiǎng)h除的DOM節(jié)點(diǎn)),需要遞歸他的子樹,對(duì)子孫fiber的ref執(zhí)行類似commitDetachRef的操作。
更新ref
接下來進(jìn)入ref的更新階段。
執(zhí)行這一步的操作叫commitAttachRef:
function?commitAttachRef(finishedWork:?Fiber)?{
??//?finishedWork為含有Ref?effectTag的fiber
??const?ref?=?finishedWork.ref;
??
??//?含有ref?prop,這里是作為數(shù)據(jù)結(jié)構(gòu)
??if?(ref?!==?null)?{
????//?獲取ref屬性對(duì)應(yīng)的Component實(shí)例
????const?instance?=?finishedWork.stateNode;
????let?instanceToUse;
????switch?(finishedWork.tag)?{
??????case?HostComponent:
????????//?對(duì)于HostComponent,實(shí)例為對(duì)應(yīng)DOM節(jié)點(diǎn)
????????instanceToUse?=?getPublicInstance(instance);
????????break;
??????default:
????????//?其他類型實(shí)例為fiber.stateNode
????????instanceToUse?=?instance;
????}
????//?賦值ref
????if?(typeof?ref?===?'function')?{
??????ref(instanceToUse);
????}?else?{
??????ref.current?=?instanceToUse;
????}
??}
}
可以看到,對(duì)于包含ref屬性的fiber,針對(duì)ref的不同類型,執(zhí)行調(diào)用/賦值操作。
至此,ref的生命周期完成。
總結(jié)
通過本文我們學(xué)習(xí)了ref的數(shù)據(jù)結(jié)構(gòu)及生命周期。
對(duì)于賦值了ref屬性的HostComponent與ClassComponent,他會(huì)依次經(jīng)歷:
在render階段賦值Ref effectTag
如果ref變化,在commit階段會(huì)先刪除之前的ref。
接下來,會(huì)進(jìn)入ref的更新流程。
所以,對(duì)于內(nèi)聯(lián)函數(shù)的ref:
?this.dom?=?dom}>div>
由于每次render ref都對(duì)應(yīng)一個(gè)全新的內(nèi)聯(lián)函數(shù),所以在commit階段會(huì)先執(zhí)行commitDetachRef刪除再執(zhí)行commitAttachRef更新。
即內(nèi)聯(lián)函數(shù)會(huì)被調(diào)用兩次,第一次傳參dom的值為null,第二次為更新的DOM。
參考資料
[1]#1373: https://github.com/facebook/react/issues/1373
[2]#8333: https://github.com/facebook/react/pull/8333#issuecomment-271648615
[3]PR: https://github.com/facebook/react/pull/16587
推薦閱讀
我的公眾號(hào)能帶來什么價(jià)值?(文末有送書規(guī)則,一定要看)
每個(gè)前端工程師都應(yīng)該了解的圖片知識(shí)(長文建議收藏)
為什么現(xiàn)在面試總是面試造火箭?
瀏覽
53
