React ref的原理和应用
本篇内容介绍了“React ref的原理和应用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
提到 ref或者 refs 如果你用过React 16以前的版本 第一印象都是用来访问DOM或者修改组件实例的,
正如官网所介绍的这样:
然后到了React 16.3出现的 createRef 以及16.8 hooks中的 useRef出现时,发现这里的ref好像不仅仅只有之前的绑定到DOM/组件实例的 作用?本文将带你逐一梳理这些知识点,并尝试分析相关源码。
前置知识
这部分知识点不是本文重点,每个点展开都非常庞大,了方便本文理解先在这里简单提及。
Fiber架构
Fiber是React更新时的最小单元,是一种包含指针的数据结构,从数据结构上看Fiber架构 ≈ 树 + 链表。
Fiber单元是从 jsx createElement之后根据ReactElement生成的,相比 ReactElement,Fiber单元具备动态工作能力。
React 的工作流程
使用chrome perfomance录制一个react应用渲染看函数调用栈会看到下面这张图
这三块内容分别代表: 1.生成react root节点 2.reconciler 协调生成需要更新的子节点 3.将节点更新commit 到视图
Hooks基础知识
在函数组件中每执行一次use开头的hook函数都会生成一个hook对象。
type Hook = { memoizedState: any, // 上次更新之后的最终状态值 queue: UpdateQueue, //更新队列 next, // 下一个 hook 对象 };
其中memoizedState会保存该hook上次更新之后的最终状态,比如当我们使用一次useState之后就会在memoizedState中保存初始值。
React 中大部分 hook 分为两个阶段:第一次初始化时`mount`阶段和更新`update`时阶段
hooks函数的执行分两个阶段 mount和 update,比如 useState只会在初始化时执行一次,下文中将提到的
useImperativeHandle 和 useRef也包括在内。
调试源码
本文已梳理摘取了源码相关的函数,但你如果配合源码调试一起食用效果会更加。
本文基于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 执行yarn link 同理 react-dom
4.在 build/node_modules/react/cjs/react.development.js中加入link标记console以确保检查link状态
5.使用create-react-app创建一个测试应用 并link react,react-dom
ref prop
组件上的ref属性是一个保留属性,你不能把ref当成一个普通的prop属性在一个组件中获取,比如:
const Parent = () => { return <Child ref={{test:1}}> } const Child = (props) => { console.log(props); // 这里获取不到ref属性 return <div></div> }
这个ref去哪里了呢, React本身又对它做了什么呢?
我们知道React的解析是从createElement开始的,找到了下面创建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开始就已经创建了对ref属性的引用。
createElement之后我们需要构建Fiber工作树,接下来主要讲对ref相关的处理。
React对于不同的组件有不通的处理
先主要关注 FunctionComponent/ClassComponent/HostComponent(原生html标签)
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就是指我们的函数组件 return children; }
我们可以看到函数组件在渲染的时候就是直接执行。
Class组件和原生标签的ref prop
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) { // 标记是否有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则是通过构造函数生成实例并标记了ref属性。
回顾一下之前提到的React工作流程,既然是要将组件实例或者真实DOM赋值给ref那肯定不能在一开始就处理这个ref,而是根据标记到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 这里调用了DOM API 返回了DOM对象 instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } // 对函数回调形式设置ref的处理 if (typeof ref === 'function') { { ref(instanceToUse); } } else { ref.current = instanceToUse; } } }
在commit阶段,如果是原生标签则将真实DOM赋值给ref对象的current属性, 如果是class componnet 则是组件instance。
函数组件的ref prop
如果你对function组件未做处理直接加上ref,react会直接忽略并在开发环境给出警告
函数组件没有实例可以赋值给ref对象,而且组件上的ref prop会被当作保留属性无法在组件中获取,那该怎么办呢?
forwardRef
React提供了一个forwardRef函数 来处理函数组件的 ref prop,用起来就像下面这个示例:
const Parent = () => { const childRef = useRef(null) return <Child ref={childRef}/> } const Child = forWardRef((props,ref) => { return <div>Child</div> }}
这个方法的源码主体也非常简单,返回了一个新的elementType对象,这个对象的render属性包含了原本的这个函数组件,而$$typeof则标记了这个特殊组件类型。
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: { .... // 第三个参数type就是forwardRef创建的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的主要区别仅仅是把ref保留属性当成普通属性传入 renderWithHooks方法!
那么又有一个问题出现了,如果只是传了一个ref引用,而没有像Class组件那样可以attach的实例,岂不是没有办法操作子函数组件的行为?
用上面的例子验证一下
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> }}
结合输出可以看出如果单独使用forwardRef仅仅只能转发ref属性。如果ref最终没有绑定到一个ClassCompnent或者原生DOM上那么这个ref将不会改变。
假设一个业务场景,你封装了一个表单组件,想对外暴露一些接口比如说提交的action以及校验等操作,这样应该如何处理呢?
useImperativeHandle
react为我们提供了这个hook来帮助函数组件向外部暴露属性
先看下效果
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(', ') + '}'); } } // 这里执行了传给hook的第二个参数 var _inst2 = create(); refObject.current = _inst2; return function () { refObject.current = null; }; } }
其实就是将我们需要暴露的对象及传给useImperativeHandle的第二个函数参数执行结果赋值给了ref的current对象。
同一份引用
到此为止我们大致梳理了组件上ref prop 的工作流程,以及如何在函数组件中使用ref prop,貌似比想象中简单。
上面的过程我们注意到从createElement再到构建WorkInProgess Fiber树到最后commit的过程,ref似乎是一直在被传递。
中间过程的代码过于庞大复杂,但是我们可以通过一个简单的测试来验证一下。
const isEqualRefDemo = () => { const isEqualRef = useRef(1) return <input key="test" ref={isEqualRef}> }
对于 class component 和 原生标签来说 就是 createElement 到 commitAttachRef之前:
在createElement里将ref挂载给window对象,然后在commitAttachRef里判断一下这两次的ref是否全等。
对于函数组件来说就是 createElement 到 hook执行 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出现在组件上时是一个保留属性
2.ref在组件存在的生命周期内维护了同一个引用(可变对象 MutableObject)
3.当ref挂载的对象是原生html标签时会ref对象的current属性会被赋值为真实DOM 而如果是React组件会被赋值为React"组件实例"
4.ref挂载都在commit阶段处理
创建ref的方式
ref prop相当于在组件上挖了一个“坑” 来承接 ref对象,但是这样还不够我们还需要先创建ref对象
字符串ref & callback ref
这两种创建ref的方式不再赘述,官网以及社区优秀文章可供参考。
https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
https://blog.logrocket.com/how-to-use-react-createref-ea014ad09dba/
createRef & useRef
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} /> } }
为什么不能在函数组件中使用createRef
结合第一节的内容以及 createRef的源码,我们发现,这不过就是在类组件内部挂载了一个可变对象。因为类组件构造函数不会被反复执行,因此这个createRef自然保持同一份引用。但是到了函数组件就不一样了,每一次组件更新, 因为没有特殊处理createRef会被反复重新创建执行,因此在函数组件中使用createRef将不能达到只有同一份引用的效果。
const CreateRefInFC = () => { const valRef = React.createRef(); // 如果在函数组件中使用createRef 在这个例子中点击后ref就会被重新创建因此将始终显示为null const [, update] = React.useState(); return <div> value: {valRef.current} <button onClick={() => { valRef.current = 80; update({}); }}>+ </button> </div> }
useRef
React 16.8中出现了hooks,使得我们可以在函数组件中定义状态,同时也带来了 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数据结构,第一次useRef时将创建的值保存在memoizedState中,之后每次更新阶段则直接返回。
这样在函数组件更新时重复执行useRef仍返回同一份引用。
因此实际上和 createRef一样本质上只是创建了一个 Mutable Object,只是因为渲染方式的不同,在函数组件中做了一些处理。而挂载和卸载的行为全部交由组件本身来维护。
被扩展的ref
从 createRef开始我们可以看到,ref对象的消费不再和DOM以及组件属性所绑定了,这意味着你可以在任何地方消费他们,这也回答了本文一开始的那个问题。
useRef的应用
解决闭包问题
由于函数组件每次执行形成的闭包,下面这段代码会始终打印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值的更新而被不断创建,一方面会带来性能问题(这个例子中没有那么明显),更重要的一个方面是它不符合我们的开发语义,因为很明显我们希望定时器本身是不变的。
另外一个方式也可以处理这个问题
export const ClosureDemo = () => { const [ count,setCount ] = useState(0); useEffect(()=> { const interval = setInterval(()=>{ setCount(count=> count + 1) // 使用setSate函数式更新可以确保每次都取到新的值 }, 1000) return () => clearInterval(interval) }, []) return <div>{ count }</div> }
这样做确实可以处理闭包带来的影响,但是仅限于需要使用setState的场景,对数据的修改和触发setState是需要绑定的,这可能会造成不必要的刷新。
使用useRef创建引用
export const ClosureDemo = () => { const [ count,setCount ] = useState(0); const countRef = useRef(0); countRef.current = count useEffect(()=> { const interval = setInterval(()=>{ // 这里将更新count的逻辑和触发更新的逻辑解耦了 if(countRef.current < 5){ countRef.current++ } else { setCount(countRef.current) } }, 1000) return () => clearInterval(interval) }, []) return <div>{ count }</div> }
封装自定义hooks
useCreation
通过factory函数来避免类似于 useRef(new Construcotr)中构造函数的重复执行
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; }
usePrevious
通过创建两个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;
useClickAway
自定义的元素失焦响应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; } // 鼠标点击事件,click 不会监听右键 const defaultEvent = 'click'; type EventType = MouseEvent | TouchEvent; export default function useClickAway( onClickAway: (event: EventType) => void, target: BasicTarget | BasicTarget[], eventName: string = defaultEvent, ) { // 使用useRef保存回调函数 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的原理和应用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341