前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之HostComponent的更新(下)

React源码解析之HostComponent的更新(下)

作者头像
进击的小进进
发布2020-03-18 18:20:42
2.7K0
发布2020-03-18 18:20:42
举报
前言

在上篇 React源码解析之HostComponent的更新(上) 中,我们讲到了多次渲染阶段的更新,本篇我们讲第一次渲染阶段的更新

一、HostComponent(第一次渲染)

作用: (1) 创建 DOM 实例 (2) 插入子节点 (3) 初始化事件监听器

源码:

代码语言:javascript
复制
     else {
        //如果是第一次渲染的话

        //如果没有新 props 更新,但是执行到这里的话,可能是 React 内部出现了问题
        if (!newProps) {
          invariant(
            workInProgress.stateNode !== null,
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
          // This can happen when we abort work.
          break;
        }
        //context 相关,暂时跳过
        const currentHostContext = getHostContext();
        // TODO: Move createInstance to beginWork and keep it on a context
        // "stack" as the parent. Then append children as we go in beginWork
        // or completeWork depending on we want to add then top->down or
        // bottom->up. Top->down is faster in IE11.
        //是否曾是服务端渲染
        let wasHydrated = popHydrationState(workInProgress);
        //如果是服务端渲染的话,暂时跳过
        if (wasHydrated) {
          //暂时删除
        }
        //不是服务端渲染
        else {
           //创建 DOM 实例
           //1、创建 DOM 元素
           //2、创建指向 fiber 对象的属性,方便从DOM 实例上获取 fiber 对象
           //3、创建指向 props 的属性,方便从 DOM 实例上获取 props
          let instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          //插入子节点
          appendAllChildren(instance, workInProgress, false, false);

          // Certain renderers require commit-time effects for initial mount.
          // (eg DOM renderer supports auto-focus for certain elements).
          // Make sure such renderers get scheduled for later work.
          if (
            //初始化事件监听
            //如果该节点能够自动聚焦的话
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            //添加 EffectTag,方便在 commit 阶段 update
            markUpdate(workInProgress);
          }
          //将处理好的节点实例绑定到 stateNode 上
          workInProgress.stateNode = instance;
        }
        //如果 ref 引用不为空的话
        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          //添加 Ref 的 EffectTag
          markRef(workInProgress);
        }
      }

解析: (1) 执行createInstance(),创建该 fiber 对象对应的 DOM 对象 (2) 执行appendAllChildren(),插入所有子节点 (3) 执行finalizeInitialChildren(),初始化事件监听,并且判断该节点如果有autoFocus属性并为true时,执行markUpdate(),添加EffectTag,方便在commit阶段update (4) 最后将创建并初始化好的 DOM 对象绑定到fiber对象的stateNode属性上 (5) 最后更新下RefEffectTag即可

我们先来看下createInstance()方法

二、createInstance

作用: 创建DOM对象

源码:

代码语言:javascript
复制
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  if (__DEV__) {
    //删除了 dev 代码
  } else {
    //确定该节点的命名空间
    // 一般是HTML,http://www.w3.org/1999/xhtml
    //svg,为 http://www.w3.org/2000/svg ,请参考:https://developer.mozilla.org/zh-CN/docs/Web/SVG
    //MathML,为 http://www.w3.org/1998/Math/MathML,请参考:https://developer.mozilla.org/zh-CN/docs/Web/MathML
    //有兴趣的,请参考:https://blog.csdn.net/qq_26440903/article/details/52592501
    parentNamespace = ((hostContext: any): HostContextProd);
  }
  //创建 DOM 元素
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  //创建指向 fiber 对象的属性,方便从DOM 实例上获取 fiber 对象
  precacheFiberNode(internalInstanceHandle, domElement);
  //创建指向 props 的属性,方便从 DOM 实例上获取 props
  updateFiberProps(domElement, props);
  return domElement;
}

解析: (1) 一开始先确定了命名空间,一般是htmlnamespace

SVGnamespacehttp://www.w3.org/2000/svg, 请参考: https://developer.mozilla.org/zh-CN/docs/Web/SVG

MathMLnamespacehttp://www.w3.org/1998/Math/MathML, 请参考: https://developer.mozilla.org/zh-CN/docs/Web/MathML

