React | 2020-08-25 22:31:18 735次 1次
ref 包括类组件和函数组件的写法:
类组件中的 createRef
函数组件中使用 useRef,其中还有 forwardRef、useImperativeHandle 等相关
一、类组件中
createRef 声明
// 这个使用时直接返回一个对象,然后肯定是在react-dom中赋值 export function createRef(): RefObject { const refObject = { current: null, }; return refObject; }
markRef 标记(render阶段)
在 beginWork中,对于类组件(ClassComponent)或者普通的 DOM 节点(HostComponent),会先执行 markRef,因为 React.creatElement 会将指定的 ref 属性解析完毕,初始值就是 createRef 函数返回的那个对象。
注意 shouldComponentUpdate 的返回值不会影响这个标记。
function markRef(current: Fiber | null, workInProgress: Fiber) { // 读取 ref 属性 :ref = {current: null} const ref = workInProgress.ref; // 初始 || 更新 if ( (current === null && ref !== null) || (current !== null && current.ref !== ref) ) { // 这里仅仅是打上标记 workInProgress.effectTag |= Ref; } }
对于 HostComponent 在 completeWork 中又进行一次创建标记(没有理解为什么执行这么多次)。
旧 ref 移除(commit阶段)
对于更新的 fiber 来说,需要清除下 current 中的 ref。
回顾:wip 是正在构建中的 fiber,如果是更新则有一个 alternate 相互指向,createWorkInProgress方法创建这个关系,判断是否可复用,否则就直接创建新节点:
commitMutationEffects -- commitDetachRef function commitMutationEffects(){ ... if (effectTag & Ref) { const current = nextEffect.alternate; // 初始没有这个指向 if (current !== null) { commitDetachRef(current); } } ... } function commitDetachRef(current: Fiber) { const currentRef = current.ref; if (currentRef !== null) { // ref 传入一个方法 回调传null if (typeof currentRef === 'function') { currentRef(null); } else { // 就是将 current 置为空 currentRef.current = null; } } }
对于删除的节点,同样需要删除 ref:
commitDeletion -> unmountHostComponents -> commitUnmount -> ClassComponent | HostComponent -> safelyDetachRef
// 同样的逻辑,在很多地方可以发现 react 中绝对不会考虑功能的复用,而是分工明确做事情 function safelyDetachRef(current: Fiber) { const ref = current.ref; if (ref !== null) { if (typeof ref === 'function') { ref(null); } else { ref.current = null; } } }
ref 赋值(commit阶段)
上面步骤完事后,最后一步就是赋值,在 commitLayoutEffects 阶段将真实 dom 引用暴露出去:
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { // 获取真实 dom const instance = finishedWork.stateNode; ... if (typeof ref === 'function') { ref(instance ); } else { ref.current = instance ; } } }
二、函数组件中
useRef 声明
在函数组件中使用 ref 需要初始加载和更新两个阶段:
function mountRef<T>(initialValue: T): {|current: T|} { const hook = mountWorkInProgressHook(); const ref = {current: initialValue}; // 参照之前的数据结构,ref挂载到 memoizedState hook.memoizedState = ref; return ref; } function updateRef<T>(initialValue: T): {|current: T|} { const hook = updateWorkInProgressHook(); // 更新时直接返回初始创建时挂载的数据即可 return hook.memoizedState; }
接下来的流程和类组件中提到的保持一致
三、forwardRef
对于类组件,赋值 ref 属性,可以获取其实例,调用实例上的一些方法;
对于普通 DOM 节点,通过 ref 属性可以获取原生对象,进行节点操作;
对于函数组件,直接使用 ref 属性没有意义,并不可以直接获取函数组件中的某些方法;
但是可以通过 forwardRef 方法,传递 ref,能直接获取函数组件中指定的 dom,也可以配合 useImperativeHandle 方法取得函数组件内部的方法。另外,还可以进行高阶组件的 ref 转发。
类型创建:
export default function forwardRef(render) { return { $$typeof: REACT_FORWARD_REF_TYPE, render, }; }
普通一个组件的返回值如下:
const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { $$typeof: REACT_ELEMENT_TYPE, type: type, ... }; ... return element; };
这样当使用 forwardRef 包裹之后,返回的类型就发生了改变:
const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { $$typeof: REACT_ELEMENT_TYPE, type: { $$typeof: REACT_FORWARD_REF_TYPE, render, }, ... }; ... return element; };
这样就会在 createFiberFromTypeAndProps 方法中创建对应的组件类型--ForwardRef:
... switch (type) { case REACT_FRAGMENT_TYPE: ... case REACT_STRICT_MODE_TYPE: ... ... default: { if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { ... case REACT_FORWARD_REF_TYPE: // 标记为 ForwardRef fiberTag = ForwardRef; break getTag; ... } } } } ...
执行:
创建完节点类型,再次进入 beginWork 时进入 ForwardRef 分支,执行 updateForwardRef:
function updateForwardRef( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderExpirationTime: ExpirationTime, ) { // 传入的函数组件 const render = Component.render; // 给函数组件身上挂载的 ref 属性 const ref = workInProgress.ref; ... nextChildren = renderWithHooks( current, workInProgress, render, nextProps, ref, renderExpirationTime, ); if (current !== null && !didReceiveUpdate) { //跳过更新 ... } ... // 进入调和过程,和之前的内容对接起来了 reconcileChildren(...); return workInProgress.child; }
这里面核心的地方就是通过 renderWithHooks 方法调用,传入 ref 值,函数组件使用时第二个参数为转发的 ref,通常这个值也是 context。
function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, secondArg: any, nextRenderExpirationTime: ExpirationTime, ){ ... let children = Component(props, secondArg); ... }
然而,到目前为止,我们仍然可以通过一些手段越过 react 中目前的限制,不使用 forwardRef 也可以让函数组件中的某些节点使用 ref,比如就当作一个普通属性传入进去,从数据流上看也没有什么问题。
四、useImperativeHandle
这个方法看似复杂,实则很简单,和 useEffect 类似,除了传入的回调函数不一致。
初始加载
function mountImperativeHandle<T>( ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, create: () => T, deps: Array<mixed> | void | null, ): void { ... // 创建依赖 const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null; // 和 useEffect不同的是这个方法 imperativeHandleEffect return mountEffectImpl( UpdateEffect, HookLayout, imperativeHandleEffect.bind(null, create, ref), effectDeps, ); }
更新
function updateImperativeHandle<T>(...): void { ... return updateEffectImpl( ... imperativeHandleEffect.bind(null, create, ref), ... ); }
mountEffectImpl 和 updateEffectImpl 方法参考上一篇中 useEffect 部分 。
function imperativeHandleEffect<T>( create, ref ) { if (typeof ref === 'function') { ... // 都一个样 ,是函数则执行回调,传入参数。。 } else if (ref !== null && ref !== undefined) { const refObject = ref; // useImperativeHandle第二个参数是一个函数,返回一个对象结构 const inst = create(); // 直接挂载到了 current 简单粗暴 refObject.current = inst; return () => { refObject.current = null; }; } }
到这里和 ref 相关的东西就齐全了,对于 forwardRef 和 useImperativeHandle 这两个 api 的设计,如果单纯的从开发角度,是完全没有必要的,但是到这里再看这两个方法以及使用,很明显的感觉到 react 在极力的推动我们走向函数式编程,尽管 ref 原本可以灵活的使用。
比如我们在一个类组件中直接取 this.props.ref 时,会提示错误,在 defineRefPropWarningGetter 方法中做出了限制:
function defineRefPropWarningGetter(props, displayName) { const warnAboutAccessingRef = function() { if (__DEV__) { if (!specialPropRefWarningShown) { specialPropRefWarningShown = true; console.error( '%s: `ref` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName, ); } } }; warnAboutAccessingRef.isReactWarning = true; // 属性拦截 Object.defineProperty(props, 'ref', { get: warnAboutAccessingRef, configurable: true, }); }
1人赞