掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
提到 ref或者 refs 如果你用過(guò)React 16以前的版本 第一印象都是用來(lái)訪問DOM或者修改組件實(shí)例的,

正如官網(wǎng)所介紹的這樣:
然后到了React 16.3出現(xiàn)的 createRef 以及16.8 hooks中的 useRef出現(xiàn)時(shí),發(fā)現(xiàn)這里的ref好像不僅僅只有之前的綁定到DOM/組件實(shí)例的 作用?本文將帶你逐一梳理這些知識(shí)點(diǎn),并嘗試分析相關(guān)源碼。
這部分知識(shí)點(diǎn)不是本文重點(diǎn),每個(gè)點(diǎn)展開都非常龐大,了方便本文理解先在這里簡(jiǎn)單提及。
Fiber是React更新時(shí)的最小單元,是一種包含指針的數(shù)據(jù)結(jié)構(gòu),從數(shù)據(jù)結(jié)構(gòu)上看Fiber架構(gòu) ≈ 樹 + 鏈表。
Fiber單元是從 jsx createElement之后根據(jù)ReactElement生成的,相比 ReactElement,F(xiàn)iber單元具備動(dòng)態(tài)工作能力。
使用chrome perfomance錄制一個(gè)react應(yīng)用渲染看函數(shù)調(diào)用棧會(huì)看到下面這張圖
這三塊內(nèi)容分別代表: 1.生成react root節(jié)點(diǎn) 2.reconciler 協(xié)調(diào)生成需要更新的子節(jié)點(diǎn) 3.將節(jié)點(diǎn)更新commit 到視圖
在函數(shù)組件中每執(zhí)行一次use開頭的hook函數(shù)都會(huì)生成一個(gè)hook對(duì)象。
- type Hook = {
- memoizedState: any, // 上次更新之后的最終狀態(tài)值
- queue: UpdateQueue, //更新隊(duì)列
- next, // 下一個(gè) hook 對(duì)象
- };
其中memoizedState會(huì)保存該hook上次更新之后的最終狀態(tài),比如當(dāng)我們使用一次useState之后就會(huì)在memoizedState中保存初始值。
React 中大部分 hook 分為兩個(gè)階段:第一次初始化時(shí)`mount`階段和更新`update`時(shí)階段
hooks函數(shù)的執(zhí)行分兩個(gè)階段 mount和 update,比如 useState只會(huì)在初始化時(shí)執(zhí)行一次,下文中將提到的
useImperativeHandle 和 useRef也包括在內(nèi)。
本文已梳理摘取了源碼相關(guān)的函數(shù),但你如果配合源碼調(diào)試一起食用效果會(huì)更加。
本文基于React v17.0.2。
拉取React代碼并安裝依賴
將react,scheduler以及react-dom打包為commonjs
yarn build react/index,react-dom/index,scheduler --type NODE
3.進(jìn)入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)建一個(gè)測(cè)試應(yīng)用 并link react,react-dom
組件上的ref屬性是一個(gè)保留屬性,你不能把ref當(dāng)成一個(gè)普通的prop屬性在一個(gè)組件中獲取,比如:
- const Parent = () => {
- return
- }
- const Child = (props) => {
- console.log(props);
- // 這里獲取不到ref屬性
- return
- }
這個(gè)ref去哪里了呢, React本身又對(duì)它做了什么呢?
我們知道React的解析是從createElement開始的,找到了下面創(chuàng)建ReactElement的地方,確實(shí)有對(duì)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)建了對(duì)ref屬性的引用。
createElement之后我們需要構(gòu)建Fiber工作樹,接下來(lái)主要講對(duì)ref相關(guān)的處理。
React對(duì)于不同的組件有不通的處理
先主要關(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ù)組件在渲染的時(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實(shí)例掛載到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則是通過(guò)構(gòu)造函數(shù)生成實(shí)例并標(biāo)記了ref屬性。
回顧一下之前提到的React工作流程,既然是要將組件實(shí)例或者真實(shí)DOM賦值給ref那肯定不能在一開始就處理這個(gè)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對(duì)象
- instanceToUse = getPublicInstance(instance);
- break;
- default:
- instanceToUse = instance;
- }
- // 對(duì)函數(shù)回調(diào)形式設(shè)置ref的處理
- if (typeof ref === 'function') {
- {
- ref(instanceToUse);
- }
- } else {
- ref.current = instanceToUse;
- }
- }
- }
在commit階段,如果是原生標(biāo)簽則將真實(shí)DOM賦值給ref對(duì)象的current屬性, 如果是class componnet 則是組件instance。
如果你對(duì)function組件未做處理直接加上ref,react會(huì)直接忽略并在開發(fā)環(huán)境給出警告
函數(shù)組件沒有實(shí)例可以賦值給ref對(duì)象,而且組件上的ref prop會(huì)被當(dāng)作保留屬性無(wú)法在組件中獲取,那該怎么辦呢?
React提供了一個(gè)forwardRef函數(shù) 來(lái)處理函數(shù)組件的 ref prop,用起來(lái)就像下面這個(gè)示例:
- const Parent = () => {
- const childRef = useRef(null)
- return
- }
- const Child = forWardRef((props,ref) => {
- return
Child- }}
這個(gè)方法的源碼主體也非常簡(jiǎn)單,返回了一個(gè)新的elementType對(duì)象,這個(gè)對(duì)象的render屬性包含了原本的這個(gè)函數(shù)組件,而$$typeof則標(biāo)記了這個(gè)特殊組件類型。
- function forwardRef(render) {
- ....
- var elementType = {
- $$typeof: REACT_FORWARD_REF_TYPE,
- render: render
- }
- ....
- return elementType;
- }
那么React對(duì)forwardRef這個(gè)特殊的組件是怎么處理的呢
- 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:
- {
- ....
- // 第三個(gè)參數(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方法!
那么又有一個(gè)問題出現(xiàn)了,如果只是傳了一個(gè)ref引用,而沒有像Class組件那樣可以attach的實(shí)例,豈不是沒有辦法操作子函數(shù)組件的行為?
用上面的例子驗(yàn)證一下
- const Parent = () => {
- const childRef = useRef(null)
- useEffect(()=>{
- console.log(childref) // { current:null }
- })
- return
- }
- const Child = forwardRef((props,ref) => {
- return
Child- }}
- const Parent = () => {
- const childRef = useRef(null)
- useEffect(()=>{
- console.log(childref) // { current: div }
- })
- return
- }
- const Child = forwardRef((props,ref) => {
- return
Child- }}
結(jié)合輸出可以看出如果單獨(dú)使用forwardRef僅僅只能轉(zhuǎn)發(fā)ref屬性。如果ref最終沒有綁定到一個(gè)ClassCompnent或者原生DOM上那么這個(gè)ref將不會(huì)改變。
假設(shè)一個(gè)業(yè)務(wù)場(chǎng)景,你封裝了一個(gè)表單組件,想對(duì)外暴露一些接口比如說(shuō)提交的action以及校驗(yàn)等操作,這樣應(yīng)該如何處理呢?
react為我們提供了這個(gè)hook來(lái)幫助函數(shù)組件向外部暴露屬性
先看下效果
- const Parent = () => {
- const childRef = useRef(null)
- useEffect(()=>{
- chilRef.current.sayName();// child
- })
- return
- }
- const Child = forwardRef((props,ref) => {
- useImperativeHandle(ref,()=>({
- sayName:()=>{
- console.log('child')
- }
- }))
- return
Child- }}
看一下該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的第二個(gè)參數(shù)
- var _inst2 = create();
- refObject.current = _inst2;
- return function () {
- refObject.current = null;
- };
- }
- }
其實(shí)就是將我們需要暴露的對(duì)象及傳給useImperativeHandle的第二個(gè)函數(shù)參數(shù)執(zhí)行結(jié)果賦值給了ref的current對(duì)象。
到此為止我們大致梳理了組件上ref prop 的工作流程,以及如何在函數(shù)組件中使用ref prop,貌似比想象中簡(jiǎn)單。
上面的過(guò)程我們注意到從createElement再到構(gòu)建WorkInProgess Fiber樹到最后commit的過(guò)程,ref似乎是一直在被傳遞。
中間過(guò)程的代碼過(guò)于龐大復(fù)雜,但是我們可以通過(guò)一個(gè)簡(jiǎn)單的測(cè)試來(lái)驗(yàn)證一下。
- const isEqualRefDemo = () => {
- const isEqualRef = useRef(1)
- return
- }
對(duì)于 class component 和 原生標(biāo)簽來(lái)說(shuō) 就是 createElement 到 commitAttachRef之前:
在createElement里將ref掛載給window對(duì)象,然后在commitAttachRef里判斷一下這兩次的ref是否全等。
對(duì)于函數(shù)組件來(lái)說(shuō)就是 createElement 到 hook執(zhí)行 imperativeHandleEffect 之前:
- const Parent = () => {
- const childRef = useRef(1)
- useEffect(()=>{
- chilRef.current.sayName();// child
- })
- return
- }
- const Child = forwardRef((props,ref) => {
- useImperativeHandle(ref,()=>({
- sayName:()=>{
- console.log('child')
- }
- }))
- return
Child- }}
從createElement添加ref到React整個(gè)渲染過(guò)程的末尾(commit階段)被賦值前,這個(gè)ref都是同一份引用。
這也正如 ref單詞的本意 reference引用一樣。
1.ref出現(xiàn)在組件上時(shí)是一個(gè)保留屬性
2.ref在組件存在的生命周期內(nèi)維護(hù)了同一個(gè)引用(可變對(duì)象 MutableObject)
3.當(dāng)ref掛載的對(duì)象是原生html標(biāo)簽時(shí)會(huì)ref對(duì)象的current屬性會(huì)被賦值為真實(shí)DOM 而如果是React組件會(huì)被賦值為React"組件實(shí)例"
4.ref掛載都在commit階段處理
ref prop相當(dāng)于在組件上挖了一個(gè)“坑” 來(lái)承接 ref對(duì)象,但是這樣還不夠我們還需要先創(chuàng)建ref對(duì)象
這兩種創(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這個(gè)api
createRef的源碼就是一個(gè)閉包,對(duì)外暴露了 一個(gè)具有 current屬性的對(duì)象。
我們一般會(huì)這樣在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
- }
- }
結(jié)合第一節(jié)的內(nèi)容以及 createRef的源碼,我們發(fā)現(xiàn),這不過(guò)就是在類組件內(nèi)部掛載了一個(gè)可變對(duì)象。因?yàn)轭惤M件構(gòu)造函數(shù)不會(huì)被反復(fù)執(zhí)行,因此這個(gè)createRef自然保持同一份引用。但是到了函數(shù)組件就不一樣了,每一次組件更新, 因?yàn)闆]有特殊處理createRef會(huì)被反復(fù)重新創(chuàng)建執(zhí)行,因此在函數(shù)組件中使用createRef將不能達(dá)到只有同一份引用的效果。
- const CreateRefInFC = () => {
- const valRef = React.createRef(); // 如果在函數(shù)組件中使用createRef 在這個(gè)例子中點(diǎn)擊后ref就會(huì)被重新創(chuàng)建因此將始終顯示為null
- const [, update] = React.useState();
- return
- value: {valRef.current}
- valRef.current = 80;
- update({});
- }}>+
- }
React 16.8中出現(xiàn)了hooks,使得我們可以在函數(shù)組件中定義狀態(tài),同時(shí)也帶來(lái)了 useRef
再來(lái)看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時(shí)將創(chuàng)建的值保存在memoizedState中,之后每次更新階段則直接返回。
這樣在函數(shù)組件更新時(shí)重復(fù)執(zhí)行useRef仍返回同一份引用。
因此實(shí)際上和 createRef一樣本質(zhì)上只是創(chuàng)建了一個(gè) Mutable Object,只是因?yàn)殇秩痉绞降牟煌?,在函?shù)組件中做了一些處理。而掛載和卸載的行為全部交由組件本身來(lái)維護(hù)。
從 createRef開始我們可以看到,ref對(duì)象的消費(fèi)不再和DOM以及組件屬性所綁定了,這意味著你可以在任何地方消費(fèi)他們,這也回答了本文一開始的那個(gè)問題。
由于函數(shù)組件每次執(zhí)行形成的閉包,下面這段代碼會(huì)始終打印1
- export const ClosureDemo = () => {
- const [ count,setCount ] = useState(0);
- useEffect(()=> {
- const interval = setInterval(()=>{
- setCount(count+1)
- }, 1000)
- return () => clearInterval(interval)
- }, [])
- // count顯示始終是1
- return
{ count }- }
將 count 作為依賴傳入useEffect可以解決上面這個(gè)問題
- export const ClosureDemo = () => {
- const [ count,setCount ] = useState(0);
- useEffect(()=> {
- const interval = setInterval(()=>{
- setCount(count+1)
- }, 1000)
- return () => clearInterval(interval)
- }, [count])
- return
{ count }- }
但是這樣定時(shí)器也會(huì)隨著count值的更新而被不斷創(chuàng)建,一方面會(huì)帶來(lái)性能問題(這個(gè)例子中沒有那么明顯),更重要的一個(gè)方面是它不符合我們的開發(fā)語(yǔ)義,因?yàn)楹苊黠@我們希望定時(shí)器本身是不變的。
另外一個(gè)方式也可以處理這個(gè)問題
- export const ClosureDemo = () => {
- const [ count,setCount ] = useState(0);
- useEffect(()=> {
- const interval = setInterval(()=>{
- setCount(count=> count + 1) // 使用setSate函數(shù)式更新可以確保每次都取到新的值
- }, 1000)
- return () => clearInterval(interval)
- }, [])
- return
{ count }- }
這樣做確實(shí)可以處理閉包帶來(lái)的影響,但是僅限于需要使用setState的場(chǎng)景,對(duì)數(shù)據(jù)的修改和觸發(fā)setState是需要綁定的,這可能會(huì)造成不必要的刷新。
使用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
{ count }- }
通過(guò)factory函數(shù)來(lái)避免類似于 useRef(new Construcotr)中構(gòu)造函數(shù)的重復(fù)執(zhí)行
- import { useRef } from 'react';
- export default function useCreation
(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;
- }
通過(guò)創(chuàng)建兩個(gè)ref來(lái)保存前一次的state
- import { useRef } from 'react';
- export type compareFunction
= (prev: T | undefined, next: T) => boolean; - function usePrevious
(state: T, compare?: compareFunction ): T | undefined { - const prevRef = useRef
(); - const curRef = useRef
(); - 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 | null)
- | T
- | null
- | MutableRefObject
; - export function getTargetElement(
- target?: BasicTarget
, - 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)點(diǎn)擊事件,click 不會(huì)監(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以及倉(cāng)庫(kù)比如react-use都基于useRef自定義了很多好用的hook。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流