(2) 执行createElement(),创建DOM对象

(3) 执行precacheFiberNode(),在DOM对象上创建指向fiber对象的属性:'__reactInternalInstance$'+Math.random().toString(36).slice(2),方便从DOM对象上获取fiber对象

(4) 执行updateFiberProps(),在DOM对象上创建指向props的属性:__reactEventHandlers$'+Math.random().toString(36).slice(2),方便从DOM实例上获取props

(5) 最后,返回该DOM元素:

我们来看下createElement()precacheFiberNode()updateFiberProps()

三、createElement

作用: 创建DOM元素

源码:

代码语言:javascript
复制
export function createElement(
  type: string,
  props: Object,
  rootContainerElement: Element | Document,
  parentNamespace: string,
): Element {
  let isCustomComponentTag;

  // We create tags in the namespace of their parent container, except HTML
  // tags get no namespace.
  //获取 document 对象
  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    //根据 DOM 实例的标签获取相应的命名空间
    namespaceURI = getIntrinsicNamespace(type);
  }
  //如果是 html namespace 的话
  if (namespaceURI === HTML_NAMESPACE) {
    //删除了 dev 代码

    if (type === 'script') {
      // Create the script via .innerHTML so its "parser-inserted" flag is
      // set to true and it does not execute

      //parser-inserted 设置为 true 表示浏览器已经处理了该`<script>`标签
      //那么该标签就不会被当做脚本执行
      //https://segmentfault.com/a/1190000008299659
      const div = ownerDocument.createElement('div');
      div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
      // This is guaranteed to yield a script element.
      //HTMLScriptElement:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLScriptElement
      const firstChild = ((div.firstChild: any): HTMLScriptElement);
      domElement = div.removeChild(firstChild);
    }
    //如果需要更新的 props里有 is 属性的话,那么创建该元素时,则为它添加「is」attribute
    //参考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is
    else if (typeof props.is === 'string') {
      // $FlowIssue `createElement` should be updated for Web Components
      domElement = ownerDocument.createElement(type, {is: props.is});
    }
    //创建 DOM 元素
    else {
      // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
      // See discussion in https://github.com/facebook/react/pull/6896
      // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240

      //因为 Firefox 的一个 bug,所以需要特殊处理「is」属性

      domElement = ownerDocument.createElement(type);
      // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
      // attributes on `select`s needs to be added before `option`s are inserted.
      // This prevents:
      // - a bug where the `select` does not scroll to the correct option because singular
      //  `select` elements automatically pick the first item #13222
      // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
      // See https://github.com/facebook/react/issues/13222
      // and https://github.com/facebook/react/issues/14239

      //<select>标签需要在<option>子节点被插入之前,设置`multiple`和`size`属性
      if (type === 'select') {
        const node = ((domElement: any): HTMLSelectElement);
        if (props.multiple) {
          node.multiple = true;
        } else if (props.size) {
          // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
          // it is possible that no option is selected.
          //
          // This is only necessary when a select in "single selection mode".
          node.size = props.size;
        }
      }
    }
  }
  //svg/math 的元素创建是需要指定命名空间 URI 的
  else {
    //创建一个具有指定的命名空间URI和限定名称的元素
    //https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElementNS
    domElement = ownerDocument.createElementNS(namespaceURI, type);
  }

  //删除了 dev 代码

  return domElement;
}

(1) 执行getOwnerDocumentFromRootContainer(),获取获取根节点的document对象, 关于getOwnerDocumentFromRootContainer()源码,请参考: React源码解析之completeWork和HostText的更新

(2) 执行getIntrinsicNamespace(),根据fiber对象的type,即标签类型,获取对应的命名空间: getIntrinsicNamespace()

代码语言:javascript
复制
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';

// Assumes there is no parent namespace.
//假设没有父命名空间
//根据 DOM 实例的标签获取相应的命名空间
export function getIntrinsicNamespace(type: string): string {
  switch (type) {
    case 'svg':
      return SVG_NAMESPACE;
    case 'math':
      return MATH_NAMESPACE;
    default:
      return HTML_NAMESPACE;
  }
}

