ref的实现过程
1 )概述
- 在更新流程当中如何去设置ref上面的对象的过程
- 在我们创建fiber的时候去处理ref这个属性
- 那我们什么时候创建fiber对象?
- 就是我们去更新某一个节点,然后要去调和它的子节点的时候
- 这个时候我们会对每一个子节点去创建这个fiber对象
- 创建这个fiber对象的过程,我们就会去处理这个ref
- commit开始之前先detach
2 )源码
定位到 packages/react-reconciler/src/ReactChildFiber.js#L1108
js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
// ... 跳过很多代码
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type
) {
// ... 跳过很多代码
// 注意这里
existing.ref = coerceRef(returnFiber, child, element);
// ... 跳过很多代码
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
// ... 跳过很多代码
return created;
} else {
// ... 跳过很多代码
// 注意这里
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
-
进入
coerceRef
jsfunction coerceRef( returnFiber: Fiber, current: Fiber | null, element: ReactElement, ) { let mixedRef = element.ref; // 拿到 ref // function ref 和 object ref 是不需要经过特殊处理的 // 自己处理节点对象挂载到 class component 的 this 上面这一个过程 // 对于 string ref,它只是一个string,它没有任何功能,它的挂载是要react这边来帮着去做的 // 所以这边主要去 去处理一下 string ref 的一个实现过程 if ( mixedRef !== null && typeof mixedRef !== 'function' && typeof mixedRef !== 'object' ) { if (__DEV__) { // 跳过 } // 在 ReactElement.js 中可看到 _owner 是 ReactCurrentOwner.current // 在更新 class component 的时候,调用 finishClassComponent 就会设置 // ReactCurrentOwner.current = workInProgress // 在后面调用 instance.render() 去重新渲染子节点的过程中 // 就会调用 React.createElement, 因为 ref 只有 // 因为ref它肯定是在 class component,它这个过程当中才会被创建的 // 因为只有 class component 有 this 去挂载 ref 的那个对象 // 所以我们在调用 instance.render() 的时候,那么在 react element 里面拿到的这个 ReactCurrentOwner.current // 就是我们那个 class component,它对应的fiber对象 if (element._owner) { const owner: ?Fiber = (element._owner: any); let inst; // 有了这个 fiber 对象之后,那么我们可以拿到它的 _owner,拿到它的 _owner 之后 // 如果 owner 的存在,ownerFiber 就等于 owner if (owner) { const ownerFiber = ((owner: any): Fiber); invariant( ownerFiber.tag === ClassComponent, 'Function components cannot have refs.', ); // 然后它的 inst 就是 ownerFiber.stateNode // 也就是我们 class component,那个 instance 也就是 this inst = ownerFiber.stateNode; } invariant( inst, 'Missing owner for string ref %s. This error is likely caused by a ' + 'bug in React. Please file an issue.', mixedRef, ); const stringRef = '' + mixedRef; // Check if previous string ref matches new string ref // 有了 inst 之后,可以为它去构建一个方法 // 这边是一个对比,就是说我们每次设置完这个 ref 之后,都会给它设置一个属性 _stringRef // 用来在我们更新这个组件的过程当中,去判断一下它这个 _stringRef 对应的那个值是否有变化 if ( current !== null && current.ref !== null && typeof current.ref === 'function' && current.ref._stringRef === stringRef ) { // 如果没有变化,我们不需要去为它重新生成一个方法了,我们只需要 return 就可以了 return current.ref; } // 对于新的情况,我们就需要去生成一个方法 // 这个 value 就是后期dom节点或者是class instance它被挂载的时候 // 它会调用这个 ref 这个方法,然后传入它自己的那个实例 // 也就是给一个 dom 节点设置了 ref 之后,在后期就是 commitRoot 的过程当中,这个节点最终被挂载到 dom 上面了 // 这个时候会把这个 dom 节点去调用 ref 这个方法,然后作为参数传入进来 // 这个时候去设置到就是当前去创建这个 ref 的时候,这个 class component的对象上面的 ref 这个属性上面 // 也就达到了 ref 可以设置在 this.refs 上面这个功能 const ref = function(value) { let refs = inst.refs; if (refs === emptyRefsObject) { // This is a lazy pooled frozen object, so we need to initialize. refs = inst.refs = {}; } if (value === null) { delete refs[stringRef]; } else { refs[stringRef] = value; } }; ref._stringRef = stringRef; return ref; } else { invariant( typeof mixedRef === 'string', 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.', ); invariant( element._owner, 'Element ref was specified as a string (%s) but no owner was set. This could happen for one of' + ' the following reasons:\n' + '1. You may be adding a ref to a function component\n' + "2. You may be adding a ref to a component that was not created inside a component's render method\n" + '3. You have multiple copies of React loaded\n' + 'See https://fb.me/react-refs-must-have-owner for more information.', mixedRef, ); } } return mixedRef; }
- 这就是在调和子节点的过程当中,要处理 ref 的一个内容
- 因为 stringRef 它是一个特殊的存在,它没有什么功能性
- 而对于 function ref 传进来的就是一个方法,可以直接调用它
- 而对于 object ref,只需要设置它的 .current 就可以了
- 这也是为什么以后 string ref 要被移除的一个原因
- 因为它比较麻烦,需要我们自己去处理
- 这是我们去处理 ref 这个属性的过程
关于 commit开始之前先detach
定位到 packages/react-reconciler/src/ReactFiberScheduler.js#L392
查看 commitAllHostEffects
js
function commitAllHostEffects() {
while (nextEffect !== null) {
if (__DEV__) {
ReactCurrentFiber.setCurrentFiber(nextEffect);
}
recordEffect();
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 对于有 ref 这个 SideEffect 的节点
// 如果current不等于null,要先调用 commitDetachRef
// 先把这个 ref 从之前挂载的地方去给它卸载下来,看下这个 commitDetachRef
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted
// does and isMounted is deprecated anyway so we should be able
// to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
if (__DEV__) {
ReactCurrentFiber.resetCurrentFiber();
}
}
定位到 packages/react-reconciler/src/ReactFiberCommitWork.js#L623
查看 commitDetachRef
js
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
到 commitAllLifeCycles
时
js
function commitAllLifeCycles(
finishedRoot: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
// ... 跳过很多代码
while (nextEffect !== null) {
// ... 跳过很多代码
// 注意这里
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}
// ... 跳过很多代码
}
}
-
再次调用
commitAttachRef
把真正的更新过后的节点给它挂载上去jsfunction commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); // 获取到了 dom节点对应到的实例 break; default: instanceToUse = instance; } // function 的处理 if (typeof ref === 'function') { ref(instanceToUse); } else { // 跳过 if (__DEV__) { if (!ref.hasOwnProperty('current')) { warningWithoutStack( false, 'Unexpected ref object provided for %s. ' + 'Use either a ref-setter function or React.createRef().%s', getComponentName(finishedWork.type), getStackByFiberInDevAndProd(finishedWork), ); } } // 其他情况,直接设置 ref.current = instanceToUse; } } }
-
如果是
HostComponent
执行getPublicInstance
jsexport function getPublicInstance(instance: Instance): * { return instance; }
-
-
这个时候就完成了对于我们的 class component 上面的this,去挂载ref它的一个过程
-
这边最主要的是去注意对于 stringRef 在调和子节点的过程当中
-
会对它进行一个预先的处理,把它转化成一个方法
-
以上就是ref在整个react应用更新的过程当中,如何被实现的原理