React16源码: React中处理ref的核心流程源码实现

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

    js 复制代码
    function 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 把真正的更新过后的节点给它挂载上去

    js 复制代码
    function 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

      js 复制代码
      export function getPublicInstance(instance: Instance): * {
        return instance;
      }
  • 这个时候就完成了对于我们的 class component 上面的this,去挂载ref它的一个过程

  • 这边最主要的是去注意对于 stringRef 在调和子节点的过程当中

  • 会对它进行一个预先的处理,把它转化成一个方法

  • 以上就是ref在整个react应用更新的过程当中,如何被实现的原理

相关推荐
y先森2 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy2 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189112 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿3 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡4 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒5 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员5 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐5 小时前
前端图像处理(一)
前端