(3) 之后则是一个if...else的判断,如果是html的命名空间的话,则需要对一些标签进行特殊处理; 如果是SVG/MathML的话,则执行createElementNS(),创建一个具有指定的命名空间URI和限定名称的元素, 请参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElementNS

(4) 绝大部分是走的if里情况,看一下处理了哪些标签:

① 如果是<script>标签的话,则通过div.innerHTML的形式插入该标签,以禁止被浏览器当成脚本去执行

关于HTMLScriptElement,请参考: https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLScriptElement

② 如果需要更新的props里有is属性的话,那么创建该元素时,则为它添加「is」attribute, 也就是自定义元素, 请参考: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is

③ 除了上面两种情况外,则使用Document.createElement()创建元素

还有对<select>标签的bug修复,了解下就好

四、precacheFiberNode

作用:DOM对象上创建指向fiber对象的属性

源码:

代码语言:javascript
复制
const randomKey = Math.random()
  //转成 36 进制
  .toString(36)
  //从index=2开始截取
  .slice(2);

const internalInstanceKey = '__reactInternalInstance$' + randomKey;

export function precacheFiberNode(hostInst, node) {
  node[internalInstanceKey] = hostInst;
}

解析: 比较简单,可以学习下 React 取随机数的技巧:

代码语言:javascript
复制
Math.random().toString(36).slice(2)

五、updateFiberProps

作用:DOM对象上创建指向props的属性

源码:

代码语言:javascript
复制
const randomKey = Math.random().toString(36).slice(2);
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey;

export function updateFiberProps(node, props) {
  node[internalEventHandlersKey] = props;
}

解析: 同上

是对createInstance()及其内部function的讲解,接下来看下appendAllChildren()及其内部function

六、appendAllChildren

作用: 插入子节点

源码:

代码语言:javascript
复制
appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    // We only have the top Fiber that was created but we need recurse down its
    // children to find all the terminal nodes.
    //获取该节点的第一个子节点
    let node = workInProgress.child;
    //当该节点有子节点时
    while (node !== null) {
      //如果是原生节点或 text 节点的话
      if (node.tag === HostComponent || node.tag === HostText) {
        //将node.stateNode挂载到 parent 上
        //appendChild API:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
        appendInitialChild(parent, node.stateNode);
      } else if (node.tag === HostPortal) {
        // If we have a portal child, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
      }
      //如果子节点还有子子节点的话
      else if (node.child !== null) {
        //return 指向复建点
        node.child.return = node;
        //一直循环,设置return 属性,直到没有子节点
        node = node.child;
        continue;
      }
      if (node === workInProgress) {
        return;
      }
      //如果没有兄弟节点的话,返回至父节点
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }
        node = node.return;
      }
      //设置兄弟节点的 return 为父节点
      node.sibling.return = node.return;
      //遍历兄弟节点
      node = node.sibling;
    }
  };

解析: (1) 基本逻辑是获取目标节点下的第一个子节点,将其与父节点(即return属性)关联,子子节点也是如此,循环往复;

然后依次遍历兄弟节点,将其与父节点(即return属性)关联,最终会形成如下图的关系:

(2) appendInitialChild()

代码语言:javascript
复制
export function appendInitialChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void {
  parentInstance.appendChild(child);
}

本质就是调用appendChild()这个 API

是对appendAllChildren()及其内部function的讲解,接下来看下finalizeInitialChildren()及其内部function,接下来内容会很多

七、finalizeInitialChildren

作用: (1) 初始化DOM对象的事件监听器和内部属性 (2) 返回autoFocus属性的布尔值

源码:

代码语言:javascript
复制
export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  //初始化 DOM 对象
  //1、对一些标签进行事件绑定/属性的特殊处理
  //2、对 DOM 对象内部属性进行初始化
  setInitialProperties(domElement, type, props, rootContainerInstance);
  //可以 foucus 的节点返回autoFocus的值,否则返回 false
  return shouldAutoFocusHostComponent(type, props);
}

解析: (1) 执行setInitialProperties(),对一些标签进行事件绑定/属性的特殊处理,并且对DOM对象内部属性进行初始化

(2) 执行shouldAutoFocusHostComponent(),可以foucus的节点会返回autoFocus的值,否则返回false

八、setInitialProperties

