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应用更新的过程当中,如何被实现的原理

相关推荐
旺旺大力包6 分钟前
【 Git 】git 的安装和使用
前端·笔记·git
雪落满地香22 分钟前
前端:改变鼠标点击物体的颜色
前端
余生H1 小时前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿1 小时前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~1 小时前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫1 小时前
高德地图自定义折线矢量图形
前端·vue.js·vue
m0_748250931 小时前
html 通用错误页面
前端·html
来吧~1 小时前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
han_2 小时前
不是哥们,我的console.log突然打印不出东西了!
前端·javascript·chrome
魔术师卡颂2 小时前
最近看到太多 cursor 带来的焦虑,有些话想说
前端·aigc·openai