作用: 初始化DOM对象

源码:

代码语言:javascript
复制
export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document,
): void {
  //判断是否是自定义的 DOM 标签
  const isCustomComponentTag = isCustomComponent(tag, rawProps);
  //删除了 dev 代码

  // TODO: Make sure that we check isMounted before firing any of these events.
  //确保在触发这些监听器触发之间,已经初始化了 event
  let props: Object;
  switch (tag) {
    case 'iframe':
    case 'object':
    case 'embed':
      //load listener
      //React 自定义的绑定事件,暂时跳过
      trapBubbledEvent(TOP_LOAD, domElement);
      props = rawProps;
      break;
    case 'video':
    case 'audio':
      // Create listener for each media event
      //初始化 media 标签的监听器

      // export const mediaEventTypes = [
      //   TOP_ABORT, //abort
      //   TOP_CAN_PLAY, //canplay
      //   TOP_CAN_PLAY_THROUGH, //canplaythrough
      //   TOP_DURATION_CHANGE, //durationchange
      //   TOP_EMPTIED, //emptied
      //   TOP_ENCRYPTED, //encrypted
      //   TOP_ENDED, //ended
      //   TOP_ERROR, //error
      //   TOP_LOADED_DATA, //loadeddata
      //   TOP_LOADED_METADATA, //loadedmetadata
      //   TOP_LOAD_START, //loadstart
      //   TOP_PAUSE, //pause
      //   TOP_PLAY, //play
      //   TOP_PLAYING, //playing
      //   TOP_PROGRESS, //progress
      //   TOP_RATE_CHANGE, //ratechange
      //   TOP_SEEKED, //seeked
      //   TOP_SEEKING, //seeking
      //   TOP_STALLED, //stalled
      //   TOP_SUSPEND, //suspend
      //   TOP_TIME_UPDATE, //timeupdate
      //   TOP_VOLUME_CHANGE, //volumechange
      //   TOP_WAITING, //waiting
      // ];

      for (let i = 0; i < mediaEventTypes.length; i++) {
        trapBubbledEvent(mediaEventTypes[i], domElement);
      }
      props = rawProps;
      break;
    case 'source':
      //error listener
      trapBubbledEvent(TOP_ERROR, domElement);
      props = rawProps;
      break;
    case 'img':
    case 'image':
    case 'link':
      //error listener
      trapBubbledEvent(TOP_ERROR, domElement);
      //load listener
      trapBubbledEvent(TOP_LOAD, domElement);
      props = rawProps;
      break;
    case 'form':
      //reset listener
      trapBubbledEvent(TOP_RESET, domElement);
      //submit listener
      trapBubbledEvent(TOP_SUBMIT, domElement);
      props = rawProps;
      break;
    case 'details':
      //toggle listener
      trapBubbledEvent(TOP_TOGGLE, domElement);
      props = rawProps;
      break;
    case 'input':
      //在 input 对应的 DOM 节点上新建_wrapperState属性
      ReactDOMInputInitWrapperState(domElement, rawProps);
      //浅拷贝value/checked等属性
      props = ReactDOMInputGetHostProps(domElement, rawProps);
      //invalid listener
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      //初始化 onChange listener
      //https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral
      //暂时跳过
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    case 'option':
      //dev 环境下
      //1、判断<option>标签的子节点是否是 number/string
      //2、判断是否正确设置defaultValue/value
      ReactDOMOptionValidateProps(domElement, rawProps);
      //获取 option 的 child
      props = ReactDOMOptionGetHostProps(domElement, rawProps);
      break;
    case 'select':
      //在 select 对应的 DOM 节点上新建_wrapperState属性
      ReactDOMSelectInitWrapperState(domElement, rawProps);
      //设置<select>对象属性
      props = ReactDOMSelectGetHostProps(domElement, rawProps);
      //invalid listener
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      //初始化 onChange listener
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    case 'textarea':
      //在 textarea 对应的 DOM 节点上新建_wrapperState属性
      ReactDOMTextareaInitWrapperState(domElement, rawProps);
      //设置 textarea 内部属性
      props = ReactDOMTextareaGetHostProps(domElement, rawProps);
      //invalid listener
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      //初始化 onChange listener
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    default:
      props = rawProps;
  }
  //判断新属性,比如 style 是否正确赋值
  assertValidProps(tag, props);
  //设置初始的 DOM 对象属性
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
  //对特殊的 DOM 标签进行最后的处理
  switch (tag) {
    case 'input':
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      //
      track((domElement: any));
      ReactDOMInputPostMountWrapper(domElement, rawProps, false);
      break;
    case 'textarea':
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      track((domElement: any));
      ReactDOMTextareaPostMountWrapper(domElement, rawProps);
      break;
    case 'option':
      ReactDOMOptionPostMountWrapper(domElement, rawProps);
      break;
    case 'select':
      ReactDOMSelectPostMountWrapper(domElement, rawProps);
      break;
    default:
      if (typeof props.onClick === 'function') {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        //初始化 onclick 事件,以便兼容Safari移动端
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }
}

解析: (1) 判断是否 是自定义的DOM标签,执行isCustomComponent(),返回true/false

isCustomComponent()

代码语言:javascript
复制
function isCustomComponent(tagName: string, props: Object) {
  //一般自定义标签的命名规则是带`-`的
  if (tagName.indexOf('-') === -1) {
    //https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is
    return typeof props.is === 'string';
  }
  //以下的是SVG/MathML的标签属性
  switch (tagName) {
    // These are reserved SVG and MathML elements.
    // We don't mind this whitelist too much because we expect it to never grow.
    // The alternative is to track the namespace in a few places which is convoluted.
    // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
    case 'annotation-xml':
    case 'color-profile':
    case 'font-face':
    case 'font-face-src':
    case 'font-face-uri':
    case 'font-face-format':
    case 'font-face-name':
    case 'missing-glyph':
      return false;
    default:
      return true;
  }
}

(2) 然后是对一些标签,进行一些额外的处理,如初始化特殊的事件监听、初始化特殊的属性(一般的标签是没有的)等

(3) 看下对<input>标签的处理: ① 执行ReactDOMInputInitWrapperState(),在<input>对应的DOM节点上新建_wrapperState属性

ReactDOMInputInitWrapperState()

代码语言:javascript
复制
//在 input 对应的 DOM 节点上新建_wrapperState属性
export function initWrapperState(element: Element, props: Object) {
  //删除了 dev 代码

  const node = ((element: any): InputWithWrapperState);
  //Input 的默认值
  const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
  //在 input 对应的 DOM 节点上新建_wrapperState属性
  node._wrapperState = {
    //input 有 radio/checkbox 类型,checked 即判断单/多选框是否被选中
    initialChecked:
      props.checked != null ? props.checked : props.defaultChecked,
    //input 的初始值,优先选择 value,其次 defaultValue
    initialValue: getToStringValue(
      props.value != null ? props.value : defaultValue,
    ),
    //radio/checkbox
    //如果type 为 radio/checkbox 的话,看 checked 有没有被选中
    //如果是其他 type 的话,则看 value 是否有值
    controlled: isControlled(props),
  };
}

export function getToStringValue(value: mixed): ToStringValue {
  switch (typeof value) {
    case 'boolean':
    case 'number':
    case 'object':
    case 'string':
    case 'undefined':
      return value;
    default:
      // function, symbol are assigned as empty strings
      return '';
  }
}

function isControlled(props) {
  const usesChecked = props.type === 'checkbox' || props.type === 'radio';
  return usesChecked ? props.checked != null : props.value != null;
}

② 执行ReactDOMInputGetHostProps(),浅拷贝、初始化value/checked等属性

getHostProps()

代码语言:javascript
复制
//浅拷贝value/checked等属性
export function getHostProps(element: Element, props: Object) {
  const node = ((element: any): InputWithWrapperState);
  const checked = props.checked;
  //浅拷贝
  const hostProps = Object.assign({}, props, {
    defaultChecked: undefined,
    defaultValue: undefined,
    value: undefined,
    checked: checked != null ? checked : node._wrapperState.initialChecked,
  });

  return hostProps;
}

③ 执行ensureListeningTo(),初始化onChange listener

(4) 看下对< option>标签的处理:

① 执行ReactDOMOptionValidateProps(),在 dev 环境下: [1] 判断<option>标签的子节点是否是number/string [2] 判断是否正确设置defaultValue/value

ReactDOMOptionValidateProps()

代码语言:javascript
复制
export function validateProps(element: Element, props: Object) {
  if (__DEV__) {
    // This mirrors the codepath above, but runs for hydration too.
    // Warn about invalid children here so that client and hydration are consistent.
    // TODO: this seems like it could cause a DEV-only throw for hydration
    // if children contains a non-element object. We should try to avoid that.
    if (typeof props.children === 'object' && props.children !== null) {
      React.Children.forEach(props.children, function(child) {
        if (child == null) {
          return;
        }
        if (typeof child === 'string' || typeof child === 'number') {
          return;
        }
        if (typeof child.type !== 'string') {
          return;
        }
        if (!didWarnInvalidChild) {
          didWarnInvalidChild = true;
          warning(
            false,
            'Only strings and numbers are supported as <option> children.',
          );
        }
      });
    }

    // TODO: Remove support for `selected` in <option>.
    if (props.selected != null && !didWarnSelectedSetOnOption) {
      warning(
        false,
        'Use the `defaultValue` or `value` props on <select> instead of ' +
          'setting `selected` on <option>.',
      );
      didWarnSelectedSetOnOption = true;
    }
  }
}

② 执行ReactDOMOptionGetHostProps(),获取optionchild

ReactDOMOptionGetHostProps()

代码语言:javascript
复制
//获取<option>child 的内容,并且展平 children
export function getHostProps(element: Element, props: Object) {
  const hostProps = {children: undefined, ...props};
  //展平 child,可参考我之前写的一篇:https://juejin.im/post/5d46b71a6fb9a06b0c084acd
  const content = flattenChildren(props.children);

  if (content) {
    hostProps.children = content;
  }

  return hostProps;
}

可参考: React源码解析之React.children.map()

(5) 看下对< select>标签的处理: ① 执行ReactDOMSelectInitWrapperState(),在select对应的DOM节点上新建_wrapperState属性

ReactDOMSelectInitWrapperState()

代码语言:javascript
复制
export function initWrapperState(element: Element, props: Object) {
  const node = ((element: any): SelectWithWrapperState);
  //删除了 dev 代码

  node._wrapperState = {
    wasMultiple: !!props.multiple,
  };

  //删除了 dev 代码
}

② 执行ReactDOMSelectGetHostProps(),设置<select>对象属性

ReactDOMSelectGetHostProps()

代码语言:javascript
复制
//设置<select>对象属性
//{
// children:[],
// value:undefined
// }
export function getHostProps(element: Element, props: Object) {
  return Object.assign({}, props, {
    value: undefined,
  });
}

③ 执行trapBubbledEvent(),初始化invalid listener

④ 执行ensureListeningTo(),初始化onChange listener

(6) <textarea>标签的处理逻辑,同上,简单看下它的源码:

ReactDOMTextareaInitWrapperState()

代码语言:javascript
复制
//在 textarea 对应的 DOM 节点上新建_wrapperState属性
export function initWrapperState(element: Element, props: Object) {
  const node = ((element: any): TextAreaWithWrapperState);
  //删除了 dev 代码

  //textArea 里面的值
  let initialValue = props.value;

  // Only bother fetching default value if we're going to use it
  if (initialValue == null) {
    let defaultValue = props.defaultValue;
    // TODO (yungsters): Remove support for children content in <textarea>.
    let children = props.children;
    if (children != null) {
      //删除了 dev 代码

      invariant(
        defaultValue == null,
        'If you supply `defaultValue` on a <textarea>, do not pass children.',
      );
      if (Array.isArray(children)) {
        invariant(
          children.length <= 1,
          '<textarea> can only have at most one child.',
        );
        children = children[0];
      }

      defaultValue = children;
    }
    if (defaultValue == null) {
      defaultValue = '';
    }
    initialValue = defaultValue;
  }

  node._wrapperState = {
    initialValue: getToStringValue(initialValue),
  };
}

ReactDOMTextareaGetHostProps()

代码语言:javascript
复制
//设置 textarea 内部属性
export function getHostProps(element: Element, props: Object) {
  const node = ((element: any): TextAreaWithWrapperState);
  //如果设置 innerHTML 的话,提醒开发者无效
  invariant(
    props.dangerouslySetInnerHTML == null,
    '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
  );

  // Always set children to the same thing. In IE9, the selection range will
  // get reset if `textContent` is mutated.  We could add a check in setTextContent
  // to only set the value if/when the value differs from the node value (which would
  // completely solve this IE9 bug), but Sebastian+Sophie seemed to like this
  // solution. The value can be a boolean or object so that's why it's forced
  // to be a string.

  //设置 textarea 内部属性
  const hostProps = {
    ...props,
    value: undefined,
    defaultValue: undefined,
    children: toString(node._wrapperState.initialValue),
  };

  return hostProps;
}

(7) 标签内部属性和事件监听器特殊处理完后,就执行assertValidProps(),判断新属性,比如 style是否正确赋值

assertValidProps()

代码语言:javascript
复制
//判断新属性,比如 style 是否正确赋值
function assertValidProps(tag: string, props: ?Object) {
  if (!props) {
    return;
  }
  // Note the use of `==` which checks for null or undefined.
  //判断目标节点的标签是否可以包含子标签,如 <br/>、<input/> 等是不能包含子标签的
  if (voidElementTags[tag]) {
    //不能包含子标签,报出 error
    invariant(
      props.children == null && props.dangerouslySetInnerHTML == null,
      '%s is a void element tag and must neither have `children` nor ' +
        'use `dangerouslySetInnerHTML`.%s',
      tag,
      __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '',
    );
  }
  //__html设置的标签内有子节点,比如:__html:"<span>aaa</span>" ,就会报错
  if (props.dangerouslySetInnerHTML != null) {
    invariant(
      props.children == null,
      'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
    );
    invariant(
      typeof props.dangerouslySetInnerHTML === 'object' &&
        HTML in props.dangerouslySetInnerHTML,
      '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
        'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +
        'for more information.',
    );
  }
  //删除了 dev 代码

  //style 不为 null,但是不是 Object 类型的话,报以下错误
  invariant(
    props.style == null || typeof props.style === 'object',
    'The `style` prop expects a mapping from style properties to values, ' +
      "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
      'using JSX.%s',
    __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '',
  );
}

(8) 执行setInitialDOMProperties(),设置初始的 DOM 对象属性,比较长

setInitialDOMProperties()

代码语言:javascript
复制
//初始化 DOM 对象的内部属性
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  //循环新 props
  for (const propKey in nextProps) {
    //原型链上的属性不作处理
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    //获取 prop 的值
    const nextProp = nextProps[propKey];
    //设置 style 属性
    if (propKey === STYLE) {
      //删除了 dev 代码

      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      //设置 style 的值
      setValueForStyles(domElement, nextProp);
    }
    //设置 innerHTML 属性
    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    }
    //设置子节点
    else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a <textarea> will cause the placeholder to not
        // show within the <textarea> until it has been focused and blurred again.
        // https://github.com/facebook/react/issues/6731#issuecomment-254874553

        //当 text 没有时,禁止设置初始内容
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      }
      //number 的话转成 string
      else if (typeof nextProp === 'number') {

        setTextContent(domElement, '' + nextProp);
      }
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
      // Noop
    } else if (propKey === AUTOFOCUS) {
      // We polyfill it separately on the client during commit.
      // We could have excluded it in the property list instead of
      // adding a special case here, but then it wouldn't be emitted
      // on server rendering (but we *do* want to emit it in SSR).
    }
    //如果有绑定事件的话,如<div onClick=(()=>{ xxx })></div>
    else if (registrationNameModules.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        //删除了 dev 代码
        //https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral
        ensureListeningTo(rootContainerElement, propKey);
      }
    } else if (nextProp != null) {
      //为 DOM 节点设置属性值
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

逻辑是循环DOM对象上的新props,对不同的情况做相应的处理

① 如果是style的话,则执行setValueForStyles(),确保 正确初始化style属性:

setValueForStyles()

代码语言:javascript
复制
// 设置 style 的值
export function setValueForStyles(node, styles) {
  const style = node.style;
  for (let styleName in styles) {
    if (!styles.hasOwnProperty(styleName)) {
      continue;
    }
    //没有找到关于自定义样式名的资料。。
    //可参考:https://zh-hans.reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html
    const isCustomProperty = styleName.indexOf('--') === 0;
    //删除了 dev 代码
    //确保样式的 value 是正确的
    const styleValue = dangerousStyleValue(
      styleName,
      styles[styleName],
      isCustomProperty,
    );
    //将 float 属性重命名
    //<div style={{float:'left',}}></div>
    if (styleName === 'float') {
      styleName = 'cssFloat';
    }
    if (isCustomProperty) {
      style.setProperty(styleName, styleValue);
    } else {
      //正确设置 style 对象内的值
      style[styleName] = styleValue;
    }
  }
}

dangerousStyleValue(),确保样式的value是正确的:

代码语言:javascript
复制
//确保样式的 value 是正确的
function dangerousStyleValue(name, value, isCustomProperty) {
  // Note that we've removed escapeTextForBrowser() calls here since the
  // whole string will be escaped when the attribute is injected into
  // the markup. If you provide unsafe user data here they can inject
  // arbitrary CSS which may be problematic (I couldn't repro this):
  // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
  // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
  // This is not an XSS hole but instead a potential CSS injection issue
  // which has lead to a greater discussion about how we're going to
  // trust URLs moving forward. See #2115901

  const isEmpty = value == null || typeof value === 'boolean' || value === '';
  if (isEmpty) {
    return '';
  }

  if (
    //-webkit-transform/-moz-transform/-ms-transform
    !isCustomProperty &&
    typeof value === 'number' &&
    value !== 0 &&
    !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])
  ) {
    //将 React上的 style 里的对象的值转成 px
    return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
  }

  return ('' + value).trim();
}

② 如果是innerHTML的话,则执行setInnerHTML(),设置innerHTML属性

setInnerHTML()

代码语言:javascript
复制
const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
  node: Element,
  html: string,
): void {
  // IE does not have innerHTML for SVG nodes, so instead we inject the
  // new markup in a temp node and then move the child nodes across into
  // the target node

  //兼容 IE
  if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) {
    reusableSVGContainer =
      reusableSVGContainer || document.createElement('div');
    reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';
    const svgNode = reusableSVGContainer.firstChild;
    while (node.firstChild) {
      node.removeChild(node.firstChild);
    }
    while (svgNode.firstChild) {
      node.appendChild(svgNode.firstChild);
    }
  } else {
    node.innerHTML = html;
  }
});

③ 如果是children的话,当子节点是string/number时,执行setTextContent(),设置textContent属性

setTextContent()

代码语言:javascript
复制
let setTextContent = function(node: Element, text: string): void {
  if (text) {
    let firstChild = node.firstChild;

    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  node.textContent = text;
};

④ 如果有绑定事件的话,如<div onClick=(()=>{ xxx })></div>,则执行,确保绑定到了document上,请参考:

https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral

registrationNameModules

⑤ 不是上述情况的话,则setValueForProperty(),为DOM节点设置属性值(这个 function 太长了,暂时跳过)

(9) 最后又是一串switch...case,对特殊的DOM标签进行最后的处理,了解下就好

九、shouldAutoFocusHostComponent

作用: 可以foucus的节点会返回autoFocus的值,否则返回false

源码:

代码语言:javascript
复制
//可以 foucus 的节点返回autoFocus的值,否则返回 false
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
  //可以 foucus 的节点返回autoFocus的值,否则返回 false
  switch (type) {
    case 'button':
    case 'input':
    case 'select':
    case 'textarea':
      return !!props.autoFocus;
  }
  return false;
}

解析: 比较简单

是对finalizeInitialChildren()及其内部function的解析,本文也到此结束了,最后放上 GitHub

GitHub

ReactFiberCompleteWork.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCompleteWork.js

ReactDOMHostConfig.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/ReactDOMHostConfig.js

ReactDOMComponent.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/Reac


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、HostComponent(第一次渲染)
  • 二、createInstance
  • 三、createElement
  • 四、precacheFiberNode
  • 五、updateFiberProps
  • 六、appendAllChildren
  • 七、finalizeInitialChildren
  • 八、setInitialProperties
  • 九、shouldAutoFocusHostComponent
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档